はじめに
下記の記事で VBA マクロのデバッグ時,全ての VBA マクロモジュールを解放したり,エクスポート,あるいはインポートする作業を頻繁に行いました。
この種の作業を自動化する VBA マクロ自体よく知られていますが,わざわざ VBA マクロを解放するためだけに専用の VBA マクロを組むというのも馬鹿らしい気がします。もちろん VBA マクロモジュールを外部ファイルとして用意しておき,そのファイルをインポートして実行するだけで良いのですが,その手間すら煩わしいと思っています。
この手の作業を VBA マクロを組む,あるいはインポートするのではなく,外部スクリプトとして(文字通り?)アウトソーシングしたいと思いました。
仕様案(お品書き)
- WSH + JavaScript で作ります。これまで VBA によく似た VBScript を愛用していましたが,もう使用期限が迫っているので JavaScript に移行します。
- 下記の機能を実装したいと思います。
- マクロモジュールの一覧表示
- マクロモジュールの解放
- マクロモジュールのエクスポート
出力先フォルダを指定できるようにしたい - マクロモジュールのインポート
複数ファイル名を指定かつワイルドカードも使いたい
- せっかくなので対象は Excel に限らず,Word,PowerPoint も含めることにします。
実装コード
実装コードはコチラ ※300行弱あります
//------------------------------------------------------------------------------
// モジュール属性テーブル
//------------------------------------------------------------------------------
var table = {
1: { ext: ".bas", name: "標準モジュール" }, // vbext_ct_StdModule
2: { ext: ".cls", name: "クラスモジュール" }, // vbext_ct_ClassModule
3: { ext: ".frm", name: "MSフォーム" }, // vbext_ct_MSForm
11: { ext: null, name: "ActiveXデザイナ" }, // vbext_ct_ActiveXDesigner
100: { ext: null, name: null } // vbext_ct_Document
};
//------------------------------------------------------------------------------
// メイン関数の呼び出し
//------------------------------------------------------------------------------
var args = WScript.Arguments.Unnamed;
var ret = main(args);
try {
WScript.Quit(ret);
} catch(e) {
/* 何もしない */
}
//------------------------------------------------------------------------------
// メイン関数
//------------------------------------------------------------------------------
function main(args) {
//--------------------------------------------------------------------------
// ヘルプメッセージ
//--------------------------------------------------------------------------
if(args.Count < 2) {
var msg = [
"Microsoft Office ファイルの VBA マクロユーティリティ",
"",
"VBAUTIL(.JS) [ファイル名] [コマンド] ...",
"",
"<ファイル名>",
"Word, Excel, PowerPoint ファイルを指定可能です。",
" Word:拡張子 *.docx, *.docm",
" Excel:拡張子 *.xlsx, *.xlsm, *.xlsb",
"PowerPoint:拡張子 *.pptx, *.pptm",
"",
"<コマンド>",
" LIST マクロ一覧を表示します。",
"RELEASE マクロを解放します。",
" IMPORT [ファイル名]",
" 指定したファイルをインポートします。",
" ファイル名は複数指定可能です。",
" ワイルドカードを使用できます。",
" EXPORT (フォルダ名)",
" マクロをエクスポートします。",
" フォルダ名を省略するとカレントディレクトリに出力します。",
" 標準モジュール:拡張子 *.bas",
" クラスモジュール:拡張子 *.cls",
" MSフォーム:拡張子 *.frm"
];
WScript.Echo(msg.join("\n"));
return -1;
}
//--------------------------------------------------------------------------
// コマンドライン解析
//--------------------------------------------------------------------------
var filename = args(0);
var command = args(1);
var params = [];
for(var i = 2; i < args.Count; i++)
params.push(args(i))
//--------------------------------------------------------------------------
// コマンド分岐
//--------------------------------------------------------------------------
switch(command.toUpperCase()) {
case "LIST": return command_list (filename);
case "RELEASE": return command_release(filename);
case "IMPORT": return command_import (filename, params);
case "EXPORT": return command_export (filename, params);
default:
WScript.Echo("コマンド " + command + " には対応していません!!");
}
}
//------------------------------------------------------------------------------
// Microsoft Office ファイルの VBAComponents オブジェクトを取得する。
//------------------------------------------------------------------------------
function get_components(filename) {
//--------------------------------------------------------------------------
// ファイルの存在チェック
//--------------------------------------------------------------------------
var fso = WScript.CreateObject("Scripting.FileSystemObject");
if(!fso.FileExists(filename)) {
WScript.Echo("ファイル " + filename + " が存在しません!!");
return null;
}
//--------------------------------------------------------------------------
// 拡張子に応じたアプリケーションの起動
//--------------------------------------------------------------------------
var ext = fso.GetExtensionName(filename);
var fullpath = fso.GetAbsolutePathName(filename);
var app = null;
var collect;
switch(ext.toLowerCase()) {
case "docx": case "docm":
try {
app = GetObject("", "Word.Application");
} catch(e) {
app = WScript.CreateObject("Word.Application");
}
if(app == null) {
WScript.Echo("Word の起動に失敗しました!!");
return null;
}
collect = app.Documents;
break;
case "pptx": case "pptm":
try {
app = GetObject("", "PowerPoint.Application");
} catch(e) {
app = WScript.CreateObject("PowerPoint.Application");
}
if(app == null) {
WScript.Echo("PowerPoint の起動に失敗しました!!");
return null;
}
collect = app.Presentations;
break;
case "xlsx": case "xlsm": case "xlsb":
try {
app = GetObject("", "Excel.Application");
} catch(e) {
app = WScript.CreateObject("Excel.Application");
}
if(app == null) {
WScript.Echo("Excel の起動に失敗しました!!");
return null;
}
collect = app.Workbooks;
break;
default:
WScript.Echo("拡張子 " + ext + " には対応していません!!");
return null;
}
app.Visible = true;
//--------------------------------------------------------------------------
// ドキュメント/プレゼンテーション/ワークブックの取得
//--------------------------------------------------------------------------
var obj = null;
for(var i = 1; i <= collect.Count; i++) {
if(collect(i).FullName.toLowerCase() == fullpath.toLowerCase()) {
obj = collect(i);
break;
}
}
//--------------------------------------------------------------------------
// ドキュメント/プレゼンテーション/ワークブックの新規オープン
//--------------------------------------------------------------------------
if(obj == null) {
obj = collect.Open(fullpath);
if(obj == null) {
WScript.Echo("ファイル " + filename + " のオープンに失敗しました!!");
return null;
}
}
//--------------------------------------------------------------------------
// VBAcomponents の取得
//--------------------------------------------------------------------------
try {
return obj.VBProject.VBComponents;
} catch(e) {
var msg = [
"Visual Basic プロジェクトのアクセスが許可されていません!!",
"",
"アプリケーションを立ち上げて以下の設定を行って下さい。",
"",
"[開発] タブの [マクロのセキュリティ] を選択するか,もしくは [ファイル] タブの",
"[オプション][セキュリティセンター][セキュリティセンターの設定][マクロの設定]",
"を選んで,[VBA プロジェクトオブジェクトモデルへのアクセスを信頼する] に",
"チェックして下さい。"
];
WScript.Echo(msg.join("\n"));
return null;
}
}
//------------------------------------------------------------------------------
// コマンド LIST
//------------------------------------------------------------------------------
function command_list(filename) {
var vba = get_components(filename);
if(vba == null) return -1;
for(var i = 1; i <= vba.Count; i++) {
var module = vba(i);
var name = table[module.Type].name;
if(name) WScript.Echo(name + ": " + module.Name);
}
return 0;
}
//------------------------------------------------------------------------------
// コマンド RELEASE
//------------------------------------------------------------------------------
function command_release(filename) {
//--------------------------------------------------------------------------
// 解放対象をリストアップ
//--------------------------------------------------------------------------
var vba = get_components(filename);
if(vba == null) return -1;
var list = [];
for(var i = 1; i <= vba.Count; i++) {
var module = vba(i);
var name = table[module.Type].name;
if(name) list.push(module);
}
if(list.length == 0) return 0;
//--------------------------------------------------------------------------
// 解放の実行
//--------------------------------------------------------------------------
WScript.Echo("マクロを解放します。");
for(var i = 0; i < list.length; i++) {
var module = list[i];
var name = table[module.Type].name;
WScript.Echo(name + ": " + module.Name);
vba.Remove(module);
}
return 0;
}
//------------------------------------------------------------------------------
// コマンド EXPORT
//------------------------------------------------------------------------------
function command_export(filename, params) {
//--------------------------------------------------------------------------
// 出力先フォルダの存在チェック
//--------------------------------------------------------------------------
var fso = WScript.CreateObject("Scripting.FileSystemObject");
var path = (params.length > 0) ? params[0] : ".";
if(!fso.FolderExists(path)) {
WScript.Echo("フォルダ " + path + " が存在しません!!");
return -1;
}
var abspath = fso.GetAbsolutePathName(path);
//--------------------------------------------------------------------------
// エクスポートの実行
//--------------------------------------------------------------------------
var vba = get_components(filename);
if(vba == null) return -1;
WScript.Echo("マクロをエクスポートします。");
for(var i = 1; i <= vba.Count; i++) {
var module = vba(i);
var ext = table[module.Type].ext;
if(ext == null) continue;
var fullpath = fso.BuildPath(abspath, module.Name + ext);
WScript.Echo(fullpath);
module.Export(fullpath);
}
return 0;
}
//------------------------------------------------------------------------------
// コマンド IMPORT
//------------------------------------------------------------------------------
function command_import(filename, params) {
//--------------------------------------------------------------------------
// インポートリストの作成
//--------------------------------------------------------------------------
if(params.length == 0) {
WScript.Echo("インポートファイルを指定して下さい!!");
return -1;
}
var fso = WScript.CreateObject("Scripting.FileSystemObject");
var shell = WScript.CreateObject("WScript.Shell");
var list = [];
for(var i = 0; i < params.length; i++) {
var proc = shell.Exec("CMD /Q /C for %I in (" + params[i] + ") do echo %~fI");
while(!proc.StdOut.AtEndOfStream) {
var fullpath = proc.StdOut.ReadLine();
if(fso.FileExists(fullpath)) list.push(fullpath);
}
}
if(list.length == 0) {
WScript.Echo("インポートファイルがありません!!");
return -1;
}
//--------------------------------------------------------------------------
// インポートの実行
//--------------------------------------------------------------------------
var vba = get_components(filename);
if(vba == null) return -1;
WScript.Echo("マクロをインポートします。");
for(var i = 0; i < list.length; i++) {
var fullpath = list[i];
WScript.Echo(fullpath);
vba.Import(fullpath);
}
return 0;
}
技術解説
アプリケーション起動
コマンドラインから Microsoft Office ファイル名 filename が与えらえているものとします。ファイル名は相対パスとします。
- ファイルの拡張子に応じて起動するアプリケーションを切り替えます。
- すでにアプリケーションが起動していれば,そのアプリケーションオブジェクトを取得し,そうでなければアプリケーションを起動します。ここで例外処理が必要なのがちょっと気持ち悪いところですが,この手の処理のテンプレートなので仕方ないところです。
- アプリケーションに応じてコレクション
collectを取得します。Word であればDocumentコレクション,PowerPoint であればPresentationコレクション,Excel であればWorkbookコレクションを取得します。 - 最後にアプリケーションを表示させる
Visilbe = trueを忘れてはいけません。筆者はこれを忘れてゾンビプロセスを多数発生させたことがあります。
var fso = WScript.CreateObject("Scripting.FileSystemObject");
var ext = fso.GetExtensionName(filename);
var fullpath = fso.GetAbsolutePathName(filename);
var app = null;
var collect;
switch(ext.toLowerCase()) {
case "docx": case "docm":
try {
app = GetObject("", "Word.Application");
} catch(e) {
app = WScript.CreateObject("Word.Application");
}
if(app == null) {
WScript.Echo("Word の起動に失敗しました!!");
return null;
}
collect = app.Documents;
break;
case "pptx": case "pptm":
try {
app = GetObject("", "PowerPoint.Application");
} catch(e) {
app = WScript.CreateObject("PowerPoint.Application");
}
if(app == null) {
WScript.Echo("PowerPoint の起動に失敗しました!!");
return null;
}
collect = app.Presentations;
break;
case "xlsx": case "xlsm": case "xlsb":
try {
app = GetObject("", "Excel.Application");
} catch(e) {
app = WScript.CreateObject("Excel.Application");
}
if(app == null) {
WScript.Echo("Excel の起動に失敗しました!!");
return null;
}
collect = app.Workbooks;
break;
default:
WScript.Echo("拡張子 " + ext + " には対応していません!!");
return null;
}
app.Visible = true;
ドキュメント/プレゼンテーション/ワークブックの取得
指定したファイル名と同じファイルを既にオープンしていれば,そのファイルのオブジェクトを取得し,オープンしていなければ新規オープンします。
Word, PowerPoint, Excel のいずれもメソッド名が同じなので共通処理となっています。
var obj = null;
for(var i = 1; i <= collect.Count; i++) {
if(collect(i).FullName.toLowerCase() == fullpath.toLowerCase()) {
obj = collect(i);
break;
}
}
if(obj == null) {
obj = collect.Open(fullpath);
if(obj == null) {
WScript.Echo("ファイル " + filename + " のオープンに失敗しました!!");
return null;
}
}
VBAコンポーネントの取得
Word, PowerPoint, Excel のいずれもメソッド名が同じなので共通処理となっています。
なお,セキュリティ設定によっては例外を起こす場合があるので,ここはエラーメッセージをいつもより親切に書いています。
try {
return obj.VBProject.VBComponents;
} catch(e) {
var msg = [
"Visual Basic プロジェクトのアクセスが許可されていません!!",
"",
"アプリケーションを立ち上げて以下の設定を行って下さい。",
"",
"[開発] タブの [マクロのセキュリティ] を選択するか,もしくは [ファイル] タブの",
"[オプション][セキュリティセンター][セキュリティセンターの設定][マクロの設定]",
"を選んで,[VBA プロジェクトオブジェクトモデルへのアクセスを信頼する] に",
"チェックして下さい。"
];
WScript.Echo(msg.join("\n"));
return null;
}
LIST コマンド
上記で取得した VBA コンポーネントオブジェクト vba とします。
指定した Microsoft Office ファイルに含まれる VBA モジュールの種類とモジュール名を表示します。ドキュメントモジュールは表示しません。
var table = {
1: { ext: ".bas", name: "標準モジュール" }, // vbext_ct_StdModule
2: { ext: ".cls", name: "クラスモジュール" }, // vbext_ct_ClassModule
3: { ext: ".frm", name: "MSフォーム" }, // vbext_ct_MSForm
11: { ext: null, name: "ActiveXデザイナ" }, // vbext_ct_ActiveXDesigner
100: { ext: null, name: null } // vbext_ct_Document
};
for(var i = 1; i <= vba.Count; i++) {
var module = vba(i);
var name = table[module.Type].name;
if(name) WScript.Echo(name + ": " + module.Name);
}
RELEASE コマンド
指定した Microsoft Office ファイルに含まれる VBA モジュールを解放(削除)します。ドキュメントモジュールは解放しません。
ループを二つに分け,まず1回目のループで削除対象のモジュールを列挙します。
var list = [];
for(var i = 1; i <= vba.Count; i++) {
var module = vba(i);
var name = table[module.Type].name;
if(name) list.push(module);
}
if(list.length == 0) return 0;
2回目のループで削除します。
WScript.Echo("マクロを解放します。");
for(var i = 0; i < list.length; i++) {
var module = list[i];
var name = table[module.Type].name;
WScript.Echo(name + ": " + module.Name);
vba.Remove(module);
}
EXPORT コマンド
配列の先頭要素 params[0] に出力フォルダ名が相対パスで格納されているものとします。ただし,出力フォルダ名は省略可能とし,省略した場合はカレントディレクトリとします。
var fso = WScript.CreateObject("Scripting.FileSystemObject");
var path = (params.length > 0) ? params[0] : ".";
if(!fso.FolderExists(path)) {
WScript.Echo("フォルダ " + path + " が存在しません!!");
return -1;
}
var abspath = fso.GetAbsolutePathName(path);
エクスポートするファイル名は VBA モジュール名とし,拡張子はモジュールのタイプに応じて決定します。
WScript.Echo("マクロをエクスポートします。");
for(var i = 1; i <= vba.Count; i++) {
var module = vba(i);
var ext = table[module.Type].ext;
if(ext == null) continue;
var fullpath = fso.BuildPath(abspath, module.Name + ext);
WScript.Echo(fullpath);
module.Export(fullpath);
}
IMPORT コマンド
インポートするファイル名は複数指定可能とし,配列 params[] に格納されているものとします。ワイルドカードも使用可能とします。このため子プロセスで for コマンドを用いてインポートファイルを列挙し,その出力を取り込むようにしました。
再帰的に検索するのであれば dir /s /b コマンドの出力を取り込むと検索結果を絶対パスで得られるので便利ですが,非再帰的に検索しようとして単に dir /b コマンドだと絶対パスを得られないので使えません。
本スクリプトで VBA マクロモジュールを再帰的に検索するような用途は考えにくいので,for コマンドでファイルを検索し,その出力を絶対パス %~fI で出力するようにしました。
dir コマンドと異なり,for コマンドは存在しないファイルを列挙できます。このためファイルの存在チェックを行うようにしました。
var fso = WScript.CreateObject("Scripting.FileSystemObject");
var shell = WScript.CreateObject("WScript.Shell");
var list = [];
for(var i = 0; i < params.length; i++) {
var proc = shell.Exec("CMD /Q /C for %I in (" + params[i] + ") do echo %~fI");
while(!proc.StdOut.AtEndOfStream) {
var fullpath = proc.StdOut.ReadLine();
if(fso.FileExists(fullpath)) list.push(fullpath);
}
}
if(list.length == 0) {
WScript.Echo("インポートファイルがありません!!");
return -1;
}
WScript.Echo("マクロをインポートします。");
for(var i = 0; i < list.length; i++) {
var fullpath = list[i];
WScript.Echo(fullpath);
vba.Import(fullpath);
}
実行例
引数なしで実行するとヘルプメッセージを表示します。
c:\Qiita>vbautil
Microsoft Office ファイルの VBA マクロユーティリティ
VBAUTIL(.JS) [ファイル名] [コマンド] ...
<ファイル名>
Word, Excel, PowerPoint ファイルを指定可能です。
Word:拡張子 *.docx, *.docm
Excel:拡張子 *.xlsx, *.xlsm, *.xlsb
PowerPoint:拡張子 *.pptx, *.pptm
<コマンド>
LIST マクロ一覧を表示します。
RELEASE マクロを解放します。
IMPORT [ファイル名]
指定したファイルをインポートします。
ファイル名は複数指定可能です。
ワイルドカードを使用できます。
EXPORT (フォルダ名)
マクロをエクスポートします。
フォルダ名を省略するとカレントディレクトリに出力します。
標準モジュール:拡張子 *.bas
クラスモジュール:拡張子 *.cls
MSフォーム:拡張子 *.frm
LIST コマンドの実行例です。
c:\Qiita>vbautil sample.xlsb list
標準モジュール: Module1
標準モジュール: Module2
標準モジュール: Module3
EXPORT コマンドの実行例です。出力フォルダ名を省略するとカレントディレクトリに出力します。
c:\Qiita>vbautil sample.xlsb export
マクロをエクスポートします。
C:\Qiita\Module1.bas
C:\Qiita\Module2.bas
C:\Qiita\Module3.bas
RELEASE コマンドの実行例です。
c:\Qiita>vbautil sample.xlsb release
マクロを解放します。
標準モジュール: Module1
標準モジュール: Module2
標準モジュール: Module3
IMPORT コマンドの実行例です。カレントディレクトリにある全ての *.bas ファイルをインポートします。
c:\Qiita>vbautil sample.xlsb import *.bas
マクロをインポートします。
C:\Qiita\Module1.bas
C:\Qiita\Module2.bas
C:\Qiita\Module3.bas