ExcelVBA のソースを .xls ファイルから抽出する JScript を書いた

Excel VBA の保守をしなければいけないんだけども…

  • そのアプリは 開いただけで自動的に VBA が自動的に起動してしまう
  • xlt ファイルなので、名前を変えないまま保存できず、毎回自分自身を選択しなくてはいけない

こういう場合、Ariawase という Windows Scripting Host のツールで VBA のソースをエクスポート / インポートするのが定石です。が、ちょっと User Interface が好みではない。

ということで、同ツールのソースから必要最小限の部分をパクって 参考にして 、エクスポートするコードを JScript で書いてみました (本当は Go でいきなり書きたかったけど、COM の構成を理解するために、まず JScript でプロトタイプを書いた)

exportvba.js

var args = WScript.Arguments;

if( args.length < 1 ){
    WScript.Echo( "cscript exportvbs.js XLSNAME" );
    WScript.Quit();
}
var fsObj = new ActiveXObject("Scripting.FileSystemObject");
var excel = new ActiveXObject("Excel.Application");
try{
    excel.Visible = true;
    var targetPath = fsObj.GetAbsolutePathName(args(0));
    WScript.Echo( "extract: " + targetPath );
    var book = excel.WorkBooks.Open(targetPath);
    for( var p = new Enumerator(book.VBProject.VBComponents) ; ! p.atEnd() ; p.moveNext() ){
        var obj = p.item();
        var name = obj.Name;
        switch( obj.Type ){
            case 1: name += ".bas"; break;
            case 2: name += ".cls"; break;
            case 3: name += ".frm"; break;
            case 100: name += ".dcm";break;
        }
        var fullpath = fsObj.GetAbsolutePathName( name );
        WScript.Echo( fullpath );
        obj.Export( fullpath );
    }
}finally{
    if ( excel != null ){
        excel.Quit();
    }
}

cscript exportvba.js foo.xls とすると、カレントディレクトリに foo.xls 内の VBA ソースを展開するという本当にシンプルな動作です。

さて、次のステップは

  • インポート版を作る
  • Go 言語で作り直す

かな。これ、いつになったら出来るかな!?

ガーディアントラップ完成

ここ最近 GitHub の緑が明らかに減っています。

f:id:zetamatta:20190908145929p:plain

↓ 主にコレのせい

f:id:zetamatta:20190908150146p:plain

そう、マインクラフトでガーディアントラップを作っていたのでした。シーランタンと呼ばれる発光するブロックがあるのですが、これは危険に満ちた海底神殿の建材か、あるいはその周辺に湧くガーディアンというモンスターが落とすプリズマリンクリスタルからしか作れません。

ガーディアントラップというのは、このガーディアンを自動的にかりとるシステムなのです。

今回は「[マイクラ JE] ガーディアントラップを作ろう⑤ ~ドM向け落下式 参~  : ばばクラ=ばばあのマインクラフト」にあるガーディアントラップをお手本にしてトライしてみました。


一番たいへんなのは、「水抜き」と「海底神殿そのものの解体」。これは本当に根気ですねぇ。自分は砂利集めのために地下洞窟を探検したり、竹を栽培したりと、いろいろ気分転換しながらボチボチと作業をすすめました。

f:id:zetamatta:20190908152041p:plain

ある程度、水抜きができたら、湧き層を作り始めます。

f:id:zetamatta:20190908152159p:plain

底を「開いた状態のフェンスゲート」にすると、ガーディアンやスティーブは通れるけど、水は通れないマスになるそうです。看板でも機能的には同じだけど字を処理する CPU パワーを消費するので、最近はフェンスゲートの方がよいとされています。

f:id:zetamatta:20190908152838p:plain

フェンスゲートの上に上から下に流れる水流のある水槽(湧き層)を作ります。この水槽の大きさですが、ガーディアンのスポーン範囲は y=38~62、イカのスポーン範囲は 46~62 なので、y=38~45 の高さの 2✕20マスくらいにしました。

とりあえず、この方式でちゃんと機能するかを検証したかったので、1個だけ試験的に水槽を作ってみたところ、ギリギリ少し湧きました。性能についてはイマイチでしたが、それは水槽を増やして、周辺の湧き潰し(水抜き)を進めればいい話なので、この時点では機能することさえ証明できれば御の字でした(そして正解だった)

水槽に湧いたガーディアンは水流でフェンスゲートの下に落下してくるので、それをまた水流で流して落とし穴に誘導します。

f:id:zetamatta:20190908153634p:plain

落とし穴に落ちたら、落とし穴の底より2マス上にある溶岩の層でコンガリ焼けていただきます。

f:id:zetamatta:20190908154009p:plain

|   |落とし穴断面図
|マグマ|
|+++|←フェンスゲート
|   |
|▽▽▽|←ホッパー
| 箱 |

そのドロップをホッパーで回収して、チェストに格納します。

さて、こうして最終的に8個ほど湧き層を作って、旧海底神殿の敷地9割を水抜きした結果、ちゃんと計測はしていないのですが数秒ことに2,3匹ガーディアンが落とし穴に落ちてくるようになったので、まぁまぁな性能になったと思います。これで、シーランタンが躊躇なく使えるようになりました。うれしいな!(一度設置するとシルクタッチでないと回収できないのが辛いですが)

得られた知見:

  • 湧き層はおそらく別にガラスで作る必要はなかったと思われる。でも、見栄えを考えるとガラスでよかった
  • 「プリズマリンの欠片」と「焼き魚」がすごく余るので、プリズマリンのブロックやレンガはなんぼでも作れる。だが、イカスミがないので、2番めにかっこいいダークプリズマリンブロックだけ作れない。湧き層の上限をもっと高くして、イカを湧くようにした方がよかった。
  • 砂利・砂をすごく消費する。今回は用途が少ない砂利を使用したが、砂利って回収するたびに1割が火打ち石になるので、海底から定期的に補給していても、油断すると不足する。その点、砂は目減りしないので安心だが、ガラス材料になるのでモッタイナイし使えなかった。
  • フェンスゲートとマグマ層の空間は2マスがちょうどよい模様。3マスだと火が消えて、生き残ってしまう。
  • ドロップアイテムの消失ロスを避けるためにも本当はマグマで処理をするのではなく、落下死させたかったが、落下高度が足りなかった。YouTube 動画を見ると、湧いたガーディアンをわざわざ高い場所まで移動させてから再度落とすようなことをしているものがあったし、参考にしたウェブでは落下距離を稼ぐため、湧き層の下限を高くしているようだ。この y 方向の設計がガーディアントラップの肝のような気がする

以上

nyagos 4.4.5_0 を公開しました。

4.4.4_4 は欠番になりました。今回の修正は、パッチリリースというには、少し修正が多めになりましたので。

今回の修正は、~/.nyagos の簡素化がメインです。

  • Lua関数: nyagos.dirname() を実装

これは UNIX の dirname 相当を行うための関数です。

$ lua_e "print(nyagos.dirname('C:/User/hymko/Share'))"
C:\User\hymko

みたいに使います。実は nyagos.dirname('A')nyagos.pathjoin('A','..') と等価なのは秘密です。

  • C-o で複数ファイル選択をサポート(Space,BackSpace,Shift-H/J/K/L,Ctrl-Left/Right/Down/Up)

f:id:zetamatta:20190901102126p:plain

ごらんのとおり、複数ファイル選択ができるようになりました。

  • Alt-Y(引用符つきペースト)で、改行前後に引用符を置くようにした

これは git status でリストされた修正ファイルをマウス操作でコマンドラインにペーストする時、空白が含まれたファイル名も扱えるようにするための修正です。 (Visual Studio だと、My Project なんていう名前のフォルダーがあって、これが難儀なんですよね)

  • C-o で表示される選択肢がディレクトリの時、末尾に \ (Linux では /) をつけるようにした。

これで選択後に TAB を1回打鍵するのが省けます。

  • nyagos.envadd("ENVNAME","DIR") と nyagos.envdel("ENVNAME","PATTERN") を実装

.nyagos での%PATH% の追加/削除の支援関数です。こんな感じで使います。

nyagos.envdel("PATH",
    "Oracle","Lenovo","Skype",
    "chocolatey","TypeScript","WindowsApps",
    "Wbem","dotnet")

これは %PATH% から OracleLenovo といったキーワードを含むフォルダーを削除します。長過ぎる %PATH% は誤動作を引き起こすこともあるので、いつも Lua 関数を作って短くしていたのですが、いい加減定番コードなので本体にツール関数として内蔵させた方がよいのではないかということで実装してみました。

nyagos.envadd("PATH",
    "C:\\go\\bin",
    "C:\\TDM-GCC-64\\bin",
    "%ProgramFiles%\\Git\\bin",
    "%ProgramFiles%\\Git\\cmd",
    "%ProgramFiles%\\Git\\cmd",
    "%ProgramFiles%\\Git\\usr\\bin",
    "%ProgramFiles(x86)%\\Git\\bin",
    "%ProgramFiles(x86)%\\Git\\cmd",
    "%ProgramFiles(x86)%\\Git\\usr\\bin",
    "%ProgramFiles%\\Subversion\\bin",
    "%ProgramFiles(x86)%\\Subversion\\bin",
    "~\\Share\\Program Files\\idmanager",
    "%VBOX_MSI_INSTALL_PATH%",
    "~\\Share\\bin",
    "~\\Share\\cmds")

こちらは逆に %PATH% にフォルダーを追加します。こちらは少々機能追加してありまして、%~% というワードを環境変数、~\ をホームディレクトリ名に展開するといったことを行います。また、ディレクトリとして存在しない場合は追加を行いませんので、.nyagos を DropBox などで共有化して、各PCでありえるケースをとにかく書くといったことも可能になります。

  • nyagos.pathjoin() で %ENVNAME% と ~\,~/ を展開するようにした

これは文字通りです。いちいち nyagos.env.ENVNAME とかいちいち書くの面倒なので。

以上になります。

strings.EqualFold はどの程度の速度か

strings.EqualFold について言及がなかったので、測ってみました。

追加行:

func isDevNull4(name string) bool {
    return strings.EqualFold(name,"nul")
}
func BenchmarkS4(b *testing.B) {
    test(b, isDevNull4)
}

結果:

$ go test -bench .
goos: windows
goarch: amd64
BenchmarkS1-4           50000000                29.5 ns/op
BenchmarkS2-4           50000000                29.1 ns/op
BenchmarkS3-4             200000              9844 ns/op
BenchmarkS4-4           20000000               101 ns/op

自分的には「なんか、まぁまぁ速かったけど、速すぎるので、何かミスってる可能性大」と思っていたんですが、「そんなもんだと思います」とのコメントをいただきました。

まぁ、考えてみれば、S3 で使われている strings.ToLower(name) == "nul" は文字列の領域確保とインスタンス作成が入っているため、この結果は当然かもしれません。

mattn さん、どうもありがとうございました。

そのソリューションを開くべき Visual Studio のバージョンは?

ソリューションファイル(*.sln)の記述

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "HOGEHOGE", "HOGEHOGE\HOGEHOGE.vbproj", "{E951CFF8-148E-40D5-93CD-17118375A437}"

コメント行に Visual Studio 2010 というテキストがあるので参考になる。だが、これは、このソリューションファイルを作成した時の Visual Studio のバージョンであるので、現在使うべき Visual Studio であるという保証はない。(新しいバージョンの Visual Studio 向けにアップグレードしても、このコメント欄は更新されない)

プロジェクトファイル(*.vbproj等)の記述

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

古い Visual Studio は、新しい ToolsVersion をサポートしていない場合があるため、これで使用すべき Visual Studio は制限される。

MSBuild ツールセット (ToolsVersion) - Visual Studio | Microsoft Docsによると

if you open a Visual Studio 2008 project in Visual Studio 2010, the project file is updated to include ToolsVersion="4.0". If you then try to open that project in Visual Studio 2008, it doesn't recognize the upgraded ToolsVersion and therefore builds the project as though the attribute was still set to 3.5.

Visual Studio 2010 and Visual Studio 2012 use a ToolsVersion of 4.0. Visual Studio 2013 uses a ToolsVersion of 12.0. Visual Studio 2015 uses ToolsVersion 14.0, and Visual Studio 2017 uses ToolsVersion 15.0.

つまり

ということになるようだ。

(追記)ソリューションファイル

ソリューションファイルには、さらに次のような文もあることが確認された。奥が深い

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.40629.0
MinimumVisualStudioVersion = 10.0.40219.1

VMware Workstation 12 のイメージがディスクを圧迫してきたので整理した

どうも VMware はバージョンやエディションによって、インターフェースがだいぶと違うようで、ググっても違う製品向けのものが出てきてしまう。

試行錯誤の結果、下記の手順でディスク消費を減らした。
(10個ちかく VM があったのだが、500GB くらい浮いた)

  • 不要なスナップショットを削除する
    • ただし、この時点ではディスクは開放されない(ように見える)
  • VM(M)」→「管理(M)」→「ディスクのクリーンアップ」
    • こちらは確実にディスクが解放される(ようだ)
    • この機能の場所が分からなくて、今まで放置していたのは秘密だ。
  • 使用していない VM の削除
    • 「ファイル(F)」→ 「OVF へのエクスポート(E)」で、OVF フォーマットでエクスポートする
    • VM(M)」→「管理(M)」→「ディスクから削除(D)」
    • エクスポートした OVF ファイル一式は DropBox のフォルダーにおいて、スマートシンクで「オンラインのみ」にすることで、ローカルディスクの消費容量をゼロにする(会社の DropBox はアホほど容量が余っているのだ)
    • 削除した VM がクローンの場合、クローン元の VM のクローン時のバックアップも削除してよくなる(はず)

なお、今回はリストアのテストまでしていないので、もしかしたらインポートできないという事態もありえないともいえない。真似る時は OWN YOUR RISK で頼む

(備考)リンククローンについて

  • リンククローンして作った VM は、仮想マシンの詳細欄に「クローン: クローン元のイメージのパス 」が表示される
    • このVMでは、イメージファイルのフルコピーはバックアップにならない(リストア時にクローン元のVMも必要になる)。OVFファイルにエクスポートした方がよい
  • リンククローンを過去作ったことがある VM は、スナップショットマネージャで見た時に、リンククローンした時のスナップショットが自動作成される。これを削除しようとすると警告される。
    • クローン先のイメージを完全削除しないとリンク時のスナップショットを消すわけにはいかない

以上

*.vcxproj/vbproj/csproj のパーサーを書いているが、$(VCTargetsPath)の導出元がわからない

Visual Studio のプロジェクトで生成されるプロダクト(実行ファイル・DLLファイル)のパスを生成する機能が vf1s.exe に必要なので、"encoding/xml".Unmarshal関数 で読み取っていました。

Unmarshal 関数は、タグに合わせてフィールドを用意した構造体に解析結果を放り込むというもので、解釈する XML タグの種類がそれほど多くない場合などには非常にお手軽で便利なんですが、プロジェクトファイルのように「任意の Condition 属性の中にある条件式が真の時だけタグの中身を有効にする」というif 文的な処理はさすがに厳しいので、こちらの記事を参考にして、タグの開始時・終了時・その他の部分をトークン化して読み出すDecoder関数を使うようにしました。 (一応、GoDoc さらしときます→ https://godoc.org/github.com/zetamatta/vf1s/projs

これで、手元にあるプロジェクトファイルでは概ねうまくゆくようになったんですが、まだ一部ちゃんと解釈できていないところがありまして… $(VCTargetsPath) などデフォルトで設定されている変数の値なんですね。これがわからないと <Import> タグで取り込むファイルのフルパスも導出できないんです。

Visual Studio 2019 Community Edition の $(VCTargetsPath) に限って言うと、C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Microsoft\VC\v160\ になるみたいなんですが、あらゆるバージョンの Visual Studio での普遍的な値を得るための方法が不明。ググってみてはいるんですが

  • Visual Studio というよりは MSBuild がこれらの変数を定義している
  • レジストリに定義があるようだが、これがとんでトラブルになるケースが多い

ということくらいしかわかりませぬ。これさえ分かれば、他の変数もインポート先の XML 経由で得られるんですが、ぐぬぬ