Windows バッチに JScript・VBScript・Oracle SQL スクリプトを混在させてバッチ処理の中で実行する
2016年も終わりに近付いている昨今、今更ですが Windows バッチの黒魔術的な挙動にハマっていて、レガシーな職場で培ったレガシーな知識の総決算をしておこうかなと思うなど。
Windows バッチスクリプトを置いておく GitHub リポジトリを作っていますので、よかったらご覧ください。
- GitHub - Neos21/windows-batch-scripts: ちょいと便利な Windows バッチ製のスクリプトやスニペット集 (
https://github.com/Neos21/windows-batch-scripts
)- 2021-01-01 : 現在は shell-scripts というリポジトリに移動
今日はその中から、Windows バッチファイル1つの中に、別の言語のスクリプトを混在させて実行する手法をいくつか紹介する。
目次
- JScript を混在させる Shebang
- VBScript を混在させる技
- Oracle DB に渡す SQL ファイルを混在させ
SQL*Plus
を起動する - WSH (JScript・VBScript) を混在させる方法 (その他のファイルにも使える手法)
- 以上
JScript を混在させる Shebang
「@if・@elif・@else・@end ステートメント」という JScript 独自の構文を利用した手法が有名。
@if(0)==(0) Echo Off
Echo Windows バッチによる前処理
Rem JScript の呼び出し
CScript //NoLogo //E:JScript "%~f0" %*
Echo Windows バッチによる後処理
Pause
Exit /b
@end
// ココから JScript
WScript.echo("WSH JScript による処理");
このファイルはWindows バッチファイル (.bat
) として保存するので、起動時はまずは Windows バッチとして1行目が評価される。@
でコンソール出力はされず、0 == 0
は当然 true なので Echo Off
が実行される。
そこから @end
という行の直前までは Windows バッチとして動作する。Exit /b
なり Goto :EOF
なりでバッチファイルは終了させれば、それ以降の行は読み込まれない。そのため、それ以降の行のコードが Windows バッチとして正しくない内容でも影響がない。
途中で CScript
で自分自身を JScript として実行させている。
JScript としてこのファイルを1行目から評価していくと、@if
から @end
までは「@if・@elif・@else・@end ステートメント」という JScript 独自の構文として解釈される。
1行目の @if(0)==(0)
は @if
ステートメントで if(0)
とみなされる。0
は JScript では false 扱いなので、条件に一致せず、@end
までの中身は評価・実行されなくなる。そのため、この中のコードが JScript として正しくない内容でも影響がない。
Windows バッチはコマンドを大文字小文字どちらで書いても良いが、JScript は if
は小文字でないと予約語として認識しないため、@if
・@end
は小文字で記述する必要がある。
- 参考 : JScript でハマる日々 - m2
- 参考 : BATとWSHのコードを1ファイルに混在させるためのshebang記法(複雑なバッチを1ファイルで実現) - モバイル通信とIT技術をコツコツ勉強するブログ
- 参考 : @if...@elif...@else...@end ステートメント | Microsoft Docs
- 参考 : Studio ODIN - blog風小ネタ集 > MS-DOSのバッチファイルに、WSH(JScript)のコードを記述する
こちらは@if(1==1)
で JScript にも true 判定させるが、直後からブロックコメントとして囲む書き方。
VBScript を混在させる技
.vbs
ファイルを起動させたときに、常に CScript で起動させる手法としては、以下のようなコードをスクリプトの最初の方に書くやり方がある。起動しているプロセスが WScript だったら、自身を CScript で開き直し、WScript で開かれた自分は終了させてしまうというものだ。
If Instr(LCase(WScript.FullName), "wscript") > 0 Then
WScript.CreateObject("WScript.Shell").Run("CScript //NoLogo """ & WScript.ScriptFullName & """")
WScript.Quit
End If
そうではなく、Windows バッチファイル (.bat
) として保存したときに、Windows コマンドと VBScript をそれぞれ実行させる方法として、こんなやり方がある。
' 2> Nul & @Cls & @Cscript //NoLogo //E:VBScript "%~f0" %* & @Goto :EOF
WScript.Echo "VBScript で標準出力。2秒後に終了します。"
WScript.Sleep(2000)
WScript.Quit
先頭の ' 2> Nul
は、Windows バッチとしてはエラーを表示させないようにさせつつ、これが VBScript として実行させるときはコメントアウト '
として扱うためのもの。
この文字列がどうしてもプロンプトに出てしまうので、直後に @Cls
でコンソールをクリアしている。
あとは「&
」で Windows コマンドを繋いでいく。CScript
で実行させるファイルは .bat
ファイルなので、//E:VBScript
の指定がないと正しく VBScript エンジンで起動させられない。
複数行に渡って Windows コマンドを書きたい場合は、以下のようにするとそれらしく見えるかもしれない。
' 2> Nul & @Echo Off & Cls
' 2> Nul & Pause
' 2> Nul & Cscript //NoLogo //E:VBScript "%~f0" %*
' 2> Nul & Goto :EOF
VBScript は言語仕様上、複数行コメントを書く方法がないので、VBScript として実行させた時に影響を与えないようにするには、どうしても行頭に「シングルクォート '
」を置いて、コメントアウト行に見せかけないといけない。しかし、シングルクォートで始まって問題ない Windows コマンドはないため、エラーを無視するために ' 2> Nul
までは必須。
1行目で @Echo Off
したらすぐに Cls
することで、以降は余計なコマンドは表示させずに Windows コマンドを記述できる。If
文などのブロックは1行に収めないとおかしくなるので注意。
Oracle DB に渡す SQL ファイルを混在させ SQL*Plus
を起動する
次は、Oracle DB において、Sqlplus コマンドに SQL ファイルを指定し、DB 接続と同時に SQL を実行する処理を、Windows バッチファイル1つでやってしまおうというモノ。
Rem ^
/*
@Echo Off
Cls
Sqlplus USER/PASS@ORCL @"%~f0"
Pause
Exit /b
*/
-- ここから SQL*Plus で読み込む SQL
Set lines 32767
Set pages 50000
SELECT 1 FROM DUAL;
1~2行目の Rem ^
/*
と、*/
の行がミソ。
Windows バッチとして起動すると、1~2行目は ^
で改行をエスケープし、Rem /*
として処理される。この Rem
がコンソールに表示されるため、直後に @Echo Off
と Cls
を行っておく。この行は SQL*Plus でも Rem
コマンドと認識させるため、@Rem
と書くことはできない。
任意の処理を挟んで Sqlplus
コマンドで DB 接続し、@
で自分自身を SQL スクリプトとして実行させる。
SQL ファイルとしては、1行目は SQL*Plus
の Rem
コマンドとして無視、2行目からはブロックコメント /* */
として無視される。ブロックコメントの終了以降に SQL を記述しておけば、それが実行される。
Sqlplus
コマンドが終了すると、Exit /b
でバッチファイルを終了する。次の行の */
は読み込まれないため無視される。
別の書き方
Windows コマンド部分を1行に集約して、以下のように書くことも可能。
Rem /? > Nul & @Cls & @Sqlplus USER/PASS@ORCL @"%~0" & @Pause & @Goto :EOF
1行にする場合、Windows バッチに Rem
以降をコマンドとして解釈させるために Rem /?
でヘルプを表示させ、Nul
にリダイレクトしている。あとは &
でコマンドを繋いでいくだけ。これで2行目以降に SQL を記述すれば良い。
より実践的な使い方
Sqlplus
コマンドでファイルを実行するときには、パラメータを引数として渡せるので、Windows コマンド部分で Set /p
構文でユーザから何か文字列を入力してもらい、その値を検索するようなバッチファイルを作ったりできる。
Rem ^
/*
@Echo Off
:LOOP
Cls
Set /p NAME=検索したいユーザ名を入力してください (やめるときは q と入力) :
If "%NAME%" == "q" Goto :EOF
Sqlplus USER/PASS@ORCL @"%~f0" "%NAME%"
Rem 変数の初期化・ループ処理
Set NAME=
Pause
Goto :LOOP
Exit /b
*/
-- 変数の置換前後を表示させない
Set verify off
SELECT NAME, AGE FROM MY_USERS WHERE NAME = '&1';
こんなスクリプトを作れば、ユーザ情報を検索したりできる。開発環境でデータの存在や内容を簡易チェックする機会が頻繁にあるのであれば、このようなバッチファイルがあると、DB 接続が楽になるかも。
WSH (JScript・VBScript) を混在させる方法 (その他のファイルにも使える手法)
外部ファイルを指定して実行できるコマンドがある言語であれば、Windows バッチ内にスクリプトを記述しておき、その場でスクリプトファイルを生成して実行し、使い終わったらファイルを削除する、というやり方で1ファイルに収めることができる。
今回の例では、WSH のスクリプトファイルをその場で生成して CScript を呼んでいる。
@Echo Off
Echo VBScript ファイルの生成と実行
Set VBS=TempVBScript.vbs
Setlocal EnableDelayedExpansion
(
For /f "delims=:, tokens=1*" %%a In ('Type "%~f0" ^| Findstr "^VBS:"') Do (
Set LINE=%%b
Echo.!LINE:~1!
)
) > "%VBS%"
Endlocal
Cscript //NoLogo "%VBS%"
Del /q /f "%VBS%" > Nul 2>&1
Echo JScript ファイルの生成と実行
Set JS=TempJScript.vbs
Setlocal EnableDelayedExpansion
(
For /f "delims=:, tokens=1*" %%a In ('Type "%~f0" ^| Findstr "^JS:"') Do (
Set LINE=%%b
Echo.!LINE:~1!
)
) > "%JS%"
Endlocal
Cscript //NoLogo //E:JScript "%JS%"
Del /q /f "%JS%" > Nul 2>&1
Rem Windows バッチの終了
Pause
Exit /b
Rem ココから VBScript
Rem 各行、VBScript のコードの手前に「VBS: 」と書いておく (空行も半角スペースを付与する)。
VBS: Option Explicit
VBS:
VBS: Sub test()
VBS: WScript.Echo "VBScript による処理"
VBS: End Sub
VBS: test()
Rem ココから JScript
Rem 各行、JScript のコードの手前に「JS: 」と書いておく (空行も半角スペースを付与する)。
JS: var test = function() {
JS: WScript.Echo("JScript による処理");
JS: }
JS:
JS: test();
外部ファイルとして一時的に生成したいコードは、各行頭に決まった接頭文字列を書いておく。例で言えば「VBS:
」や「JS:
」がそれに当たる。Setlocal
と Endlocal
の間のファイル生成処理がミソ。
Find
やFindstr
コマンドは最終行を解釈しないバグがあり、このバッチファイルの最終行が空行でないと処理が完了しなくなってしまう。そこで、Type
を使ってバッチファイル自身をパイプで渡してFindstr
するようにしている。こうして、バッチファイル自身の中から、行頭が「VBS:
」や「JS:
」で始まる行を返し、For
文に使われている。delims=:
によって、「VBS: [コード]
」や「JS: [コード]
」のコロン部分で区切れる。tokens=1*
とアスタリスクを使うと、指定していた最後のトークン =1
=%%a
が解析された後で、行に含まれる残りのテキストがその次のトークン =%%b
に全て設定される。%%b
の中にdelims
と同じ文字列が含まれていても、それは分割されない。
これにより、%%a
が行頭の「VBS
」や「JS
」という文字列を取得し、delims
によって「:
」が除去される。%%b
には残りのテキスト、つまりコードが設定される。- このままだと
%%b
の先頭には「VBS:
」や「JS:
」の末尾の半角スペースが含まれてしまっている。
そこで、遅延展開を使って%%b
をいったん遅延環境変数!LINE!
に入れ、!LINE:~1!
とすることで1文字目 = 半角スペースを除去している。 Echo.
のドットは、!LINE:~1!
が空になった時に空行として出力させるためのもの。Echo
コマンドの直後は「.,:;(
」あたりの文字を繋げて置いても無視して解釈される。- 遅延展開を使わないようにするのであれば、「
VBS:[コード]
」のように、接頭文字列に半角スペースを入れないルールにしておけば、%%b
の文字列をちぎる処理が不要になる。
その場でファイルを生成して、直後に CScript
コマンドにそのファイルを渡したりしているので、ファイルが正しく生成できているかの存在チェックとかした方が親切かも。Del
コマンドは色々指定して強制的に一時ファイルを削除し、削除に失敗しても無視するようにしている。
以上
Windows バッチコマンドの言語仕様も相まって、可読性の低い地雷みたいなコードになりがちで、やってることもかなり乱暴なんだけど、開発者が自分の環境だけでうまく動けばいいというスクリプトはよくある。予期せぬエラーを引き起こしてしまっても、Windows バッチや WSH は強制終了するだけで影響は少ないし、これでいいのだ感満載。
可読性を向上させるのであれば、積極的に罫線コメントを付与するなどすれば良いのではないだろうか (個人的には =========
みたいな区切り線コメントを入れるのは好きじゃないけど)。
保守性を向上させるのであれば、スクリプトの意図や「変更されると困る箇所」をコメントに残しておけば良い。
信頼性・堅牢性を高めるために、引数チェックや例外処理などを盛り込んでおけたらなお良い。
それらはそのスクリプトを使う人たちのスキルに合わせて作れば良い。「実行するスクリプトは全部読んでおいてから使うのが当たり前」という真っ当なエンジニアもいれば、「ぼく英語とかよくわかんないんでコードも読めないっす (ヘラヘラ」みたいな偽エンジニアもいるので、誰を対象にして、どこまで親切にするかによって、決めれば良いと思う。