目次
- コマンドプロンプト・プログラミング 序 <- ★
- コマンドプロンプト・プログラミング 々
- コマンドプロンプト・プログラミング 宴
- コマンドプロンプト・プログラミング 二次会
- コマンドプロンプト・プログラミング 三次会
- コマンドプロンプト・プログラミング 壮行会
実行環境
Windows7 Pro 64bit
コマンドプロンプトの仕様
- データ型:文字列のみ
- 変数:環境変数のみ(局所化は可能)
識別子に大文字・小文字の区別はない。
評価結果は通常、標準出力を受け取る。
直近のエラーは環境変数 ErrorLevel
に設定される。
ErrorLevel
が 0
ならば「エラー無し」を示す。
環境変数1つには 32,767 文字の容量があるそうだが、
コマンドプロンプトの入力バッファは 8,190 文字らしい。
UTF-16 が 8,190 文字である。
set xxx=
自体も含まれるので値に全てを使えるわけではない。
メタ文字
- 読込時変数展開:
%
- 実行時変数展開:
!
(setLocal enableDelayedExpansion
)
変数展開は "
の有無に関わらず行われる。
- エスケープシーケンス:
^
- メタ文字解釈トグル:
"
- グループ化:
(
)
- コマンド連結:
&
- 短絡評価連結:
&&
||
(ErrorLevel
依存) - パイプ:
|
- リダイレクト:
<
>
>>
- 代入・比較演算子:
=
==
(鬼門)
コマンドプロンプトはワイルドカード *
?
を解釈しない。
それは個々のコマンドに任される。
"
は文字列を作るわけではなく、
(そもそも文字列しか型がない)
「メタ文字を解釈するかしないか」をトグルする。
最初に出現した "
の意味は「メタ文字解釈オフ」である。
したがって、"
・・・"
と囲む必要すらない。
Hello World!
> echo Hello World!
Hello World!
> set value=World
> echo "Hello %value%! () & && || | < > >> =
"Hello World! () & && || | < > >> =
> echo "一行目\n二行目"
"一行目\n二行目"
> (
More? echo 一行目
More? echo 二行目
More? )
一行目
二行目
改行を入れるだけでも一苦労である。
バッチファイルの仕様
goto :eof
でスクリプトを抜けれるが exit /b
と等価なのか不明。
exit /b
と exit /b 0
は等価なのか不明。
そもそも exit
無しの場合は何なのか不明。
仕様が存在するのか神龍に問いたい。
文字列操作以前
文字列しか扱えないのだから操作しよう。
しかし、文字列を得る時点で泥沼。
文字列を得るには標準出力から受け取るしかない。
標準出力といえば echo
である。
echo
の罠
echo a b c
で生成される文字列は、何個で何文字だろうか?
> echo a b c
a b c
生成されたのは a b c
1つで5文字である。
先頭に空白を1+3、計4つ入れてみる。
> echo a
a
ああ、やめてくれ。
リダイレクトでテキストファイルに書き込んでみる。
> echo a > sample.txt
> type sample.txt
a
実はこれ、先頭の空白はちゃんと(?)3つある。
しかし、末尾に余計な空白1つが書き込まれている。
echo
が >
の直前の空白を拾っているからだ。
これに対処するには echo a>
とやるか、
(echo a) >
とすればよいが、なんともきもい。
そして次の挙動で泣く。
> echo
ECHO は <ON> です。
引数無し・空白だけの引数、共に同じ結果となる。
echo
を標準出力コマンドだと思ってはならない。
エコーバック設定・確認コマンドなのである。
謎挙動
例えば、
> echo. > sample.txt
これで「空白+改行」が書き込まれたりする。
なぜ?いや、そんなことは考えてはいけない。
たぶん正解は echo
にはそもそも仕様など無いのだ。
欲しいものを考える
puts
コマンドがあるとして、
> puts
> puts "Hello World!"
Hello World!
> puts Hello World! "Yeah!"
Hello World! Yeah!
こんな感じを目指そう。
ユーティリティ
token:トークンを得る
@echo off
for %%a in (%*) do (
if "%%~a" == "" goto :done
echo %%~a
)
:done
exit /b
%*
にはコマンドライン引数が一行の文字列として収まっている。
引数の先頭にある「空白の連続」はこの時点で省かれる。
これを for
(というか cmd.exe
)は、
以下をデリミタ(セパレータ)とみなして分割する。
- 空白(半角・全角)
- タブ
- 改行
,
;
-
=
(やめれ
そして、ループの度に局所変数 %%a
に順次代入する。
しかし、二重引用符に囲まれたものはトークンとみなされる。
%%~a
で二重引用符を(存在すれば)省くことができる。
ちなみに for
の局所変数に使える識別子は、
アルファベット1文字で、ややこしいことに大文字・小文字を区別する。
したがって、[A-Za-z]
の 52種類である。
> token
> token 1 2 3
1
2
3
> token 1,2,3
1
2
3
> token one=1; two=2; three=3
one
1
two
2
three
3
> token "1, 2; three=3"
1, 2; three=3
> token ""
> token "abc
コマンドの構文が誤っています。
> token """"
""
=
をデリミタとみなさるのはいただけないが、
とりあえずこれを「空白がセパレータの文字列」に変換する。
line:出力を一行にまとめる
そのためにはなんとかしてパイプからトークン行を受け取る必要がある。
今回は findstr
ではなく find
を使うことにした。
私、正規表現が嫌いです。
> token a b c | find /v ""
a
b
c
find /v ""
の意味は、
「空文字列以外を含む行を表示せよ」
言い替えると、
「全ての行を表示せよ」
だ。
doskey
で find /v ""
にエイリアスをつけたくなるが、
そうするとパイプを受け取れなくなってしまうという謎仕様。
なのでバッチでラップするしかない。
@echo off
for /f "delims=" %%a in ('find /v ""') do (
echo %%a
)
exit /b
for /f "delims="
の意味は「デリミタは何も無し」である。
ちなみに、制御文字をデリミタにする方法は無い、たぶん。
> token hello world! "yeah!" | rap
hello
world!
yeah!
これを「空白がセパレータの文字列」に変換する。
@echo off
setLocal
set line=
for /f "delims=" %%a in ('find /v ""') do (
if "%line%" == "" (
set line=%%a
) else (
set line=%line% %%a
)
)
echo %line%
endLocal
exit /b
setLocal
endLocal
内の環境変数は局所変数となる。
このコマンドは「まとめる」ことが目的なので、
二重引用符を省く処理はしないことにした。
> token hello world! "yeah!" | rap
yeah!
で、なぜこうなる?
変数展開の罠
変数はコマンドラインの読込時に即時展開される。
今回は if
文の読込時には %line%
を展開しても値が入っていない。
(そう、シェルスクリプトと違って if
for
は文なのだ)
そのため set line=%%a
しか評価されず、
%%a
の最終値が echo %line%
によって出力される。
この挙動を変えるためには、
setLocal enableDelayedExpansion
とし、
「実行時まで展開を遅延しろ」と指示しなければならない。
加えて、展開対象 %line%
を !line!
に書き換える。
@echo off
setLocal enableDelayedExpansion
set line=
for /f "delims=" %%a in ('find /v ""') do (
if "!line!" == "" (
set line=%%a
) else (
set line=!line! %%a
)
)
echo %line%
endLocal
exit /b
> token hello world!!! "yeah!!!!!" | rap
hello world yeah
お気づきだろうか?
私のカルシウムは 98% 減少した!
!
がメタ文字に変化したのだ。。。
cmd.exe /v
コマンドプロンプトのヘルプにはこう書いてある。
/V:ON 区切り文字として ! を使って遅延環境変数の展開を有効にします。
たとえば、/V:ON とすると、!var! は、実行時に変数 var を展開します。
var 構文は、FOR ループ中とは違い、入力時に変数を展開します。
/V:OFF 遅延環境展開を無効にします。
どーだいっ さっぱりわからんだろぉ?
コマンドプロンプトのデフォルトは /v:off
だ。
デフォで遅延展開を使えるようにしたいなら /v:on
で起動する。
一部だけ挙動を変えたいなら setLocal enable-
endLocal
で囲む。
これは部分上書き設定で優先度が一番高い。
「遅延展開オン」とは「!
メタ文字化」を意味する。
> set greet=待たせたなっ
> echo %greet%
待たせたなっ
> echo !greet!
!greet!
> cmd /v:on
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
> echo %greet%
待たせたなっ
> echo !greet!
待たせたなっ
- Read:
%var%
を展開
- Eval:
!var!
を展開(するかも) - Loop
です。
遅延展開はゴミ箱へ
まず打鍵数で腱鞘炎になるわ。
変数展開って結局はマクロ置換だよね。
だから "%var%"==""
なんてイディオムがあるわけで。
それを実行時展開に変更すると・・・動的スコープ?
ああ、なるほど、PowerShell 挙動ってことね?
うざーいっ
試行錯誤の結果、サブルーチンでなんとかなった。
@echo off
setLocal
set line=
for /f "delims=" %%a in ('find /v ""') do (
call :join %%a
)
if "%line%" == "" goto :done
echo %line%
endLocal
:done
exit /b
:join
if "%line%" == "" (
set line=%*
) else (
set line=%line% %*
)
サブルーチンにして実装できたってことは、
:join
以降は call
の時に即時展開されてることになる。
呼出元の状態によって %line%
の値は決まる。
これこそ動的スコープだ。
ということは PowerShell と変わらんから、
:join
をクロージャだと言っちゃうわけですね!?
きもーいっ
> token | line
> token 1,2,3 | line
1 2 3
> token hello world! "yeah!" | line
hello world! yeah!
これ、もう一度思い出して書けと言われたら無理。
puts:標準出力
@echo off
call token %* | call line
exit /b
> puts 1,2,3; zero=0 one="1" "two"=2
> 1 2 3 zero 0 one 1 two 2
> puts "Hello World!"
Hello World!
> puts Hello World! " yeah!"
Hello World! yeah!
=
がデリミタ扱いされるのを、
バッチファイルだけでどうにかできないか数日考えたが、
「そこまでやるぐらいなら別言語使った方がよくない?」
となって諦めた。
それと Bash の echo
の場合、
最後の出力の Hello World!
と yeah!
の間には空白列が残り結合される。
これはどうなんだろ、どっちがいいのかなぁ。