第30回シェル芸勉強会問題 解いた

今回も勉強会にはいけませんでした。金欠であるが故、頼みの綱であるバイトのシフトに入らざるを得なかったのです…

まだ未完ですが、とりあえずということで解答を出します。問題はここで。

Q1

$ find . -name "*.md" | xargs -n1 awk '/Keywords/&&!x{print gensub("main.md", "", "g", gensub("./", "", "g", FILENAME)), $0;x++}'

大好き awkFILENAME という変数は初めて知りました。これは今後のシェル芸に地味に役立ちそうな予感。

追記:
こっちの方がより建設的ですね(?)。もとより、自然かつサッパリとできています。

$ find . -name "*.md" | xargs grep Keywords | awk '!a[$1]++' | sed -r 's/:/ /1;s_(\./posts/|/main\.md)__g'

Q2

$ cat url.html | perl -pe 's/((href|src)\=)"\.*(?!https*:\/\/)+/\1"\/files\//g' | sed 's/files\/\//files\//g'

初めて perl を置換コマンドとして使いました。理由は否定先読みが出来るからです。便利ですねぇ。s/sed/perl/g したいかも(((

ここで自分が試行錯誤した内容を軽く述べます。

最初に $ cat url.html | sed -r 's/((href|src)\=)"[^(http)/]//g' | sed 's/files\/\//files\//g' としていたのですが、テキスト中の href="huge.html" にマッチしないんですね。おかしいなぁとおもったのですが、このワンライナー中にある [^(http)/] は、「(もhもtもpも)も/も含まない文字1つ」になっていなのですね。

この場合の hugeh がバッチリこの条件にあってしまっているので置換が行われないのでした。はてと困って調べた所、

(?!hage)

と言うものがあるらしい?!(ボケたつもりです) これはおそらく「hage という文字列を含まない位置で」ということだと思います。自分はそう理解しているのです。ですので例えば ABC から始まらない任意の3文字は

(?!ABC)...

でパターンが作れますね。厳密には色々とあるようですが僕にはわかりません。ということで 特定の文字列を含まないという正規表現 (Weblog on mebius.tokaichiba.jp) がいい感じに述べてくれているので参照してください。

今回の場合はダブルクウォートの始まりが http:// か https:// にマッチしなければいいので

s/((href|src)\=)"\.*(?!https*:\/\/)+/\1"\/files\//g

としたわけです。この否定先読みは sed では扱えないらしいので、とうとう perl に手を出してしまいました。午前中もなんか perl 芸やっていたらしいので深みに嵌りそう…

Q3

$ cat list | awk 'BEGIN{print"Content-Type: text/html\n\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset="utf-8">\n</head>\n<body>\n<ul>"} {print "<li>"$2"</li>"} END{print"</ul>\n</body>\n</html>"}'

スミマセン!!サボりました!!!!!!

Q5

$ cat complex | sed 's/\*i/j/g' | awk '{a[NR]=$0} END{printf("print(%s*%s)\n", "("a[1]")", "("a[2]")")}' | python

色々とゴリ押ししてしまいました。そして複素数と言ったら python ですよねぇ。最近は Haskell 芸やってみたいです。

なお、tukubai の mojihame を使うともうちょっと短くなります。python使っていますが。

$ mojihame <(echo 'print((%1)*(%2))') <(sed 's/ //g;s/\*i/j/g' complex | xargs) | python

Q6

$ echo 0 1 | awk '{while(1){$0 = $0 " " $NF + $(NF-1);print $(NF-1) " "}}' | sed '/6765/q' | tail -5 | head -1

すみませんまた awk です。

この awk をより短くしてみました。しかもこっちの方がわかりやすい。

$ echo | awk '{m=1;while(1){l=m;m=r;print r=l+m}}' | sed '/6765/q' | tail -5 | head -1

Q7

$ comm -3 <(seq 0 99 | xargs -n1 printf "%02d\n") <(seq 0 99 | xargs -n1 printf "%02d\n" | xargs -I {} grep -o {} nums | uniq)

なんか2度手間な気がしますが、「差分を取る」という割りと基本的なところに落ち着きました。

追記:
より短くできました↓

$ echo comm -3 "<(seq -w 99 "{," | xargs -I @ grep -o @ nums | uniq"}\) | bash

技巧的ですが、ブレース展開を使っています。 “<(seq -w 99 “{,” | xargs -I {} grep -o {} nums | uniq”}\) といった感じです。後半の bash文字列をコマンドとして実行するコマンドですので、これで実際に動かしています。

初めの解答ではなんか seq した後に printf していましたが、これは1桁の数字の十の位を0でパディングするためです。実際の所 seq 自身にこの機能がありまして、-w オプションを使えば速攻で解決というわけです。このオプションですが、manページには書いてありません。seq --help でご覧になれます。

更に追記:
そうでした。echo からの bash をやるときはたいてい eval が効くんでした。と言うわけで以下のようにまた短くなりましたよ↓

$ eval comm -3 "<(seq -w 99 "{," | xargs -I @ grep -o @ nums | uniq"}\)

Q8

$ cat alphabet | sed 's/\(.\)-\(.\)/\1-\2 \2 \1/g' | perl -ale 'print abs((ord $F[1]) - (ord $F[2]))' | nl -nln | sort -k2nr | head -1 | cut -f1

awk に逃げがちな僕ですが、ここでやっと生きた心地のするシェル芸が出来ました。ここでも思うことですが、perl マジ便利perlawk の両刀使いになりたいなぁ。

追記:
ゴチャゴチャやらずに素直な気持ちになったら結構短くなりました↓

$ cat alphabet | sed -r 's/(.)-(.)/echo {\1..\2}/e' | awk '{if(m<NF){m=NF;l=NR}} END{print l}'

また、出力を行番号とせずに、その行の中身としたい場合は以下のようになります。

$ cat alphabet | sed -r 's/(.)-(.)/echo \1-\2 {\1..\2}/e' | awk '{if(m<NF){m=NF;c=$1}} END{print c}'