2013年8月17日土曜日

BATファイルの書き方・パターン集

http://d.hatena.ne.jp/language_and_engineering/20130502/PatternsOfMSDOSorBAT

Windows上の処理を自動化するプログラムが,BATファイルである。
「コマンドプロンプト」上での手作業を省略し,自動実行できる。

Windowsが存続する限り,BATファイルはなくならないだろう。
バッチ・プログラミングの需要は,生き残る。このWindows 8の時代でもそうだ。

BATは,MS-DOSの時代から長く使われてきた。
そのため,各コマンドに関する個別のノウハウや情報は多い。
だが,実用的なノウハウを体系的に整理したものは,あまり見かけない。

そこで以下では,BATをコーディングする際の良質なパターンを列挙する。
  • (0)BATプログラミングの特徴
  • (1)BATファイルの雛型
    • (1-1)冒頭と末尾のテンプレート
    • (1-2)反復して実行可能に
  • (2)バッチの構造化
    • (2-1)ルーチンの分割
    • (2-2)ファイルやプロセスの分割
    • (2-3)外部ツールの呼び出し
  • (3)ファイル操作
    • (3-1)for文によるスキャン
    • (3-2)if文による判定
    • (3-3)中間ファイルの利用
  • (4)変数と演算
    • (4-1)コマンド実行結果の保管
    • (4-2)数値演算
    • (4-3)日付や時間の加工
    • (4-4)ファイル名の加工

本稿は,コマンドプロンプトの中級レベルのテクニックの整理,とも言える。
これらを習得すれば,あなたもコマプロ (※コマンドプロンプトのプロフェッショナル)*1


(0)BATプログラミングの特徴

最初に,BATの長所と短所を知っておこう。
そうすれば,BATの正確な使いどころが分かる。
BATのメリット:
  • プロセスの起動処理を簡素化できる(起動引数などをまとめて保管しておける)

BATのデメリット:
  • 構造型プログラムが書けない,極めて書きづらい
    • →本エントリで,各種の構文を理解すべし。サブルーチンやfor文の使いようがキモ。
  • 複雑な処理は困難
    • →別のプログラムを呼び出して処理させるべし。cscriptでWSF・WSHを呼び出すのもよし。mshtaでワンライナーを記述するのもよし。

BATでできない難しいことを,無理やりBATでコーディングする必要はない。
それは非生産的で,時間と労力の浪費だ。SEのすべきことではない。

BATの「得意分野」はBATでコーディングし,なおかつ,
BATの「守備範囲外」の事柄であれば,BATから別のプログラムを呼び出せばよいのだ。

BATはプログラムの起動が得意なのだから,多いに外部プログラムに頼って良いのだ。
むしろ,「外部プログラムの呼び出し方」を,順番にうまく制御する役目を担うのがBATである。と考えよう。


ここから先の実務的な情報を読むにあたり,コマンドプロンプトの基礎的なツボを網羅しておこう。
以下のエントリが参考になる。
コマンドプロンプトで,暗記するべき10の必須コマンド  (前半) ファイル処理系 - 主に言語とシステム開発に関して

コマンドプロンプトで,暗記するべき10の必須コマンド (後半)ネットワーク系 - 主に言語とシステム開発に関して

(1)BATファイルの雛型

(1-1)冒頭と末尾のテンプレート

BATの最初と最後に記述する内容は,だいたい決まっている。
冒頭と末尾は,便利なので暗記してしまおう。
@echo off

rem
rem このバッチの説明
rem

rem 設定事項
set HOGE="変数の値"

rem このバッチが存在するフォルダをカレントに
pushd %0\..
cls


~処理~


pause
exit


まず冒頭の説明。
  • コマンドの先頭に@をつければ,バッチ実行時に,そのコマンド自体は表示されなくなる。
  • echo offとすれば,実行しているコマンド自体の表示を全面的にOFFにできる。
    • →全ての行の先頭に「@」と書かなくて済む。
  • remはコメント行。冒頭には,バッチの目的などを記載する。各行では,各行の処理内容を説明する。
  • setは,環境変数の定義。フォルダのパスとかを定義する。あとから変更しやすいよう,バッチの先頭部分にまとめて記述する。
  • pushdは,ディレクトリの移動。
    • cdコマンドだと,異なるドライブに移動できない。
    • cd /dとすれば,異なるドライブに移動できるが,UNCパス上でネットワークの共有フォルダ上を移動できない。
    • pushdは,ドライブの違いや,ネットワークの違いを気にせず,必ず移動できる。
    • したがって,cdではなく,常にpushdコマンドを使うのが無難。なぜなら,このバッチがどこで実行されるのか,前もって知ることができないから。
  • %0 は,このバッチファイルのファイルパス。
    • %0\.. とすれば,このバッチファイルが存在するフォルダを指す。
    • したがって,バッチの存在ずるフォルダをカレントディレクトリにすることができる。
  • clsは,画面の消去。前回のコマンドの実行結果などを削除して,まっさらな画面でバッチの実行を開始できる。
ここまでで1セットである,と考えよう。

次に,バッチの末尾の説明。
  • pauseコマンドは,何かキーを押すまで待機する。バッチの終了を目視でよく確認した上で,次に進む事ができる。
    • 突然ウィンドウが消えてしまうと,処理が成功したのか,失敗したのか,知ることができないから。
  • exitでバッチの終了。

この雛型を利用したバッチのサンプル:
bat中でforループをネストし,サブルーチンを呼び出して,条件付きファイル検索の結果を一斉コピーしよう (ファイル名の重複防止機能付き) - 主に言語とシステム開発に関して

(1-2)反復して実行可能に

便利なバッチほど,繰り返し実行するものだ。
例えば,コンパイルバッチとか。

下記のサンプルコードは,バッチの実行終了後,押したキーによって,処理をふたたび反復するかどうかを分岐する。
@echo off

:start
cls

rem ----- メイン処理 -----

~~

rem ----- キー入力で分岐 -----

echo.
set userkey=
set /p userkey=終了する (Enter) / メイン処理を再度実行 (o + Enter) / サブ処理を実行 (p + Enter) ?
if not '%userkey%'=='' set userkey=%userkey:~0,1%
if '%userkey%'=='o' goto start
if '%userkey%'=='p' goto exec
goto quit



rem ----- サブ処理 -----

:exec

~~


rem ----- 終了 -----

:quit

exit

解説:
  • コロンで始まる行は,ラベル。goto文は,ラベルにジャンプすることができる。ラベル名は,「goto ~~」と書いた時にわかりやすく,処理の流れを理解しやすいものを。
  • set /p は,ユーザ入力値を環境変数に格納する。
  • %変数名:~0,1%で,文字列の0文字目から1文字分を切りだして抽出する。つまり,先頭の文字を判別する。
  • if文とgotoをうまく組み合わせて,特定の条件で振り出しに戻るようにする。その際に,clsしてあげると親切。

反復実行可能なバッチのサンプル:
繰り返し実行可能なコンパイルバッチ - 主に言語とシステム開発に関して

このコードの要点は,「プログラム通りに条件分岐しているのではなく,
ユーザの意思を尋ね,ユーザの意思に従って条件分岐している」という点だ。

この発想を応用すれば,ユーザフレンドリーなバッチを作れる。
例えば下記のサンプルを参照。
開発用のフォルダ構成を,自動的に生成してくれるバッチ (プロジェクト用のリポジトリ立ち上げに便利。ついでに,用が済んだら自動消滅!) - 主に言語とシステム開発に関して
  • 処理が完了すると,「このバッチを消去しますか?」と聞いてくる。つまり,自爆してくれるバッチ。

(2)バッチの構造化

上から下に向かってコマンドをダラダラと並べるだけのコードは,保守性も可読性もない。
プログラマは,そういうコードを決して書くべきではない。
※そういうコードを書くプログラマに限って,コメントを何も書かなかったりするので,事態は悪化する。

BATは文法の制約上,ソースコードを構造化し辛いが,それでも,できる範囲で構造を作っておこう。
できない事は要求すべきでないが,できるのにやらない,というのはおかしい

(2-1)ルーチンの分割

メインルーチンと,サブルーチンを分割しよう。

バッチファイルの内部で,共通的な処理をくくりだし,
ルーチンとして再利用したい場合がある。

あるいは,バッチの処理の全体の見通しを良くするために,
複雑で長い処理をルーチンに切り分けて,メインの流れを読みやすくするケースもある。

サンプルコード:
@echo off

rem メイン処理:サブルーチンのテスト
call :routine_hello hoge
call :routine_hello fuga
call :routine_hello boo


pause
rem メイン処理はここで終了
exit



rem 引数を受け取って,Hello と表示するルーチン。
:routine_hello

echo Hello, %1!

exit /b

実行結果:
Hello, hoge!
Hello, fuga!
Hello, boo!
続行するには何かキーを押してください . . .

解説:
  • ラベルをgotoではなくcallで呼び出せば,サブルーチンになる。
  • サブルーチンには,そのサブルーチン独自の引数を渡すことが可能。サブルーチン内では%1などで参照。
  • サブルーチンからメインルーチンに戻る際には,exit /b する。/b オプションを付けないと,バッチ全体が終了してしまう。


次に,ルーチンからの戻り値について。
これは数値しか返却できず,大したことはできないので,あまり期待しないように。

バッチ内のルーチンから,呼び出し元に値を返却するには,errorlevelを使う。
LinuxBashでいうところの「$?」(終了ステータス)に相当する。
するのは失敗、何もしないのは、、、 BAT errorlevel exit = バッチファイルを外部ファイル化
  • call文で外部のバッチファイルを呼べる。でも、外部バッチで 「exit 0」 とかがあったりすると、処理が終わってしまう(CMD.EXEの終了)
  • exit /b 数字 と記述のある外部バッチファイルを call で呼べば、数字を %errorlevel% に設定して、ちゃんと戻ってきてくれる

ERRORLEVELについてのメモ (1) - とあるソフトウェア開発者のブログ
  • ERRORLEVELは、行したコマンドが終了コードを返却した場合や,callしたバッチスクリプトが「exit /b 終了コード」の形式で終了した場合に設定される

終了コード errorlevel の考え方について - その他(プログラミング) - 教えて!goo
  • exit /b ○ で設定した値を消去するには cd を行えばよい

Linux上でシェルが実行される仕組みを,体系的に理解しよう (bash 中級者への道) - 主に言語とシステム開発に関して
  • (2-6)各コマンドの実行結果は,終了ステータスに格納される
  • 直前のコマンドの終了ステータスは $? という変数に格納されている

(2-2)ファイルやプロセスの分割

.batファイル自体を分割する。
複数のバッチファイル間で,処理を共有したい場合などに使う。
共通の処理は,1つのbat内にまとめておいて,複数のBATから呼び出せるようにする。

バッチの行数を長くせず,ソースコードのコピー&ペーストを減らすために必要。

2つ方法がある:
  • call バッチファイル名
  • start バッチファイル名

callでラベルを指定すればサブルーチンを呼び出せたのと同じように,
callでファイル名を指定すれば,別バッチを呼び出せる。

また,startで呼び出した場合は,プロセスも分かれる。

別バッチの呼び出し方について:
Windows、バッチファイルからバッチファイルを呼び出す方法あれこれ|マコトのおもちゃ箱 ~ぼへぼへ自営業者の技術メモ~
  • callは,呼び出し先の終了を待った後で,呼び出し元に戻る
  • startは別プロセスで起動し,並行して実行する
  • バッチファイル名を直接指定すると,呼び出したバッチが終了した時点で全ての処理が終わる

バッチファイ(batch,.batファイル)について質問します。 ある.. - 人力検索はてな
  • start b.bat はb.batを呼び出して,次に進む
  • call b.bat はb.batを呼び出して,b.batが終わってから,次に進む

4.7 別バッチファイルの呼び出し
  • 呼び出した子のバッチファイルが終了したら、 処理は元 (親) のバッチファイルに戻り、 その call 行の次の行に処理が移る
  • exit /b [終了コード] の形式で終了コードが指定してあれば、親のバッチファイル側でそれを if errorlevel の分岐に利用できる
Windowsでのプロセス起動の仕組みは,下記を参照。
コマンドラインからプロセスを起動・終了する方法 (環境変数とレジストリについて) - 主に言語とシステム開発に関して
  • PATH上に存在すれば,ファイル名だけで起動できる
  • AppPathのレジストリに登録されていれば,PATH環境変数に登録されていないフォルダでも,startで呼び出せる

(2-3)外部ツールの呼び出し

BATをコーディングしていて,BATでは対処しきれない,とわかったら
下記のようなコマンドを使って,別のプログラムに処理させよう。
  • cscript:WSHやWSFを呼び出せる。言語はJScriptVBScript。BATよりも高度な処理が可能。
  • mshta:ワンライナーでWSHのコードを記述可能。
それぞれのノウハウは,下記を参照。
バッチ職人になろう (WindowsとLinux上での開発業務を自動化するノウハウ集) - 主に言語とシステム開発に関して
  • WSH, JScriptの項目を参照

JavaScript をコマンドラインで実行する方法  (mshta.exeの使い方) - 主に言語とシステム開発に関して

コマンドプロンプトから,Win32 APIや任意のDLLを呼び出して実行しよう (コマンドプロンプトから画面キャプチャする方法の仕組みを理解) - 主に言語とシステム開発に関して
  • mshtaからVBSのコードを実行し,VBSからExcel VBAのマクロを起動し,VBAマクロからWin32 APIを呼び出している。BATだけで,たった1行で,なかなか恐ろしい事ができてしまう。
mshtaは非常に便利だ。BATだけで,いろいろできる。
  • mshta.exe "javascript:JavaScriptのコード"
  • mshta vbscript:execute("VBScriptのコード")
良いBATをコーディングするためには,WSHの知識も必要なのだ。

(3)ファイル操作

(3-1)for文によるスキャン

拡張子が.txtであるような全てのファイルを検索・列挙してみよう。

カレントディレクトリだけを対象とした,単純なスキャンであれば,下記のように書ける。
for %i in (*.txt) do ( echo %i )

for文+ワイルドカードで,現在のフォルダをファイル検索できる。
doの中で自由に処理できる。ここではファイル名を表示している。
※コンソール上では%iでOKだが,BATファイル中では%%iのように%を重ねることを忘れずに。

しかし,
「カレントフォルダ以下の全てのフォルダ内を,再帰的にファイル検索したい」
というニーズのほうが多いだろう。

これは,下記のようにコーディングできる。
for /F "usebackq" %i in (`dir /s /b *.txt`) do ( echo %i )

フォルダを除外して,全ての拡張子のファイルを再帰的に取得したい場合は,下記のように書ける。
for /F "usebackq" %i in (`dir /A-D /s /b *.*`) do (
  rem %i がファイルパス
  ~  
)

forコマンドにusebackqオプションを渡せば,inの中身にコマンド出力を利用できる。
doの中では,そのコマンドの実行結果を1行ずつ処理させる。
ここではコマンドとして,ファイルの再帰検索コマンドを記述する。
もし検索の仕方をカスタマイズしたければ,forコマンドではなく,dirコマンドのオプションを自由に変えればよい。

この記法を覚えると,BATでできる事の幅が,ぐっと広がる。
Bashで言うと,$(~) とか `~` に相当する記法だ。

この便利さを,よく覚えてほしい。

「あるコマンドの実行結果を,別のコマンドに渡す」ための方法として,普通は「|」(パイプ)を使うだろう。
例えば
ipconfig | more

のように。

だが,「あるコマンドの実行結果を,別のコマンドに渡して,1行ずつ自由に処理する」場合は,for文が非常に便利なのだ。
後述するが,コマンドの実行結果を動的に変数に格納したりする場合にも,forが一役買う。
中級のBATコーディングにおいて,for文はパイプよりも重宝する,と言っていい。

ここでは,for文によるファイルスキャンを応用してみよう。
rem 不要なファイルを再帰的に検出して削除
for /F "usebackq" %%i in (`dir /s /b /a-D ^| findstr /V ".*\.html$" ^| findstr /V ".*\.css$" `) do (
  rem 削除確認しながら削除
  del /P %%i
)

ファイルスキャンをした後で,スキャン結果から特定のファイルだけを除外し,その後,削除している。
これはつまり,フォルダツリー上にhtmlとcssだけを残して,他の余計なファイルが存在したら削除する,という整理バッチだ。

解説:
  • dirコマンドの /a-D オプションは,出力からフォルダは除外するということ。
  • findstrの /V オプションは,マッチしたものを除外するという意味。


(3-2)if文による判定

ファイル操作時には,ファイル存在判定が付きもの。
なければ作る,とか,あれば削除,とか。
rem ファイルがあれば削除
if exist %TMP_FILE_NAME% ( del %TMP_FILE_NAME% )

if文についてもっと詳しく知りたければ,「if /?」して,マニュアルを参照のこと。

ifの中では,あまり複雑な判定はできない。
一応,else if と else の構文は実現可能。
バッチファイルで、if~else - nursの日記
  • 括弧"("または")"の両側には必ずスペースが必要

(3-3)中間ファイルの利用

例えば,あるファイルの中味を加工して保存したい,という場合,どうするか。

もとのファイルを直接,いきなり加工してしまうことも,確かに可能だ。
だがその場合,もし処理の途中で,加工が失敗したらどうするのか?
もとのファイルは失われてしまう。意図しない実行結果になるかもしれない。

1ステップずつ,処理結果を慎重に保管しながら進めるためには,中間ファイルを利用することになる。
そして,作業が完了した段階で,中間ファイルからターゲットに結果を書き出す。

例として,ファイルの中味を逆ソートして保存し直す例:
rem 中間ファイルに出力
type hoge.txt | sort /r > temp.txt

rem 処理結果を保存
type temp.txt > hoge.txt

rem 中間ファイルを削除
del temp.txt

もしソート以外にも途中の処理を追加したくなったら,temp.txtに対して処理を追記すればよい。
中間ファイルを使わずに,一行で sort /r hoge.txt /O hoge.txt と書いてしまうと,そこで処理がいったん確定してしまうので,「確定前の途中の処理を,柔軟に追加」し辛い。

中間ファイルを設けて,ステップ数を増やす(=あえて分割しておく)ことによって,
途中のステップを追記しやすくなるので,プログラムを変更しやすくなるのだ。
変更だけでなく,動作確認のためのテストもしやすくなる。

普通は,ステップ数やコードの行数を無駄に増やさないように注意するものだが,
逆にあえてステップを分割したほうが,のちのち便利。という場合もあるのだ。


あるいは,必要に迫られて,仕方なく中間ファイルを作る場合もある。
その場合,それがBATの限界なので,甘受する必要がある。
不必要な一時ファイルは作らないのが望ましいが,BATの都合で避けられない場合は,サッと作ってサッと消す。

参考:
NN Space BLOG-NN空間ブログ
  • WindowsXPとか7で確認した限り、並べ替え対象ファイルと同じファイル名をソート後の書き込みファイル名にしても問題ない
  • SORT TARGET.txt /O TARGET.txt とすれば一時ファイルは必要無い。ただし,コマンドが成功すると元のファイルはどこにも無くなってしまう


(4)変数と演算

環境変数には,基本的に文字列を格納することになる。
ファイルパスも,日付や時間も,ぜんぶ文字列である。
これらの上手い扱い方が,BATによる情報処理を左右しうる。

(4-1)コマンド実行結果の保管

可能である。
cdコマンドの実行結果を変数に保管する場合:
rem カレントフォルダを変数に保持
for /F "usebackq" %%i in (`cd`) do (
  set BAT_DIR="%%i"
)

rem 変数の内容を表示
echo %BAT_DIR%

pause

このように,コマンドの実行結果を別の文に渡すために,for文は多いに役立つ。
forはもともと,複数回の処理をループして実行するための制御構文だが,このように1回きりの処理にも応用できる。

(4-2)数値演算

文字列ではない演算も,やればできる。整数限定で。
カウンターなどの用途に,かろうじて使える程度だが。
setコマンドの/aオプションを使えば,算術演算が可能だ。

サンプルコード:
@echo off

set /a CNT=1
  rem ※算術変数は遅延展開され加算が可能
  rem   http://fpcu.on.coocan.jp/dosvcmd/bbs/log/cat3/4-0873.html

call :routine_echo_count
call :routine_echo_count
call :routine_echo_count

pause
exit



:routine_echo_count

echo %CNT% 回目

rem echo 数値をインクリメントします。
set /A CNT=%CNT%+1

exit /b

実行結果:
1 回目
2 回目
3 回目
続行するには何かキーを押してください . . .


(4-3)日付や時間の加工

%date% 変数を使いやすく加工する。

日付(1ケタの数は0埋めされるので心配なく):
set today_YYYYMMDD=%date:~0,4%%date:~5,2%%date:~8,2%

時間:
set now_HHmmSS=%time:~0,2%%time:~3,2%%time:~6,2%

部分文字列の抽出で,使いやすい形式に加工できる。
これらを,バッチで生成するファイルの名称などに埋め込んでおけば便利だ。

(4-4)ファイル名の加工

%1という環境変数から拡張子だけ抜き出すには,%~x1とする。
同じような操作がいろいろ可能なので,一覧表を参照のこと。
バッチファイルの制御用コマンド [FPCU]DOS/V&Windowsコマンド・プロンプト・リファレンス
  • 環境変数の記法の一覧表

利用中の拡張子を抽出するバッチ (存在するファイルの拡張子の種類を,バッチで全取得する) - 主に言語とシステム開発に関して

(5)その他の処理

(5-1)スリープ

5秒待つ:
ping localhost -n 5 > nul

「コマンドプロンプトで,処理を指定時間秒だけ sleep させるために ping を使う」という裏技である。
waitコマンドとかsleepコマンドがないので,pingで代替している。

これを使ったサンプル:
コマンドラインからマウスを操作する方法 (rundll32.exeで動くDLLの作成法) - 主に言語とシステム開発に関して

離席中のチャットのログを自動でメール送信してくれるソフトの作り方 - 主に言語とシステム開発に関して

(5-2)ログとリダイレクト

処理の合間に,頻繁にechoでログ出力するとよい。

以下はエラー出力の扱い方について。

サンプル:
main.bat(サブバッチの出力を,ログファイルにリダイレクトしている)
call sub.bat > log.txt

pause
exit

sub.bat(存在しないコマンドを呼んでエラーを発生させている)
@echo off

cd

cdHOGE

cd

exit /b

実行結果:(コンソール上)
'cdHOGE' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
続行するには何かキーを押してください . . .

log.txt内:
D:\temp
D:\temp

ふつうのリダイレクトは,標準出力(「1」)に書き込まれる。
エラーメッセージは,エラー出力(「2」)に出てくる。
なので,これだと,エラーメッセージをログに記録しておくことができない。

これを解決するためには,リダイレクタの操作を行う。Linuxと同じである。
main.bat内で,リダイレクトの書き方をちょっと変える。
call sub.bat > log.txt

↓

call sub.bat > log.txt 2>&1


これで実行すれば,log.txtには,エラーメッセージもちゃんと漏れずに記録されている。
リダイレクト先がマージされたためである。
D:\temp
'cdHOGE' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
D:\temp


詳しくはBashと同じなので,下記のエントリを参照。
Linux上でシェルが実行される仕組みを,体系的に理解しよう (bash 中級者への道) - 主に言語とシステム開発に関して
  • (2-4)標準入力や標準出力とは,プログラムへの入出力を抽象化・一般化したものである
  • 「2>&1」は,「2の代案を1にする」という風に音読すると暗記しやすい

結びに

ここまでの情報を使いこなせば,ベターなBATをコーディングできるだろう。

0 件のコメント: