TL;DR
コマンドプロンプトでは快適にシェル芸できなかったので
wsh で動く unix っぽい名前のコマンドをチマチマ作っていた。
その結果、なんかそれっぽく動くようになったので、
最近仕事で見たデータなどをネタにデモなどしてみようかと思った。
この記事で使用しているコマンド群は、以下の URL から取得できます。
https://github.com/asterisk9101/wincmd
はじまりはじまり
突然ですが以下のようなファイルがあります。
C:\> cat text01.log
colA, colB
----, ----
hoge, 1000
fuga, 1200
piyo, 3041
// comment
C:\>
text01.log は空白区切りで2列を持つテーブル状のテキストファイルです。
最終行には //
から始まる日本語のコメントが入力されています1。
textXX.log は連番になっており、01 から 30 まで 30 ファイル前後あるものとします。
また、このテーブルのレコード数は不定とします。
ここで、ファイルごとの colB を合計し、最大の colB 合計を持つファイルを取り出したいと思います。
突然過ぎて目的が呑み込めないかもしれませんが、とにかくそういう状況があったとします。
Step1 不要な行を削除する
とりあえず 1 つのファイルの colB 合計を求めようとしてみます。
text01.log にはいくつか不要な情報が含まれています。
まず不要な行を削除しましょう。不要な行は、カンマを含まない行とヘッダになっている 1,2 行目です。
C:\> cat text01.log | findstr ","
colA, colB
----, ----
hoge, 1000
fuga, 1200
piyo, 3041
C:\> :: 行末にあった空白行とコメントを削除できました
C:\>
C:\>
C:\> cat text01.log | findstr "," | sed /e "1d;2d"
hoge, 1000
fuga, 1200
piyo, 3041
C:\> :: 1 行目と 2 行目も削除できました
C:\>
findstr
は、Unix でいうところの grep
です。","
を含む行だけ取り出しています。
続いて sed
です。sed
には様々な機能があり、引数に与えた命令によって動作が変わります。ここでは d
命令を使って 1 行目と 2 行目を削除しています。
この記事では、このようにコマンドを数珠つなぎにして処理していく方法で、目的の達成を試みます。いわゆるシェル芸です。
Step2 2列目だけ取り出す
colB
の合計を求めるのに colA
は不要です。2 列目だけを取り出しましょう。
列を取り出すには cut
を使います。ただし、cut
は区切り文字を 1 文字しか指定できないので、前処理を挟みます。
C:\> cat text01.log | findstr "," | sed /e "1d;2d" | tr /d " "
hoge,1000
fuga,1200
piyo,3041
C:\> :: 空白が削除されました。
tr
は文字を置換するコマンドです。ここでは tr
に /d
オプションを指定しているので、" "
は置換されずに単に削除されました。
続いて cut
を使って 2 列目だけを取り出します。
C:\> cat text01.log | findstr "," | sed /e "1d;2d" | tr /d " " | cut /d "," /f 2
1000
1200
3041
C:\> :: 2 列目だけ取り出せました。
cut
の /d
は区切り文字を指定するオプションで、ここではカンマを指定しています。続く /f
オプションで 2 番目のフィールド(列)を選択しています。
元のテキストから欲しいデータだけ取り出すことができました。次は取り出したデータを使って計算していきます。
Step3 合計を計算する
計算するには eval
を使います2。
eval
を使うには eval
が受け付けられるようにな形(式)を作らなければいけません。
再び sed
を使用します。
C:\> cat text01.log | findstr "," | sed /e "1d;2d" | tr /d " " | cut /d "," /f 2 | sed /e "i+"
+
1000
+
1200
+
3041
C:\> :: 各行に + を挿入しました
sed
の i
命令は、各入力行の前に別の行を挿入する命令です。ここでは +
を挿入しています。
C:\> cat text01.log | findstr "," | sed /e "1d;2d" | tr /d " " | cut /d "," /f 2 | sed /e "i+" | xargs
+ 1000 + 1200 + 3041
C:\> :: 一行に集約された
xargs
は入力を 1 行に集約するコマンドです。各行は空白区切りで一行に集約されています。
xargs
はその名の通り、集約した入力を別のコマンドの引数(args)として、そのコマンドを実行することができます。以下のように使います。
C:\> cat text01.log | findstr "," | sed /e "1d;2d" | tr /d " " | cut /d "," /f 2 | sed /e "i+" | xargs eval
5241
C:\> :: xargs によって eval "+ 1000 + 1200 + 3041" が実行された
eval
は数式を評価するコマンドで、式 "+ 1000 + 1200 + 3041"
の計算結果が出力されています。
やっと colB の合計を求めることができました。ここまでで使用しているコマンドは 7 つです3。
欲しい情報は最大のcolB合計を持つファイルのファイル名ですので、ファイル名とセットで colB 合計を表示する方法を考えてみましょう。
Step4 ファイル名とセットで表示する
これは少し難しい話です。一連のコマンドでは実現できないような気がします。
2 つのコマンドの結果を並べて出力できると良い気がしますのでやってみましょう。
2 つのコマンドを一度に実行するには &
を使います。
C:\> cat text01.log | findstr "," | sed /e "1d;2d" | tr /d " " | cut /d "," /f 2 | sed /e "i+" | xargs eval | tr /d "\r\n" & puts ,text01.log
5241,text01.log
C:\> :: カンマ区切りでファイル名と colB 合計が表示されました
puts
は引数をそのまま出力するコマンドです。puts
の結果と eval
の結果を並べて表示するために eval
の後ろで tr
を使って改行を削除しています。
準備が整いました。次は、対象となる全てのファイルに対して、これまで組み立ててきたコマンドを実行します。
Step5 複数のファイルに対してコマンドを実行する
このステップでは、これまでのステップと違って、複数の値に対してコマンドを実行する方法を考えます4。
これまで組み上げてきたコマンドよりも上位の話ですね。
なにはともあれ対象のファイルの一覧を取得します。
C:\> ls
text01.log
text02.log
text03.log
text04.log
...
text30.log
C:> :: 30 個のファイルの一覧が表示されます。
ls
コマンドは、作業フォルダにあるファイルの一覧を表示するコマンドです5。このコマンドの出力を、対象ファイルのリストとして使いたいと思います。
C:\> for /f "usebackq" %i in (`ls`) do @( echo %i )
text01.log
text02.log
text03.log
text04.log
...
text30.log
C:\> :: ls と同じ結果が表示されます。
for
コマンドは do
に続くコマンドを繰り返し実行するコマンドです6。%i
には ls
の結果が一行ずつ代入されます。そのためカッコの中身に echo %i
とだけ書いておくと、単純に ls
を実行した場合と同じ結果が得られます。
作業フォルダに何種類かのファイルが混在している場合、単純な
ls
では力不足になることがあります。
そのような場合ls | findstr 絞り込み条件 | for /f "usebackq" %i in ('cat のようなコマンド') do @( hogehoge %i )
とする方が汎用性があります。
では、for
と ls
を使って、前の Step で作ったコマンドを実行してみましょう。
C:\> for /f "usebackq" %i in (`ls`) do @( cat %i | findstr "," | sed /e "1d;2d" | tr /d " " | cut /d "," /f 2 | sed /e "i+" | xargs eval | tr /d "\r\n" & puts ,%i )
5241,text01.log
1932,text02.log
1983,text03.log
,text04.log
8453,text05.log
8818,text06.log
3340,text07.log
...
C:\> :: ファイル名と各 colB 合計が並んで表示されました!
無事 colB 合計とファイル名が並んで表示されました。text04.log は colB が空白になっているようですが普通に実行されるようです。
Step6 結果を取得する
既に忘れているかも知れませんが、この記事の目的は最大のcolB 合計を持つファイルのファイル名を取得することです。
残り一息ですので頑張りましょう。
では先ほどの結果をソートします。for
の結果は単純にパイプに渡せないようなのでカッコで括ってから次のコマンドに繋ぎます。
C:\> ( for /f "usebackq" %i in (`ls`) do @(略) ) | msort /t "," /k 1nr
8818,text06.log
8453,text05.log
5241,text01.log
3340,text07.log
1983,text03.log
1932,text02.log
,text04.log
...
C:\> :: カンマ区切りデータの 1 列目を数値としてソート
msort
コマンドは、入力をソートします7。/k
はソートキーを指定するオプションで、1
列目を数値(n
)として逆順(r
)にソートするように指定しています。/t
オプションは列の区切り文字をカンマに指定しています。
最大の colB 合計を持つのは text06.txt のようです。では、ファイル名以外の出力を除去していきましょう。
C:\> ( for /f "usebackq" %i in (`ls`) do @(略) ) | msort /t "," /k 1nr | head /n 1
8818,text06.log
C:\> :: 最初の 1 行を取得できました
head
は入力の先頭部分だけを表示するコマンドです。/n
オプションにより入力の先頭 1 行だけを出力しています。
あとは以前にも出てきた cut
コマンドで、出力の 2 列目を取り出すだけです。
C:\> ( for /f "usebackq" %i in (`ls`) do @(略) ) | msort /t "," /k 1nr | head /n 1 | cut /d "," /f 2
text06.log
C:\> :: 最大の colB 合計を持つファイルのファイル名を取得することができました!
おしまい
長い道のり(コマンド)でしたが、コマンドだけで比較的複雑な処理ができることを確認できました。
コマンドプロンプト上では大仰に見えますが、バッチスクリプトにまとめてしまうとスマートに見えるようになるでしょう。
ここまでで使用した標準外のコマンドは 9 種類です8。Unix のシェルスクリプトに比べて貧弱と言われているコマンドプロンプトですが、いくつかのコマンドを追加してやることで、そこそこ使えることが理解頂けたかと思います。レガシー技術は嫌われることが多い気がしますが、制限が強い環境でも色々できるようにしておくと良いのではないでしょうか。
-
業務っぽいですね ↩
-
Unix の eval とは全く違った動きをします。どちらかというと
expr
です。リネームするかも。 ↩ -
sed は反則ですかそうですか ↩
-
理解不足のせいか
xargs
では実行できませんでした。 ↩ -
実体はただの
dir /b
コマンドです。 ↩ -
for
コマンドは非常に多機能、というかコマンド作業に必須の機能をいくつも持っており、for
が使えるようになるとバッチスクリプト中級者であると言われるほど重要なコマンドです。が、詳しい機能・文法は他の記事に譲ります。 ↩ -
コマンドプロンプト標準の sort コマンドはフィールドごとのソートができませんので、
msort
を使います。 ↩ -
cat
は使わなくてもよかった。。。 ↩