【最新記事へ】
【CMD.EXE Wiki】
【このページについて】
それはそれ。これはこれ。
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
◇最新の記事◇
|
■ 2005年02月09日 / 【続々々々々々】ディレクトリかどうかの判断 数ヶ月ぶりの続きであるが、まだまだ終わらない。「続」が6つなので7回目ということだ。 ネットワーク上のファイル・ディレクトリに対して、if exist での判断が出来ないということで終わっていたが、何とかなりそうである。
と、 if exist "%~1\\*" echo ディレクトリだ が使えそうである。空ディレクトリやルートディレクトリでも大丈夫。 同じネットワーク上のファイルでも、UNC形式とドライブにマウントしての場合で、NULをつけたときの真・偽が変わってくるのがちょっと怪しげ。
■ 2005年02月01日 / コマンドプロンプト機能Wikiの公開 「はてなダイアリー」に記事を移し終わった。 まだまだ作成途上だが、「コマンドプロンプト機能Wiki」を公開します。 http://it-is-it.net/CMD/ TIPSだけにするか、スクリプトの公開までするかは未定。
■ 2004年12月16日 / (echo)の空白付加の謎 echo ABC (ABCの後に空白は入れない)とやると、ABCとCRLFの5バイトが出力される。echo ABC>abc.txt とやると同じく5バイトがファイル abc.txt に入る。 (echo ABC) や (echo ABC)>abc.txtとカッコで囲んでも同じだ。 しかし、(echo ABC)| とパイプで次のプログラムにデータを渡すと、ABC空白CRLF と6バイトのデータが渡る。不思議だ。 (echo ABC&echo DEF)| とパイプすると、ABC空白CRLF DEF空白CRLFと合計12バイトが渡る。 もちろん、(echo ABC&echo DEF)>abc.txt だと、ABCとCRLF DEFとCRLFの合計10バイトがファイルに入る。 バグとまでは言えないかも知れないが、理解できない「仕様」だ。 表示だけなら問題ないだろうが、パイプを繋げてデータを加工する時には注意が必要である。 あ、もちろん、echo ABC| というようにカッコを使わなければパイプでも空白付加は無い。
■ 2004年11月29日 / 遅延環境変数のechoの謎 環境変数を echoするとき、値が空かも知れないときに、「ECHO は <OFF> です。」等と表示されないために、echo. 等とするのは基本的なTIPSである。私はピリオドを好んで使っていたが、他の記号を好む人もいるようだ。今まで特に問題は無かったが、遅延環境変数で、部分文字列や文字列置換を使うと不都合が起こるようだ。 @echo off setlocal enabledelayedexpansion set A=abcdefg echo !A! echo !A:~1! echo !A:~1,3! echo !A:~10! echo !A:~10,3 echo !A:*d=! echo !A:*g=! echo ................... echo.!A! echo.!A:~1! echo.!A:~1,3! echo.!A:~10! echo.!A:~10,3! echo.!A:*d=! echo.!A:*g=! echo =================== echo=!A! echo=!A:~1! echo=!A:~1,3! echo=!A:~10! echo=!A:~10,3! echo=!A:*d=! echo=!A:*g=! 表示結果は abcdefg bcdefg bcd ECHO は <OFF> です。 ECHO は <OFF> です。 efg ECHO は <OFF> です。 ................... abcdefg bcdefg A:~1,3 A:~10,3 A:*d= A:*g= =================== abcdefg bcdefg bcd efg となる。 echo. だと長さを指定した部分文字列や文字列置換を使った時に意図しない表示となっている。echo= や echo, や echo; だと問題ない。この3文字は、 for %%A in (A=B;C,D) do echo %%A のように、for文の(セット)の部分で、各要素の区切りに使える文字列だ。どの文字も echo の直後に書くのは違和感があるが、このように空になる可能性のある遅延環境変数の展開ではこの3つのうちどれかを使わざるを得ないであろう。 バグなのか仕様なのか微妙なところだが、遅延展開でなければピリオド等でも問題ないのと比べると仕様が異なるのは意図不明なので仕様と強弁するのは困難だろう。まあ、アンドキュメントな部分ではあるが。
■ 2004年09月26日 / CD コマンドの機能拡張 第二版 CDコマンドの拡張について二回書いたが([1]、[2])、仕様を一部変更追加削除して、ネットワークパスにも対応した。 ・移動はすべて pushd で行う。 ・cd のみのときはディレクトリスタックの表示 ・ホーム(%HOME%)への移動は cd ~ ・cd - で前のディレクトリに戻る ・cd + で現ディレクトリと一つ前のディレクトリの交換 ・cd 存在するディレクトリ だとそこへ移動 ・cd ショートカット の場合、ショートカット先がディレクトリならそこへ移動し、ファイルだとそのファイルのあるディレクトリへ移動 ・cd 存在するファイル だとそのファイルのあるディレクトリへ移動 (explorerからcmdプロンプトへファイルをドラッグ&ドロップしたとき便利) ・cd それ以外 の場合環境変数CDPATHを探索してオペラランドがそこにディレクトリとして存在すればそこに移動 前版との相違点は以下の通り。 ・ディレクトリ名を値に持つ環境変数名を指定した時の対応はコードの量の割に使い出が無いので削った ・/s の機能を削った。時間がかかりすぎて実用的でないため。適切にCDPATHを設定すれば十分のはず ・ディレクトリ表示順を新しい順で現ディレクトリも含めることにした(bashのdirs仕様と合わせた) ・cd + 機能の追加(cd +n や cd -n は bash だとディレクトリスタックのローテートだがこれくらいのほうがシンプルで使いやすいと思い独自仕様にしてある) ・ディレクトリ判断方法を変更し、ネットワークパスやネットワークドライブにも対応(したはず) ・ショートカット先がCドライブ以外や共有フォルダ内の時も対応(したはず) 最後の2点はテストが足りてないかもしれない。 @echo off if "%~1"=="" ( %引数が無ければ表示のみ% call :dirs goto :eof ) if "%~1"=="-" ( % - なら戻って表示% popd call :dirs goto :eof ) if "%~1"=="+" ( % + なら前回と交換% for /f "delims=" %%X in ('echo %%CD%%') do ( popd for /f "delims=" %%Y in ('echo %%CD%%') do ( popd for /f "delims=" %%Z in ('echo %%CD%%') do ( if not "%%Y"=="%%Z" (pushd %%X) else (cd /d %%X) if not "%%X"=="%%Y" pushd %%Y ))) call :dirs goto :eof ) if "%~1"=="~" if defined HOME ( % ~ ならホームへ% call :pushd %HOME% goto :eof ) if exist "%~1" ( %存在するか?% dir /b/ad "%~1" >NUL 2>NUL if not ERRORLEVEL 1 ( %ディレクトリか?% call :pushd "%~1" ) else ( %普通のファイルだ% if "%~x1"==".lnk" ( %ショートカットだ% call :shortcut "%~1" "%~0" if not ERRORLEVEL 1 goto :eof %リンク先が見つかった% ) call :pushd "%~dp1" ) goto :eof ) if not "%~$CDPATH:1"=="" dir /b/ad "%~$CDPATH:1" >NUL 2>NUL && ( %CDPATHに見つかった% call :pushd %~$CDPATH:1 goto :eof ) call :pushd %* goto :eof :pushd %pushdが成功しても元と同じならpopdする(履歴を残さないため)% for /f "delims=" %%X in ("%CD%") do ( pushd %* if not ERRORLEVEL 1 for /f "delims=" %%Y in ('echo %%CD%%') do ( if "%%X"=="%%Y" popd ) ) goto :eof :dirs %ディレクトリスタックを横に並べて表示する% setlocal enabledelayedexpansion set TEMPFILE="%TEMP:"=%\\cd$$%TIME::=.%.tmp" pushd > %TEMPFILE% set DIRS=%CD% for /f "usebackq delims=" %%A in (%TEMPFILE%) do set "DIRS=!DIRS! %%A" del %TEMPFILE% 2>NUL set /p=%DIRS%<NUL endlocal goto :eof :shortcut setlocal enabledelayedexpansion set F=&set FILE= for /f "delims=" %%A in ('more "%~1"^|^ %findstrはWin2Kの日本語バグのため必要% findstr /v /b aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') do ( set A=%%A if "!A:~1,2!"==":\\" for %%B in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do if "!A:~0,1!"=="%%B" set FILE=%%A if defined F (if defined FILE (set FILE=!FILE!%%A) else (set FILE=!F!\\%%A))&set F= if "!A:~0,2!"=="\\\\" set F=%%A ) if defined FILE endlocal&call "%~2" "%FILE%"&exit /b 0 endlocal&exit /b 1 なお、コード中のWin2Kのバグについては以前の記事を参照。
■ 2004年09月25日 / 【続々々々々】ディレクトリかどうかの判断 半年ぶりの続きである。なかなか奥が深い。約1年前に、このような表を作った。
しかし、対象がネットワーク共有名またはネットワークドライブの内部だとこの通り行かない。
つまり、ファイルとディレクトリの区別が付かない。また、\NUL を付加する方法でも駄目である。 したがって、ネットワーク共有先も含めて考えるなら、やはり if exist では駄目で、前回書いたように、 dir /b/ad FILE >NUL 2>NUL && echo ディレクトリ if exist FILE dir /b/ad FILE >NUL 2>NUL || echo ふつうのファイル のような方法を取る必要がある。
■ 2004年09月02日 / いくつかの投稿から ¥ が抜けてます サーバープログラムのバグ/仕様のため、1バイト文字の ¥ を書くときは¥¥と書いて、さらに再編集するときに¥1つになっているのをまた¥¥と全部書きなおさないといけないのだが、すっかり忘れて編集した記事が幾つかある。つまりその記事からは¥が全部欠落している。 従って最近の記事中のバッチスクリプトのうちいくつかはそのままでは動かない。 そのうちに何とかします。とりあえずお知らせまで。 2004-9-8 追記 おかしいのは CDコマンド拡張 の記事だけだった模様。復旧しました。
■ 2004年08月30日 / CD コマンドの機能拡張〜ショートカット対応 少し前に、cd コマンドの拡張を行った。ちょっと思いついてショートカットにも対応してみた(多分)。.lnk ファイルの内容をきちんと解析しているわけじゃないので駄目なケースもあるのかもしれないが今のところ動いている。 面倒なのでショートカット先はCドライブしか対応していない。 長いので変更点の周辺だけ示す。 if exist "%~1" ( %存在するか?% if exist "%~1\\" ( %ディレクトリか?% call :pushd "%~1" ) else ( %普通のファイルだ% if "%~x1"==".lnk" ( %ショートカットか?% for /f "delims=\\ tokens=1*" %%A in ('more "%~1"') do ( if "%%A"=="C:" call "%0" "C:\\%%B"&goto :eof ) ) call :pushd "%~dp1" ) goto :eof ) DOSKEY cd=cdd $* でエイリアス定義を行うと、cd の後ではディレクトリ名しか補完しないので、ショートカットファイルを対象にするときは cdd と元のバッチスクリプト名を使ったほうがいい。 2004-08-31 追記 フォルダを共有した状態でその中のファイル・フォルダのショートカットを作ったものに対しては上記の方法では駄目なようだ。一旦共有を削除してからショートカットを作ればOK。その後また共有を設定する。共有した状態で作ったショートカットにはローカルディスク上のファイル・フォルダについても共有名が記録されている。 ショートカットの内部構造を解説しているウェブページがあれば紹介いただきたい。
■ 2004年08月15日 / WSHスクリプトのBATスクリプトへの組み込み(後編) 前回の続き。 VBScrirpt と異なり、JScript には、行末までのコメント記号 // の他に、 /* */ で囲んだ複数行コメントが書ける。従って、バッチスクリプト部分をこれで囲めばコンパイル対象外に出来るため VBScript のようなコンパイルエラーを回避できる。 後は、/* がバッチスクリプトとしてもエラーにならなく書け、それが JScript としてもエラーにならない書き方が出来ればいい。共通のコマンド/ステートメントとして、@set 文が使えそうである。JScript の @set でセットできる変数名は @ で始まり、値は真偽か数字と制約がある。一方、バッチスクリプトの set コマンドとして見ると @ のために行がエコーされないので都合が良い。 今回も、一週間後の日付けを表示するという具体例でいく。 @set @_=1/* @echo off for /f "delims=" %%A in ('cscript //nologo //E:jscript "%~f0"') do set OUT=%%A echo 一週間後の日付けは %OUT:~0,4%-%OUT:~4,2%-%OUT:~6,2% goto :eof */ d=new Date((new Date).getTime()+7*24*3600*1000); WScript.Echo(d.getYear()*10000+(d.getMonth()+1)*100+d.getDate()); これで一応動作する。バッチスクリプト内に */ が出現する場合はまずいが、例えば、 echo コメントの終わりは */ という文を書く必要があれば、 set STAR=* echo コメントの終わりは %STAR%/ のように書き直せばよい。 別解として、条件コンパイルを使って、 @set @_=1//&echo off&goto bat @cc_on @if (false) :bat for /f "delims=" %%A in ('cscript //nologo //E:jscript "%~f0"') do set OUT=%%A echo 一週間後の日付けは %OUT:~0,4%-%OUT:~4,2%-%OUT:~6,2% goto :eof @end d=new Date((new Date).getTime()+7*24*3600*1000); WScript.Echo(d.getYear()*10000+(d.getMonth()+1)*100+d.getDate()); というのもあるが、/* */ を使うのが簡単だろう。 なお、いずれの手段でも、外側のバッチ環境変数を一つ(この例では @_ )の値を破壊してしまう副作用がある。
■ 2004年08月08日 / WSHスクリプトのBATスクリプトへの組み込み(中編) 前回の続き。まず直感的に考え付くのが、中間ファイル版のパイプ化である。今回は、一週間後の日付けを表示するという具体例でいく。もちろんこの例のように表示だけなら VBScript だけでやればいいのだが、本来はバッチスクリプト部分でバッチスクリプトの方が書きやすい処理が書いてあるものと思ってもらいたい。 @echo off for /f "delims=" %%A in ('findstr /e "'VBS" "%~f0" ^| cscript - //nologo //E:vbs') do set OUT=%%A echo 一週間後の日付けは %OUT%. goto :eof WSCript.echo dateadd("ww",1,date()) 'VBS しかしだめである。cscript コマンドはパイプからの入力に対応していないようで、 CScript エラー: スクリプト "-" の読み込みに失敗しました (パイプは終了しました。)。 というエラーメッセージを出すだけだ。findstr 直接でなく1行ずつゆっくり出力するフィルターをかませても駄目である。 次に、ruby や bash のように一方では実行され他方では読み飛ばされるような書き方が出来ないか考えてみる。 set A=nothing'&@echo off&setlocal&goto bat WScript.Echo DateAdd("ww",1,date()) if false then :bat for /f "delims=" %%A in ('cscript "%~f0" //nologo //E:vbs') do set OUT=%%A echo 一週間後の日付けは %OUT% goto :eof end if しかし、これもだめ。VBScript では複数行にまたがるコメントが書けないので、if false で バッチスクリプト部分を囲って、この内部もコンパイル時にパースされて、VBScript のコンパイル時に文法エラーになる。While false でも同じ。 仮に上手く行っても、set A=nothing' & がエコーバックされるのは抑止できない。 そこで、VBScript は諦めて JScript を使うことを考えることにする。もしかしたら、VBScript でも何か技があるのかもしれないが。 JScript なら複数行にまたがるコメントが書けるので、バッチスクリプト部をコメント化出来る。 つづく。
■ 2004年08月07日 / 複数行のnet sendメッセージ 以前書いた、環境変数に改行文字をセットの話だが、今一ついい使い道が思い浮かばなかった。今回、net send での複数行メッセージ送出に使うことを思いついた。 @echo off setlocal enabledelayedexpansion set DEST=%2 if not defined DEST set DEST=%COMPUTERNAME% set MSG= for /f "usebackq delims=" %%A in ("%~1") do ( set MSG=!MSG!^ %%A ) net send %DEST% "!MSG!" 引数1のファイルの内容を引数2の宛先に net send する。ただし、文字数に制約があるようで、長い場合は切り縮められる。宛先が * の場合はさらに送れる文字数は少ないようだ。 また、特殊文字を考慮していないので、ファイル中にある " ^ は消える。! があるとエラーになるケースもある。その他の特殊文字は単独文字での簡単なテストでは大丈夫そうだが、注意が必要。普通の日本語メッセージなら大丈夫。
■ 2004年08月02日 / WSHスクリプトのBATスクリプトへの組み込み(前編) バッチスクリプトの中で、どうしてもバッチスクリプトの機能で実現できない部分だけを他のプログラムで補わないといけないことがある。既存プログラムなら、for /f の機能で処理結果を標準出力から受け取ればいいのだが、1コマンドで書けないスクリプトの場合は困る。 簡単な方法は一時ファイルを使う方法だ。例えば、何らかの処理をしてその標準出力を OUT にセットする場合、 @echo off findstr /e "'VBS" "%~f0" > tmp.vbs for /f "delims=" %%A in ('cscript //nologo tmp.vbs') do set OUT=%%A del tmp.vbs 何らかのバッチスクリプト goto :eof VBScript をここに書く 'VBS 。。。。。 'VBS VBScript の最終行 'VBS のように、目印となるコメントをつけた VBSript 部だけを別ファイルに書き出してそれを呼び出せばいい。 でも、何か一時ファイルを作ると言うのはあまり気分の良いものではない。無理ならしょうがないが避けられるなら避けたいものだ。他の言語の場合は1ファイルに両方のスクリプトを書いてどちらの言語で実行してもOKということも出来る。例えば、ruby の場合、 @echo off for /f "delims=" %%A in ('ruby -x "%~f0"') do set OUT=%%A 何らかのバッチスクリプト goto :EOF #! ruby ruby スクリプトをここに書く 。。。。。 ruby スクリプトの終わり という風に、このような使い方を想定した -x オプション(#! ruby 行まで読み飛ばす)が存在する。 bash の場合だと、 : " @echo off goto bat " bash スクリプトをここに書く 。。。。。 bash スクリプトの終わり exit 0 :bat for /f "delims=" %%A in ('bash "%~f0"') do set OUT=%%A 何らかのバッチスクリプト のように、bash の : コマンドと、バッチスクリプトのラベル定義行が同形式なことを使えば出来る。@echo off と goto は " " で囲んで : コマンドの一引数としてしまう。 このようなことが、VBScirpt または JScript で出来ないかどうかを考えてみる。 つづく。
■ 2004年07月31日 / CMD.EXE だけで翌日の日付けを求める 前回、前日日付けを求めるスクリプトを書いたので、今回は翌日版。2月の処理がちょっと複雑になってしまった。 @echo off REM 日付の翌日を求める。結果は ANS にセット。 REM 引数(YYYYMMDD or YYYY-MM-DD or YYYY/MM/DD)がないときは本日とする。 if "%1"=="" (for /F %%A in ('date /t') do set ANS=%%A) else set ANS=%1 if not "%ANS:~8%"=="" set/a ANS=%ANS:~0,4%%ANS:~5,2%%ANS:~8,2% set/a ANS+=1 if 1%ANS:~-2% leq 128 goto ans if %ANS:~-2%==32 set/a ANS+=100-31&if not %ANS:~4,2%==12 goto ans if %ANS:~4,2%==13 set/a ANS+=10000-1200&goto ans if %ANS:~4,2%==02 if %ANS:~-2% geq 29 set/a ANS+=2-(!(%ANS:~0,4%%%4)^^!(%ANS:~0,4%%%100)^^!(%ANS:~0,4%%%400))*(31-%ANS:~-2%) for %%M in (02 04 06 09 11) do if %ANS:~-4%==%%M31 set/a ANS+=100-30 :ans 引数形式や使い方、日付けの正当性チェック無しなど、すべて前回と同じ。 if 1%ANS:~-2% leq 128 goto ans は不要だが、多くのケースでの速度向上のために入れてある。 n日前後を求めるJScript内蔵版はまた別途書くことにする。
■ 2004年07月28日 / CMD.EXE だけで昨日の日付けを求める 質問掲示板を見ていると良く見かける質問で、今日の日付けをファイル名につける等の日付け関係のものがある。今日の日付けだと、date/t や %DATE% を加工するだけなので簡単。たまに、昨日の日付けを求めると言うのがある。簡単に行うのは、VBScriptやJavaScriptを使う方法だが、使わない範囲で出来るだけコンパクトに作ってみた。 @echo off REM 日付の前日を求める。結果は ANS にセット。 REM 引数(YYYYMMDD or YYYY-MM-DD or YYYY/MM/DD)がないときは本日とする。 if "%1"=="" (for /F %%A in ('date /t') do set ANS=%%A) else set ANS=%1 if not "%ANS:~8%"=="" set/a ANS=%ANS:~0,4%%ANS:~5,2%%ANS:~8,2% set/a ANS-=1 if not %ANS:~-2%==00 goto ans if %ANS:~4,2%==01 set/a ANS+=-10000+1131&goto ans for %%M in (02 04 06 08 09 11) do if %ANS:~4,2%==%%M set/a ANS+=-100+31&goto ans if not %ANS:~4,2%==03 set/a ANS+=-100+30&goto ans set/a ANS+=-100+28+(!(%ANS:~0,4%%%4)^^!(%ANS:~0,4%%%100)^^!(%ANS:~0,4%%%400)) :ans 一応、サブルーチン形式で作ってみた。引数はyyyymmddでも、区切り文字入りでも可能。ただし日付けの正当性まではチェックしてない。 ワーク用の環境変数を使っていないため、サブルーチンでなく、スクリプト中に埋め込んで使うことも出来るだろう(それを考慮して goto :eof を使ってない)。 ポイントは、年・月・日を分けずに8桁の数字として扱うことと、閏年の算出の工夫(排他的論理和を使用)。 また、ANSの値は、最初に8桁の数字をセットして1引いた後は set/a で最後に一度しか変更しないので、goto を使わないで、if ( ) else ( ) でも書けるかも知れない。 何日前、何日後をバッチだけでやるのだと、ジュリアンデイトに直してから計算するんだろうけど、VBS, JS に頼ってしまいそう。これはまた別途書くことにする。
■ 2004年07月16日 / CD コマンドの機能拡張 このところBATスクリプトの話が途絶えてたので、自作スクリプトの紹介。cd コマンドの pushd 化と csh 風拡張。移動はすべて pushd で行う。 ・cd のみのときはディレクトリスタックの表示 ・ホーム(%HOME%)への移動は cd ~ ・cd - で前のディレクトリに戻る ・cd 存在するディレクトリ だとそこへ移動 ・cd 存在するファイル だとそのファイルのあるディレクトリへ移動 (explorerからcmdプロンプトへファイルをドラッグ&ドロップしたとき便利) ・cd 英数字 で、その英数字が環境変数であり、その値が "?:\\*" つまりフルパスのディレクトリならそこへ移動 ・cd 文字列 で環境変数CDPATHを探索してオペラランドがそこにディレクトリとして存在すればそこに移動 ・cd /s 文字列 だと、カレントディレクトリの下を再帰的に探して最初に見つかった該当ディレクトリに移動 ディレクトリスタックの表示は、単に pushd を使うと新しい順に縦に1つずつ表示されるが、古い順に横に並べたかった。for /f "delims=" %%A in ('pushd') doでは現在のディレクトリスタックを取得できない。'pushd' の出力は空となる。おそらく、'pushd' がサブシェルで実行され、そのサブシェルに現在のディレクトリスタック情報が継承されないためと思われる。そのため、一時ファイルを作らざるを得なかったのが少し残念。 このスクリプトを cdd.bat 等として、doskey cd=cdd $* と定義して使う。 実際使ってみると、環境変数名指定での移動機能までは要らなかったかなと思う。それを省けば20行ほど短く出来る。 @echo off if "%~1"=="" ( %引数が無ければ表示のみ% call :dirs goto :eof ) if "%~1"=="-" ( % - なら戻って表示% popd call :dirs goto :eof ) if "%~1"=="~" if defined HOME ( % ~ ならホームへ% call :pushd %HOME% goto :eof ) if exist "%~1" ( %存在するか?% if exist "%~1\\" ( %ディレクトリか?% call :pushd "%~1" ) else ( %普通のファイルだ% call :pushd "%~dp1" ) goto :eof ) setlocal enabledelayedexpansion call :isname "%~1" if %ERRORLEVEL%==0 ( %英数字だけの名前か?% if defined %1 ( %環境変数名か?% set "Q=!%1!" if "!Q:~1,2!"==":\\" if exist !%1!\\ ( %内容がディレクトリ絶対パスか?% endlocal&call :pushd %%%1%% goto :eof ) ) ) set "P=%~$CDPATH:1" %CDPATHの探索% if defined P if exist "!P!\\" ( %見つかった?% endlocal&call :pushd %~$CDPATH:1 goto :eof ) endlocal if /i "%1"=="/s" if not "%2"=="" for /r %%A in (%2) do if exist %%A\\ ( %第一引数が /s ならサブディレクトリを探す% call :pushd %%A goto :eof ) call :pushd %* goto :eof :isname %英数字と一部の記号だけかチェック% setlocal enabledelayedexpansion set "W=%~1" for %%A in (@ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z _ a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) do ( if not defined W endlocal&exit /b 0 set "W=!W:%%A=!" ) endlocal exit /b 1 :pushd %pushdが成功しても元と同じならpopdする(履歴を残さないため)% for /f "delims=" %%X in ("%CD%") do ( pushd %* if not ERRORLEVEL 1 for /f "delims=" %%Y in ('echo %%CD%%') do ( if "%%X"=="%%Y" popd ) ) goto :eof :dirs %ディレクトリスタックを古い順に横に並べて表示する% setlocal enabledelayedexpansion set TEMPFILE="%TEMP:"=%\\cd$$%TIME::=.%.tmp" pushd > %TEMPFILE% set DIRS= for /f "usebackq delims=" %%A in (%TEMPFILE%) do set "DIRS=%%A !DIRS!" del %TEMPFILE% 2>NUL if defined DIRS echo %DIRS% endlocal goto :eof ディレクトリスタックに同じものが連続しないように工夫。そのために :pushd というサブルーチンを作ったが、setlocal が使えない( setlocal と endlocal の間で pushd すると、endlocal で元に戻ってしまう。unix の シェルスクリプト内で cd 出来ないのと同じ) ので環境変数を汚さないために for/f の制御変数に新旧のディレクトリをセットして比較せざるを得なかった。後で考えると、OLDCD のような環境変数を新設して使う手もあった。
■ 2004年05月22日 / Win2000のFOR文のバグ とある掲示板で回答して(今頃)気づいたのだが、Win2000の for /f %A in (\'command ....\') do の日本語処理にはバグがある。(XPでは直っている) for /f %%A in (\'echo あいう123456789\') do echo %%A for /f %%A in (\'echo あ123456789\') do echo %%A では、それぞれ、「あいう123456789」「あ123456789」が表示されるはずだが、実際には、「あいう123456」「あ12345678」が表示される。(\' \') の2バイト文字の数だけ (\' \') 内のコマンド末尾から文字が削られるようだ。かといって空白を末尾につけても無駄で同じ結果に終わる。空白だけでなく、セミコロン、カンマ、イコールなどの in ( ) 内リストの区切り文字も駄目。また、usebackq と ` ` を使った場合も同じである。 現実的には、 for /f \"delims=\" %%A in (\'find \"日本語\" filename\') do .... のようなケースで問題になるだろう。2バイト文字の数が分かっていれば、 for /f \"delims=\" %%A in (\'find \"日本語\" filename@@@\') do .... のようにその数だけ余分の文字をあらかじめ末尾に付けて置けば良いが、2バイト文字の数が不明の場合(及びWin2000とWinXPで共通に使う場合)は、例えば、 for /f \"delims=\" %%A in (\'find \"%STR%\" filename^|findstr /v qwertyuiopasdfghjkl\') do .... のように末尾からある程度文字が消えても良いようにする必要がある。もうちょっとましな手段は無いものか? WinXPでは直っているのにWin2000の最新SPでも直っていないということは、直す気が無いということだろう。初歩的過ぎて情けないバグである。
■ 2004年04月04日 / %~sI のバグについて 先日予告した通り、alt.msdos.batch.nt で発見した ~s 修飾子のバグについて。 for変数や、バッチ引数には、%~sI や %~s1 という修飾子でショートネーム(または8.3形式。以下ではSFNと書く)形式のパス名・ファイル名を得ることが出来る。しかしこれにはバグがある。NT4時代からあって、直っていないらしい。このバグは簡単に再現できる。 md \"long long long dir\" cd \"long long log dir\" echo.>\"A B.txt\" for %%A in (*.txt) do echo %%~sA 本来は、\\LONGLO~1\\ABBE64~1.TXT のように表示されるべき部分が、\\LONGLO~1\\ABBE64~1.TXTB.txt のように後ろにごみ(LFNの残骸)が付いてしまう。 こういうバグがある以上、特定の環境で一時的に使うバッチならともかく、汎用ツールを作る時は ~s 修飾子を安易に使えないと言うことだ。 解決法としては、パスの上位部分から順にSFN形式にしていくしか無いようだ。汎用サブルーチン :sfnsub とその(上記スクリプトで作ったファイルでの)テストスクリプトを示す。:sfnsub の仕様としては、\" で囲んだフルパスを引数にすると、環境変数 SFN にそのSFNを返す。\" で引数を囲むのは必須。なお、これと次のスクリプトは私とfoxidrive氏の合作である。 @echo off setlocal for %%A in (\"A B.txt\") do @echo %%~sA&call :sfnsub \"%%~fA\" echo %SFN% goto :eof :sfnsub set ARG=%1 set ARG=%ARG:\\=\\\" \"% set SFN= call :loop %ARG% goto :eof :loop for %%A in (\"%SFN%%~1\") do set SFN=%%~sA shift if not \"%~1\"==\"\" goto :loop set \"SFN=%SFN:&=^&%\" set \"SFN=%SFN:^^=^%\" goto :eof 汎用と書いたが、% はスクリプト中でサブルーチンの引数として渡せない(注1)ので % を含んだファイル名は駄目だがそれ以外のファイル名に使える特殊文字には対応しているはずだ(まだ漏れがあるかもしれないが)。なお、この方法と別に COMMAND.COM を使ってカレントディレクトリを SFN に変換する別法もある。 @echo off setlocal for %%A in (\"A B.txt\") do @echo %%~sA&call :sfnsub \"%%~fA\" echo %SFN% goto :eof :sfnsub pushd . cd /d \"%~dp1\" command /c rem for %%A in (\"%~nx1\") do set SFN=%%~sA popd goto :eof こっちのほうがわかりやすい。ただし、日本語版Windowsの場合は初回のCOMMAND.COM実行時に画面がクリアされていろいろメッセージが出る(注2)のでいまいちである。 いずれにせよたかがSFNを得るために大げさな話であり、スクリプトではSFNを使うのを避けたほうがいいだろう。 \\Program files\\ の下だけ調べてみたが、%~sI が正しい結果を返さないファイルは結構ある。 (注1:もちろん、% を %%%% に置換してから引数に使えばよいが、for変数には置換が使えないし、置換するために一般の環境変数に一旦セットすればさらに多くの問題を引き起こすので現実的でない) (注2:英語版では出ないらしい。これを出さない方法は無いだろうか?)
■ 2004年03月27日 / 【続】日付け時刻を得る 先日、日付け時刻について、 >まあ、date < NUL の2行目を見れば年月日の順序はわかるのでそれで何とかなるかもしれないが、 >他国語版にも対応するのは大変だろう > ( date < NUL の実行時だけコードページを英語にするんでしょうね)。 と書いた。 alt.msdos.batch.nt とかを最近読み書きしだしたのだが、そこに年月日の順序に依存しない日付け取得スクリプトが載っていた。ただし、date < NUL の2行目に表示順に (mm-dd-yy) 等が表示されることを利用しているが、構文解析めいたことをしない巧妙なやり方でそんなに複雑ではない。ヨーロッパ版Windowsでは (dd-mm-yy) とか表示されるんだろう。もちろん、(yy-mm-dd) でもOKだ。しかし、(年-月-日)だと駄目なのでそのままでは日本語版Windowsでは使えない。 日本語版対応するためには、date コマンド実行前後に、call us と call jp を書くか、スクリプト中の yy mm dd を 年 月 日 に書き換えればいいわけだが、そんなことをするくらいなら、yy mm dd の順序を仮定しているその辺のスクリプトで十分な気もする。 ちゃんと他国語対応するには、date コマンド実行前にコードページ番号を調べて保存し、実行後にコードページをそれに戻すように書く必要があるが、コードページを調べるための chcp や mode con cp コマンドの出力形式自体がコードページ依存なので、やはり他国語対応の道は遠そうである。 このスクリプトは曜日付き表示にも対応しており、惜しいのは年の2桁表示に対応していないことだけだけど、それくらいはまあいいか。 ということで、前回書いた、 >結論としては、BATスクリプトだけで日付を得るのは特定環境向けなら簡単だが、汎用を求めるなら労多くして益少なしということ。 は98%くらい撤回する。 時刻についても先日は >12時間制と設定されているかもしれない。 と書いたが、コントロールパネルで12時間制に設定しても time < NUL や %TIME% は影響を受けず24時間制のままだった(念のため再起動してみたがOK)のでこれらを使えば問題ない。ただし、time/t だけは12時間制になる。もちろんExplorerの表示やDIRコマンドの表示も12時間制になるが、何故 time/t だけ道連れなのかは謎。 alt.msdos.batch.ntでは、%~sI のバグについて初めて気づいたがそれはまた別項で。
■ 2004年03月13日 / 日付時刻を得る 日付や時刻の入ったファイル名・フォルダ名を作るために、日付・時刻を BAT スクリプトで得たいという話が良くある。掲示板では、 for /f "tokens=1-3 delims=/ " %%A in ('date/t') do set YMD=%%A%%B%%C と答えたりするわけだが、これはWin2000やWinXPでも地域のコントロールパネルで日付の短い形式を変更していない場合の話だ。 日本語版Win2000の場合デフォルトで、date/t の結果は 「2004/03/14 日」、%DATE% の値は「日 20004/03/14」のようになっているが、コントロールパネルで区切り文字を / でなく - や . にも変更できる。ただし、日付の形式を「mm/dd/yy」のように変更しても形式自体(年月日の順序や桁数、曜日の有無や位置)は変わらないようだ。これだけだと、 for /f "tokens=1-3 delims=/-. " %%A in ('date/t') do set YMD=%%A%%B%%C と、区切り文字を増やせば大丈夫のようだが、区切り文字はプルダウンで表示される / - . 以外にも指定できるので万全ではない。 また、WinXP では date/t の結果や %DATE% の値は、コントロールパネルの短い形式の日付で指定した通りになるので、年月日の順序もわからないし、年が2桁かもしれない。従って一旦環境変数にセットして、文字位置で切り出すことも出来ない。WinXPの場合は、reg コマンドがあるので、 for /f "tokens=1-3" %%I in ('reg query "HKCU\Control Panel\International" /v sShortDate') do if %%J == REG_SZ set YMDFMT=%%K で、日付書式がわかるのでこれを元に解析すれば何とかならなくもない。 Win2000の場合は日本語版に限定すれば区切り文字が変わるだけなので、環境変数%DATE%を文字位置で切り出すことで出来るが、英語版はおそらく mm/dd/yyyy の順序なので、確実に処理しようとすると、やはりレジストリを見る必要がある。reg コマンドが無いのでレジストリを見ようとすると、VBS+WSH 等を使う必要がある。 でも、VBS を使うなら最初からそれで日付を得れば簡単だ。 結論としては、BATスクリプトだけで日付を得るのは特定環境向けなら簡単だが、汎用を求めるなら労多くして益少なしということ。 まあ、date < NUL の2行目を見れば年月日の順序はわかるのでそれで何とかなるかもしれないが、他国語版にも対応するのは大変だろう ( date < NUL の実行時だけコードページを英語にするんでしょうね)。 時刻についても同様の問題がある。コントロールパネルで時分秒の順序を変えて設定する人は居ないだろうが、12時間制と設定されているかもしれない。
■ 2004年03月08日 / パラメータ区切り文字の謎 ビルトインコマンドの多く( echo は違うようだが)で、パラメータの区切りに、空白の他、, ; = が使える。コマンド処理の時点で空白になっているようだ。 例えば、 dir a.txt;b.txt,c.txt も可能だし、 for=%I=in=(A;B=C),do=@echo %I では、A B C が1行ずつ表示される。 区切り文字として、空白やカンマはわかる。セミコロンも、PATH 等の要素区切りがそうなので、わからないでもない。しかし、イコールはどうしてだろうか? 昔を思い出すと、CP/M のコマンドでは、REN newname=oldname というリネームコマンドや、PIP destfile=srcfile というコピーコマンドがあった。そのあたりからの互換性を引きずっているのだろうか? DOS コマンドでも、 ren oldname=newname とは書けるわけだが、= の意味が逆だ。
■ 2004年03月07日 / 【続々々々】ディレクトリかどうかの判断〜whichコマンドの改訂版 以前の記事において、 >昨日の記事に対し、 >>私は、dir "対象" /a:d してみて errorlevel を見ます。 NUL に頼るより確実では? >というコメントを頂いた。 >ただ、if existで一気にチェックできるコンパクトさは捨てがたい。ということで、もう少しあがいてみました。 ということで、 if exist "%%A" if not exist "%%A\" echo 普通のファイル という方法を取った。しかし、|| を使うと、コンパクトさを損なわずに dir を使う方法も取れる。 if exist "%%A" dir /b/ad "%%A" >NUL 2>NUL||echo 普通のファイル である。/b は必須ではないがわずかに速くなる。これと元の方式を使った which.bat の処理時間を比べてみた。 exist 方式が 0.2 秒で、dir 方式が 0.4 秒である。時間自体は PATH や PATHEXT の要素数に依存するがいずれにしても exist 方式のほうが速いようだ。 わずかの差ではあるが、0.2 秒だと瞬間的に終わるのに対して、0.4 秒だとちょっともたつく感じである。PenIII/650MHzでの結果なので、もっと速いCPUだと dir 方式でも気にならないかもしれない。アンドキュメントの機能を使わないわけだし。 私の場合は当面 exist 方式を使い続ける。また、同じような処理が4回出てくるのでこれをサブルーチンにしてみたが、処理時間が 0.8 秒とかなり長くなる。ソースは見やすくなるがこれではもたつきが気になるので、サブルーチン化はしないことにする。 高速CPUの人は、dir 方式 & サブルーチン方式のほうがソースが見やすいだろう。 うーん、PCの買い替え時かなぁ。。。高速CPU使用者向けに一応紹介する。 @echo off for %%X in (%*) do ( for /f "delims=" %%A in ('DOSKEY /MACROS') do ( for /f "delims==" %%B in ("%%A") do if /i "%%B" == "%%X" echo %%A ) for %%A in (ASSOC BREAK CALL CD CHDIR CLS COLOR COPY DATE DEL DIR ECHO ENDLOCAL ERASE EXIT FOR FTYPE GOTO IF MD MKDIR MOVE PATH PAUSE POPD PROMPT PUSHD RD REM REN RENAME RMDIR SET SETLOCAL SHIFT START TIME TITLE TYPE VER VERIFY VOL) do if /i "%%X" == "%%A" echo %%X cmd.exe builtin if not "%%~xX" == "" call :sub "%%~X" for %%S in (%PATHEXT%) do call :sub "%%~X%%S" set _= if "%%~X" == "%%~nxX" ( for %%P in ("%PATH:;=";"%") do ( if not "%%~xX" == "" call :sub "%%~P\%%~X" for %%S in (%PATHEXT%) do call :sub "%%~P\%%~X%%S" ) ) ) if not defined _ goto :eof if not "%_%" == "%_: =%" set _="%_%" goto :eof :sub if exist "%~1" dir /b/ad "%~1" >NUL 2>NUL||(echo %~1&if not defined _ set "_=%~1") 以前書いたものからは、seltlocal を使わないなど若干改善してある。 2004-09-25 誤りがあったので少し修正
■ 2004年03月06日 / [続々々]tailコマンドの作成 tail コマンドの完成版。+ オプションもつけたが、単に more の機能を使っているだけ。 @echo off setlocal set MORE= if not defined LINE set LINE=20 set \"OPT=%~1\" if not defined OPT goto pipe if \"%OPT:~0,1%\"==\"/\" echo>&2 コマンドの構文が誤っています&exit /b 1 set OPTLINE=0 set /a \"OPTLINE=%~1\" 2>NUL >NUL if \"%OPT:~0,1%\"==\"+\" if %OPTLINE% geq 0 set /a L=OPTLINE&shift /1&goto plus if \"%OPT:~0,1%\"==\"+\" echo>&2 コマンドの構文が誤っています&exit /b 1 if \"%OPT:~0,1%\"==\"-\" if %OPTLINE% lss 0 set /a LINE=-OPTLINE&shift /1 if \"%~1\"==\"\" goto pipe for /f %%N in (\'find /c /v \"\" ^< %1\') do set /a L=%%N-LINE :plus if %L% lss 0 set L=0 more +%L% %1 goto :eof :pipe set TEMPFILE=\"%TEMP:\"=%\\$$$%TIME::=.%.TMP\" set I=-%LINE% set J=0 for /f \"delims=\" %%A in (\'more\') do ( set /a J+=1, I+=1 echo.%%A>%TEMPFILE% call :setsub SAVE%%J%% SAVE%%I%% ) del %TEMPFILE% 2>NUL set /a I+=1 if %I% leq 0 set I=1 setlocal enabledelayedexpansion for /l %%K in (%I%,1,%J%) do echo.!SAVE%%K! goto :eof :setsub set /p %1=< %TEMPFILE% set %2=
■ 2004年03月05日 / call 命令のオーバーヘッド〜添え字付き環境変数へのセット 前回書いた環境変数へセットする方式の tail コマンドでは、添え字付き環境変数へのセットで、call 命令を使う。前回書いたような eval 的な call の使い方と、実際にサブルーチンコールする場合とでの処理時間を念のため計ってみた。 for /l %%I in (1,1,1000) do set J=%%I&call set SAVE%%J%%=1 と、 for /l %%I in (1,1,1000) do set J=%%I&call :sub SAVE%%J%% goto :eof :sub set %1=1 goto :eof の処理時間を計ってみると、意外なことに、前者が5.5秒、後者が4.5秒とサブルーチンコールのほうが速い。 制御が行って戻る分だけ遅い気がするのだが、繰り返し回数を増やしたり、= の右辺をなくしてセットでなくアンセットにしても同じで、必ずサブルーチンコールのほうが早い。前回書いた tail コマンドでは、call が2回出てくるのでこれをまとめてひとつのサブルーチンコールに変えればさらに速くなるはずだ。 (前略) echo.%%A>%TEMPFILE% call :sub SAVE%%J%% SAVE%%I%% (中略) :sub set /p %1=< %TEMPFILE% set %2= goto :eof (1) 前回の中間ファイル方式と、(2) 改訂版中間ファイル方式で、同じファイルで実行してみたところ、 (1) 21秒 (2) 17秒 と少しだが速くなった。(1) の時間が前回と違うのはバックグラウンドプロセスやメモリ状況の違いであろう。前回・今回とも数回計ったが誤差は1秒以内である。
■ 2004年03月02日 / [続々]tailコマンドの作成 前回書いた2つの方法で書いた tail コマンドの基幹部分を示す。 (1) 中間ファイル方式 set I=-%LINE% set J=0 for /f \"delims=\" %%A in (\'more %1\') do ( set /a J+=1, I+=1 echo.%%A>%TEMPFILE% call set /p SAVE%%J%%=< %TEMPFILE% call set \"SAVE%%I%%=\" ) set /a I+=1 if %I% leq 0 set I=1 setlocal enabledelayedexpansion for /l %%K in (%I%,1,%J%) do echo.!SAVE%%K! del /f %TEMPFILE% 2>NUL goto :eof (2) for (%%) 方式 set I=-%LINE% set J=0 for /f \"delims=\" %%A in (\'more %1\') do ( set /a J+=1, I+=1 for /f \"delims=\" %%B in (\'echo SAVE%%J%%\') do set %%B=%%A call set \"SAVE%%I%%=\" ) set /a I+=1 if %I% leq 0 set I=1 setlocal enabledelayedexpansion for /l %%K in (%I%,1,%J%) do echo.!SAVE%%K! goto :eof これと、(3) [続]tailコマンドの作成で書いた、more + を使った方式とで処理時間を比べてみた。テストでは 1000行、約32KB のファイルに対して、tail -10 を行った。結果は、 (1) 25 秒 (2) 99 秒 (3) 1 秒 である。(3) が早いのは当然として、(2)が遅いのはサブシェル起動に時間が掛かっているためと思われる。検証のために、 for /l %I in (1,1,1000) do @cmd /c \"@rem\" と、 for /l %I in (1,1,1000) do for /f %J in (\'rem\') do @echo. の時間を計ってみると、それぞれ、102秒、75秒である。(2) の結果とも考え合わせると、for /f (\' \') によるサブシェル起動は、明示的に cmd を起動するよりは効率よく出来ているようだ。 結論としては、いくら“パイプ入力にも対応”“中間ファイルを使わない”という利点があるとしても(2)は遅すぎる。オペランドでファイルを与えた時は(3)で、そうでない場合は(1)の方式をそれぞれ使うようにするのがいいかと思う。
■ 2004年02月29日 / for制御変数から添え字付き環境変数へのセット for制御変数から添え字付き環境変数へのセットの話の続き。もちろん、任意の内容を安全にセット出来なれればならない。 set の = の左辺のみ2度評価して、右辺は1度しか評価してはいけないわけだが、1文でやるのは前回無理とわかった(実は書いた以上にもいろいろ試行錯誤している)ので、2文以上に分けるしかない。まず思いつくのは、 echo %%A | call set /p SAVE%%J%%= だが、これでは何もセットされなかった(SAVE%J%は未定義状態)。 何が悪いのか切り分けのために、テストとして、 echo %%A | call set /p SAVE%%J%%= set SAVE と echo %%A | ( call set /p SAVE%%J%%=&set SAVE) をやってみると、環境変数にセットはされるもののその値を次の行で参照できないと言うことがわかる。おそらく、パイプの各要素がサブシェルのCMD.EXEで実行されて親のCMD.EXEの環境変数に反映されないためと思われる。 echo %%A>FILE call set /p SAVE%%J%%=< FILE と、ファイルを経由すれば問題はない。ただ、あまり中間ファイルは使いたくない。 後は、複合文中での環境変数参照の記事での、(4)の方法である。 for /f %%B in (\'echo SAVE%%J%%\') do set %%B=%%A これはうまく行く。ただし、(\' \') の中もサブシェルで実行されるため、tail コマンドで使うと、読み込んだファイル1行ごとにサブシェル起動が入るため遅そうである。
■ 2004年02月27日 / [続]call 命令の謎 for /f 版の tail を作るに当たっての難関は、遅延展開を使わないで、添え字付き環境変数へのセットだ。unix で 配列変数機能の無い古い sh では、eval SAVE$J=\\$VALUE とやるのが定石だ。これを参考に、左辺は call set SAVE%%J%%= と call を使う方法が思いつく。しかし、単純に、 call set SAVE%%J%%=%%A とやってしまうと、右辺のfor変数Aに入っている読んだ行に含まれる特殊文字が call 時に効いて来るので駄目だ。最初の評価ではfor変数は展開されず、call での再評価時にfor変数が展開されるとうまく行きそうだ。テストのために、 for %%Q in (%%%%) do for /f %%A in (\"aaa/?\") do call echo %%QA としてみた。for変数Qの値は %% なので、初回の評価では call echo %%A となり、call による2度目の評価でfor変数Aが展開されると期待した。ところが、表示されたのは、%A である。他にも % の個数を変えたり、^ を使ったりいろいろやってみたがどうもうまく行かない。 どうも、call による再評価は、% 関連処理つまり、 (1) % による環境変数展開 (未定義変数を空にするのも含む) (2) 連続した %% を % に (3) 単独の % を削除 を再度行うだけで、for変数や%1等の引数の展開や ^ の処理は call の時点で再度はされないようだ。 call による方法は無理そうだ。 つづく
■ 2004年02月22日 / [続]tailコマンドの作成 昨日の head コマンドだが、第一パラメータで、+10 とか /E 等の more の有効なオプションを指定してしまうと、第二パラメータにファイル名を指定しても無視され標準入力待ちになってしまう。とりあえず、 set \"OPT=%~1\" if \"%OPT:~0,1%\"==\"/\" echo>&2 コマンドの構文が誤っています&exit /b 1 if \"%OPT:~0,1%\"==\"+\" echo>&2 コマンドの構文が誤っています&exit /b 1 を追加しておく。 tail コマンドだが、ファイルからの読み込みに限定すると別の解法がある。まず、find /c /v \"\" < file 等でファイル全体の行数を求め、表示行数を引いて、何行目以降を表示するか決める。以前考えた時には、find で求められるのは空行も含めた全体行数で、for /f では空行が飛ぶので行数が合わないと思っていた。しかし、for /f の skip= オプションを使うと、空行も含めた行数で先頭から読み飛ばすことが出来る。 @echo off setlocal if not defined LINE set LINE=20 set \"OPT=%~1\" if not defined OPT echo>&2 コマンドの構文が誤っています&exit /b 1 if \"%OPT:~0,1%\"==\"/\" echo>&2 コマンドの構文が誤っています&exit /b 1 if \"%OPT:~0,1%\"==\"+\" echo>&2 コマンドの構文が誤っています&exit /b 1 set OPT=0 set /a \"OPT=%~1\" 2>NUL >NUL if %OPT% lss 0 set /a LINE=-OPT&shift /1 for /f %%N in (\'find /c /v \"\" ^< %1\') do set N=%%N set /a L=N-LINE if %L% gtr 0 (set S=skip=%L%) else (set S=) for /f \"%S% delims=\" %%A in (\'more %1\') do echo.%%A どうせ more を使うんだったら、最後の2行は、 if %L% lss 0 set L=0 more +%L% %1 でいいか。何のことは無い簡単なスクリプトになった。しかも速い。 しかし、これらでは標準入力を扱うことは出来ない。せっかく head コマンドが標準入力対応に出来たので、前回のような環境変数に覚えておく方式の for /f 版を引き続き考える。
■ 2004年02月21日 / head コマンド再び 昨日書いたように、for 文中での環境変数の直近値の参照方法はいろいろある。そこで、 以前あきらめた head コマンドの for /f での実現をやってみた。ただし、空行はスキップされると言う for /f の仕様だけは我慢することになる。一方、副産物だが、do ( ) の部分を do (\'more %1\') とすることで、 (1) 引数無しの時にパイプ入力にも対応できる (2) UTF-16 のファイルでもOK というメリットも出来た。 @echo off setlocal set MORE= if not defined LINE set LINE=20 set OPT=0 set /a \"OPT=%1\" 2>NUL >NUL set /a OPT*=-1 if %OPT% gtr 0 set LINE=%OPT%&shift /1 set L=0 for /f \"delims=\" %%A in (\'more %1\') do ( call :check if ERRORLEVEL 1 goto :eof echo.%%A ) goto :eof :check set /a L+=1 if %L% gtr %LINE% exit /b 1 exit /b 0 ! < | > & ^ 等の特殊文字を含んだファイルもOK。dir | head -10 もOK。 意外と簡単だったので、ついでに、行数オプションにも対応。ただし、 set /a \"OPT=%1\" 2>NUL >NUL と、手抜きのためちょっと無理やりなところがあるので、穴があるかも。 複数ファイルの引数にも対応しようかと思ったけど、そうすると、引数無しの場合を別扱いしないといけないので止めた。 先日のものに比べると欠点は空行が扱えないだけで、利点が大きいので前のは捨てよう。次回は tail の書き直し版。
■ 2004年02月20日 / 複合文中での環境変数参照 if や for や ( ) の内部での % での環境変数置換は最初に行われるため、複合文内部で環境変数の値を変更してもその新しい値を参照しにくい。この参照の仕方を整理しておく。 (1) 遅延環境変数展開を使う これが一番簡単であるが、デメリットとしては、% で展開する環境変数や、for制御変数の値の中に ! が含まれているとそれも展開されてしまい、予期しない値になることがある。これが理由で、以前 head コマンドを作成しようとした際、採用できなかった。 例: setlocal enabledelayedexpansion for /l %%I in (1,1,10) do ( set /a X=%%I %% 2 if !X!==1 ( echo %%I 奇数) else ( echo %%I 偶数) ) (2) call を使う 環境変数の直近の値を他の環境変数にセットしたい場合、call set B=%%A%% とすると、その文を実行する時点での A の値が B にセットできる。ただし、if や for には使えないため、call if %%A%%==1 のような直近の値での判断が出来ない。 (3) サブルーチンコールを使う call :sub とすると、:sub の中では各環境変数の直近の値が参照できる。ただ、その参照結果をどうやって呼び出し元に伝えるか。これには、ERRRORLEVEL を使う方法と、DEFINED を使う方法が考えられる。 例1: for /l %%I in (1,1,10) do ( call :sub %%I if ERRORLEVEL 1 ( echo %%I 奇数) else ( echo %%I 偶数) ) goto :eof :sub set /a X=%1 %% 2 exit /b %X% 例2: for /l %%I in (1,1,10) do ( call :sub %%I Z if defined Z ( echo %%I 奇数) else ( echo %%I 偶数) ) goto :eof :sub set %2= set /a X=%1 %% 2 if %X%==1 set %2=1 goto :eof ERRORLEVEL を使うと数字の結果を返すことが出来る。ただし、%ERRORLEVEL% での参照が出来ないので、旧来の if ERRORLEVEL の書式しか使えない。環境変数の定義状態では基本的には2値状態しか返せないし、setlocal してないとスクリプトの外の環境変数が書き換わってしまう。 この機能を使うと、以前断念した、for /f を使った head コマンドが作成できそうだ。 (4) for 環境変数にセットする for 環境変数は直近の値が参照できるので、それに環境変数の直近値を代入できれば良い。しかし、以前書いた通り、 call if /f \"delims=\" %%%%B in (\"%%A%%\") do echo %%%%B のようなことは出来ない。%%A%% を再度評価できればいいので、ちょっと工夫して、 for /f \"delims=\" %%B in (\'echo %%A%%\') do echo %%B とすると、\' \' の内部はサブシェルのcmd.exeで実行されるため、再度 % の評価が出来、目的を果たすことが出来る。しかも、setlocal しなくても外の環境変数に影響が無いので、何らかの理由(外の環境変数をセットしたい or カレントディレクトリ変更の結果を外に伝えたい)で setlocal を使えない場合にも応用できる。 例: for /l %%I in (1,1,10) do ( set /a X=%%I %% 2 for /f %%Z in (\'echo %%X%%\') do ( if %%Z==1 ( echo %%I 奇数) else ( echo %%I 偶数) ) )
■ 2004年02月17日 / for文とgoto文 for 文から goto 文で抜け出ることが出来る。 が、抜け出なくても goto しただけで、for 文の機能が無効になるようだ。 @echo off for /l %%I in (1,1,5) do ( if %%I==3 goto x echo A %%I :X echo B %%I ) を実行すると、 A 1 B 1 A 2 B 2 B %I と、制御変数が展開されないばかりか、ループが終了してしまう。 @echo off for /f \"tokens=1-9\" %%A in (\'dir\') do ( if \"%%D\"==\".\" goto X echo A %%A :X echo B %%A ) のような他の形式の for 文でも同様だ。 goto でどこに飛ぶかは関係なく、goto した時点で for 文の外に出たと言う認識のようだ。長い for 文を書くときは注意が必要。goto 文が回避できない時は、do 部をサブルーチン化して回避する。
■ 2004年02月15日 / コマンド実行時のエコー表示の謎 バッチススクリプトでは通常先頭で、@echo off するので、デバッグ時以外はコマンド実行のエコーを目にすることはあまり無い。 最近気づいたのだが、以下の一部のオプションなどについて、小文字で入力しても大文字になってエコー表示される(WXPで確認)。 (1) if の /i オプション および equ, neq, lss, leq, gtr, geq の各比較演算子 (2) for の /r /f /l 各オプション (3) for の /d オプションは何と d が表示されず / だけが表示される。これは /D と大文字で書いても同じだ 既に書いたように if と for の構文解析は早い段階で行われるので、何かその辺が関係しているのか?しかし、for の in や do は小文字のままである。if の /d に至ってはバグだろうが、何故 d だけ?どんなバグなんだろう。
■ 2004年02月14日 / [続]環境変数に改行文字をセット 前回からの続き。 set X=echo 1^ echo 2 という風に環境変数AAにセットした2行の命令をそれぞれ実行するにはどうするか。前回書いたように、 %X% !X! とも駄目である。 if や for や ( ) の構文解析が % による環境変数置換に先立って行われることを思い出して、 (%X%) (!X!) とやってみると前者で希望通りの動きをする。後者は !X! と同様1命令と扱われる。 横道にそれてしまったが、環境変数に複数の命令をセットして実行したいなら、別に改行をはさまなくても、 set Y=echo 1 ^& echo 2 と言う風に、& を ^ でエスケープして環境変数にセットすれば、 %Y% で実行できる。このときも、 !Y! だと、1命令と扱われ、1 & echo 2 が表示される。 本題に戻って、改行文字の使い道を考える。 環境変数に複数行を記録するときの1つのやり方は、tail コマンドでやったように環境変数名に連番をつけて複数の変数に1行ずつ記録するやり方がある。tail コマンドは記憶する内容が順次変動するのでそのやり方しかないが、そうでなければ1つの環境変数に改行文字を挟みながら記録するというのも考えられる。サンプルとしては以下のよう。複数行をセットした後、改行を @ に置換してみた。 @echo off setlocal enabledelayedexpansion set AA= for /f \"delims=\" %%I in (\'DIR\') do ( set AA=!AA!^ %%I ) set AA1=!AA:^ =@! set AA なお、今回いろいろ試していた時に発見したのだが、 set AA=echo 123 %AA% | more ⇒ これは期待通り 123 が表示される !AA! | more ⇒ これは \'echo 123\' がコマンド名と見なされエラー (!AA!) | more ⇒ これは \'!AA!\' がコマンド名と見なされエラー である。ちょっと不思議だが、この謎は解けるか?
■ 2004年02月11日 / 環境変数に改行文字をセット ^ による行の継続を試している時に、環境変数に改行を含んだ文字列をセットできることに気づいた。 set AA=123^ 456 のように間に空行をはさむと、AA に 123 改行 456 がセットされる。 set AAA=123^ 456^ 789 のようにすれば何行でも入る。ただし、この値を表示させようとして、echo %AA% では駄目である。1行目の 123 しか表示されないし、その後に何を書いても無視される。 echo %AA% xyz & date/t ⇒ 123 だけが表示 表示させるためには、遅延展開を有効にして、 echo !AA! xyz & date/t ⇒ 123 改行 456 XYZ 改行 日付 が表示 部分文字列も同様に、%による展開だと改行で途切れるが、改行を飛び越えればOK。 echo %AA:~1% ⇒ 23 が表示 echo %AA:~4% ⇒ 456 が表示(改行はCR+LFの2文字でなく1文字のようだ) echo !AA:~1! ⇒ 23 改行 456 が表示 次に、命令文としての実行はどうか? set X=echo 1^ echo 2 として、 %X% xyz & date/t ⇒ 1 のみが表示(2行目やxyzやdate/tは無視) !X! xyz & date/t ⇒ 1 改行 echo 2 xyz 改行 日付 が表示(echo 2は命令でなく最初のechoのパラメータになっている) つづく
■ 2004年02月10日 / findstrコマンドの使い方 findstr コマンドに、 /L 検索文字列をリテラルとして使用します。 /R 検索文字列を正規表現として使用します。 /C:文字列 指定された文字列をリテラル検索文字列として使用します。 というオプションがあるが、/L の「リテラル」と /C の「リテラル」の意味合いがよくわからなかったが、使い方がようやくわかった。 findstr "abc xyz" は、"abc xyz"というパターンを検索するのではなく、"abc" または "xyz" というパターンを検索する。unix の grep とは空白の意味が違う。 "abc xyz" というパターンを検索したい時は、findstr /C:"abc xyz" とする。 これはこれでいいのだが、空白を含む正規表現の時に困っていた。 findstr "^@echo off" だと、"行頭の@echo" または "off" にマッチしてしまう。かといって findstr /C:"^@echo off" だと正規表現で無くなり、"^@echo off" という山記号を含んだ文字列にしかマッチしない。findstr "^@echo\ off" や findstr "^@echo[ ]off" も駄目である。 もしかして、/L と /C の「リテラル」という言葉の意味が違うのではないかと思って、findstr /R /C:"^@echo off" とやってみたところ、うまく希望通りの動作をする。 /R(正規表現) と /L(単純文字列) の両オプションで、/R がデフォルトだが /C を指定すると /L がデフォルトに変わると言うことのようだ。/R を明示指定すればそちらが優先して正規表現となる。 まとめとしては、findstr を grep のように使うためには、/C: で文字列を指定して /R も指定するということだ。 12/26の記事で、行頭に空白以外の文字がある行の抽出が上手く行かなかったが、 findstr /r /c:"^[^ ]" で行ける。
■ 2004年02月08日 / tail コマンドの作成 昨日、の続き。 昨日書いた、:getline サブルーチンだが、「空行とEOFを区別できない」という set /p コマンドの仕様からああいう機能になってしまった。しかし、実際 tail コマンドを書こうとするとどうも使いにくい。そこで、まず普通の1行入力機能を持ったサブルーチンに書き直した。どうしても状態を覚えておく必要があるので、固有の環境変数を2個使う。 ---------ちょっと長いがここから-------- :getline if not defined __D ( set __N=0 :g1 set __D= set /p __D= if not defined __D ( if !__N! geq 100 ( set __N= set %1= set %2=1 goto :eof ) set /a __N=__N+1 goto g1 ) ) if %__N% gtr 0 ( set %1= set /a __N=__N-1 ) else ( set __N= set %1=!__D! set __D= ) set %2=0 ---------ここまで-------- ラベルg1の次の set __D= はこのロジックでは不要のはずだが、set /p を使う時はペアで必ず書くことにした。 機能は、 ・第一引数に入力行が返る。ただし空行ならnot definedとなる ・第二引数は EOF なら 1 が返る。EOF でなければ 0 が返る ・初回呼び出しの時点で、環境変数 __D が未定義であること 制約は昨日のと同じ。 これを使って、tailコマンドは、 @echo off setlocal enabledelayedexpansion set __D= if not defined LINE set LINE=20 if not exist \"%~1\" exit /b 1 call :tail < %1 goto :eof :tail set I=-%LINE% set J=0 :loop call :getline DATA EOF if \"%EOF%\" == \"1\" goto end set /a J=J+1 set SAVE%J%=!DATA! set /a I=I+1 set SAVE%I%= goto loop :end set /a I=I+1 if %I% leq 0 set I=1 for /l %%W in (%I%,1,%J%) do ( if defined SAVE%%W (echo !SAVE%%W!) else echo. ) goto :eof <<この後にさっきの:getlineのコードが入る>> と書けた。ファイルが長いとそれなりに時間がかかるが、途中でうっかりコントロールCを押さないように。(昨日の注意書きを参照)
■ 2004年02月07日 / head コマンドその後 以前、head コマンドについて書いたが、ふと思い立って続編の tail コマンドを書いてみようかと思った。 前回のスクリプトは行入力と head のメインロジックが混じっており、ちょっと汚いので1行入力部分を :getline というサブルーチンにしてみた。 :getline の仕様は、 ・第一引数に入力行が返る ・EOF なら第一引数が not defined となる ・第二引数に入力された行の前の空白行の行数が返る 制約は以前書いた通りだが、 ・パイプ入力は不可 ・連続した100行の空行はEOFと見なす ・ファイル末尾の空行は無視 あと、 ・処理中にctrl-Cを押すと、「バッチジョブを終了しますか(Y/N)?」に対して、ファイルがリダイレクト入力されているので、キーボードからYもNも入力できない @echo off setlocal enabledelayedexpansion if not defined LINE set LINE=20 if not exist \"%~1\" exit /b 1 call :head < %1 goto :eof :head set L=0 :loop call :getline DATA SPC if not defined DATA goto :eof set /a L=L+1+SPC if %L% gtr %LINE% goto :eof for /l %%I in (1,1,%SPC%) do echo. echo !DATA! goto loop :getline set %2=0 :g1 set %1= set /p %1= if not defined %1 ( if !%2! geq 100 goto :eof set /a %2=%2+1 goto g1 )
■ 2004年02月05日 / ^ によるquoteについて 昨日の記事において、 call echo %%%A%%% と書いたが、この時、^ で % をquote出来るのでは?と思って、 call echo ^%%A%^% としてみた(sh だと、eval echo \\$$A に相当)ところ、表示されるのは、A だけである。 どうも、^ によるquote処理より、%による展開のほうが先に行われるためのようだ。% による展開後、call echo ^%A となり(注1)、^ によるquote処理でこれが call echo %A となり、さらに call によるeval処理で echo A となり、これが実行されて A が表示される。 (注1: %% が % になり、%^% が空になる) ^ より % が早いことは、下記で行が継続することでも確認できる。 set A=^^ echo aaa%A% bbb
■ 2004年02月04日 / CALL命令の謎 10月29日記事において >また、call バッチファイル名 や call :ラベル名 だけではなく、 >call CMDビルトイン>コマンド や call 実行ファイル名、call 文書ファイル名 等としても、 >コマンドプロンプトから直接ファイル名などを入れたのと同じように実行されますね。 と書いたが、ちょっと試したところ、call は単に右辺のコマンドを実行するだけでなく、sh の eval 相当の機能もあるようだ。 バッチファイルの中で、 set A=PATH echo %%%A%%% とやると、当然のことながら、 %PATH% と表示されるが、 set A=PATH call echo %%%A%%% とやると、echo %PATH% が実行され、PATHの値が表示される。 set A=echo %%PATH%% %A% call %A% も同様。 容易に推測されるように、call を重ねて前置するとその回数だけ eval される。 これは何かの時に使えるかもしれない。ただし、if や for のような構造を持った命令は call の後では使えないようだ。
■ 2003年12月28日 / lsコマンドの作成(2) オプション解析を含む完成版である。対応オプションは少ないが、自分でよく使うのは、ls (linuxでは ls -vxF にaliasしてある)、ls -l 、ls -ltr くらいなので一応満足。shift が %* に影響を及ぼさないので、%* を使えないのがいつもながらいまいち。 @echo off setlocal enabledelayedexpansion set L= set T= set R= :option set opt=%1 if \"%opt:~0,1%\" NEQ \"-\" goto optend shift /1 :opt1 set opt=%opt:~1,999% if \"%opt%\" == \"\" goto option if /i \"%opt:~0,1%\" == \"L\" set L=L&goto opt1 if /i \"%opt:~0,1%\" == \"T\" set T=T&goto opt1 if /i \"%opt:~0,1%\" == \"R\" set R=R&goto opt1 echo invalid option \"-%opt:~0,1%\" echo %0 -l -t -r filespec... exit /b 1 :optend if \"%T%%R%\" == \"TR\" set opt=/OD if \"%T%%R%\" == \"T\" set opt=/O-D if \"%T%%R%\" == \"R\" set opt=/O-N if \"%T%%R%\" == \"\" set opt= if \"%L%\" == \"\" ( for /f \"delims=\" %%X in (\'DIR /W %opt% %1 %2 %3 %4 %5 %6 %7 %8 %9\') do ( set W=%%X if \"!W:~0,1!\" NEQ \" \" echo %%X ) ) else ( DIR %opt% %1 %2 %3 %4 %5 %6 %7 %8 %9|findstr/r \"^[0-9][0-9][0-9][0-9]\" )
■ 2003年12月26日 / lsコマンドの作成(1) headコマンドも放ったらかしだが、lsコマンドを作ってみたい。 現在は、DOSKEYマクロで、 ls=dir $*|findstr/r \"^[0-9][0-9][0-9][0-9]\" と定義しているが、見ればわかる通りこれは ls -l 相当である。 あと、オプション無し(マルチカラムでファイル名のみ)と、-t -r オプションも欲しい。 まずは、オプション無しの場合。dir /w の出力のヘッダとフッタを消したものを出力したい。findstr と正規表現を使って、 dir /w | findstr/r \"^[^ ]\" でいけるはずだが、何故か [ ] を含んだ行にしかマッチしない。 しょうがないのでスクリプトを書く。 setlocal enabledelayedexpansion for /f \"delims=\" %%X in (\'DIR /W %*\') do ( set W=%%X if \"!W:~0,1!\" NEQ \" \" echo %%X ) /W オプションは、ls の -x オプション相当である。ls のマルチカラム表示には -C オプションもあり、これは dir の /D オプションが相当するが、 /W の方が好みなのでそうしてある。 他のオプションは次回。
■ 2003年11月15日 / dirコマンドと特殊文字 10/29に書いた、 >あと、dir ^< や dir ^> の動作も謎。よくわからない。 >dir ^< だと一部のファイル・ディレクトリだけが表示されるが、どういう条件のものが表示されるのかわからない。 だが、 dir ^< →ファイル名にピリオドを含まないものを表示 dir ^> →ファイル名が1文字のものを表示 かな?
■ 2003年10月29日 / CALL 文パラメータ中の特殊文字 随分日にちが空いてしまいました。 CALL 文パラメータ中の特殊文字について (1) /? があればヘルプを表示 (2) | があればcallが実行されない (3) < または > があれば次の1ワードがバッチスクリプトに渡らない ということのようです。 (註: | < > の場合はリダイレクトやパイプと解釈されないよう適切に変数値としてまたは ^ でエスケープして指定された場合) また、call バッチファイル名 や call :ラベル名 だけではなく、call CMDビルトインコマンド や call 実行ファイル名、call 文書ファイル名 等としても、コマンドプロンプトから直接ファイル名などを入れたのと同じように実行されますね。 あと、dir ^< や dir ^> の動作も謎。よくわからない。 dir ^< だと一部のファイル・ディレクトリだけが表示されるが、どういう条件のものが表示されるのかわからない。ディレクトリ中の物理位置とかに関係するのだろうか。 また、dir ^< | more だと、「コマンドの構文が誤っています」になる。
■ 2003年09月08日 / 今までの残件は 今まで、別途調べてみる等としていた事項を整理しておく。 (1) 8/15の、環境変数遅延展開と構文解析の整理。これは、一度、8/28:変数置換の順序に書いたが、もう一段の突込みが必要と思っている。 (2) 8/19の、ディレクトリの存在チェックが、最後に \\ を付けるだけでいいのかどうか。今のところ問題なさそう。 (3) 9/5の、call 時に、実引数に | & < > 等があった場合の振る舞い。これは未着手。 (4) 昨日の、head コマンドのプロトタイプがファイルの内容に特殊な文字があった場合にも問題が無いかの確認。これも今のところ問題はなさそう。
■ 2003年09月07日 / head コマンドが作れるか? (4) さて、前回までのことはすっぱり忘れよう。 思い出すのは、8/8: 【続々】標準入力からの読み取りの回だ。 そこに書いたスクリプトを再掲する。標準入力からの標準出力へのコピーである。(100個連続した空行がないと仮定) なお、何度か、「2バイト空白でインデントしているのでコピーペーストするときに注意」と書いたが要らぬ注意だったようだ。 2バイト空白でインデントしていてもスクリプトは問題なく動く。 スクリプトA: @echo off setlocal enabledelayedexpansion set N=0 :loop set X= set /p X= if not defined X ( if %N% == 100 goto :eof set /a N=N+1 goto loop ) for /l %%I in (1,1,%N%) do echo. set N=0 echo !X! goto loop これを使って、リダイレクトでファイルが読めればうまく行くのではないか? スクリプトB: @echo off call :subr < %1 goto :eof :subr setlocal enabledelayedexpansion set N=0 set L=0 :loop set X= set /p X= set /a L=L+1 if %L% geq 20 goto :eof if not defined X ( if %N% geq 100 goto :eof set /a N=N+1 goto loop ) for /l %%I in (1,1,%N%) do echo. set N=0 echo !X! goto loop これでうまくいっているようだ。% %% ! < > | & 等を含んだファイルでもそのまま表示されている。もちろん空行もカットされない。 もう少しテストしてみて、問題なければ引数処理を整えれば出来上がりだ。 一旦終わり
■ 2003年09月06日 / お便りに答えて ファンさんからまたコメントをいただいた。いつも何か得るところがある。ありがとうございます。 >あれ、文が途中できれちゃった。処理系で適切なエスケープしてないんですね メールで届くコメント通知では全文が届いてました。 >for 文からの途中脱出は、遅延展開を有効にしておいて、サブルーチンからの戻りを判定して goto すれば可能ですよね。 遅延展開を使うなら、サブルーチンの必要も無くて for 文の中で行数を数えればいいんですが。 >その場合に \"! が含まれるとダメ\" というのは、\"空行がスキップされる\" と同様のレベルであきらめるしかないのでは。 今ちょっと考えてるのが駄目だったらそうですね。まあ、その時は、バッチスクリプトで head コマンドが書けなかったと言うことです。 ^ によるエスケープは知りませんでした。環境変数の内容をあらかじめ置換しておくということだと思いますが、ちょっと大変ですね。ただ、エスケープ機能自体は知っておけば今後何かの場面で使えそうです。遅延展開時の ! の環境変数へのセットにも効きました。 setlocal enabledelayedexpansion set A=^^^! % ^ が3個 (これは考えてやったらうまくいった)% echo !A! set A=^^^^^^^^^^! % ^ が10個 (これは考えたら7個かなと思ったら駄目だった。順に個数を増やしていって10個でうまくいった)% echo %A% >gawk \"NR <= %MAX%\" \"%FILE%\" で簡単に実現できることを、制約の多い(こういうことの得意でない) CMD.exe でがんばって >(しかも headコマンドのような単純なものを)やろうとすることに意義があるのか個人的には疑問です(実は自分で昔いろいろ >チャレンジしたことがあるのでそう思うのかもしれませんが)。 それを言っちゃあおしまいと言うか、CMD.exe でどこまで出来るかを探るのも目的のひとつなんで。 別に、head コマンドが必要だから作ってるわけじゃありませんから。 ファンさんのノウハウがうかがえるウェブページがあるなら是非見てみたい。
■ 2003年09月05日 / head コマンドが作れるか? (3) 昨日書いたように、途中で for を終了させるのは出来なさそうなので、ループをファイルの行数回だけまわるのは止むを得ないとすると、一定回数以上まわったら echo をスキップするようなことを考える。 スクリプト5: setlocal set N=0 for /f \"delims=\" %%A in (x) do ( call :sub %%A ) goto :eof :sub set /a N=N+1 if %N% lss 20 echo %* ファイル中に変な文字が無ければこれで期待通りの動きはする。しかし、ファイル中の文字によっては、 (1) /? という文字列があると、call のヘルプが表示される (2) & | 等の文字があるとその行は表示されない(サブルーチンが実行されない) (3) < > 等の文字があるとそれ以降右の文字が表示されない(サブルーチンに渡らない) などという不具合が生じる。(1) はわからないでもないが、(2) (3) については別途もう少し調べてみたい。 サブルーチンコールの際に引数を \" \" で囲えば呼ぶ段階では安全であり上記のような問題は生じない。 スクリプト6: setlocal set N=0 for /f \"delims=\" %%A in (x) do ( call :sub \"%%A\" ) goto :eof :sub set /a N=N+1 if %N% lss 20 echo %~1 しかし、8/28:変数置換の順序で書いた通り、引数の置換は早くに行なわれるため、< > & | 等の文字列が含まれると echo の段階でおかしくなるため、どのみちこれも駄目である。 つづく
■ 2003年09月04日 / head コマンドが作れるか? (2) for 文の中で行数をカウントできないとすると外でやるしかない。一旦外に出て帰ってくるわけだ。このとき、外でも for 制御変数が有効かどうかを調べてみる。 スクリプト1: for /f \"delims=\" %%A in (test.txt) do call :sub goto :eof :sub echo %%A スクリプト2: for /f \"delims=\" %%A in (test.txt) do ( goto :sub :ret ) goto :eof :sub echo %%A goto :ret まずスクリプト1だが、「%A」が行数分表示されるだけである。つまり for 制御変数は for 文の外では有効でない。スクリプト2に至っては「) の使い方が誤っています。」という構文エラーとなる。 :ret と ) の間に rem の行を入れるとエラーは出なくなるが、「%A」が1つ表示されただけで終わってしまう。for 文から goto で飛び出すとその段階で for 文が終結してしまうようだ。 いずれにせよ、だめである。そこで、表示だけ中でやって行数カウントを外でやることを考える。 goto で外へ行くのは駄目なことはわかったので、call で外のサブルーチンを呼ぶことにする。 スクリプト3: setlocal set N=0 for /f \"delims=\" %%A in (test.txt) do ( call :sub echo %%A ) goto :eof :sub set /a N=N+1 if %N% geq 20 exit /b exit /? によると、exit /b は「現在のバッチ スクリプトを終了する」と書いてあるので使ってみた。しかし、20行でなく全行表示されてしまう。サブルーチンの中で使った場合は、バッチスクリプトの終了でなく、サブルーチンの終了になってしまうようだ。ということは、goto :eof と等価である。意味ねー。 goto で飛び出れば for が終了するのでいいのでは? と、次のようにしてみる。 スクリプト4: setlocal set N=0 for /f \"delims=\" %%A in (test.txt) do ( call :sub echo %%A ) :out goto :eof :sub set /a N=N+1 if %N% geq 20 goto out が、駄目である。さっきと同じく全行が表示される。サブルーチンから飛び出ても for から飛び出たと判断してもらえないようだ。 つづく
■ 2003年09月03日 / head コマンドが作れるか? (1) 次なるコマンドとして、head コマンド (unixのhead相当;以下同) を作ろうとしてしばらく苦戦している。 ここに書きながら進めていきたいと思う。 まず cat コマンドのサブセットは比較的簡単に出来る。サブセットと言うのは、 (1) オプションなし (2) 標準入力サポートなし である。ファイルからの入力のみの機能だ。直感的には、こうなる。 @echo off for %%F in (%*) do for /f \"delims=\" %%X in (%%F) do echo %%X 表示には、for /f の機能を使ったので、空行はスキップされる (see. for/?) が、ここでは一旦、空行がスキップされてもまあいいとしよう。 ただし、上記だと、引数が \" で囲まれていた場合、内側の for が文字列指定とみなされるためうまくない。次のようにすれば \" で囲まれても大丈夫だ。一旦、%%~F と \" を外して囲みなおすことで、必ず一重の \" に囲まれた状態となり、usebackq オプションでそれをファイル指定とみなす。 @echo off for %%F in (%*) do for /f \"usebackq delims=\" %%X in (\"%%~F\") do echo %%X さて、これを head コマンドに進化させるためには行数を数えないといけない。単純に for の中で環境変数を使ったのでは、for の最初に置換されてしまうため駄目である。 かといって、for /? に書いてあるように遅延展開を使ったのでは、8/28:変数置換の順序に書いたように、! を含んだファイルを正しく表示できない。 つづく
■ 2003年08月28日 / 変数置換の順序 変数の置換や構文解析の順序だが、いろいろやってみた結果次のようである。 (1) 環境変数の置換、引数の置換が同順序で最初 (2) if/for/( ) 等の構造を持った文の構文解析、およびリダイレクトの判断等が同順序 (3) for制御変数の置換 (4) 環境変数遅延展開の置換 (5) echo 等の単純なビルトインコマンドの解析 例えば、遅延展開が有効な状態で、for制御変数の値に !が含まれたりするとそれによる置換のほうが後で起こるため、予想外の結果になったりする。具体的には、 for /f %%A in (file1.txt) do echo %%A は、file1.txt の内容を(空行を省いて)表示するが、setlocal enabledelayedexpansion で遅延展開が有効になっていて、file1.txt に ! が含まれていると正しくない表示となる。
■ 2003年08月23日 / スクリプト中のコメント 8/15: 【続】スクリプト中の未定義環境変数の謎で、 >「スクリプト中の任意の場所に、%\" と \"%で囲んだコメントが書ける」ということだ。 と書いたが、ちょっと間違っていた。%\" \"% で囲むというよりは、% % で囲まれていればいい。その時テストした際には、\" も特殊な働きをしたような気がしていたのだが気のせいだったようだ。 % に始まり、次の % または : までが環境変数名と見なされ未定義であれば空に置換される(コメントが書ける)。 遅延展開を有効にした場合、スクリプト中で、! は変数展開に使えるだけで、それ以外では消えてしまう。%は2つ連続して書くことで %自身を書くことができるが、! はいくつ書いてもだめだ。
■ 2003年08月19日 / 【続々々】ディレクトリかどうかの判断 昨日の記事に対し、 >私は、dir \"対象\" /a:d してみて errorlevel を見ます。 NUL に頼るより確実では? というコメントを頂いた。 DOSの時代から、ディレクトリのチェックは\\NULを使ってやるものという先入観を持っていた。 いまや、dirコマンドにも/adオプションができ、そっちが正攻法かなという気がしてきました。ということで前回のコメントで頂いた、\\NULをtypeしてみるというのは中途半端。 ただ、if existで一気にチェックできるコンパクトさは捨てがたい。ということで、もう少しあがいてみました。
これを見ると、 if exist %%X if not exist %%X\\ echo \"普通のファイル\" でいけそうである。 でも、こんなに簡単でいいのか何か見落としは無いのか気になる。引き続き調べてみることにする。
■ 2003年08月18日 / 【続々】ディレクトリかどうかの判断 体調が悪い(先週金〜土は38度の熱でうなりながら寝ていた)ので、小ネタ。 7/28:ディレクトリかどうかの判断で、パスが \" \" で囲まれると、if exist によるる \\NULの存在チェックがうまく行かないことに対する対策を書いた。 >(1) for制御変数にいれ、ショートネーム修飾子を使い、if exist %%~sA\\NULのようにする >(2) for制御変数にいれ、属性修飾子を使い、その先頭がdかどうかで判断 (追加) type \"%%~A\\NUL\" >NUL 2>NUL して ERRORLEVEL をチェック この(1)ではショートネームを使っている。ご存知の方もいるだろうが、ファイル名にショートネームを付けるのをレジストリで抑止できる。 HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\FileSystem\\NtfsDisable8dot3NameCreation である。通常は 0 なのでEnableの状態だ。これを、「スピード向上のため」とか称してdisableにする人がいるらしい。 その人の場合は、(1)の方法を使えない。まあ、他にもいろいろ使えないソフトが出てくるだろうから誤差の範囲か。 そんな人達ってどんな人達なんだろう?減量のために鼻毛を抜くようなもんじゃないかとおもうんだけど。
■ 2003年08月16日 / 隠し環境変数の謎 昨日書いた %\" \"% のコメントに機能に絡んで、\" \" に囲まれた環境変数名が使えないものかやってみた。が、 set \"AA\"=123 環境変数 AA が定義されていません となる。つまり、 set AA と同じ結果になる。ついでに、 set \"\"=123 をやってみたところ、set と同じように全環境変数が表示される。しかし、それだけではない。 =C:=C:\\temp =ExitCode=00000000 というのも表示される。つまり、=C: や =ExitCode という環境変数が存在するわけだ。念のため、 echo %=C:% %=ExitCode% とやってみたが、ちゃんと表示される。意味は名前から想像される通りのようだ。カレントドライブを Dドライブにすると、=D:という環境変数が増える。つまり、ドライブごとのカレントディレクトリをあらわしているものと思われる。cdするとちゃんと値が変わる。=ExitCode と ERRORLEVELの違いだが、ERRORLEVELがビルトインコマンドでも変化するのに対して、=ExitCode は、外部コマンド実行のときにだけ値が変化するようだ。 せっかく見つけた隠し機能だが、これといった使い道が思い浮かばないのが難点。
■ 2003年08月15日 / 【続】スクリプト中の未定義環境変数の謎 昨日の続きである。 環境変数名に空白が含まれても良いとか、数字でも良いとか書いたが、日本語ももちろんOKのようである。 またいくつかの記号も使える。なんと % も使える。スクリプト中では、% は %% と書くので、以下のようになる。 setlocal enabledelayedexpansion set %%=123 echo !%%! さて、本題である。昨日の記事で何かピンと来た人はいるだろうか。ヒントは以下の部分である。 >5行目であるが、これが難問。%\" と \"% に囲まれた部分が丸ごと消えてしまう。 >\" から \" までが1つの未定義環境変数名とみなされたのではないか。 これを別の言い方で言うと、「スクリプト中の任意の場所に、%\" と \"%で囲んだコメントが書ける」ということだ。日本語はもちろんほとんどの記号も書ける。書けない(コメントの終わりとみなされる)のは、% \" : の3つくらい。なお、遅延展開を有効にすれば、!\" と \"!の間にもコメントが書ける。 i%\"特に意味は無いがここにも書ける\"%f not exist %file% goto :eof % の展開は構文解析前なのでほんとにどこにでも書ける。しかし、 f!\"これはだめ\"!or %%I in (*) do echo %%I \'for\' は、内部コマンドまたは外部コマンド、 操作可能なプログラムまたはバッチ ファイルとして認識されていません。 という不思議なエラーメッセージが出る。 ! の遅延展開は構文解析後なので、if文やfor文のような構造を持った文の認識を違えさせる場所には書けない。 一方、set文や、echo文の場合は途中に書いても良いようだ。このあたりの違いはいずれ突っ込んでみたい。 さて、ひょんなことから任意の場所にコメントが書ける隠し機能が見つかったわけだ。 問題はこれがどれくらい嬉しいかだ。2000行のツールを書く人には嬉しいかもしれない。他の人だと、rem で十分なのかも。 私?私は実験のネタが見つかって嬉しい。
■ 2003年08月14日 / スクリプト中の未定義環境変数の謎 昨日、スクリプト中での未定義環境変数の振る舞いが良くわからないと書いた。 実際 set _= if defined _ if not \"%_%\" == \"%_: =%\" set _=\"%_%\" と書いて実行すると、 コマンドの構文が誤っています。 if defined _ if not \"\" == \" =_\" となってしまう。なぜこうなってしまうかをもう少し調べてみた。変数名 _ が1文字なのが影響を与えているかもしれないので、未定義環境変数には、XYZを使った。
コマンドプロンプトで直接未定義環境変数をecho %XYZ%とすると、%XYZ%とそのまま表示されるが、スクリプト中では空に置換されれるようである(表3行目参照)。 4行目は環境変数中の文字列置き換えの形式だが、形式全体が空に置換されるのでなく、コロンまでが空に置換され、置き換え文字列はそのまま残されてしまう。その後の%は消える。 5行目であるが、これが難問。%\" と \"% に囲まれた部分が丸ごと消えてしまう。\" から \" までが1つの未定義環境変数名とみなされたのではないか。 以上のことから、 if defined _ if not \"%_%\" == \"%_: =%\" set _=\"%_%\" が if defined _ if not \"\" == \" =_\" に変数置換されることがわかる。これでは、if文の後半のコマンド部が無いため、エラーになる。 で、今気づいたが、環境変数名は空白を含んでよい。 set A A=123 echo %A A% で、123 が表示される。数字でもいいみたい。 set 123=popo echo %123% で、popoが表示される。
■ 2003年08月13日 / お便りに応えて コメントにはコメントでと思いましたが、本文のネタが無いので本文でお返事します。 >バッチコマンドスクリプトで個人用ツールをちょくちょく組んでいるものです。長いものだと 2000行近くなることもあります。 これには驚きました。BATにそのレベルのものを記述するだけの言語能力があるとは思ってませんでした。 「個人用ツール」とのことなので単純バッチの連続とかとは違うんですよね。 うーん。わたしも2000行レベルだと他の言語に逃げる気がします。vbscriptかrubyか、あるいは bash on cygwin。 >・for 文の do の後を () で括って長々と書くよりは、丸ごとサブルーチンにして call で引数渡しにした方が制約が少ないのではないでしょうか。(for の do ( ) の中で参照する変数に ( や ) が含まれているとおかしくなるとか) 通常の環境変数の置換は構文解析前なのでそうでしょうが、遅延環境変数の展開を有効にして、! !で置換すれば問題は起きないと思います。また、for制御変数の置換も構文解析後のようで、特殊文字が含まれても大丈夫そうです。 >・^ で継続行がかけるので、 set BLT= を何段かに分けているところは ^ を使った書き方の方がすっきりしませんでしょうか。 これは知りませんでした!!。どこにも書いてないと思ったら、CMD /?に、特殊文字一覧としてちょろっと登場してますね。 ちょっと使ってみましたが、奇数個の\"のあとでは継続の機能を失ってしまいます。ということは、\" \"に囲まれた文字列を継続行で長く伸ばすことは出来ないようです。どんなところに書けるかは研究の余地ありと思います。どっかに書いてあるかな。 キーワードであるifのiとfの間で^によって行を分けられるとは思いませんでしたが出来ますね >・PATH環境変数の探索は %~$PATH:x% などの形式でコマンドにまかせてはだめでしょうか。 これはだめです。要求仕様が for PATHEXTのそれぞれについて for PATHのそれぞれを調べる なら、2番目のfor文を%~$PATH:x%を使って置き換えることは出来ますが、これだと、CMD.EXEの仕様に合わない。 for PATHのそれぞれについて for PATHEXTのそれぞれを調べる というのがコマンド探索の仕様です。この場合は、%~$PATH:x%の出る幕はない。 >・未定義の環境変数に対する %xxx:yyy=zzz% 形式の参照は、そのまま全体が返るのではないでしょうか。構文エラーになりますか? そう思うんですけどね。おかしい。コマンドライン直接なら確かにそうなる。でもファイルの中に set _= if defined _ if not \"%_%\" == \"%_: =%\" set _=\"%_%\" と書いて実行すると、 コマンドの構文が誤っています。 if defined _ if not \"\" == \" =_\" って表示される。これは、どうも次回の宿題か? >・cygwin を入れていると、 which がそのまま使えて便利です(しかし cygwin のパスで表示されますが)。 win95時代はcygwinを入れてましたが、Win環境との親和性が悪いのが気になってました。W2K以降、コマンドプロンプトのコマンド機能が十分になり、不十分とはいえ、ヒストリ、アリアス、コンプリーションも使えるので bash の魅力も薄れ、BorlandからフリーのCコンパイラが出、gccの魅力も薄れ現在はcygwinは入れなくなってしまいました。 とはいえ、現在unix由来のソフトとして meadow、ruby(mswin32版)、gawk、less、ack、nkf なんかは良く使ってますね。
■ 2003年08月12日 / WHICHコマンド (6) 〜最後の聖戦〜 最終回は残りの部分。1回目の仕様で書いた、 (5) PATHをサーチして見つけた最初の実行ファイルのフルパスを環境変数「 _ 」にセットする の部分であるが、最後なので全文を掲載する。例によってインデントは2バイト空白なのでコピーペーストする人は注意。 PATHをサーチして見つけた部分ということで、20-22行および28-30行である。最初かどうかは、最初に02行で _ を未定義にしておいて、if not definedで調べている。endlocal/setlocalについては7/24:setlocalしたスクリプトから外部の環境変数をセットも参照のこと。 このスクリプトで使用しているローカル変数はBLTだけであるが、8/1:setlocal/endlocalスコープの謎 (結)で書いた通り、12行目の%BLT%の置換は08行のfor文の時点で行われるため、08行目の後にendlocalを1回だけ書いておいても良い。実は2回目以降のsetlocalは不要である。 01 @echo off 02 set _= 03 setlocal 04 set BLT=ASSOC BREAK CALL CD CHDIR CLS COLOR COPY DATE DEL DIR ECHO 05 set BLT=%BLT% ENDLOCAL ERASE EXIT FOR FTYPE GOTO IF MD MKDIR MOVE 06 set BLT=%BLT% PATH PAUSE POPD PROMPT PUSHD RD REM REN RENAME RMDIR 07 set BLT=%BLT% SET SETLOCAL SHIFT START TIME TITLE TYPE VER VERIFY VOL 08 for %%X in (%*) do ( 09 for /f \"delims=\" %%A in (\'DOSKEY /MACROS\') do ( 10 for /f \"delims==\" %%B in (\"%%A\") do if /i \"%%B\" == \"%%X\" echo %%A 11 ) 12 for %%A in (%BLT%) do if /i \"%%X\" == \"%%A\" echo %%X cmd.exe builtin 13 if not \"%%~xX\" == \"\" if exist %%X if not exist %%~sX\\NUL echo %%~X 14 for %%S in (%PATHEXT%) do if exist %%X%%S if not exist %%~sX%%S\\NUL echo %%~X%%S 15 if \"%%~X\" == \"%%~nxX\" ( 16 for %%P in (\"%PATH:;=\";\"%\") do ( 17 if not \"%%~xX\" == \"\" if exist %%P\\%%X ( 18 for /f \"delims=\" %%W in (\"%%~P\\%%~X\") do if not exist %%~sW\\NUL ( 19 echo %%~P\\%%~X 20 endlocal 21 if not defined _ set _=%%~P\\%%~X 22 setlocal 23 ) 24 ) 25 for %%S in (%PATHEXT%) do if exist %%~sP\\%%X%%S ( 26 for /f \"delims=\" %%W in (\"%%~P\\%%~X%%S\") do if not exist %%~sW\\NUL ( 27 echo %%~P\\%%~X%%S 28 endlocal 29 if not defined _ set _=%%~P\\%%~X%%S 30 setlocal 31 ) 32 ) 33 ) 34 ) 35 ) 36 endlocal 37 if not defined _ goto :eof 38 if not \"%_%\" == \"%_: =%\" set _=\"%_%\" 37行で、_ が未定義か判断してそうなら終了している。 38行で、_ に空白が含まれているかを判断して、そうなら \" \" で囲っている。空白を含むかの判断は、: 修飾子で空白を削除して元の値と等しいかどうかで行っている。 この2行は、 if defined _ if not \"%_%\" == \"%_: =%\" set _=\"%_%\" と1行で書けそうである。しかし、環境変数が未定義のときの : 修飾子の振る舞いからこれは構文エラーとなる。遅延環境変数を使って、 if defined _ if not \"!_!\" == \"!_: =!\" set _=\"!_!\" と書けば問題ないが、スクリプト中で遅延を有効にできるのはsetlocalコマンドしかないので、かえって長くなってしまう。 環境変数未定義時の : 修飾子の振る舞いはいまいちよくわからないので、いずれ調べてみる。 以上で連載は終わりである。 いよいよネタが無いので、ネタ求む。トリビアの種と違って賞品は出ないが。
■ 2003年08月11日 / WHICHコマンド (5) 〜コマンド探索(後編)〜 5回目は、PATH環境変数のサーチ。基本的には前回と同じチェックを、PATHの内容を順次付加しながら行っていく。今回は最初にスクリプトを示す。 全体については2回目を参照。そこで書いた【パスのサーチ】というのが以下である。入力された文字列は、%%Xに入っている。 01 if \"%%~X\" == \"%%~nxX\" ( 02 for %%P in (\"%PATH:;=\";\"%\") do ( 03 if not \"%%~xX\" == \"\" if exist %%P\\%%X ( 04 for /f \"delims=\" %%W in (\"%%~P\\%%~X\") do if not exist %%~sW\\NUL ( 05 echo %%~P\\%%~X 06 【後処理】 07 ) 08 ) 09 for %%S in (%PATHEXT%) do if exist %%~sP\\%%X%%S ( 10 for /f \"delims=\" %%W in (\"%%~P\\%%~X%%S\") do if not exist %%~sW\\NUL ( 11 echo %%~P\\%%~X%%S 12 【後処理】 13 ) 14 ) 15 ) 16 ) 01行ではまず、入力された文字列%%Xにパス部分(\\)が含まれているかどうかを判断している。含まれていればPATH環境変数のサーチは行われないので以下の処理をスキップする。for制御変数に対して直接\\の有無をチェックできないので、ファイル名部分(~n)と、拡張子部分(~x)を取り出して、それが元の文字列全体と等しければ、元の文字列にパス部分が無かったことがわかる。空白が含まれている可能性があるので、\" \"で囲んで比較する。 02行では、PATH環境変数をここの要素を\" \"を囲みながら、for /f文で展開する。この部分の詳細は、7/22:%PATH%の要素分解を参照。 03-04行でのチェックは、前回と同じ。 「もし、拡張子があり、かつ、%%Pのディレクトリに%%Xが存在して、かつ、それがディレクトリでなければ、パス名を表示する」 ディレクトリかどうかの判断はちょっとやっかい。7/28:ディレクトリかどうかの判断では、ショート形式のフルパスが必要である。フルパスを構成するのは、%%P、%%Xの2つのfor制御変数だが、これは、それぞれショート形式修飾子をつけて連結した%%~sP%%~sXは知りたいものとは一致しない(自分で実験ください)。 求めるショート形式を得るためには、一旦ひとつのfor環境変数にセットする必要がある。 これが04行である。%%P、%%Xから\" \"を除去(構文エラーを避けるため)して連結し、全体を%%Wにセットする。 後は前回の通り、if not exist %%~sW\\NULでディレクトリでないことを確認できればOKである。 09行目からは、PATHEXTの内容を順次付加して前回と同じようにチェックする。今回のフルパス名を構成するのは、%%P、%%X、%%Sの3つである。これらを\" \"を除去して連結し、全体を%%Wにセットする。 チェックがOKであれば、実行されるファイル名を、\" \"を除去して表示する。 次回は、サーチ結果の環境変数 _ へのセット部分である。 つづく
■ 2003年08月10日 / WHICHコマンド (4) 〜コマンド探索(前編)〜 4回目は、PATH環境変数を使ったサーチの前にcmd.exeが行う実行ファイルチェックの部分について。 このケースには、 (1) コマンドとして入力された文字列がそのまま実行ファイル名である場合 (2) コマンドとして入力された文字列にPATHEXTの内容を付加したものが実行ファイル名である場合 の2パターンがある。 (1)のパターンの場合には、入力された文字列に拡張子が含まれていることが必要である。ファイル名の部分にピリオドが含まれていない場合は、そのファイルが存在しても実行されない。具体的に言うと、カレントディレクトリにabcというファイルが存在したとして、abcと入力してもフルパスで入力しても実行されない。一方、abc.deというファイルが存在したら、abc.deと入力すると実行される。ここで、「実行される」とは、 〜〜 は、内部コマンドまたは外部コマンド、 操作可能なプログラムまたはバッチ ファイルとして認識されていません。 というメッセージが表示されないという意味である。場合分けすると、 (a) 実行可能プログラムである場合 (b) 拡張子が何らかの実行可能プログラムに関連付けられた(ドキュメント)ファイルである場合 (c) 拡張子が実行可能プログラムに関連付けられていないため実行プログラム指定を求めるダイアログが表示される場合 の3パターンがある。 入力した文字列のファイルが存在しない場合には、(2)のパターンとなり、PATHEXT環境変数にセットされた拡張子が順に付加されて(1)の場合と同じ実行ファイルのチェックが行われる。このとき、元の文字列に拡張子があってもその後ろに付加され、この付加されたほうが新たな拡張子となる。例えば、aaa.jpgの入力に対して、aaa.jpgが存在せず、aaa.jpg.comが存在すればそれが実行される。 いずれのパターンの場合でも、入力された文字列に\\が含まれていない場合はカレントディレクトリのファイルがチェックされる。これがunixと大きく違う点だ。unixの場合、トラップにかからないよう、PATH環境変数にはカレントディレクトリを含めないのが常識となっているが、windowsの場合は(DOSの時代から)、そんな考慮など知らん顔でまずカレントディレクトリがチェックされてしまう。カレントディレクトリのファイルを実行する際、unixでの./a.outのように先頭に . を付けなければいけないのを嫌ってのことであろう。セキュリティより見た目の利便性を優先するのがMicrosoftの伝統だから一貫性はある。 さて最後に上記をチェックするスクリプトである。全体については2回目を参照。そこで書いた【カレントディレクトリおよびパス指定時のサーチ】というのが以下である。入力された文字列は、%%Xに入っている。 if not \"%%~xX\" == \"\" if exist %%X if not exist %%~sX\\NUL echo %%~X for %%S in (%PATHEXT%) do if exist %%X%%S if not exist %%~sX%%S\\NUL echo %%~X%%S if notとかあってわかりにくいので、最初の行を日本語で書くと、 「もし、%%Xに拡張子があって、かつ、%%Xの実体が存在して、かつ、それがディレクトリでなければ、それが実行される」 ディレクトリかどうかのチェックは、7/28:ディレクトリかどうかの判断およびその翌日の続編を参照。 次の行は、%%Xの後にPATHEXTの内容を順に付加した文字列に対して最初の行と同じチェックを行う。 実行ファイル名の表示の際には入力された文字が\" \"で囲まれていてもそれを~修飾子ではずして表示している。これは好みの領域である。 なんか、長くなってしまった。次回はいよいよ、PATH環境変数にセットされたディレクトリのサーチである。 つづく
■ 2003年08月09日 / WHICHコマンド (3) 〜DOSKEYマクロのサーチ〜 3回目は、DOSKEYマクロのサーチである。関連したスクリプト部分を以下に示す。2回目も参照。 01 【前処理】 02 for %%X in (%*) do ( 03 for /f \"delims=\" %%A in (\'DOSKEY /MACROS\') do ( 04 for /f \"delims==\" %%B in (\"%%A\") do if /i \"%%B\" == \"%%X\" echo %%A 05 ) 06 【ビルトインコマンドのチェック】 07 【カレントディレクトリおよびパス指定時のサーチ】 08 【PATHのサーチ】 09 ) 10 【後処理】 03行のfor /f文では、\' \'とコマンド指定なのでDOSKEY /MACROSコマンドが実行され、マクロ定義内容が1行ずつ出力される。この出力をfor /f文が捕まえて、制御変数に1行ずつセットする。制御変数へのセットの仕方は、delims=とパラメータで指定されているためデリミタ無しつまり行全体が%%Aにセットされる。 04行のfor /f文では、その%%Aの値をdelims==の指定で制御変数にセットする。文字列指定なのでループはしない。つまり、イコールをデリミタとしてその前(マクロ名に相当)が%%Bにセットされる。tokens=パラメータが指定されていないので、その後ろ(マクロ提示の実体)はどこにもセットされない。 04行のif文ではマクロ名の%%Bとオペランド%%Xを/i指定により大文字小文字を無視して比較し、一致すればそれはDOSKEYマクロ名であるので、その定義行である%%Aを表示する。 マクロ名が見つかったわけなので、03行〜05行のループから抜け出すべきだがスクリプトが長くなるため省略した。ループ回数はせいぜい数回だから。 04行のfor /f文の形式は、文字列のデリミタによる分解の際の定石である。ループの意味は無い。 つづく
■ 2003年08月08日 / 【続々】標準入力からの読み取り 8/5記事においていったんギブアップした標準入力の読み取りではあるが、ちょっと思いついたので、WHICHコマンドの連載を中断してこっちを書く。 set /pによる標準入力読み取りにおいて空行とEndOfFileが区別できないのは確かである。しかし、空行には次にいずれ空行で無い行がつづく(可能性が高い)のに対して、EndOfFileはそれ以降ずっとEndOfFileであるという違いがある。 そこで、ファイル中に100行以上の空行が続くことは無いという前提を置くと、次のようなスクリプトが書ける。100でまずければもっと増やせばいい。 なお、2バイト空白でインデントしてあるのでコピーペーストして試してみる人は注意されたし。 @echo off setlocal enabledelayedexpansion set N=0 :loop set X= set /p X= if not defined X ( if %N% == 100 goto :eof set /a N=N+1 goto loop ) for /l %%I in (1,1,%N%) do echo. set N=0 echo !X! goto loop 注意だが、:loopの次とその次のX=のイコールの後はすぐ改行。また、後ろから4行目のecho.は、空行出力の定石である。後ろから2行目の!X!については7/27記事:【続】変数中のリダイレクト文字を参照。 残念ながら、ファイル末尾の空行は削除されてしまうがこれは致し方ない。通常は困ることはないであろう。 これで借りは返したぞ。
■ 2003年08月07日 / WHICHコマンド (2) 〜メインループとビルトインコマンド〜 昨日の1回目はDOSKEYの説明で疲れたので今日の、2回目は軽く、全体のメインループと、比較的簡単なビルトインコマンドサーチ部分を書く。【 】の部分は3回目以降の説明である。便宜上、行番号をつける。また、インデントを2バイト空白で付けているので、コピーペーストして実行してみる場合は注意されたし。 01 @echo off 02 set _= 03 setlocal 04 set BLT=ASSOC BREAK CALL CD CHDIR CLS COLOR COPY DATE DEL DIR ECHO 05 set BLT=%BLT% ENDLOCAL ERASE EXIT FOR FTYPE GOTO IF MD MKDIR MOVE 06 set BLT=%BLT% PATH PAUSE POPD PROMPT PUSHD RD REM REN RENAME RMDIR 07 set BLT=%BLT% SET SETLOCAL SHIFT START TIME TITLE TYPE VER VERIFY VOL 08 for %%X in (%*) do ( 09 【DOSKEYマクロのサーチ】 10 for %%A in (%BLT%) do if /i \"%%X\" == \"%%A\" echo %%X cmd.exe builtin 11 【カレントディレクトリおよびパス指定時のサーチ】 12 【PATHのサーチ】 13 ) 14 endlocal 15 【環境変数 _ の後処理】 01は定石。02は、環境変数 _ への結果セットに備えあらかじめ未定義状態にしておく( = の後は空白をあけず改行)。03および14、ワーク変数を使うためsetlocalとendlocalで囲む。 04から07で、ローカル変数BLTにビルトインコマンドを空白区切りでセットする。好みで ; 区切りでも良い。 HELPコマンドで表示されるコマンドのうち、COMやEXEの実行ファイルが存在しないものをビルトインコマンドとしてある。もしかしたらこれら以外に隠しコマンドがあるかもしれないけど。 08と13がメインループである。複数個のオペランドに対してそれぞれ、DOSKEYマクロサーチ、ビルトインコマンドチェック、カレントディレクトリサーチ、PATHの各ディレクトリサーチを順次実行する。見つからなかった場合のエラー表示は特に行っていない。 10のforループでビルトインコマンドのチェックを行っている。大文字小文字の同一視のために、ifの/iオプションを使って比較し、一致すれば cmd.exe builtin と表示する。 つづく
■ 2003年08月06日 / WHICHコマンド (1) 〜仕様とDOSKEYマクロ〜 実験のネタが尽きてきたので、これから何回かに分けて自作のWHICH.BATというスクリプトの解説でもしようかと思います。 まずは、仕様から。 unixにwhichというコマンドがある。シェルがコマンドを実行する際にPATH環境変数をサーチするが、その実際に実行される実行ファイルのフルパス名を表示する。組み込みコマンドや別名定義名、関数定義名であればその旨を表示する。 今回紹介するWHICH.BATでは、それにならった機能を持つが一部機能を違えてある。 (1) PATHEXT環境変数を参照し、拡張子を付加してサーチする(CMD.EXEの機能と合わせるため) (2) 該当する実行コマンドが複数ある場合はすべて表示する(そのほうが便利と思う) (3) シェルの別名定義に代わってDOSKEYマクロ定義を参照する(CMD.EXEの機能と合わせるため) (4) その他、CMD.EXEのコマンド探索方法に合わせる (5) PATHをサーチして見つけた最初の実行ファイルのフルパスを環境変数「 _ 」にセットする (unixでは、ls -l `which ruby` のようなことが出来るが、CMDでは出来ない。この機能で、which ruby 実行後 dir %_% と出来る) 最初に探索するのは、DOSKEYマクロである。これはCMD組み込みコマンドに優先する。 例えば、DOSKEY rm=del $* と実行しておくと、del コマンドと同じように rm コマンドを使える。 以下、DOSKEYマクロについて少々解説。まあ、これはきっとどこかのウェブページにも書いてあると思うのでここでわざわざ書かなくてもいいかもしれないが。 マクロは行頭だけで有効。空白があってもだめである。if や for do、|、&、&&、|| 等の後ろに書きたいときは、( を書いてブロックにし、次の行の行頭から書く。例えば、rm が上記のようにマクロ定義されているとして、 if exist %AAA% ( rm %AAA% ) で、%AAA%のファイルを削除できる。これを、 if exist %AAA% rm %AAA% と書くと、rm が行頭でないためエラーになる。 あと、BATスクリプトの中でも使えない。BATスクリプトの中でDOSKEYで定義してもだめ。unixでもシェルスクリプトの中でalias名を使うことはあまり無い(alias名が効かないように書くのが普通かな)のでまあいいか。 コマンドプロンプトを開く際に、最初にいくつかのマクロ定義を行うと便利である。これには、DOSKEY /MACROFILE=C:\\xxx\\yyy を実行すればいい。C:\\xxx\\yyy (私は C:\\usr\\doskey.ini に置いてます) には、 CD=CD /D $* ED=C:\\usr\\tpad\\TeraPad.exe $* VI=C:\\usr\\vim3j21b\\VIM32.EXE $* 等と、1行に1つずつマクロ定義(マクロ名=コマンド)を書いておく。詳しくは コマンドプロンプトを開く際に自動的にこのコマンドを実行するには2通りの方法がある。 1つめはレジストリを設定する方法で、6/14記事もしくは CMD /? を参照。 2つめは、レジストリをいじりたくない人向き。コマンドプロンプトのショートカットで、リンク先が CMD.EXE となっているところを、 CMD.EXE /K DOSKEY /MACROFILE=C:\\xxx\\yyy とする。ただし、この方法だと、コマンドプロンプト起動時の、 Microsoft Windows XP [Version 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp. というようなメッセージが出ない(まあ、出なくてどうということはないですが)。 つづく
■ 2003年08月05日 / 【続】標準入力からの読み取り 7/21標準入力からの読み取りにおいて、set /p コマンドによる標準入力からの読み取りをパイプから行った場合、タイミングが合わないので数行おきにしか読めないと書いた。これを確かめてみた。 1行出力ごとに1秒待つプログラムを作成した。(プログラムはフリーのCコンパイラBorland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borlandで作成。簡単なプログラムなのでソース掲載は省略)これをwaitcopy.exeとする。前回のzz.batと組み合わせて、 dir | waitcopy | zz 結果は、 \" ドライブ C のボリューム ラベルは XP です\" \" ボリューム シリアル番号は 74E4-83F1 です\" と2行表示した後、固まったようになり(*注1*)、数十秒後終わる。おかしい。 普通に dir とやってみると、dir の3行目は空行である。あやしい。 set /p X= に対して空行を入力してみた。 echo %ERRORLEVEL% 1 駄目だ。zz.bat を再掲するが、 @echo off setlocal :loop set /p X= if not %ERRORLEVEL% == 0 goto :eof echo \"%X%\" goto loop つまり、空行ではERRORLEVELが1となるため、EOFと見なされてまい、スクリプトが終わっていたわけだ。 空行とEOFを見分ける手段は無いのか?色々考えて、次のように書き直してみた。 @echo off set X= setlocal :loop set X= set /p X= if not defined X goto :eof echo \"%X%\" goto loop set /p の前に一旦、X を未定義化しておき、set /p の後で定義されたかどうかで判断してみた。 でも、結果は同じ。 今日の結論: (1) タイミングをゆっくりにすれば、パイプからでも読める(*注2*) (2)空行入力とEOFを区別する手段は無い なかなか、「明日使えるトリビア」にはならない。トリビアの種募集中。 注1: fputs時にエラーチェックするのをサボったため。エラーチェックするときっとbroken pipeのエラーになるでしょう。 注2: 途中にはさむCプログラムを、「読んだのが空行だったら空白1文字を書く」ように改造し、空行が発生しないようにして問題ないことを確認した。
■ 2003年08月01日 / setlocal/endlocalスコープの謎 (結) おおボケでした。 for文実行時に、doの後のカッコの終わりまでの範囲について、%A% の変数展開がされるため for %%I in (1 2 3 4) do ( if %%I == 2 ( endlocal echo now global setlocal ) echo %%I,123 ) となっていただけです。ローカル変数の値が保存されるわけではない。 enabledelayedexpansion を指定して、% でなく ! で変数展開すると7月24日に書いた通りとわかります。
■ 2003年07月31日 / setlocal/endlocalスコープの謎 (1) 7月24日に、 >また、その後でまたsetlocalしたい場合、最初のsetlocal〜endlocalのスコープとは独立なので >最初のスコープのローカル変数を参照することは出来ない。 と書いた。これは、 set A=xyz setlocal set A=123 endlocal setlocal echo %A% が、123 でなく xyz と表示するということだ。実際そうなる。 つまりendlocalを実行した時点でそれまでのローカル変数が捨てられ、2回目のsetlocalでは別のローカル変数の環境が作られたわけだ。 このことから、 set A=xyz setlocal set A=123 for %%I in (1 2 3 4) do ( if %%I == 2 ( endlocal echo now global setlocal ) echo %%I,%A% ) は、2回目のループで一旦endlocalが実行されるため、Iが2以降ではAの値として xyz が表示されると予想したが、実際は 123 が表示される。 つまり、この場合endlocalを実行してもローカル変数が捨てられていない。スコープが動的でなく、レキシカルなものだとしても、setとechoは別のスコープに属するのではないか?いったいローカル変数のスコープはどのように決定されているのだろうか。 ・・・続く (はず)
■ 2003年07月30日 / 【外伝】ページブラウザ(PGBROW32.EXE) 昨日の記事で、\" \" で囲むとおかしくなるケースで1つ思い出した。 ページブラウザというテキストビューアーがある。Win3.1のころからあるが、JIS/SJIS/EUCJPの3種の文字コードを認識・表示し、CR/NL/CRLFの3種の改行コードを自由に変換できる機能を持っているので今だに使ってます。 このソフトも、空白を含んだパスのファイルを扱えないと長い間思ってたんだが、実は違って、\" \"で囲まれたパスがだめなのに最近気づいた。 ドラグ&ドロップやsendtoからの起動では、空白を含んだパスは勝手に \" \" で囲まれてプログラムに渡される。 したがって、\" \" をはずしてからPGBROW32に渡してやることで今までアクセスできなかった空白を含んだパスのファイルも扱えるようになる。 下の1行だけのPGBROW.BATを作って、このBATファイルのショートカットをsendtoフォルダに入れておくと便利。 pgbrow32.exe %~1 (ショートカットのプロパティで実行時の大きさを最小にするのを忘れないように)
■ 2003年07月29日 / 【続】ディレクトリかどうかの判断 if exist %A%\\NUL を使ったディレクトリ判断について、 昨日、空白を含んでいるとだめだと書いたが、それは正しくないようだ。
偽となっているものはすべて本当は真のはず。 つまり、\" \" で囲まれていると、NULが存在しないと間違った判断をしてしまう。 NUL,CONのような特殊名でないファイルは \" \" で囲んでも正しく判断される 空白を含んでいると \" \" で囲まざるを得ないため結果的には間違った判断となる。 つまり、NUL,CONのようなデバイスファイルの存在チェック時には \" \" で囲んではいけない。 ってどうもバグのように思えるので、いずれもう少し調べてみたい。 対処法は、昨日書いたものに加えて、コメントでいただいた次のものでもOK。 type %A%\\NUL >NUL 2>NUL & if %ERRORLEVEL%==0 echo ディレクトリ typeでなくcopy等でもいい。とにかく、if existだけが特殊なようだ。
■ 2003年07月28日 / ディレクトリかどうかの判断 変数Aに何らかのパス名が入っているとして、それがディレクトリかファイルかの判断には、 if exist %A%\\NUL echo ディレクトリ を愛用していた。しかし、Aが空白を含んでいるときは、これが正しくない。%A%\\NULが存在しないのだ。 バグではないか?(未調査) 対応方法としては、 (1) for制御変数にいれ、ショートネーム修飾子を使い、if exist %%~sA\\NULのようにする (2) for制御変数にいれ、属性修飾子を使い、その先頭がdかどうかで判断 set W=%%~aA & if \"!W:~0,1!\" == \"d\"
■ 2003年07月27日 / 【続】変数中のリダイレクト文字 変数の遅延展開が、構文解析後に起こることを考えると、7/20メモの「変数中のリダイレクト文字」の奇妙な振る舞いについても対処が出来そうである。 @echo off setlocal enabledelayedexpansion set /p A=A: echo (!A!) というスクリプトを作り、Aに色々な文字、< > | & && || \" などを組み合わせて設定してみてもちゃんとechoされるようだ。 変なファイルが出来たりはしない。今後はローカル変数参照は基本的に遅延展開を使うべきかな。
■ 2003年07月26日 / 【続】環境変数のFOR制御変数へのセット 昨日書いた for /f \"delims=\" %%A in (\"%A%\") do だが、環境変数Aに \" が奇数個含まれるとおかしくなるので駄目。 for /f \"delims=\" %%A in (\"aaa\"bbb\") do のようになって構文解析が失敗するため。 遅延展開を使って、 for /f \"delims=\" %%A in (\"!A!\") do とすると構文解析の後で変数Aの値が置換されるためOKのようである。 色々なパターンで試してみたが今のところAがどんな値でも良さそう。 遅延展開を行う手段は、下記のいずれか (1) setlocal enabledelayedexpansion (2) レジストリを変更( cmd /? を参照) (3) cmdを /v:on オプション付で実行
■ 2003年07月25日 / 環境変数のFOR制御変数へのセット 環境変数の値をそのままFOR制御変数にセットしたい場合、 for /f \"delims=\" %%A in (\"%A%\") do for /f \"tokens=*\" %%A in (\"%A%\") do 前者ではそのままセットされる。 後者では、先頭の空白が除去されてセットされる(途中や最後の空白は残る)。 環境変数に比べてFOR制御変数の利点としては、 (1) setlocal/endlocalのスコープを越えられること(7/24メモ参照)。 (2) FOR制御変数独自の修飾子(位置パラメータと同じ)を使えること。
■ 2003年07月24日 / setlocalしたスクリプトから外部の環境変数をセット setlocalすると、スクリプト実行後の環境変数にセットするには、endlocalのあとでセットするしかない。 しかしセットしたい値はローカル変数に入っているので、endlocalの後では参照できない。 endlocalの後までローカル変数を持ち越す手段を講じる必要がある。 for の制御変数のスコープはdoのあとの文もしくはブロックであり、 setlocal/endlocalの影響を受けない。つまりローカル変数でない。 したがって、endlocalの前に、ローカル変数値をfor制御変数にセットしてendlocalの外で使う。 また、その後でまたsetlocalしたい場合、最初のsetlocal〜endlocalのスコープとは独立なので 最初のスコープのローカル変数を参照することは出来ない。スコープをまたがってのローカル変数の 引継ぎにもfor制御変数を使うことが出来る。 setlocal set A=local set B=result for /f \"tokens=*\" %%X in (\"%A%\") do ( ローカル変数の値を制御変数にセット for /f \"tokens=*\" %%Y in (\"%B%\") do ( endlocal set OUT=%%Y グローバルな環境変数へのセット setlocal set A=%%X スコープをまたがったローカル変数の引継ぎ ) ) echo %A%
■ 2003年07月23日 / 引数の shift shift 命令を実行したら $* にも反映されてほしいが、しない。 バグ?
■ 2003年07月22日 / %PATH%の要素分解 %PATH% を;で切り離してそれぞれの要素を別々に処理したいとき、 for %%I in (%PATH%) do echo %%I で、おおむねいいんですが、「Program Files」のように空白が入っているとそれが区切りになって うまく処理されない。PATHに追加するときに、「PROGRA~1」のように短名をつかうとか \"Program Files\"とここだけ\" \"で囲むという見苦しい逃げを打つ手もあります。 正攻法では、for がPATHを分解するときだけ、\" \" で囲って、分解された後は \" \" をはずして処理する。 一時的に\"\"で囲うのは、PATH要素の区切りである ; を \";\" で置換して、最初と最後に \" を付ければいい。 for %%I in (\"%PATH:;=\";\"%\") do echo %%~I 制御変数Iから\" \"をはずすのは、%%~I 、はずす必要がなければ、%%I とそのまま使えばいい。
■ 2003年07月21日 / 標準入力からの読み取り set /p を使えば、sh の read 相当のことが出来るのではないかと思い、 @echo off setlocal :loop set /p X= if not %ERRORLEVEL% == 0 goto :eof echo \"%X%\" goto loop と書いてみる(zz.bat)。 set /p で ^Z を入れると ERRORLEVEL が 1 になることは別途確認済み。 コンソールからの入力はOK。^Z でちゃんと終わる。 echo AAA | zz もOK。 dir | zz だとおかしい。2行しか表示されないし、やる度に表示される行が違う。 どうもパイプでの同期が取れていない模様。 たまたまタイミングがあったdirの結果行が取り込まれた結果と思われる。 P.S. もしかして^Cのチェックが入力を食っているのかと、break off に期待したが、 WinXPの場合はbreakコマンドが無効とのことで駄目。
■ 2003年07月20日 / 変数中のリダイレクト文字 変数Aに「BBB > CCC」が入っていた場合、 echo %A% で、「BBB > CCC」が表示されると期待したが、 echo BBB > CCC とみなされる。つまり、CCCという名のファイルにBBBが書き込まれる。 なお、変数Aへの「BBB > CCC」をセットしようと、 set A=BBB > CCC とすると、Aには「BBB」がセットされ、CCCという名の空ファイルが出来る。 set A=\"BBB > CCC\" だと、Aには「\"BBB > CCC\"」と、ダブルクォート付でセットされてしまう。 シングルクォートは特殊文字でないため、 set A=\'BBB > CCC\' では、Aには「\'BBB」がセットされ、CCC\' という名の空ファイルが出来る。 set /p A=prompt で、promptに対して、「BBB > CCC」を入力すると変数Aにそのままセットされる。
|
◇今月の記事◇
◇過去の記事◇
◇カテゴリー◇
◇ブックマーク◇
|