- note -

Batch

5/10

標準入力の受け取り   - Last modified:2012/11/22

標準入力の受け取りはbatにとって悩ましい問題です。
例えば、パイプやリダイレクトからの入力を受け取って、
※WindowsにはLinuxにおけるcatのような動作をしてくれるコマンドが標準で用意されていない。 そのまま全て表示するだけのbatを作りたいと考えます(※)。
どうすれば実現できるでしょうか。
入力と言うとtypeやmore、sortなどがありますが、これらのコマンドでは期待する動作をしてくれません。
そこで利用するのがfindです。
findは標準入力にも対応していて、/vと空文字列を併用すれば入力をそのまま出力するだけの動作になります。
つまり、下のコードで目的のbat完成です。







findfindstr .*でも可。
@echo off
setlocal

find /v ""
しかし問題はここからです。
標準入力からの入力をそのまま全て表示ではなく、
1行ずつ解析して何らかの処理を行わせたかったらどうでしょうか。
1行ずつと言えばfor /fです。
@echo off
setlocal

for /f delims^=^ eol^= %%I in ('find /v ""') do (
  echo;%%I
)
便宜上echoを使用していますが、echoを任意のコマンドに置き換えれば
1行ずつ解析しての処理が実現できる寸法です。
さて、上記のようなbatで概ね期待通りの動作をしますが、
ただ一点だけ微妙に困った問題があります。
それは、空行をスキップしてしまうことです。
これはfor /fの仕様で、オプションで変更できたりするものでもないので
find /vをそのまま使っている限りはどうしようもありません。
それではちょっと細工をしてみましょう。
空行がスキップされるなら、空行を作らなければいいのです。
どういうことかと言うと、findの/nを利用します。
これで全ての行頭に行番号が付加されることになり、
空行が無くなります。
@echo off
setlocal

for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  echo;%%I
)
これでめでたく空行スキップは回避できましたが、もちろんこのままではダメなので
行頭のゴミ除去を試みます。
ゴミ除去には変数展開時のオフセット指定が使えそうです。
そのためには、for変数を一旦通常の変数に入れ直さなければなりません。
@echo off
setlocal

for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  set hoge=%%I
  echo;%hoge%
)
はい、ダメですね。
%hoge%がfor解析時に展開されてしまいます。
それでは例のイディオムを使ってみましょう。
@echo off
setlocal

for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  set hoge=%%I
  call echo %%hoge%%
)
一見すると良さそうな感じですが、
解析する文字列の中にbatの特殊記号が出現するとまずいことになります。
おとなしく遅延展開で書き直してみます。
@echo off
setlocal enabledelayedexpansion

for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  set hoge=%%I
  echo;!hoge!
)
今度こそは、と言いたいですがまだダメです。
基本的にbatの変数展開結果は、展開速度の一番遅い展開方法以外は再解析される可能性を孕んでいます。
そしてここからが重要ですが、「展開速度の一番遅い展開方法」とは
遅延展開無効時なら%%I形式のfor変数展開、
遅延展開有効時なら!hoge!形式の遅延展開、となっています。
上記のコードでは遅延展開が有効になっている影響で、
set hoge=%%Iの展開結果が再解析され、遅延展開(!の置換)が行われてしまいます。
つまり、遅延展開を有効にするタイミングをfor変数の展開時より後ろにする必要があるので、こうなります。
@echo off
setlocal

for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  set hoge=%%I
  setlocal enabledelayedexpansion
  echo;!hoge!
  endlocal
)
ここまでやってようやく、変数の値を移した上で中身を変化させることなく使えるようになりました。
しかしまだオフセット指定でゴミ除去をするための準備が整ったに過ぎません。
というわけで、次はオフセット計算処理です。
[n]という形式 … findstrの場合は n: という形式になる。 行頭のゴミは[n]という形式で入ってくるので
行番号の文字数 + 2をオフセットに指定すれば良さそうです。
毎回行番号の文字数をカウントしても出来ますが、
batにおける文字数カウント処理のコストは決して安くはないため
別のアプローチにした方が無難です。
例えば、桁数の増加だけをチェックして桁が増えたらオフセットを+1するような処理で
良さそうなので、取り敢えずそれを実装してみます。
@echo off
setlocal

set lineNo=0
set nextNo=10
set offset=3
for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  set /a lineNo+=1
  if %lineNo% == %nextNo% (
    set /a offset+=1
    set /a nextNo*=10
  )
  set hoge=%%I
  setlocal enabledelayedexpansion
  echo;!hoge!
  endlocal
)
見るからにダメですね。
今回のケースはifがあるためcallの多重展開はやっぱり使えません。
それでは処理の場所をsetlocal enabledelayedexpansionの中に移動して
遅延展開を利用してみるのはどうでしょうか。
@echo off
setlocal

set lineNo=0
set nextNo=10
set offset=3
for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  set hoge=%%I
  setlocal enabledelayedexpansion
  set /a lineNo+=1
  if !lineNo! == !nextNo! (
    set /a offset+=1
    set /a nextNo*=10
  )
  echo;!hoge!
  endlocal
)
これでオフセット計算処理も無事に出来……上がっていません。
場所を移動して遅延展開が利用できるようになったのはいいのですが、
この位置ではendlocalのタイミングでsetlocal内部のsetで設定した内容が消失します。
じゃあendlocalを呼ばなければいいのでは、という安直な発想でループ内にsetlocalのネストを作っていくと
32回 … ちなみに、batのネストでは529回で怒られた。 32回程繰り返した辺りで怒られます。
このような(数値変数を実行時展開したい)場合、簡単な解法は2つ。
サブルーチン化か、for /lの埋め込みです。
ここでは前者の方を適用してみます。
@echo off
setlocal

set lineNo=0
set nextNo=10
set offset=3
for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  call :UpdateOffset
  set hoge=%%I
  setlocal enabledelayedexpansion
  echo;!hoge!
  endlocal
)
exit /b

:UpdateOffset
set /a lineNo+=1
if %lineNo% == %nextNo% (
  set /a offset+=1
  set /a nextNo*=10
)
exit /b
一先ずこれで問題無さそうです。
さあ、これでオフセットの計算処理まで実装できたことになります。
最後の仕事は、オフセットの適用です。
適用対象は遅延展開有効時の!hoge!ですが、
展開速度の都合上、オフセット変数は遅延展開できないので
結局取り得る手段はオフセット計算処理のときと同じパターンになります。
サブルーチン化してもいいですが、変数一つだけなので
今度はfor /lを利用してみます。
@echo off
setlocal

set lineNo=0
set offset=3
set nextNo=10
for /f delims^=^ eol^= %%I in ('find /n /v ""') do (
  call :UpdateOffset
  set hoge=%%I
  setlocal enabledelayedexpansion
  for /l %%J in (!offset!,1,!offset!) do (
    echo;!hoge:~%%J!
  )
  endlocal
)
exit /b

:UpdateOffset
set /a lineNo+=1
if %lineNo% == %nextNo% (
  set /a offset+=1
  set /a nextNo*=10
)
exit /b
完成 … 動かしてみるとわかるが、重い。 ついに完成です。
これが、「空行をスキップせずに標準入力を1行ずつ解析して何らかの処理を行わせるbat」の雛形になります。
末尾に%1 … %1を書いて引数に対応する場合、findでは余計な出力が出てしまうためfindstrを利用したほうが良い。 ついでにfindの末尾に%1とか書いておけば
読み込み対象を引数としても指定できるようになって尚良いかもしれませんね。
それにしても、このページ最初のコマンドの形からどうしてこうなってしまったのか……。

おまけ

苦労してマッチポンプをやり遂げたのはいいものの、
上のやつだと重い上に複雑なだけなので、現実的な解としては下のようなbatがいいのではないかと思われます。
@echo off
setlocal

for /f "tokens=1,* delims=:" %%I in ('findstr /n .*') do (
  echo;%%J
)
行頭の:が消える、解析対象が%%Jになる、ということ以外は特に問題ありません。

おまけ2

そもそもC言語で作れば
0.
[htsuji]
はじめまして。ヒント、ありがとうございました。「こんな素朴な機能が、コンソールのコマンドにないはずはない」と思い込んで、あちこちさまよっていました。