保守しやすいバッチファイルを作るテクニック

バッチファイルはWindowsでの処理を自動化するために昔から手軽でよく使われる手段です。

ただ、コマンド構文が貧弱なのとそもそもプログラミング言語ではないため、複雑な処理や大規模な処理を実行するのには向いていません。

にもかかわらず、サイロ化したWindows業務システムを何とか低コストで継ぎ接ぎするのによく使われがちなので、巨大化したり、増えすぎて複雑化したバッチファイル群で保守が難しくなってしまったシステムの現場を沢山みてきました。

そんな状況に対抗するための、バッチファイルの保守性を上げるためのテクニックを紹介します。

おまじない

どんなバッチファイルにも必ず書いておくとよい、2行のおまじないです。
@echo off
cd /d %~dp0

@echo off

デフォルトでは、実行したコマンド文そのものが常に標準出力に出力されてしまい、邪魔な情報となってしまいます。

echo offすることで以降、標準出力に実行したコマンドが出力されなくなります(コマンドの実行結果は出力されます)。
echo offだけだとこの実行したechoコマンドが表示されてしまうので、@を付けることでechoコマンドの実行も標準出力から消しています。

cd /d %~dp0

バッチファイルは実行中のディレクトリ(カレントディレクトリ)の概念がとても重要です。
不用意なディレクトリで実行すると大事故につながる恐れがあります。
ダブルクリックで実行すればファイルの置いてあるディレクトリがカレントになりますが、ほかのバッチファイルやアプリケーションから呼び出した場合は、そのプログラムのカレントディレクトリの設定を引き継ぎます。

cd /d %~dp0は自身のファイルの置いてあるディレクトリをカレントにするコマンドです。
これを必ず先頭で実行することで、どこからバッチファイルを呼ばれてもファイルを配置したディレクトリでの動作になるため、相対パスで事故ることも減るはずです。

コメント

バッチファイルのコメントはREMコマンドで書くのが一般的です。
ただREMコマンドのコメントは可読性が低いです。
@echo off
cd /d %~dp0
rem パスを設定
set src_dir=C:\src_dir\
set dist_dir=F:\dist_dir\
rem 連携フォルダにファイルコピー
copy %src_dir%shohin.csv %dist_dir%shohin.csv
他のプログラミング言語は記号がコメントの開始になるのに対して、REMはコマンドのため他のコマンドと見分けがつけづらいのです。

そこで、GOTOコマンドの指定先のラベル記号のコロン(:)を使います。
ラベルは指定しなければ意味を持たないのでこれをコメントとして利用するのです。
@echo off
cd /d %~dp0
:: パスを設定
set src_dir=C:\src_dir\
set dist_dir=F:\dist_dir\
:: 連携フォルダにファイルコピー
copy %src_dir%shohin.csv %dist_dir%shohin.csv
REMコマンドで書いたものよりは見やすくなったのではないでしょうか。
コロンを二個続けているのは通常のGOTOコマンドのラベルと区別するためです。

ただし、このコメントの書き方はIFコマンドのようなネスト可能なコマンドの中では利用できません。
ネストの中でコメントを書きたい場合はREMコマンドを使いましょう。

インデント

バッチファイルは実はインデントを使って記述することが可能です。
半角スペースやタブ文字が使えます。
特に、IFコマンドやFORコマンドはワンライナーで書くこともできますが、改行・インデントを加えた方が読みやすいです。
:: インデントなし
set /p var=
if %var% equ true (echo true) else (echo false)

:: インデントあり
set /p var=
if %var% equ true (
    rem 真
    echo true
) else (
    rem 偽
    echo false
)

環境変数

バッチファイルが増えてくると困るのが、連携ディレクトリパスやサーバー名などの環境設定の管理です。

最初は1つのバッチファイルに書かれていた設定が、コピぺで1つ増え、2つ増え、やがて設定を変更するためには膨大なバッチファイルを書き直さなければならなくなってしまうのはありがちなパターンです。

設定情報はシステム環境変数に定義しておくことで、一元管理することができます。
コントロールパネルから設定しておいてもよいし、別途環境変数をセットするバッチファイルを用意してイニシャライズ処理として切り出しておいてもよいでしょう。

ログ

バッチファイルの動作ログは必ず取得しましょう。
ログがないと障害時のトラブルシューティングが非常に困難になります。

バッチファイルのログは標準出力と標準エラー出力をファイルにリダイレクトすることで取得するのが一般的です。

command >> logfile 2>&1

処理のサブルーチン化、外部ファイル化

バッチファイルは関数を記述できませんが、バッチファイル内にサブルーチンを書いたり別のバッチファイルを呼び出して結果を環境変数で受け取ることが可能です。
@echo off
cd /d %~dp0

call :getTimeStamp
echo %TimeStamp%
pause

exit /b

::タイムスタンプの取得
:getTimeStamp
    setlocal

    set YYYY=%DATE:~0,-6%
    set MM=%DATE:~5,-3%
    set DD=%DATE:~-2%
    set /a H=%TIME:~0,-9%
    set M=%TIME:~3,-6%
    set S=%TIME:~6,-3%
    if %H% lss 10 (
        set H=0%H%
    )

    endlocal & set TimeStamp=%YYYY%%MM%%DD%-%H%%M%%S%
exit /b
タイムスタンプ用の文字列を取得する処理をサブルーチン化しています。
CALLコマンドを使うとラベルからEXITコマンドまでの間をサブルーチンとして扱うことができます。
さらに、SETLOCALコマンドとENDLOCALコマンドを使うことで環境変数のルーチン内でのローカライズを行えます。

この例だと、call :getTimeStampの後に参照できる環境変数は「TimeStamp」だけになります。

上記の例は外部ファイルに切り出すことも可能です。
@echo off
cd /d %~dp0

call getTimeStamp
echo %TimeStamp%
pause

exit /b
setlocal

set YYYY=%DATE:~0,-6%
set MM=%DATE:~5,-3%
set DD=%DATE:~-2%
set /a H=%TIME:~0,-9%
set M=%TIME:~3,-6%
set S=%TIME:~6,-3%

if %H% lss 10 (
    set H=0%H%
)

endlocal & set TimeStamp=%YYYY%%MM%%DD%-%H%%M%%S%

exit /b
一つのバッチファイル内で複数回呼ばれる処理はサブルーチンに、複数のバッチファイルから呼ばれる処理は外部ファイルに切り出してモジュール化するとよいでしょう。
いかがでしたか。
バッチファイル一つとっても、書き方ひとつで保守しやすい形にすることが可能です。

なるべくバッチファイルを使わずにシステムを自動化するのが本来のあるべき姿な気もしますが、今あるレガシーシステムを無理ない形でリファクタしたり、枯れた技術で保守しやすいものを作るのも必要なことです。
腐らず頑張りましょう。