バッチファイルの for /f で実行したコマンドのエラーを検出する

課題

バッチファイルで、コマンドの実行結果を変数に保存(キャプチャ) したいときに for /f コマンドがしばしば用いられます。


次のような使い方ですね。

for /f "usebackq delims=" %%i in (`dir /b`) do echo Value: %%~i


ただ、コマンド(ここでは dir コマンド) が正常に終了する場合は、このような記載で問題ないのですが、多くのコマンドはたまに失敗することがあります。


例えば、下記のようなバッチファイルを実行してみます。
HOGE は存在しないファイルとします。dir コマンドは存在しないファイルを表示する場合に失敗します。

@echo off
for /f "usebackq delims=" %%i in (`dir /b HOGE`) do echo Value: %%~i

実行結果です。

ファイルが見つかりません


標準エラー出力にエラーが出力されるため、人が見ればエラーがあったことが分かりますが、バッチスクリプト的(for コマンドの do 以降で実施される処理的) にはエラーがあったかどうか分かりません。

解決

このような場合、次のように記述すれば for /f で実行したコマンドのエラーを検出することができます。

@echo off
for /f "usebackq delims=" %%i in (`dir /b HOGE ^|^| echo *ERROR*`) do if "%%i" == "*ERROR*" (
	echo dir コマンドに失敗しました。
) else (
	echo dir コマンドに成功しました。
	echo Value: %%~i
)

実行結果です。

ファイルが見つかりません
dir コマンドに失敗しました。

コマンドのエラーを検出できていることが分かります。

ポイント

下記のようなバッチの記述方法を利用しています。

コマンド1 || コマンド2

"コマンド2" は "コマンド1" が失敗した場合にのみ実行されます。


上述の例で言えば dir コマンドが失敗した場合にのみ、echo *ERROR* が実行され、文字列 *ERROR* が %%i 変数にキャプチャされます。
このため、%%i 変数の内容が *ERROR* か否かによってエラーの有無を判別することができます。


本例ではエラーの有無を判別する文字列を *ERROR* としていますが、全くの任意です。ただ、"コマンド1" の出力として現れないような文字列にしておくことがベターかと思われます。

なお、| (縦線) を for /f コマンドで記述する際は、上述の例のとおり ^ (ハット) でエスケープして ^| と記述する必要があります。

注意点

下記の記述方法で失敗と判断("コマンド2" が実行) される条件ですが、"コマンド1" が 0 以外を返却した場合のもようです。

コマンド1 || コマンド2


次の実験で確認できます。

C:\>cmd /c "exit /b 1" || echo ERROR
ERROR

C:\>cmd /c "exit /b 0" || echo ERROR

C:\>cmd /c "exit /b -1" || echo ERROR
ERROR


このため、正常に終了する場合でも 0 以外を返却するコマンドに対しては、今回の手法は利用できません。

例えば robocopy コマンドは、正常に終了する場合でも 0 以外を返却する場合があります。

バッチファイルで MD5 チェックサムを生成する

今回はバッチファイルで MD5チェックサム(メッセージダイジェスト) を生成してみます。

MD5 チェックサムを生成する方法として、通常 Linux では md5sum コマンドを利用します。下記は md5sum の実行例ですが、各ファイルとそのチェックサムが出力されます。

584da0a485f209242059e6de66aac904 test1.txt
ff0eb2864feb22354747f8c85d42ccb5 test2.txt
00539e9205b2ba8cf6dabd9f123399e2 test3.txt

一方、Windows では md5sum のような標準的なコマンドがないようです。ただ、調べてみると Windows の標準コマンド certutil を利用してチェックサムの算出を行えるもようです。


下記は certutil の実行例です。

C:\tmp> certutil -hashfile test1.txt MD5
MD5 ハッシュ (ファイル test1.txt):
58 4d a0 a4 85 f2 09 24 20 59 e6 de 66 aa c9 04
CertUtil: -hashfile コマンドは正常に完了しました。

md5sum と出力形式は異なりますが、チェックサムが算出されていることが分かります。
今回は、この certutil コマンドを利用して、md5sum コマンドの出力形式でMD5チェックサムを生成するようなスクリプトを作成してみます。


早速ですが、下記のようなコードになりました。

rem FileName: md5sum.bat

@echo off

if "%~1" == "" (
  echo Usage: %~n0 FILE...
  exit /b 0
)

setlocal
set CmdName=CertUtil
set HashMethod=MD5

for %%a in (%*) do for %%f in (%%a) do call :PrintFileHash "%%~f"
exit /b 0

:PrintFileHash
set Hash=
for /f "usebackq delims=" %%h in (`%CmdName% -hashfile "%~1" %HashMethod% ^| find /v "%HashMethod%" ^| find /v "%CmdName%:"`) do set Hash=%%h
if "%Hash%" == "" if "%~z1" == "0" (
	set Hash=d41d8cd98f00b204e9800998ecf8427e
) else (
	exit /b 0
)
set Hash=%Hash: =%
echo %Hash% %~1
exit /b 0


実行してみます。

C:\tmp> md5sum.bat *
584da0a485f209242059e6de66aac904 test1.txt
ff0eb2864feb22354747f8c85d42ccb5 test2.txt
00539e9205b2ba8cf6dabd9f123399e2 test3.txt

md5sum の出力形式にならって出力されていることが分かります。


※なお、スクリプト内に1つ MD5 チェックサム(d41d8cd98f00b204e9800998ecf8427e) が記されていますが、これはファイルサイズが 0 のファイルの MD5 チェックサムです。ファイルサイズが 0 のファイルを certutil に入力すると、エラーとなるため、この場合だけ特別扱いしています。

JScript でテキストファイルの文字列を置換する

今回は、JScript を用いてテキストファイルの文字列を置換するスクリプトを作りました。

スクリプト

// FileName: replaceText.js

var args = WScript.Arguments;
if (args.Length < 3) {
	WScript.Echo('Usage: cscript replaceText.js 入力ファイル名 置換前テキスト 置換後テキスト');
	WScript.Quit(1);
}

var srcfile = args(0);
var srckwd = args(1);
var dstkwd = args(2);
var retcode = 0;
var strm = null;

var fso = new ActiveXObject('Scripting.FileSystemObject');

try {
	strm = fso.OpenTextFile(srcfile);
} catch (e) {
	WScript.StdErr.WriteLine('Error: ' + e.description);
	WScript.Quit(e.number & 0xFFFF);
}

try {
	while (!strm.AtEndOfStream) {
		WScript.Echo(strm.ReadLine().replace(srckwd, dstkwd));
	}
} catch (e) {
	WScript.StdErr.WriteLine('Error: ' + e.description);
	retcode = e.number & 0xFFFF;
} finally {
	strm.Close();
}

strm = null;
fso = null;

WScript.Quit(retcode);

使い方

コマンドプロンプトで下記のように指定してください。

cscript replaceText.js 入力ファイル名 置換前テキスト 置換後テキスト

WSH のロゴを表示したくない場合は、下記のように指定してください。

cscript //NoLogo replaceText.js 入力ファイル名 置換前テキスト 置換後テキスト

実行例

下記のテキストファイルを入力とします。

C:\Windows\System32\drivers\1394bus.sys
C:\Windows\System32\drivers\1394ohci.sys
C:\Windows\System32\drivers\acpi.sys
C:\Windows\System32\drivers\acpipmi.sys
C:\Windows\System32\drivers\adp94xx.sys
C:\Windows\System32\drivers\adpahci.sys
C:\Windows\System32\drivers\adpu320.sys
C:\Windows\System32\drivers\afd.sys
C:\Windows\System32\drivers\agilevpn.sys
C:\Windows\System32\drivers\AGP440.sys

「C:\Windows\System32」を「D:」に置き換えてみます。
実行すると置換済みのテキストが出力されます。

C:\>cscript //NoLogo replaceText.js input.txt C:\Windows\System32 D:
D:\drivers\1394bus.sys
D:\drivers\1394ohci.sys
D:\drivers\acpi.sys
D:\drivers\acpipmi.sys
D:\drivers\adp94xx.sys
D:\drivers\adpahci.sys
D:\drivers\adpu320.sys
D:\drivers\afd.sys
D:\drivers\agilevpn.sys
D:\drivers\AGP440.sys

補足

スクリプトで扱えるテキストの文字コードは「Shift-JIS」です。
Unicode を扱いたい場合は、OpenTextFile のパラメータを変更するなどの修正が必要です。

バッチファイルで入力文字列の形式をチェックする

今回はバッチファイルネタです。


バッチファイルを書いていると、ユーザに文字列を入力してもらいたい時があります。
更に、入力された文字列の形式をチェックしたい時があります。
よくあるケースは、入力された文字列が日付の形式になっているか?等ですかね。


下記のように、findstr コマンドを用いることでチェックを行うことができます。

rem FileName: validate.bat

:loop
set str=
set /p str="YYYYMMDD 形式で日付を入力してください(Enter:終了): "
if "%str%" == "" goto end

echo %str% | findstr /r "\<[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\>" >nul 2>&1
if errorlevel 1 (
	echo %str% : NG
) else (
	echo %str% : OK
)

echo.
goto loop

:end

findstr コマンドに /r オプションを与えることにより、正規表現による検索処理を行わせています。
findstr コマンドについては、下記を参照してください。
Findstr


実行例です。

C:\bat>validate.bat
YYYYMMDD 形式で日付を入力してください(Enter:終了): abcd
abcd : NG

YYYYMMDD 形式で日付を入力してください(Enter:終了): 123456
123456 : NG

YYYYMMDD 形式で日付を入力してください(Enter:終了): 2015/04/26
2015/04/26 : NG

YYYYMMDD 形式で日付を入力してください(Enter:終了): 2015-04-26
2015-04-26 : NG

YYYYMMDD 形式で日付を入力してください(Enter:終了): 20150426
20150426 : OK

YYYYMMDD 形式で日付を入力してください(Enter:終了):

C:\bat>

「20150426」のみ OK 判定になっており、形式をチェックできていることが分かります。

JScript を用いてインターネットのファイルをダウンロードする

またまた JScript ネタです。
今回は、JScript を用いてインターネット上のファイルをダウンロードするスクリプトを作ってみます。

いきなりコードですが、下のようになりました。

// FileName: download.js

var args = WScript.Arguments;

if (args.Length <= 1) {
	var emsg = "Usage: cscript download.js ダウンロード対象のURL ダウンロード先フォルダ";
	WScript.Echo(emsg);
	WScript.Quit(1);
}

var fso = new ActiveXObject("Scripting.FileSystemObject");
var url = args(0);
var fname = fso.GetAbsolutePathName(args(1)) + "\\" + fso.GetFileName(url);

try {
	download(url, fname);
} catch(e) {
	var en = e.number & 0xFFFF;
	WScript.Echo("Error: " + e.description + "(" + en + ") ");
	WScript.Quit(en);
}
WScript.Quit(0);

function download(url, fname) {
	var http = newHttpObject();
	http.open("GET", url, false);
	http.send();
	if (http.status != 200) {
		var err = (http.status == 0)? 1 : http.status;
		throw new Error(err, http.statusText);
	}
	
	var file = new ActiveXObject("ADODB.Stream");
	file.Type = 1;
	file.Open();
	try {
		file.Write(http.responseBody);
		file.SaveToFile(fname, 2);
	} catch(e) {
		throw e;
	} finally {
		file.Close();
	}
	
	file = null;
	http = null;
	
	return;
}

function newHttpObject() {
	var http;
	try {
		http = new ActiveXObject("Msxml2.ServerXMLHTTP.6.0");
	} catch(e) {
		try {
			http = new ActiveXObject("Msxml2.ServerXMLHTTP");
		} catch(e) {
			try {
				http = new ActiveXObject("Msxml2.XMLHTTP");
			} catch(e) {
				throw e;
			}
		}
	}
	return http;
}


使い方ですが、コマンドプロンプトで下記のように指定して実行してください。

cscript download.js ダウンロード対象のURL ダウンロード先フォルダ


例です。下記を実行すると logo-ns-131205.png が C:\Temp 配下に保存されます。

cscript download.js http://k.yimg.jp/images/top/sp2/cmn/logo-ns-131205.png C:\Temp

JScript を用いて Lhaplus で自己解凍書庫を作る

久々の投稿となってしまいました。
今回は JScript を用いて Lhaplus を制御し、自己解凍書庫を作成してみます。


Lhaplus をスクリプトで制御するためには、Lhaplus のコマンドラインスイッチを利用しますが、ヘルプを参照したところ、現状コマンドラインスイッチは公開されていないようでした。他サイト様を参照すると、公開されてないものの Lhaplus 自体はコマンドラインで操作が可能とのことです。

開発リソース/Windows/Lhaplusのコマンドライン引数 - isla-plata.org Wiki


実際に試してみたところ、たしかにコマンドラインから操作することが可能でした。ただ、動かしてみた範囲では、書庫の作成が正常に完了する場合も、エラーで終了する場合も、実行ファイルの戻り値として同じ 0 が返却されるため、スクリプト側から Lhaplus の処理の成否を判断するのが難しそうです。

今回はこの部分を何とか外部から判断できないか工夫してみました。できあがったコードが以下です。(zlib/libpngライセンス)

// FileName: createSFX.js

var lpath = "C:\\Program Files\\Lhaplus\\Lhaplus.exe";
var args = WScript.Arguments;

if (args.Length <= 0) {
	var emsg = "Error: 圧縮対象が指定されていません。\n" + 
				"Usage: cscript createSFX.js 圧縮対象の絶対パス";
	WScript.Echo(emsg);
	WScript.Quit(1);
}

try {
	createSFX(lpath, args(0));
} catch(e) {
	var en = e.number & 0xFFFF;
	WScript.Echo("Error: " + e.description + "(" + en + ") ");
	WScript.Quit(en);
}
WScript.Quit(0);

function createSFX(lpath, srcpath) {
	var fso = new ActiveXObject("Scripting.FileSystemObject");
	var sh = new ActiveXObject("WScript.Shell");
	var exec = null;
	var target = "\"" + fso.GetAbsolutePathName(srcpath) + "\"";
	var outdir = "\"" + fso.GetParentFolderName(fso.GetAbsolutePathName(srcpath)) + "\"";
	var logdir = WScript.ScriptFullName.replace(WScript.ScriptName, "");
	var logpath = logdir + "lhaplus.log";
	var cmdline = lpath + " /x /c:zip /log:" + logpath + " /o:" + "" + outdir + " " + target;
	var logfile = null;
	var err = false;
	
	// 前回のログファイルの削除
	try { fso.DeleteFile(logpath, true); } catch (e) {}
	
	try {
		// 自己解凍書庫の作成
		exec = sh.Exec(cmdline);
		
		// Lhaplus の終了待ち
		for (; exec.Status == 0; WScript.Sleep(300)) {
			try {
				// ログ監視
				logfile = fso.OpenTextFile(logpath, 1, false, -2);
				if (logfile.ReadAll().indexOf("エラーまたは警告が発生しています。") > -1) {
					exec.Terminate();
					err = true;
				}
				logfile.Close;
			} catch(e) {
			} finally {
				logfile = null;
			}
		}
		
		if (err || exec.ExitCode != 0) {
			throw new Error(1, "自己解凍書庫の作成でエラーが発生しました。");
		}
	} catch(e) {
		throw e;
	} finally {
		exec = null;
		sh = null;
		fso = null;
	}
	
	return;
}

使い方は、このスクリプトにファイルをドラッグ&ドロップするだけです。ドロップしたファイルと同じフォルダに、入力ファイル名の自己解凍書庫が作成されます。


上記コードの中で、「ログ監視」コメント部分がポイントです。Lhpalus にはログファイルを出力するオプションがあるようで、エラー時はこのログファイルの中に "エラーまたは警告が発生しています。" という文字列が出力されます。これを利用して外部からエラーと判断することができます。


また、Lhaplus の既定の設定では、圧縮処理でエラーが発生した場合に下のようなログウィンドウを表示するようになっています。
f:id:norastep:20141015000830p:plain
このログウィンドウを閉じないと Lhaplus.exe が永遠に終了しないため、エラー時に限って Lhaplus に終了要求を送ります。

ログウィンドウの表示設定は、Lhaplus の詳細設定より下記赤枠の箇所が該当します。
f:id:norastep:20150422205742p:plain

なお、本スクリプトはこの設定以外では動作しません。エラー時のみログウィンドウが表示されることを期待しているためです。

JavaScript: 世界で最も誤解されたプログラミング言語へのリンク

有名な記事
JavaScript: 世界で最も誤解されたプログラミング言語
を読みたいを思って邦訳版に飛んでみると、なんとプライベートモードに。残念!

http://d.hatena.ne.jp/brazil/20050829/1125321936


ということで WayBackMachine の方を見てみたらあった。


JavaScript: 世界で最も誤解されたプログラミング言語 - oct inaodu