バッチファイルの書き方(その2)


(2012年4月24日追記)
バッチファイルを含む、Windows系のスクリプトについて学べるサイトをまとめました。合わせてご覧ください。


(追加)

    • 2009/03/23 - (その3)を書きました→こちら


アクセスログを見てみると、バッチファイルの書き方について探している方が多いようです。
Windows上で使うスクリプトと言えば、WSHやPower Shellが普通なのかと思っていましたが案外バッチファイルとかDOSコマンドというのはまだまだ需要があるようです。
たしかにWindowsのバージョンが6になったWindows VistaWindows Server 2008では使えるコマンドが非常に増えていて、よりさまざまなことがバッチファイルで実現可能となっています。Unix系のbashcshと比べたらまだまだですが、とは言え、少しずつでも確実に改善されているのは素晴らしいことだと思いますし、バッチファイラーとしてはとてもうれしいことです。


そんなわけでバッチファイルの書き方について書こうと思うのですが、普通の書き方については前に書いたことがあるので今回はわたしが普段書いていて気にしている部分を重点的に書いていこうと思います。理解していただけるようにサンプルも載せますが、すべてのOSで動作確認しているわけではありませんので動かないなどあればぜひフィードバックをお願いします。
修正出来るものについては直して再掲しますが、対象はNT系OSで2000以降(Service Packは指定なし)のみとさせてください。


とりあえず書ける分だけを書いて、残りは思いついた時に加筆/修正しますので、この記事をお気に入りに入れてたまにチェックしていただけると嬉しいです。


[確認出来るOS]


あと過去の記事へのリンクもまとめておきます。


[過去の記事への参考リンク]

これ以外の記事はカテゴリーの「バッチファイル」か、ここをクリックしてください


追加で最近ホッテントリに上がってたよい記事がありますのでそちらにもリンクしときます。


Windows の基本的なコマンド集

インデックス

1. 処理の経過/結果をログファイルに残す
2. 日付や時間を利用してログファイルを管理する
 (1) ログファイルの名前を日付ごとに変える
 (2) ログファイルの名前を日付+時刻で変える
 (3) 日付ごとにフォルダを作成してそこに時刻ごとに分割されたログファイルを配置する
3. 標準のコマンドにはない機能を使う
4. 同じ処理はラベルを利用して処理を再利用する
5. よく使うノウハウ紹介


それぞれの内容は以下の「続きを読む」以降をご覧ください。


[注意]
以下では変数のDATEやTIMEを使っていますが、この変数にセットされている日付/時刻のフォーマットはOSごとに変わります。特にWindows 2000とXPではDATEが異なりますのでWindows 2000だとこの方法では同じ結果となりませんのでご了承ください。

1. 処理の経過/結果をログファイルに残す

バッチファイルで何か処理をした場合にログを残したくなる時があります。
エラーが出たのに何があったのか分からないでは困りますし、だからと言ってひとつひとつの処理を追うのも大変です。
そんな時は標準出力/標準エラー出力に出たものをログファイルに出力しておけば、終わったあとに確認するだけで原因が判明することが多々あります*1


では具体的にログに残すというのはどうすればよいのでしょうか?
やり方はとても簡単でコマンド結果の出力先を標準出力(標準エラー出力)からログファイルへとリダイレクトしてあげればよいのです。
ちなみに標準出力とはコマンドプロンプトに表示される出力先だと思っていただければいいです。エラー出力はエラーの時に出力する先であり、標準出力とエラー出力は異なります。
# 細かい話をするとファイルディスクリプタ(ファイルハンドル)が違います


とりあえず画面に出る出力のうち、普通のもの→標準出力、エラー→標準エラー出力と言う程度の理解で十分です。
ただし、標準出力と標準エラー出力ではリダイレクトの仕方は異なります。どのようにすればリダイレクトが出来るのかを以下で説明します。

(1) 標準出力をリダイレクトする

リダイレクトには>や>>を使います。
>を使うとリダイレクト先のファイルが無い場合は新規に作成されます。ファイルがある場合は上書きです。
>>を使うとリダイレクト先のファイルが無い場合は新規に作成、ある場合は追記となります。
簡単なサンプルを使って説明します。

@ECHO OFF

SET LOGFILE="%~dp0output.log"

:// 標準出力に出力
ECHO 画面に出力します

:// 出力をoutput.logへリダイレクト
ECHO [標準出力]ファイルへと出力します > %LOGFILE%

:// output.logへをリダイレクト
ECHO 【追記】[標準出力]ファイルへと出力します >> %LOGFILE%

ECHO.
ECHO ○ ファイルの中を表示します
TYPE %LOGFILE%

PAUSE > NUL

ではこれを早速実行してみると、DOS画面上に以下の情報が表示されます。



単純にECHOした情報は画面に表示され、リダイレクトされた情報はoutput.logへ書き込まれていることが分かります。
このようにしてコマンドの実行結果をログファイルへ記録できます。

(2) 標準エラー出力をリダイレクトする

では次にエラー出力のリダイレクト方法を説明します。
エラー出力のリダイレクトは2>と2>>を使います。標準出力と似ていますが、どちらも>の前に2をつけます。

@ECHO OFF

SET LOGFILE="%~dp0output.log"

:// ECHOの結果のエラー出力をログファイルへ出力
ECHO [標準エラー出力]ファイルへと出力します 2> %LOGFILE%

:// 出力をoutput.logへリダイレクト
net stop "Windows Time" 2> %LOGFILE%

ECHO.
ECHO ○ ファイルの中を表示します
TYPE %LOGFILE%

PAUSE > NUL

細かいことは抜きにしてまずは実行してみます。



ECHOの結果はログファイルにはリダイレクトされていませんでした。これはECHOは標準出力に対する出力を行うためのコマンドであり、その結果の標準エラー出力をログにリダイレクトしたとしてもそれはリダイレクトされる対象とはならないのです。
また、時刻同期サービスを停止出来なかった際のエラーについては正しくログファイルにリダイレクトされていることが分かります。これはサービス停止コマンド実行の際のエラーが標準エラー出力に出力されているためです。

(3) 標準出力も標準エラー出力もリダイレクトする

では最後に標準出力と標準エラー出力を同時にリダイレクトする方法を説明します。
標準エラー出力を一旦標準出力にリダイレクトしてその結果をファイルへとリダイレクトすることで実現します。

@ECHO OFF

SET LOGFILE="%~dp0output.log"

:// ECHOの結果のエラー出力をログファイルへ出力
(ECHO [標準出力]ファイルへと出力します 2>&1) > %LOGFILE%

:// 出力をoutput.logへリダイレクト
(net stop "Windows Time" 2>&1) >> %LOGFILE%

ECHO.
ECHO ○ ファイルの中を表示します
TYPE %LOGFILE%

PAUSE > NUL


これもとりあえず実行してみます。



標準出力もエラー出力もログファイルに記録されていることから、どちらの出力もログへリダイレクトされていることが分かります。



ここまで紹介したリダイレクトする方法を使ってログファイルを作成してください。


2. 日付や時間を利用してログファイルを管理する

上記のようにしてログファイルを作成する場合、履歴も含めて残しておきたいという要望が出ることがあります。
その場合には日付/時間ごとにログファイルを作成して管理することで要望が実現可能です。


日付と時間を使ってログファイルを管理する方法について大きく3つのパターンを紹介します。

(1) ログファイルの名前を日付ごとに変える

例えばタスクに登録された処理が一日一回動作するとしてその時のログを残しておきたいという要望が出るのは至極当然のことのように感じます。履歴管理云々は置いておいても、わたしだったら絶対にそうするねと思うし、きっとみんなもそうしたいはずです(断言)。
というわけでログファイルを実行した日付ごとに変えるサンプルを作成してみました。

@ECHO OFF

:// 変数[ログファイル名]
SET LOGFILE="%~dp0Setup_%date:/=-%.log"

:// ログファイルがない場合のみ新規作成する
IF NOT EXIST %LOGFILE% (
  ECHO ○ xxxxx処理ログファイル[%DATE% - %TIME%作成] > %LOGFILE%
  ECHO. >> %LOGFILE%
)

:// 以降処理を書いていく

ECHO ユーザ一覧から自分以外のユーザ名を取得 >> %LOGFILE%
NET USER | FIND /I /V "itotto " >> %LOGFILE%

ECHO 処理完了[何かキーを押してください]
PAUSE > NUL

この場合、例えば今日(2008年12月29日)この処理を実行すると、ログファイル名は「[実行フォルダ]\Setup_2008-12-29.log」となります。

(2) ログファイルの名前を日付+時刻で変える

上記(1)の方法で日付ごとにログファイルを作成した場合、予想以上に大量のログを吐き出すようなケースだと日付単位でログファイルを作った場合でもファイルサイズが非常に大きくなり過ぎる場合があります。そういった場合の対処としてファイル名を日付と時刻の組み合わせで作成することをお勧めします。

@ECHO OFF

:// (1)を参考にしました
:// HH:MI:SS.SSS → HHMISSに変換
SET TM=%time:~0,8%
SET TM=%TM::=%
SET TM=%TM: =0%

:// YYYY/MM/DD → YYYY-MM-DDに変換
SET DY=%DATE:/=-%

:// 変数[ログファイル名]
SET LOGFILE="%~dp0Setup_%DY%_%TM%.log"

:// ログファイルがない場合のみ新規作成する
IF NOT EXIST %LOGFILE% (
  ECHO ○ xxxxx処理ログファイル[%DATE% - %TIME%作成] > %LOGFILE%
  ECHO. >> %LOGFILE%
)

:// 以降処理を書いていく
ECHO カレントディレクトリ以下で拡張子がbatのファイルの一覧を作成 >> %LOGFILE%
dir /s "*.bat" | FIND /V "バイト" >> %LOGFILE%

ECHO 処理完了[何かキーを押してください]
PAUSE > NUL

これを今すぐ(2008年12月29日 1:00)実行した場合には、ログファイルは「[実行フォルダ]\Setup_2008-12-29_010000.log」になります。

(3) 日付ごとにフォルダを作成してそこに時刻ごとに分割されたログファイルを配置する

(2)までやればひとつひとつのファイルサイズが大きくなってしまうことは避けられますが、一方でファイル名が長くなりすぎるというデメリットも生じます。これは、日付ごとにフォルダを作成してその中に時刻ごとにファイルを作成することで解消出来ます。
とりあえずこれもやってみます。

@ECHO OFF

:// (1)を参考にしました
:// HH:MI:SS.SSS → HHMISSに変換
SET TM=%time:~0,8%
SET TM=%TM::=%
SET TM=%TM: =0%

:// YYYY/MM/DD → YYYYMMDDに変換
SET DY=%DATE:/=%

:// 変数[ログファイル名]
SET LOGFILE="%~dp0%DY%\Setup_%TM%.log"

:// フォルダがない場合は新規作成する
IF NOT EXIST %DY% MD %DY%

:// ログファイルがない場合のみ新規作成する
IF NOT EXIST %LOGFILE% (
  ECHO ○ xxxxx処理ログファイル[%DATE% - %TIME%作成] > %LOGFILE%
  ECHO. >> %LOGFILE%
)

:// 以降処理を書いていく
ECHO カレントディレクトリ以下で拡張子がbatのファイルの一覧を作成 >> %LOGFILE%
dir /s "*.bat" | FIND /V "バイト" >> %LOGFILE%

ECHO 処理完了[何かキーを押してください]
PAUSE > NUL


これを今すぐ(2008年12月29日 1:00)実行した場合には、ログファイルは「[実行フォルダ]\20081229\Setup_010000.log」になります。


ログの管理については以上です。

3. 標準のコマンドにはない機能を使う

コマンドプロンプトbashcshに比べてその機能が貧相であると言われることがとても多いです。私も3日に1度は言われてる気がするくらいよく聞く言葉なのですが果たしてこれは本当でしょうか?
答えはもちろんYesです。もうものすごく貧弱。コマンドプロンプトとシェルでは組込みコマンドの数そのものが違いますし、制御文の使い勝手もシェルの方が大きく上回っています。それについてはコマンドプロンプト愛好家の栃木県代表であるわたしも認めざるをえないところです。体感ですが、両者にはわんぱく相撲の全国大会予選優勝者(こっちがDOS)と朝青竜(こっちはシェル)くらいの差はあると感じています。


ここではそのすべてを埋めることは当然できませんが、せめてわんぱく相撲から新弟子検査レベルまで引き上げたいなとは思うわけでそのためのノウハウをいくつか紹介します。

(1) 処理を一時的に中断する(sleep)

bashcshには処理を一時的に中断するためのsleepというコマンドがあります。例えば、1秒おきにpingを飛ばして繰り返し疎通をチェックする時など、一時的に処理を止めたくなります。
簡単に出来そうなことなのでこれくらいのコマンドはDOSにだってあるだろうと思ってしまいますが、ところがどっこいDOSにはこれに該当するコマンドはないのです。
WSH等を使うか、簡単なプログラムを作ることでも対処可能ですが、ここではOS標準のコマンドで対処する方法を紹介します。


というわけで以下の内容のテキストファイルを作成してsleep.batという名前に変更してください。

@ECHO OFF
IF "%1" == "" (
:FTIMEINPUT
  SET /PTIMEINPUT=スリープしたい時間を入力してください:
  IF "%TIMEINPUT%" == "" (
    CLS
    ECHO 時間が入力されていません
    GOTO FTIMEINPUT
  )
) ELSE (
  SET TIMEINPUT=%1
)
PING -n %TIMEINPUT% -l 1024 127.0.0.1 > NUL 2> NUL

このバッチファイルに引数として渡した秒数だけ待機します。

4. 同じ処理はラベルを利用して処理を再利用する

バッチファイルは比較的短い処理を書くことが多いのであまり気になりませんが、他のプログラミング言語のように特定の処理を抜き出して外部から呼び出すようにすることが可能です。

@ECHO OFF

SET LOGFILE="%~dp0pingconf.log"

IF EXIST %LOGFILE% (
  DEL %LOGFILE%
)

:// DellNoteへ3回pingを実施
CALL :NPING DellNote 3

:// COMPAQへ4回pingを実施
CALL :NPING COMPAQ 4

PAUSE > NUL

:// バッチを終了
EXIT /b

:// ここから再利用するための部分
:// 引数1 - ホスト名
:// 引数2 - ping回数
:NPING
(PING -n %2 %1 -l1024 2>&1) >> %LOGFILE%
EXIT /b %ERRORLEVEL%

pingは引数を多くとるコマンドですが、引数を個別に指定してしまうと面倒ですし、後日その引数をすべて変更したい場合にはすべての該当箇所を洗い出して修正する必要があります。一括置換とか使えば出来ますが、場合によっては複数回の置換を実施しないといけない可能性も考えられます。
そのため、上記のようにラベル(:で始まる名前)を利用することでその箇所の処理を関数のように呼び出すことが可能です。
個別に呼び出す場合にはCALLという組み込みコマンドを利用します。また終了ステータスはEXITコマンド(必ず/bオプションを付けてください)で返却することが可能です。
そしてこれが一番大きいのですが、呼び出されたときには引数を渡すことが可能です。引数は%1,%2,...とバッチファイル起動時に引き渡される形式と同じ形で参照可能です。ただし、バッチファイル起動時の引数はラベル部分を個別に呼び出した時の引数によって上書きされますので、ご注意ください。

5. よく使うノウハウ紹介

わたしのよく使うノウハウや、これはやっちゃダメだぜ!!という事例を紹介します。

(1) エラーが発生したことを強くアピールする方法(聴覚編)

エラーが発生した場合、それを使用者に対して通知した方が望ましいケースがあります。一番ありがちなのはエラーメッセージを表示して告知するという方法ですが、そもそもエラーに気づかなかったりする場合もありましてそうするともっとつよくエラーをアピールするべきです。


ではどうすれば気づいてもらえるのか?
いろいろと考えてみましたが、音での警告というのはなかなか注意をひくものであり、これであれば間違いなく多くの人に気づいていただけるはずです。
というわけでエラーが出た場合に気付いてもらうために音を出してみましょう。と言っても音声ファイルを起動するのでは時間もかかるので、ここではビープ音を鳴らして警告する方法を紹介します。

@ECHO OFF

:// SQLSERVERサービスを停止する
NET STOP MSSQLSERVER

if %ERRORLEVEL%.==2. (
:// ↓^Gではなく、Ctrl + G
  ECHO ^G
  ECHO サービス停止に失敗しました
  PAUSE > NUL
  EXIT 5
)

ECHO サービス停止完了
PAUSE > NUL

やり方は上記のとおり簡単で、ECHOの引数にCtrl+Gを渡してあげるだけです。これでエラーが出るごとにビープ音が鳴るという便利極まりないバッチファイルが出来上がります。
非常に便利なのですが、あまりやり過ぎるとものすごくうるさくて「こんなバッチファイルを作ったのは誰だ!!」と怒られてしまうかも知れません。その時は素直に謝ってください。ちゃんと謝れば許してくれるはずです。
くれぐれも「このブログに書いてあったんです><」とか言って責任転嫁しないでください。怒られ侍はごめんです。


(2) エラーが発生したことを強くアピールする方法(視覚編)

(1)の続きです。
音声の通知はとてもわかりやすい反面、うるさ過ぎるというデメリットがあることがわかりました。もちろんエラーをちゃんと教えてあげることは大事なのですが、スマートに、そしてわかりやすくというのがより望ましいアピールの形なのではないかと思うわけです。
というわけでもっといい方法がないか考えてみたのですが、一目見ただけで「よく分からないけどこれは絶対にエラー出てるね」と言わせる方法を思いつきました。それは画面の色を変えるのです。
例えば、

    • 処理が成功 → 
    • 処理が失敗 → 


ってすれば、途中経過を見ずとも結果が一目瞭然となりそうです。と言うわけで再びテスト用のバッチを作成してみました。

@ECHO OFF

:// SQLSERVERサービスを停止する
NET STOP MSSQLSERVER

if %ERRORLEVEL%.==2. (
:// COLOR AB - Aは文字の色でBは背景色
  COLOR CF
  ECHO サービス停止に失敗しました
  PAUSE > NUL
  EXIT 5
)

COLOR 9F
ECHO サービス停止完了
PAUSE > NUL

大分前からのOSからある組み込みコマンドのCOLORを使います。使い方はヘルプ参照。
実際に上記バッチファイルで試してみるとこんな感じになりました。


× 処理が失敗した場合


○ 処理が成功した場合


もうわかりやすい!!
これで分からなければもう無理というくらいわかりやすいです。これでもエラーを無視しちゃう人には↑のビープ音も合わせ技で適用してください。


(3) バッチファイルの名前に外部コマンドと同じ名前を付けない

バッチファイルの名前を外部コマンド(組み込みコマンドはOK)と同じ名前にするとひどい目にあう場合がありますので要注意です。
具体的に事例を挙げて説明します。


特定のサイトに対してネットワークの疎通を確認するためのバッチファイルを作成します。内容は以下のとおり、icmpで疎通確認するだけの簡単なものです。

@ping itotto.jp  -n 10

これをping.batという名前で保存します。当然この時点ではまだ何も問題は発生しません。
とりあえず実行してみます。



まったく応答が返ってきません。ネットワークがつながらないのであればエラーが出るはずですし、つながるのであれば当然正常であるというメッセージが表示されるはずです。おかしい...。


原因は内部で呼び出しているコマンドとバッチファイルの名前が同じであることと、環境変数PATHEXTに.batや.cmdが登録されていること、そしてコマンドの検索パスとして最優先されるのが実行フォルダになるというDOSの仕様によるものです。
つまり、このバッチファイルで呼び出そうとしているpingというコマンドは外部コマンドなのですが、それよりもping.bat(起動されたバッチファイル自身)が先に見つかったのでそちらを呼んでいるのです。つまり自分自身から自分自身が呼ばれるのでどんどんスタックの奥へともぐりこんでしまっているのです。
分かってしまえばどうということのないことなのですが、気付くまでは結構はまります。


どうしても同じ名前がイイ!!と言う人は、呼び出す外部コマンドについては拡張子を省略せずに記載してください。

@ping.exe itotto.jp  -n 10


こうすると正しく動作します。


完璧。



参考サイト:

(1) 時刻データを整形する方法
(2) 標準出力と標準エラー出力を同時に出力する方法
(3) sleepをバッチファイルで実現する
(4) バッチファイルでBeep音を出す方法

*1:もちろんログを出したとしても全部の解明は絶対に無理です