大人のやりなおし「第18回ニンニク入れますかシェル芸勉強会」

はじめに

今回はUstream経由での遠隔参加となった『jus共催勉強会「なぜシェルに仕事をさせてはいけないのか」と第18回ニンニク入れますかシェル芸勉強会。そして第35回アブラマシマシUSP友の会定例会』について、当日(8月29日)の解答の反省を踏まえつつ、感想などをつらつらと書いてみました。

なお、問題と模範解答については「[【問題と解答】第18回ニンニク入れますかシェル芸勉強会 上田ブログ](https://blog.ueda.asia/?p=6836)」をごらんください。

準備:

今回より問題で使うファイルが

$ git clone https://github.com/ryuichiueda/ShellGeiData.git

で一撃でダウンロードできるようになりました。

問題の作成だけでも大変であろうにもかかわらず、このような参加者への細やかな気づかいを見せるシェル芸家元には本当に頭の下がる思いです。

Q1:

1列目がキー、2列目が値になっているファイル

$ cat text
001 オトン
001 オトン
001 アカン
002 オカン
003 オトン
003 ヤカン
003 オカン
004 オカン
005 オトン
005 ミカン
005 アカン

から「オトン」と「オカン」の両方の値があるキーを探します。

A1:

当日の解答は、

$ cat text | grep 'オトン\|オカン' | sort -k1,1n | uniq | awk 'BEGIN{k="";v=""}{if($1==k&&$2!=v){print $0};k=$1;v=$2}'
003 オトン

でしたが、「オカン」に逃げられたようです。

「オカン」に帰ってきてもらうように書きなおします。

$ cat text | grep 'オ[トカ]ン' | uniq | uniq -Dw3
003 オトン
003 オカン

uniq-D-wオプションで呼び戻せました。

-D, –all-repeated[=METHOD]
   重複する行を全て出力する グループは空行で区切られる
   METHOD={none(デフォルト),prepend,separate}

-w, –check-chars=N
   行の比較を最初の N 文字で行う

あと、別解として

$ cat text | yarr num=1 | grep 'オトン' | grep 'オカン'
003 オトン ヤカン オカン

のようにTukubaiのyarr num=<n>コマンドで、値を各キーにまとめてくっつけてから条件に合う行を探す方法もあります。

Q2:

ファイルab

$ cat a
谷保
鹿島田
分倍河原
川崎
$ cat b
分倍河原
谷保
登戸
南多摩

から出力の先頭にa(aにあるレコード)、b(bにあるレコード)、c(abの両方にあるレコード)を付けて

a 鹿島田
a 川崎
b 登戸
b 南多摩
c 谷保
c 分倍河原

と出力できるようにします。

A2:

当日のシェル芸

$ cat <(sed 's/^/a /' a) <(sed 's/^/b /' b) | sort -k2,2 | uniq -cf1 | sort | sed '/2 /s/a/c/' | awk '{print $2,$3}'
a 鹿島田
a 川崎
b 登戸
b 南多摩
c 谷保
c 分倍河原

でも無問題ではあるのですが、解答後Ustreamにて

comm - ソートされた二つのファイルを行単位に比較する

というcommコマンドがあることを知りましたので、早速これを使って別解をひねり出してみます。

$ comm <(sort a) <(sort b) | sed 's/^\t\t/c /' | sed 's/^\t/b /' | sed 's/^[^bc]/a &/' | sort
a 鹿島田
a 川崎
b 登戸
b 南多摩
c 谷保
c 分倍河原

少し短くなりました。

……ところで「分倍河原」とは?

Q3:

ファイルabcについて、

$ cat a
1 2
3 4 5
$ cat b
1 2 3

$ cat c
7
8
9

それぞれの合計を求めます(総合計ではありません)。

A3:

シェル芸界隈ではおなじみのコマンドxargsbashを呼び出すのがコツといえばコツです。

$ ls | xargs -I@ bash -c "echo -n @:; cat @ | xargs | tr ' ' '+' | bc"
a:15
b:6
c:24

一撃です。

ちなみに、xargs-Iオプションには

-I replace-str
   xargs が実行するコマンドに対してユーザが引き数 (すなわち
   initial-arguments) を指定したとき、その initial-arguments
   中にある replace-str の部分すべてを、標準入力から読み込ん
   だ名前で置き換える。
   なお、空白は、クォートされていない場合も、入力される項目
   の区切りにはならない。区切り記号は改行文字だけになるの
   だ。
   -x と -L 1 が自動的に設定される。

という意味があり、このオプションを使いこなすことであなたのシェル芸ライフはきっと向上することでしょう(効果には個人差があります)。

Q4:

下記のデータ

$ cat cross
_abcdef
a_x____
b______
c______
d______
e______
f___x__

から、「x」がある場所の縦軸と横軸の記号、すなわち

a-b
f-d

を求めます。

A4:

じわじわと問題の難度が上がってきました。

解き方としては、2行目以降の各マスに横軸の記号を「縦-横」の形式で付け加え、

$ cat cross | sed 's/./& /g' | awk 'NR==1{for(n=1;n<=NF;n++){h[n]=$n}}NR!=1{for(n=1;n<=NF;n++){printf $n"-"h[n]" "};print "\n"}'
a-_ _-a x-b _-c _-d _-e _-f

b-_ _-a _-b _-c _-d _-e _-f

c-_ _-a _-b _-c _-d _-e _-f

d-_ _-a _-b _-c _-d _-e _-f

e-_ _-a _-b _-c _-d _-e _-f

f-_ _-a _-b _-c x-d _-e _-f

これをgrep ‘x’で「x」がある行を取り出した後、sedで整形する

$ cat cross | sed 's/./& /g' | awk 'NR==1{for(n=1;n<=NF;n++){h[n]=$n}}NR!=1{for(n=1;n<=NF;n++){printf $n"-"h[n]" "};print "\n"}' | grep 'x' | sed 's/^\(.-\).*x-\(.\) .*/\1\2/'
a-b
f-d

というアプローチをとってみました。

シェル芸勉強会の問題も中盤になってくると、さすがに別解までは思い付かなくなってきますが、これはいかにもシェル芸勉強会らしい良問だと思います。

Q5:

ファイルtext

$ cat text
あ
あ




い
い

う

え



お お
お
お

の連続した空行を、1つの空行にまとめて出力します。

A5:

実は、cut-sオプション

-s, –squeeze-blank
   連続した空行の出力を抑止する

で一瞬にして片付いてしまうのですが、それだとさすがに味気ないので当日の別解

$ cat text | awk 'BEGIN{p=""}{if(!(p==""&&$0=="")){print};p=$0}'
あ
あ

い
い

う

え

お お
お
お

を紹介しておきます。

awkの処理の中で、変数pに現在行の値を代入して次行に移ったときの値チェックに使う、という最近お気に入りの解き方でシェル芸をしてみました。

Q6:

縦8マス×横8マスで白黒になっているチェスボードの画像ファイルを生成します。

A6:

今回のシェル芸勉強会もいよいよ心をへし折りにきたようで、当日のライブ中継からは出席者からの悲鳴らしきものが聞こえてきたような、聞こえてこなかったような、といった雰囲気でした。

そして実にマヌケなことに、この問題が出題された時点ではシェル芸人、あるいはシェル芸人を目指す者にとってのバイブルともいえる名著(個人の見解です)『シェルプログラミング実用テクニック』(技術評論社の書籍案内ページ)の260〜263ページにある「6.2.3 画像をテキストにして正規化する」の内容を完全に忘れていました。

そこで大昔、HTML5など影も形もなかった頃にWebブラウザ上で画像生成(現在ではセキュリティの都合上ほぼ不可です)するために使っていたXBMフォーマットを思い出し、苦しまぎれの解答

$ echo -e '#define A6_width 8\n#define A6_height 8\nstatic unsigned char A6_bits[] = {0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55};' > A6.xbm

をでっち上げました。

これはこれで悪くはないと思うのですが、やはりここは先ほどの「6.2.3 画像をテキストにして正規化する」で紹介されているPPMフォーマットの仲間であるPBMフォーマットで画像を生成するべきでしょう。

$ cat <(echo -e 'P1\n8 8') <(seq 8 | sed '1~2c\0 1 0 1 0 1 0 1' | sed '2~2c\1 0 1 0 1 0 1 0') > A6.pbm

ちなみに、XBMおよびPBMフォーマットの構造についてはWikipediaの「X BitMap」(注:英語)と「PNM (画像フォーマット)」を見れば大体のことが分かるようになっています。

Q7:

ファイルchinese_characters

$ cat chinese_characters
㔀㔁㔂㔃㔄㔅㔆㔇㔈㔉㔊㔋㔌㔍㔎㔏
㔐㔑㔒㔓㔔㔕㔖㔗㔘㔙㔚㔛㔜㔝㔞㔟
㔠㔡㔢㔣㔤㔥㔦㔧㔨㔩㔪㔫㔬㔭㔮㔯
㔰㔱㔲㔳㔴㔵㔶㔷㔸㔹㔺㔻㔼㔽㔾㔿
㕀㕁㕂㕃㕄㕅㕆㕇㕈㕉㕊㕋㕌㕍㕎㕏
㕐㕑㕒㕓㕔㕕㕖㕗㕘㕙㕚㕛㕜㕝㕞㕟
㕠㕡㕢㕣㕤㕥㕦㕧㕨㕩㕪㕫㕬㕭㕮㕯
㕰㕱㕲㕳㕴㕵㕶㕷㕸㕹㕺㕻㕼㕽㕾㕿
㖀㖁㖂㖃㖄㖅㖆㖇㖈㖉㖊㖋㖌㖍㖎㖏
㖐㖑㖒㖓㖔㖕㖖㖗㖘㖙㖚㖛㖜㖝㖞㖟
㖠㖡㖢㖣㖤㖥㖦㖧㖨㖩㖪㖫㖬㖭㖮㖯
㖰㖱㖲㖳㖴㖵㖶㖷㖸㖹㖺㖻㖼㖽㖾㖿
㗀㗁㗂㗃㗄㗅㗆㗇㗈㗉㕐㗊㗋㗌㗍㗎
㗐㗑㗒㗓㗔㗕㗖㗗㗘㗙㗚㗛㗜㗝㗞㗟
㗠㗡㗢㗣㗤㗥㗦㗧㗨㗩㗪㗫㗬㗭㗮㗯
㗰㗱㗲㗳㗴㗵㗶㗷㗸㗹㗺㗻㗼㗽㗾㗿

の中に1組だけ隠されている同じ文字と、その文字が隠れている行番号を表示します。

A7:

いよいよ終盤ということで、難度も凶悪なレベルに達しています。

持ち前の思考スピードの遅さもあいまって、当日はおよそ15分ほどあったはずの制限時間内での解答はできませんでしたが、落ち着いてよく考えてみた結果、次のような方法で解くことができました。

まずは先ほど紹介した、Q4を解くときに使ったawkの処理を少し手直しした方法

$ cat chinese_characters | sed 's/./& /g' | awk '{for(n=1;n<=NF;n++){print NR" "$n}}'
1 㔀
1 㔁
1 㔂
(...長いので中略...)
16 㗽
16 㗾
16 㗿

で各文字について「行番号 文字(プラス改行)」という形式に変換します。

そして第2フィールド・第1フィールドの順番でsortした後、-Dおよび-fオプションを付けたuniqで第2フィールドの値が重複している行を出力して完了です。

$ cat chinese_characters | sed 's/./& /g' | awk '{for(n=1;n<=NF;n++){print NR" "$n}}' | LANG=C sort -k2,2 -k1,1n | LANG=C uniq -Df1
6 㕐
13 㕐

説明するのを忘れていましたが、A2でも使っていたuniq-fオプションの意味は次の通りです。

-f, –skip-fields=N
   最初の N 個のフィールドを比較しない

なお、ここで注意しなければならないのは、sortuniqの処理を行うときにLANG=Cを各コマンドの前に付ける必要があるということです。

それでは、LANG=Cを外して実行してみます。

$ cat chinese_characters | sed 's/./& /g' | awk '{for(n=1;n<=NF;n++){print NR" "$n}}' | sort -k2,2 -k1,1n | uniq -Df1
1 㔀
1 㔁
1 㔂
(...めんどくさいので中略...)
16 㗽
16 㗾
16 㗿

……全くもってダメなようです。

Q8:

ファイルnumberの中から、2回以上登場する数字の並びで最も長いものを探し出します。

$ cat number
8264611130023148519839960536022802096895154738213681101003238003191122723922378922942503388843815799

A8:

今回のシェル芸勉強会のラスボスです。もちろん当日は手も足も出ませんでした。

しかし勉強である以上、問題を解かないわけにはいかないので強引に解答してみます。

……総当り戦です。見ての通りシェルをこき使っています。当然ながら遅いです。

$ for n in $(sed ':;p;h;s/^.//;t' number); do for m in $(sed ':;p;h;s/.$//;t' <(echo $n)); do if [ ${#m} -ge 2 ]; then echo $n | sed 's/\('$m'\)/<\1>/g'; fi; done; done | grep '\(<[^>]\+>\).*\1' | sed 's/^<\([^>]\+\)>.*/\1/' | sort -u
00
003
02
(...長さが2の出力が続くので中略...)
922
96
99

を実行して30〜40秒ほど待って得られた答えは、長さが3の「003」と「922」でした。

「ところで 俺のシェル芸を見てくれ こいつをどう思う?」
「すごく・・・重たいです・・・」

awkの処理に書き改めれば高速化はできると思うのですが、いまひとつawk力に乏しいもので……。


おわりに

当日にリアルタイムで解ける問題の数が少しは増えてきたような気がしますが、あいかわらず難しいです。勉強会タイトルにある通りニンニク入りのラーメンが食べたい気分です(あと餃子も)。

それはともかく、当日のリアル会場の雰囲気は『jus共催勉強会「なぜシェルに仕事をさせてはいけないのか」と第18回ニンニク入れますかシェル芸勉強会。そして第35回アブラマシマシUSP友の会定例会 @KDDIウェブコミュニケーションズ様 - Togetterまとめ』で知ることができるのですが、参加者の技量の高さといい、扱っているネタのマニアックさといい、あらゆる意味でスゴいです。まさに「変態」です(もちろん良い意味で)。

自分のスキルから考えてみれば、こういった領域に到達するのはかなり厳しいと感じるものの、これからもあきらめずにsh行に励むことにします。

……シェル芸、やらないか。

Written on September 1, 2015