冬休みの宿題として「第39回シェル芸勉強会」の問題を解いてみた
昨年(2018年)の12月22日に「第39回シェル芸勉強会 大阪サテライト」が開催されましたが、残念ながら仕事や介護などの都合で欠席せざるを得ませんでした。今回の欠席で減少したであろうシェル芸力を少しでも回復するために冬休みの宿題として当日出題された問題を解いてみました。
相変わらずスマートといえるような解答ではありませんが、出力結果に間違いはないと思います。多分。
- 問題と解答例:
A1:
$ perl -lnE 's#[\[(](.+?)[\])][(\[](.+?)[)\]]#[$1]($2)#g;s#\[(https?://.+?|.+?\..+?)\]\((.+?)\)#[$2]($1)#g;say' wrong.md
# わたしはマークダウソちょっとできる
## 軍馬県高崎市
[軍魔県](https://ja.wikipedia.org/wiki/%E7%BE%A4%E9%A6%AC%E7%9C%8C)は、日本の県庁所在地の一つ。県庁所在地は[高崎市](https://ja.wikipedia.org/wiki/%E9%AB%98%E5%B4%8E%E5%B8%82)
* [松井常松](https://ja.wikipedia.org/wiki/%E6%9D%BE%E4%BA%95%E5%B8%B8%E6%9D%BE)
* [高崎ハム](http://takasakiham.com/?transactionid=8e5164a76108c8411e7547d69e0dd0fd443f072a)
![たかさきしししょう](群馬県高崎市市章.svg)
sed
と異なり、perl
においては正規表現で最短マッチを行う最小量指定子「+?」が使えるので、これをありがたく利用します。
s#[\[(](.+?)[\])][(\[](.+?)[)\]]#[$1]($2)#g
で、wrong.md
内の「(…)[…]」と「[…](…)」を全て「[…](…)」に置換s#\[(https?://.+?|.+?\..+?)\]\((.+?)\)#[$2]($1)#g
で、「[URL](リンクテキスト)」になっている部分を「[リンクテキスト](URL)」に置換
汎用性に乏しいですが、問題のマークダウソに対してはこの解答で十分だと思います。
A2:
$ sed -rz 's/\n( +\*)/\1/g' attendee.md | sort | awk '{P[$4]=$5;P[$7]=$8;P[$10]=$11;print $2,"*福岡:"P["福岡:"],"*大阪:"P["大阪:"],"*東京:"P["東京:"];P[$4]=P[$7]=P[$10]=""}' | sed -r 's/[^ ]+: / /g;s/:/: /g;s/^/* /;s/ \*/\n * /g'
* 第34回シェル芸勉強会
* 大阪: 16
* 東京: 19
* 第35回シェル芸勉強会
* 大阪: 10
* 東京: 27
* 第36回シェル芸勉強会
* 東京: 38
* 第37回シェル芸勉強会
* 福岡: 8
* 大阪: 10
* 東京: 21
* 第38回シェル芸勉強会
* 福岡: 3
* 大阪: 8
* 東京: 26
まず、改行文字ではなくヌル文字で行を分割する-z
オプションを付けたsed
で、勉強会1回につき1行になるように整形します。なお、-r
は拡張正規表現を使うためのオプションです。
$ sed -rz 's/\n( +\*)/\1/g' attendee.md
* 第38回シェル芸勉強会 * 東京: 26 * 大阪: 8 * 福岡: 3
* 第36回シェル芸勉強会 * 東京: 38
* 第35回シェル芸勉強会 * 大阪: 10 * 東京: 27
* 第34回シェル芸勉強会 * 大阪: 16 * 東京: 19
* 第37回シェル芸勉強会 * 東京: 21 * 大阪: 10 * 福岡: 8
この出力をsort
した後、開催地名をキーにしたawk
の連想配列P
の各要素に数値を代入したものを題意に応じて出力し、最後にこれをsed
で整形すれば完了です。
A3:
$ sed -rz 's/^.*"(世[^"]+)".*/\1\n/' index.html | nkf --numchar-input
世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。
A2の冒頭と同様にsed -rz
を使い、<meta content="世界中の&...
からメッセージを抽出して、nkf --numchar-input
でUnicode文字参照を変換すればOKです。
A4:
$ perl -0nE 'open(CSS,">","index.css");s#<style>(.*?)</style>#say CSS $1#gse;open(JS,">","index.js");s#<script(?: [^>]+)?>(.*?)</script>#say JS $1#gse' index.html
sed -z
のように改行文字ではなくヌル文字で行を分割するために-0
オプションを付けてperl
のワンライナーを実行します。ちなみに、-n
は入力1行単位でのループ処理を、-E
はPerlの新機能を有効にしてコマンドラインを実行するオプションです。
A1でも利用した正規表現で最短マッチを行う最小量指定子「*?」を使い、<style>
要素の内容をindex.css
に、<script>
要素の内容をindex.js
にそれぞれ出力すれば良いのですが、置換演算子s/.../.../
(解答では「/」の代わりに「#」をデリミタに使用)に指定する修飾子として、マッチした箇所を全て置換する/g
修飾子、置換文字列を式として評価する/e
修飾子、そして「.」を改行文字にマッチさせる/s
修飾子を指定する必要があります。
余談ですが、この問題を解く際に/s
修飾子の指定を忘れていたため、半時間ほどハマることになってしまいました。/g
や/e
と比べるといささか地味な感じのする修飾子ですが、perl
を用いたシェル芸では忘れてはならない存在です。
A5:
$ perl -0nE 'open(HTML,">","index_no_cssjs.html");s#<style>.*?</style>##gs;s#<script(?: [^>]+)?>.*?</script>##gs;print HTML' index.html
置換演算子s/.../.../
で/e
修飾子を指定していない点を除いては、A4とほぼ同じ手法の解答ですが、問題文に
今度は改行等、余計な文字は入れないでください。
という条件が設定されていますので、ファイルへの出力時にsay()
ではなくprint()
を使う必要があります。
A6:
$ sed -r 's/\\u([0-9A-F]{4})/\&#x\1;/g;s/\\x([0-9a-f]{2})/\&#x\1;/g' index.js | nkf --numchar-input | nkf --numchar-input | sed -r 's/([^\\])\\/\1/g'
A3の発展形ともいえる解答です。nkf --numchar-input
を2回実行している点を除いては、トリッキーな要素のないシンプルな解答になりました。
A7:
$ cat table.md | tr -d '\|\-\:' | grep -v '^$' | tateyoko | tr ' ' '\|' | sed 's/^/|/;s/$/|/;1p' | sed '2s/[^|]/-/g'
|回|38回|37回|36回|35回|34回|33回|32回|31回|30回|29回|
|-|---|---|---|---|---|---|---|---|---|---|
|年月|201811|201809|201807|201804|201803|201801|201712|201710|201708|201706|
|人数|37|39|38|37|35|40|39|37|46|55|
まず、マークダウンのテーブルの罫線を取り除くためにtr
とgrep
を使います。
$ cat table.md | tr -d '\|\-\:' | grep -v '^$'
回 年月 人数
38回 201811 37
37回 201809 39
36回 201807 38
35回 201804 37
34回 201803 35
33回 201801 40
32回 201712 39
31回 201710 37
30回 201708 46
29回 201706 55
この出力に、USP研究所によるOpen usp Tukubaiコマンド群に含まれるtateyoko
コマンドを適用します。
$ cat table.md | tr -d '\|\-\:' | grep -v '^$' | tateyoko
回 38回 37回 36回 35回 34回 33回 32回 31回 30回 29回
年月 201811 201809 201807 201804 201803 201801 201712 201710 201708 201706
人数 37 39 38 37 35 40 39 37 46 55
その後、tr
やsed
を使ってマークダウンのテーブルの罫線を付け直せばOKです。
A8:
$ perl -0nE '$n=1;while(1){print "\e[H\e[J";s/;(3[0-7]);(9[0-7])m/";".($1+$n<30?37:$1+$n>37?30:$1+$n).";".($2+$n<90?97:$2+$n>97?90:$2+$n)."m"/ge;say;$n*=-1;sleep 1}' yabatanien
xxd
やless
でyabatanien
を開くとわかるのですが、それぞれの文字に色を付けている箇所は
ESC
[0;1;<30〜37の整数>;<90〜97の整数>m<色を付けたい文字>ESC
[0m
のようになっています。
そこで、次のような1秒ごとに色指定用の数値を交互に切り替える無限ループをperl
で用意します。
- 無限ループに入る前の初期値として、色指定用の数値の切り替え用に数値「1」をスカラ変数
$n
に代入 while(1)
で次のような処理を行う無限ループを作成print "\e[H\e[J"
でターミナルをリセットs/;(3[0-7]);(9[0-7])m/";".($1+$n<30?37:$1+$n>37?30:$1+$n).";".($2+$n<90?97:$2+$n>97?90:$2+$n)."m"/ge
で文字色を指定している箇所に対して$n
の値を足し、その結果が色指定用の数値として無効であれば修正say()
で標準出力に出力- 次のループ用に
$n
の正負を反転 - 1秒間停止
そして、これを実行すると解答例にあるアニメーションがターミナル上に表示されます。
今回の出題に対する解答については、perl
内の正規表現(PCREのような互換品も含む)で利用できる最小量指定子「*?」、「+?」にかなり助けられました。sed
やawk
の中でもこれらの機能が使えれば便利なのですが。
あと、A8のGIFアニメーションはvokoscreenという画面録画用ツールで作成してみました。スクリーンショットを撮る感覚で手軽に実行結果をキャプチャできますのでアニメーション系シェル芸にとっての良きパートナーになるのではないでしょうか。
それでは皆さん、2019年も良いシェル芸ライフを。