Corredor

ウェブ、プログラミングの勉強メモ。

Windows バッチに JScript・VBScript・Oracle SQL スクリプトを混在させてバッチ処理の中で実行する

2016年も終わりに近付いている昨今、今更ですが Windows バッチの黒魔術的な挙動にハマっていて、レガシーな職場で培ったレガシーな知識の総決算をしておこうかなと思うなど。

Windows バッチスクリプトを置いておく GitHub リポジトリを作っていますので、よかったらご覧ください。

今日はその中から、Windows バッチファイル1つの中に、別の言語のスクリプトを混在させて実行する手法をいくつか紹介する。

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) とみなされる。0JScript では false 扱いなので、条件に一致せず、@end までの中身は評価・実行されなくなる。そのため、この中のコードが JScript として正しくない内容でも影響がない。

Windows バッチはコマンドを大文字小文字どちらで書いても良いが、JScriptif は小文字でないと予約語として認識しないため、@if@end は小文字で記述する必要がある。

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 OffCls を行っておく。この行は 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 (JScriptVBScript) を混在させる方法 (その他のファイルにも使える手法)

外部ファイルを指定して実行できるコマンドがある言語であれば、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:」がそれに当たる。
SetlocalEndlocal の間のファイル生成処理がミソ。

  • FindFindstr コマンドは最終行を解釈しないバグがあり、このバッチファイルの最終行が空行でないと処理が完了しなくなってしまう。そこで、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 は強制終了するだけで影響は少ないし、これでいいのだ感満載。

可読性を向上させるのであれば、積極的に罫線コメントを付与するなどすれば良いのではないだろうか (個人的には ========= みたいな区切り線コメントを入れるのは好きじゃないけど)。
保守性を向上させるのであれば、スクリプトの意図や「変更されると困る箇所」をコメントに残しておけば良い。
信頼性・堅牢性を高めるために、引数チェックや例外処理などを盛り込んでおけたらなお良い。

それらはそのスクリプトを使う人たちのスキルに合わせて作れば良い。「実行するスクリプトは全部読んでおいてから使うのが当たり前」という真っ当なエンジニアもいれば、「ぼく英語とかよくわかんないんでコードも読めないっす (ヘラヘラ」みたいな偽エンジニアもいるので、誰を対象にして、どこまで親切にするかによって、決めれば良いと思う。