先日実施された、第36回シェル芸勉強会に参加してきたので、その復習。なんか、ブログの記事自体をすごく久しぶりに書いた気がする(個人的にRustで簡単なツール作ってるのだけど、難しすぎてそっちにリソース全振り中…。ある程度動くようにはなったけどいつ終わるのやら…簡単なはずだったのに…)。 大体いつもむずかしめの問題が多いのだけど、今回も難しかった。

問題及び模範解答はこちら。最初に、問題等に使用するファイルをgitからcloneしておくといい。

Q1.

welcome.txt」というファイルに隠されたメッセージを読み取れ、という内容。 単純にcatしただけだとアンダーバーしか見えないのだが、大体こういった場合は非表示文字(Null文字など)が使われているので、cat -A(もしくはcat -v)で非表示文字をキャレット記法で出力させる。 で、案の定「^@(Null文字)」が隠されていた。

何かの模様が書かれているということは伺えるのだけど、横幅がわからないなー…と考えていたところ、ebanさんが70文字で回答。 こういったときは大体80から10違いのパターンが多く、その結果70でビンゴだったとのこと。(80ってのは、vt100とかのデフォルトだっただろうか?仮想端末のsize周りいじってたらよく出る数字だけど、うろ覚え…(´・ω・`)) 70というのは、toilet(figlet)という渡した文字列をアスキーアートにして出力してくれるコマンドのデフォルトの幅が70だからのようだ。

個人的に考えた方法として、(目検が必要になるのだけど)以下のように幅を10〜140の間で出力した結果をlessに渡して、読める出力を調べるという方法もありそうだ。

seq 10 140 | xargs -I{} bash -c 'echo {};cat welcome.txt | tr "\00" "@" | fold -{};echo' | less

Q2.

worstディレクトリにある以下のようなファイル群に対し、「Y年X組.doc」という形式にリネームせよ、という問題。 ちょっと前にTwitter上で話題になったのをモチーフにした問題のようだ。

blacknon@test-ubuntu:~/ShellGeiData/vol.36$ ls -al worst/
合計 8
drwxrwxr-x 2 blacknon blacknon 4096  7月 10 12:11 .
drwxrwxr-x 3 blacknon blacknon 4096  7月  7 06:32 ..
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 1-B.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 1A.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 3年D組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 3年C組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 4年a組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 5年A組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 1ーC.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 1ーD.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 1年E組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 3年A組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 3年B組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 4年B組.doc
-rw-rw-r-- 1 blacknon blacknon    0  7月  7 04:05 4年C組.doc

きれいなやり方を思いつかず、ゴリッゴリのやり方で無理やり処理。 xargsでOSのコマンドを実行する方法で解いた。

ls -1 | xargs -I@ bash -c 'mv @ $(echo @|sed -r "y/1234ABCDー/1234ABCD-/;s/[a-z]/\u&/g;s/([1-4])[^A-D]/\1年/g;s/([1-4])([A-D])/\1年\2組/g;s/DOC/doc/g;s/([^組])(.doc)/\1組\2/")'

Q3.

2018年のすべての日付から、[2,3,5,7]の数字(重複可)が4個含まれている日付を抽出しろ、という問題。 ひとまず2018年の全日付を出力し、[2357]のみを抽出するようgrep。ヒットした行数が4個の行のみを出力させる。

seq -f 'date -d "2018/01/01 %gdays" "+%%Y-%%m-%%d"' 0 364 | bash \
   | xargs -I@ bash -c 'echo @ | grep -o [2357] | wc -l | [ $(cat) -eq 4 ] && echo @'
blacknon@test-ubuntu:~/ShellGeiData/vol.36$ seq -f 'date -d "2018/01/01 %gdays" "+%%Y-%%m-%%d"' 0 364 | bash \
>    | xargs -I@ bash -c 'echo @ | grep -o [2357] | wc -l | [ $(cat) -eq 4 ] && echo @'
2018-02-22
2018-02-23
2018-02-25
2018-02-27
2018-03-22
2018-03-23
2018-03-25
2018-03-27
2018-05-22
2018-05-23
2018-05-25
2018-05-27
2018-07-22
2018-07-23
2018-07-25
2018-07-27
2018-12-22
2018-12-23
2018-12-25
2018-12-27

Q4.

俳句を考えて、tanzakuファイルの中にその俳句を入れるという問題。 何だこりゃ…(´・ω・`)。さっぱりである。行列置換した後がよくわからない…

模範解答によると、datamash等を使って行列置換をして、その結果をawkで差し込んでるようだ。

blacknon@test-ubuntu:~/ShellGeiData/vol.36$ echo うんこだよ   うんうんこだよ うんこだよ   | xargs -n 1 | sed 's/./& /g' | datamash transpose -t ' ' | sed '1i\ ' | paste <(cat tanzaku) - | awk -F '' -v OFS='' 'NF<15;NF>14{$3=$15;$5=$13;$7=$11;$15=$13=$11="";print}'
┏ ーー-┷-ーー┓
┃ う う う ┃
┃ ん ん ん ┃
┃ こ う こ ┃
┃ だ ん だ ┃
┃ よ こ よ ┃
┃   だ   ┃
┃   よ   ┃
┗ーーーーーー┛

Q5.

cowsayのAAを逆にするという問題。 そのままrevすると右側にスペースが無いため崩れてしまうので、スペースで適当な桁数でパディングしてやってからrevしてやればいい。 revした後に輪郭などを構成している記号を入れ替えるのを忘れないように注意。

cowsay $(echo oh,it is the unko|rev) | awk '{printf($0);for(i=length($0);i<=30;i++){printf(" ")};print ""}' | rev | tr tr '()<>\\/' ')(></\\'
blacknon@test-ubuntu:~/ShellGeiData/vol.36$ cowsay $(echo oh,it is the unko|rev) | awk '{printf($0);for(i=length($0);i<=30;i++){printf(" ")};print ""}' | rev | tr '()<>\\/' ')(></\\'
           ___________________
          < oh,it is the unko >
           -------------------
               ^__^   /
       _______/(oo)  /
   /\/(       /(__)
      | w----||
      ||     ||

Q6.

seqで1から20まで出力した際に、素数の値のみを丸囲み数字に置換する、という問題。 丸囲み数字は連番のUnicodeなので、seqの出力結果に数字を足してやればいい。で、awkを使った方法ですごくきれいな書き方をされている方がいて、これ以上きれいな書き方は無いかなと思ったので紹介。

seq 20 | factor | tr -d ":" | awk '$1==$2{printf("%c\n",9311+$1)} $1!=$2{print $1}'
blacknon@test-ubuntu:~/ShellGeiData/vol.36$ seq 20 | factor | tr -d ":" | awk '$1==$2{printf("%c\n",9311+$1)} $1!=$2{print $1}'
1
②
③
4
⑤
6
⑦
8
9
10
⑪
12
⑬
14
15
16
⑰
18
⑲
20

awkのprintfで%cを使ってバイナリとして出力する際、そのままUnicodeとseqの出力(ascii)差分の数字を足して出力している。 すごくシンプルな回答だ。

Q7.

textファイルに非表示文字があるので、何行目に何があるのかを出力するという問題。 最初にcat -vで開いてから処理をしようかと思ったのだが、日本語を含むためうまくいかず。 で、いつものようにebanさんが大変にシンプルな回答をしていた。これ以上にシンプルな回答は無いと思うので、参考にさせてもらう。

grepで[[:cntrl:]](制御文字)のみを-oで抽出し、-nで行番号を取得。制御文字があるためバイナリとして扱われてしまうので-aで出力できるようにして、パイプでcat -v(非表示文字をキャレット記法で出力)させている。 すごいシンプルで美しい回答だ。

blacknon@test-ubuntu:~/ShellGeiData/vol.36$ grep -n -a -o '[[:cntrl:]]' text | cat -v
1:^@
5:^B
8:^F
10:^V
blacknon@test-ubuntu:~/ShellGeiData/vol.36$ # 全行の行番号とセットで出力する場合、grep単体だと厳しいためawkで処理
blacknon@test-ubuntu:~/ShellGeiData/vol.36$ awk 'BEGIN{FS=""}/[[:cntrl:]]/{for(i=1;i<=NF;i++)if($i~/[[:cntrl:]]/){print NR":"$i}}!/[[:cntrl:]]/{print NR":"}' text | cat -v
1:^@
2:
3:
4:
5:^B
6:
7:
8:^F
9:
10:^V
11:
12:
13:
14:
15:
16:
17:

Q8.

漢字のある箇所にのみ、半角カナで上の行にルビを振るという問題。 最初はmecabでやろうとしてたのだけど、kakasiを使うと楽にできるのではないか?というツイートを見かけたので、他の人のツイート等を参考にkakasiでやってみた。

echo 嘘は嘘であると見抜ける人でないと(掲示板を使うのは)難しい|tee >(kakasi -i utf-8 -Jk|sed 's/[^ア-ン゙]/ /g') >(sleep 0.5;cat) >/dev/null|cat
blacknon@test-ubuntu:~/ShellGeiData/vol.36$ echo 嘘は嘘であると見抜ける人でないと(掲示板を使うのは)難しい|tee >(kakasi -i utf-8 -Jk|sed 's/[^ア-ン゙]/ /g') >(sleep 0.5;cat) > /dev/null|cat
ウソ ウソ    ミヌケ ニン     ケイジバン ツカウ   ムズカシイ
嘘は嘘であると見抜ける人でないと(掲示板を使うのは)難しい

…なんか微妙に違うような気がするけど、辞書の関係だと思割れるのでまぁいいか。 毎度毎度だけど、今回も難しかった。次の日とか疲れて寝てるし(´・ω・`)。