Help us understand the problem. What is going on with this article?

コマンドプロンプト・プログラミング 序

目次

実行環境

Windows7 Pro 64bit

コマンドプロンプトの仕様

  • データ型:文字列のみ
  • 変数:環境変数のみ(局所化は可能)

識別子に大文字・小文字の区別はない。

評価結果は通常、標準出力を受け取る。
直近のエラーは環境変数 ErrorLevel に設定される。
ErrorLevel0 ならば「エラー無し」を示す。

環境変数1つには 32,767 文字の容量があるそうだが、
コマンドプロンプトの入力バッファは 8,190 文字らしい。
UTF-16 が 8,190 文字である。
set xxx= 自体も含まれるので値に全てを使えるわけではない。

メタ文字

  • 読込時変数展開:%
  • 実行時変数展開:!setLocal enableDelayedExpansion

変数展開は " の有無に関わらず行われる。

  • エスケープシーケンス:^
  • メタ文字解釈トグル:"
  • グループ化:( )
  • コマンド連結:&
  • 短絡評価連結:&& ||ErrorLevel 依存)
  • パイプ:|
  • リダイレクト:< > >>
  • 代入・比較演算子:= ==(鬼門)

コマンドプロンプトはワイルドカード * ? を解釈しない。
それは個々のコマンドに任される。

" は文字列を作るわけではなく、
(そもそも文字列しか型がない)
「メタ文字を解釈するかしないか」をトグルする。
最初に出現した " の意味は「メタ文字解釈オフ」である。
したがって、"・・・" と囲む必要すらない。

Hello World!

cmd.exe
> 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 /bexit /b 0 は等価なのか不明。
そもそも exit 無しの場合は何なのか不明。

仕様が存在するのか神龍に問いたい。

文字列操作以前

文字列しか扱えないのだから操作しよう。
しかし、文字列を得る時点で泥沼。
文字列を得るには標準出力から受け取るしかない。
標準出力といえば echo である。

echo の罠

echo a b c で生成される文字列は、何個で何文字だろうか?

cmd.exe
> echo a b c
a b c

生成されたのは a b c 1つで5文字である。
先頭に空白を1+3、計4つ入れてみる。

cmd.exe
> echo    a
   a

ああ、やめてくれ。
リダイレクトでテキストファイルに書き込んでみる。

cmd.exe
> echo    a > sample.txt

> type sample.txt
   a

実はこれ、先頭の空白はちゃんと(?)3つある。
しかし、末尾に余計な空白1つが書き込まれている。
echo> の直前の空白を拾っているからだ。
これに対処するには echo a> とやるか、
(echo a) > とすればよいが、なんともきもい。

そして次の挙動で泣く。

cmd.exe
> echo
ECHO は <ON> です。

引数無し・空白だけの引数、共に同じ結果となる。
echo を標準出力コマンドだと思ってはならない。
エコーバック設定・確認コマンドなのである。

謎挙動

例えば、

cmd.exe
> echo. > sample.txt

これで「空白+改行」が書き込まれたりする。
なぜ?いや、そんなことは考えてはいけない。
たぶん正解は echo にはそもそも仕様など無いのだ。

欲しいものを考える

puts コマンドがあるとして、

cmd.exe
> puts

> puts "Hello World!"
Hello World!

> puts    Hello   World! "Yeah!"
Hello World! Yeah!

こんな感じを目指そう。

ユーティリティ

token:トークンを得る

token.cmd
@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種類である。

cmd.exe
> 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 を使うことにした。
私、正規表現が嫌いです。

cmd.exe
> token a b c | find /v ""
a
b
c

find /v "" の意味は、
「空文字列以外を含む行を表示せよ」
言い替えると、
「全ての行を表示せよ」
だ。

doskeyfind /v "" にエイリアスをつけたくなるが、
そうするとパイプを受け取れなくなってしまうという謎仕様。
なのでバッチでラップするしかない。

rap.cmd
@echo off
for /f "delims=" %%a in ('find /v ""') do (
  echo %%a
)
exit /b

for /f "delims=" の意味は「デリミタは何も無し」である。
ちなみに、制御文字をデリミタにする方法は無い、たぶん。

cmd.exe
> token   hello   world!   "yeah!" | rap
hello
world!
yeah!

これを「空白がセパレータの文字列」に変換する。

rap.cmd
@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 内の環境変数は局所変数となる。

このコマンドは「まとめる」ことが目的なので、
二重引用符を省く処理はしないことにした。

cmd.exe
> token   hello   world!   "yeah!" | rap
yeah!

で、なぜこうなる?

変数展開の罠

変数はコマンドラインの読込時に即時展開される。
今回は if 文の読込時には %line% を展開しても値が入っていない。
(そう、シェルスクリプトと違って if for は文なのだ)
そのため set line=%%a しか評価されず、
%%a の最終値が echo %line% によって出力される。

この挙動を変えるためには、
setLocal enableDelayedExpansion とし、
「実行時まで展開を遅延しろ」と指示しなければならない。
加えて、展開対象 %line%!line! に書き換える。

rap.cmd
@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
cmd.exe
> 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 で囲む。
これは部分上書き設定で優先度が一番高い。
「遅延展開オン」とは「! メタ文字化」を意味する。

cmd.exe
> 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!
待たせたなっ
  1. Read:%var% を展開
  2. Eval:!var! を展開(するかも)
  3. Print
  4. Loop

です。

遅延展開はゴミ箱へ

まず打鍵数で腱鞘炎になるわ。

変数展開って結局はマクロ置換だよね。
だから "%var%"=="" なんてイディオムがあるわけで。
それを実行時展開に変更すると・・・動的スコープ?
ああ、なるほど、PowerShell 挙動ってことね?
うざーいっ

試行錯誤の結果、サブルーチンでなんとかなった。

line.cmd
@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 をクロージャだと言っちゃうわけですね!?
きもーいっ

cmd.exe
> token | line

> token 1,2,3 | line
1 2 3

> token   hello   world!   "yeah!" | line
hello world! yeah!

これ、もう一度思い出して書けと言われたら無理。

puts:標準出力

puts.cmd
@echo off
call token %* | call line
exit /b
cmd.exe
> 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! の間には空白列が残り結合される。
これはどうなんだろ、どっちがいいのかなぁ。

次回

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away