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

# 目次

- [コマンドプロンプト・プログラミング 序](https://qiita.com/feelinguy/items/ce75348470b583b3ae42) <- ★
- [コマンドプロンプト・プログラミング 々](https://qiita.com/feelinguy/items/0b88db4aeb41bee44f19)
- [コマンドプロンプト・プログラミング 宴](https://qiita.com/feelinguy/items/ed91ed23e7baa63471a5)
- [コマンドプロンプト・プログラミング 二次会](https://qiita.com/feelinguy/items/02c70d56b2ed039db0ee)
- [コマンドプロンプト・プログラミング 三次会](https://qiita.com/feelinguy/items/f519549b0f9e8d2a714b)
- [コマンドプロンプト・プログラミング 壮行会](https://qiita.com/feelinguy/items/76dcc87020edbeb7dc14)

# 実行環境

Windows7 Pro 64bit

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

- データ型：文字列のみ
- 変数：環境変数のみ（局所化は可能）

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

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

環境変数１つには 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 /b` と `exit /b 0` は等価なのか不明。
そもそも `exit` 無しの場合は何なのか不明。

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

# 文字列操作以前

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

## `echo` の罠

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

```cmd.exe
> echo a b c
a b c
```

生成されたのは `a b c` １つで５文字である。
先頭に空白を１＋３、計４つ入れてみる。

```cmd.exe
> echo    a
   a
```

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

```cmd.exe
> echo    a > sample.txt

> type sample.txt
   a
```

実はこれ、先頭の空白はちゃんと（？）３つある。
しかし、末尾に余計な空白１つが書き込まれている。
`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：トークンを得る

```sh:token.cmd
@echo off
for %%a in (%*) do (
  if "%%~a" == "" goto :done
  echo %%~a
)

:done
  exit /b
```

`%*` にはコマンドライン引数が一行の文字列として収まっている。
引数の先頭にある「空白の連続」はこの時点で省かれる。
これを `for` （というか `cmd.exe`）は、
以下をデリミタ（セパレータ）とみなして分割する。

- 空白（半角・全角）
- タブ
- 改行
- `,`
- `;`
- `=`（やめれ

そして、ループの度に局所変数 `%%a` に順次代入する。
しかし、二重引用符に囲まれたものはトークンとみなされる。
`%%~a` で二重引用符を（存在すれば）省くことができる。

ちなみに `for` の局所変数に使える識別子は、
アルファベット１文字で、ややこしいことに大文字・小文字を区別する。
したがって、`[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 ""` の意味は、
「空文字列以外を含む行を表示せよ」
言い替えると、
「全ての行を表示せよ」
だ。

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

```sh: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!
```

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

```sh: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!` に書き換える。

```sh: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%` を展開
- Eval：`!var!` を展開（するかも）
- Print
- Loop

です。

### 遅延展開はゴミ箱へ

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

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

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

```sh: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：標準出力

```sh: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!` の間には空白列が残り結合される。
これはどうなんだろ、どっちがいいのかなぁ。

# 次回

- [コマンドプロンプト・プログラミング 々](https://qiita.com/feelinguy/private/0b88db4aeb41bee44f19)