第10回 シェル芸勉強会

シェル芸（マウスをつかわず、ソースコードものこさず、 GUI ツールを立ちあげる間もなく、あらゆる調査・計算・テキスト処理を CLI 端末へのコマンド入力一撃で終わらす）の勉強会で出題された問題をバッチファイルで解いてみました。

上田ブログ
http://blog.ueda.asia/?p=2378

```bat:2014-04-05.bat
@echo off

set SELF=%~n0
set HELP=Usage: %SELF% NUMBER
set HOME=%~dps0
set HOME=%HOME:~0,-1%

setlocal enabledelayedexpansion
  :: 位置パラメーターを確認
  if "%~1" == "" (
    echo %HELP% >&2
    exit /b 1
  )
  :: 値が数値か確認
  (echo %~1) | findstr /r "[^0-9]." > NUL
  if %ERRORLEVEL% equ 0 (
    echo %HELP% >&2
    exit /b 1
  )
  
  :: 位置パラメーターを取得
  set n=%~1
  
  :: 値が範囲内か確認(Q1からQ8まで)
  if %n% lss 1 (
    echo %SELF%: illegal value: use 1-8 >&2
    exit /b 1
  )
  if %n% gtr 8 (
    echo %SELF%: illegal value: use 1-8 >&2
    exit /b 1
  )
  
  :: 作業フォルダーを作成
  set CWD=%HOME%\A%n%
  md %CWD%
  
  :: テストを実行
  call :A%n% %CWD%
  if %ERRORLEVEL% neq 0 (
    echo %SELF%: script abended: A%n% >&2
    exit /b 1
  )
  
  :: 作業フォルダーを削除
  cd %CWD%\..
  rd /s /q %CWD%
endlocal
goto :EOF

:A1
setlocal enabledelayedexpansion
  set CWD=%~1
  
  :: 文字列を置換
  for %%t in ("2 5 9 8 1 3 7 4") do (
    set t=%%t
  )
  :: The following code doesn't do work as expected.
  :: (echo 2 5 9 8 1 3 7 4) | for /f "tokens=*" %%t in ('find /v ""') do @(
  ::   set t=%%t
  :: )
  
  :: 文字列を数式として評価
  set /a n=%t: =+%
  
  :: 結果を表示
  echo.
  echo %n%
endlocal
goto :EOF

:A2
setlocal enabledelayedexpansion
  set CWD=%~1
  
  :: ファイルを作成
  (
    echo ^ ^ ^ ^ ^ ^ ^ ^ 1
    echo.
    echo 2 3^ ^ ^ ^ ^ ^ ^ ^ ^ ^ 
    echo ^ ^ ^ ^ ^ ^ ^ ^ ^ 4^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ 5
    echo ^ ^ ^ ^ ^ ^ ^ ^ 6^ ^ ^ ^ 7
    echo.
    echo 8^ ^ ^ ^ ^ ^ 9
  ) > %CWD%\%SELF%.tmp
  
  :: ファイルから文字列を取得して結合
  for /f "tokens=*" %%t in (%CWD%\%SELF%.tmp) do (
    set t=!t!%%t 
  )
  
  :: 文字列を数値に変換して加算
  set n=0
  for %%i in (%t%) do (
    set /a n+=%%i
  )
  
  :: 結果を表示
  echo.
  echo %n%
  
  :: ファイルを削除
  del %CWD%\%SELF%.tmp
endlocal
goto :EOF

:A3
setlocal enabledelayedexpansion
  set CWD=%~1
  
  :: ファイルを作成
  (
    echo 筆者は朝、目玉焼きを食べた。
    echo 昼、著者は卵がけごはんを食べた。
    echo そして夜、著者はマンハッタンの夜景を
    echo 見ながらゆで玉子を食べた。
  ) > %CWD%\%SELF%.tmp
  
  :: ファイルから文字列を取得して結合
  for /f "tokens=*" %%t in (%CWD%\%SELF%.tmp) do (
    set t=!t!%%t
  )
  
  :: 一文字ずつ抽出して文字数を算出
  set i=0
  :A3.BOL
  if not "!t:~%i%,1!" == "" (
    set /a i+=1
    goto A3.BOL
  )
  
  :: 結果を表示
  echo.
  echo %i%
  
  :: ファイルを削除
  del %CWD%\%SELF%.tmp
endlocal
goto :EOF

:A4
setlocal enabledelayedexpansion
  set CWD=%~1
  
  :: ファイルを作成
  (echo aabbcdabbcccdd) > %CWD%\%SELF%.tmp
  
  :: ファイルから文字列を取得
  for /f "tokens=*" %%t in (%CWD%\%SELF%.tmp) do (
    set t=%%t
  )
  
  :: 一文字ずつ抽出してファイルに出力
  copy NUL %CWD%\%SELF%.1.tmp > NUL
  set i=0
  :A4.BOL
    set c=!t:~%i%,1!
  if not "%c%" == "" (
    (echo %c%) >> %CWD%\%SELF%.1.tmp
    set /a i+=1
    goto A4.BOL
  )
  
  :: 重複する文字を除外
  copy NUL %CWD%\%SELF%.2.tmp > NUL
  for /f "tokens=*" %%t in (%CWD%\%SELF%.1.tmp) do (
    findstr /x /c:"%%t" %CWD%\%SELF%.2.tmp > NUL || (echo %%t) >> %CWD%\%SELF%.2.tmp
  )
  
  :: 文字の使用回数を算出して結果を表示
  echo.
  for /f "tokens=*" %%t in (%CWD%\%SELF%.2.tmp) do (
    find /c "%%t" %CWD%\%SELF%.1.tmp | findstr /e " 3" > NUL && (
      echo %%t
    )
  )
  
  :: ファイルを削除
  del %CWD%\%SELF%.tmp
  del %CWD%\%SELF%.*.tmp
endlocal
goto :EOF

:A5
setlocal enabledelayedexpansion
  set CWD=%~1
  
  :: フォルダーを作成
  md %CWD%\a\b\c
  
  :: ファイルを作成
  for %%f in (%CWD%\a\file1 %CWD%\a\b\file2 %CWD%\a\b\c\file3) do (
    copy NUL %%f > NUL
  )
  
  :: 結果を表示
  echo.
  tree /f %CWD%
  
  :: ファイルを移動
  for /r %CWD%\a %%f in (*) do (
    move "%%f" %CWD%\ > NUL
  )
  
  :: 結果を表示
  echo.
  tree /f %CWD%
  
  :: ファイルを削除
  del %CWD%\file*
  
  :: フォルダーを削除
  rd /s /q %CWD%\a
endlocal
goto :EOF

:A6
setlocal enabledelayedexpansion
  set CWD=%~1
  
  :: フォルダーを作成
  md %CWD%\a %CWD%\b
  
  :: ファイルを作成
  (echo hoge) > %CWD%\file1
  (echo huge) > %CWD%\file2
  (echo hoge) > %CWD%\file3
  (echo hoge) > %CWD%\file4
  
  :: 結果を表示
  echo.
  tree /f %CWD%
  
  :: ファイルを移動
  for %%f in (%CWD%\file*) do (
    find "hoge" %%f > NUL && move %%f %CWD%\a > NUL || move %%f %CWD%\b > NUL
  )
  
  :: 結果を表示
  echo.
  tree /f %CWD%
  
  :: フォルダーとファイルを削除
  rd /s /q %CWD%\a %CWD%\b
endlocal
goto :EOF

:A7
setlocal enabledelayedexpansion
  set CWD=%~1
  
  :: ファイルを作成
  for /l %%i in (1,1,9) do (
    copy NUL %CWD%\file%%i > NUL
  )
  
  :: ファイル名を取得
  for %%t in (%CWD%\file*) do (
    set t=!t! %%~nxt
  )
  
  :: 結果を表示
  echo.
  for %%i in (%t%) do (
    for %%j in (%t%) do (
      if %%i lss %%j ( :: 重複する組を除外
        echo %%i %%j
      )
    )
  )
  
  :: ファイルを削除
  del %CWD%\file*
endlocal
goto :EOF

:A8
setlocal enabledelayedexpansion
  set CWD=%~1
  
  echo.
  echo Environment variable RANDOM gives a pseudo random integer between 0 and 32767.
  echo Press ENTER to continue, or CTRL-C to quit.
  pause > NUL
  
  :: 結果を表示
  echo.
  for /l %%i in (0,0,0) do (
    set t=     !RANDOM!
    echo !t:~-5!
  )
endlocal
goto :EOF
```

位置パラメーターを指定せずに実行すると

```
C:\temp>2014-04-05.bat
Usage: 2014-04-05 NUMBER
```

とヘルプが表示されるので、 1 から 8 までの数字のなかから任意の値を指定して実行してください。

---

ここからは解説です。

##Q1
ほんとうはお題の数列を標準出力から読みこんで処理したかったのですが、

```
(echo 2 5 9 8 1 3 7 4) | for /f "tokens=*" %%t in ('find /v ""') do (set t=%%t)
```

とむりくり書いても期待どおりの結果が得られませんでした。

しかたなく `for` コマンドの字句解析機能を利用したのですが…あらためて見ると `set t=2 5 9 8 1 3 7 4` を冗長にしただけでだめだめですね。

あとは半角空白を `+` 演算子に置換して、 `set /a` で数式として評価してあげれば一丁あがりです。

##Q2
お題のファイルをつくるところはちょっとめんどうです。

ここだけ見るとちょっと [tanasinn](http://matome.naver.jp/odai/2133004685689972201) ぽいものを感じますが、じつは cmd.exe 組みこみの `echo` コマンドは行頭に半角空白の連続する文字列を出力したい場合、 `^` でエスケープしないといけないのです。

まずは `for` コマンドの字句解析機能を利用して、複数の行を一行にまとめます。

それをそのまま `for` コマンドにセットしてあげると、半角空白をデリミターとして一文字ずつ切りだしてくれます。 `for` コマンドは半角空白が連続していてもひとつのデリミターとみなしてくれるのですが、そうしたくせのある挙動を利用しています。

あとはループのなかで数値をたしあわせ、合計をもとめるだけでした。

##Q3
「筆者」と「著者」、どちらかひとつに校正したくなる一品です。

お題のファイルをつくるところは **Q2** とおなじく複数の `echo` コマンドを `( ... )` でグループ化してヒアドキュメントぽく書いてみました。 `for` コマンドで一行にまとめているところもおなじです。

つぎに `if` コマンドと `goto` コマンドをくみあわせてループをつくりました。文字列の先頭から一文字ずつ切りだしながら文字数をカウントしていき、切りだす文字がなくなった時点でループからぬけます。

ちょっとくふうしたのは文字を切りわけるとき、変数の遅延展開を利用した点です。

`!t:~%i%,1!` の部分はまず `%i%` が評価され、そのあと `!t:~0,1!` のように変数展開された文字列が再評価されます。これで変数 t に格納されている文字列を先頭から一文字ずつ切りだしています。

##Q4
このお題はちょっとなやみました。

最初にたてた方策は「一文字ずつ改行しながら縦にならべていき、ソートしてから同一行をカウントする」というものでした。ところがバッチファイルには `uniq -c` に相当するコマンドがないので、あっけなく頓挫してしまいます。

そこで、まずはどんな種類の文字があるかしらべることにしました。これは **Q3** とおなじ方法で一文字ずつばらしながらファイルに書きこみ、そこから重複する文字を消していくことでもとまりました。

ちなみに重複を除外する処理は [Windows Script Programming](http://scripting.cocolog-nifty.com/blog/2009/09/uniq-77dd.html) というブログを参考にさせていただきました。（たんに `sort` コマンドとループで書くのがめんどうだったというのもあります ///）

つぎに帳面づけしたファイルから一文字ずつ読みこみ、別のファイルに書きこみます。このとき、書きこみ先にすでに該当の文字がある場合は、書きこまずにスキップしています。

この `uniq` コマンドぽい処理は書きこみ先のファイルを毎回なめるのでけっこう処理がおもたいのですが、事前にソートがいらないという点がちょっとおもしろいなと思います。

出現する文字の種類がすべて把握できたら、それらをもとに `find /c` でカウントしながら、件数が **3** のものだけひろえばおしまいです。

##Q5
`for` コマンドにはループ以外にももりだくさんの機能がつめこまれていますが、今回はそのうちのひとつ「ディレクトリを再帰的に探索する」機能を利用しました。

むかしこの機能をはじめて利用したとき、まちがえて C:\ からファイルを再帰的に消してしまったことがあります。（Windows がゆっくり停止してゆくさまをなすすべもなく見まもるしかありませんでした…）

とはいえ、つかいどころをまちがえなければ、ワンライナーでディレクトリをなめることができるのでなかなか強力な機能だと思います。

##Q6
このお題ではシンタックス・シュガーぽい書きかたをしてみました。

やっていることは `for` コマンドでカレントフォルダーにあるファイルを列挙しつつ、 `find` コマンドで「hoge」という文字列の書きこまれたファイルをしらべているだけです。

「hoge」という文字列が見つかれば `&&` 以降のコマンドを、見つからなければ `||` 以降のコマンドを実行します。

ワンライナーを書くかたにはなじみぶかい記法だと思うのですが、バッチファイル（cmd.exe）でもこのような書きかたが可能だったりします。

##Q7
ひとことでいってしまうと、シェル芸勉強会の模範解答そのままです。

`for` コマンドによる二つのループのかけあわせから 9 × 9 の組を生成し、ループカウンターの値を比較しつつ重複する組を除いていく、というのが基本的なながれです。

ちょっと脱線してしまうと、今回の勉強会にはじめて参加されたぷぅコッコさんというかたが体験記を「[ぷぅコッコの一期一会](http://pukokko.jugem.jp/?eid=978)」というブログにまとめておられます。

ここに登場するこころやさしきシェル芸師さんは、コマンドをくりだすまえにあたまのなかで入念にマトリックスを形成し、おなじ組みあわせを帳面づけしながらなんとか重複する組をわりだそうとしていました。（ずるをせず、ひたむきにお題にとりくむ姿勢が印象的でした。）

##Q8
「乱数」と聞いてどうしようかなぁとなやんだのですが、擬似乱数生成器を書くにはバッチファイル（cmd.exe）はあまりに非力なので、すなおに環境変数を利用することにしました。

環境変数 RANDOM は参照するたびに 0 から 32767 （2 ^ 15 - 1）までの値をかえしてくれます。経験上、乱数の度あいに偏りが見うけられるので、厳密には「一様な」とはいえないかもしれません。

あとはパディング用の半角空白と結合し、右から五文字ぶん抽出することで桁あわせをしています。

---

ながながと書いてしまいましたが、当日はみなさまおつかれさまでした。