冬休みの宿題として「第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においては正規表現で最短マッチを行う最小量指定子「+?」が使えるので、これをありがたく利用します。

  1. s#[\[(](.+?)[\])][(\[](.+?)[)\]]#[$1]($2)#gで、wrong.md内の「(…)[…]」と「[…](…)」を全て「[…](…)」に置換
  2. 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="&#19990;&#30028;&#20013;&#12398;&...からメッセージを抽出して、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|

まず、マークダウンのテーブルの罫線を取り除くためにtrgrepを使います。

$ 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

その後、trsedを使ってマークダウンのテーブルの罫線を付け直せば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

A8による出力

xxdlessyabatanienを開くとわかるのですが、それぞれの文字に色を付けている箇所は

ESC[0;1;<30〜37の整数>;<90〜97の整数>m<色を付けたい文字>ESC[0m

のようになっています。

そこで、次のような1秒ごとに色指定用の数値を交互に切り替える無限ループをperlで用意します。

  1. 無限ループに入る前の初期値として、色指定用の数値の切り替え用に数値「1」をスカラ変数$nに代入
  2. while(1)で次のような処理を行う無限ループを作成
    1. print "\e[H\e[J"でターミナルをリセット
    2. 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の値を足し、その結果が色指定用の数値として無効であれば修正
    3. say()で標準出力に出力
    4. 次のループ用に$nの正負を反転
    5. 1秒間停止

そして、これを実行すると解答例にあるアニメーションがターミナル上に表示されます。


今回の出題に対する解答については、perl内の正規表現(PCREのような互換品も含む)で利用できる最小量指定子「*?」、「+?」にかなり助けられました。sedawkの中でもこれらの機能が使えれば便利なのですが。

あと、A8のGIFアニメーションはvokoscreenという画面録画用ツールで作成してみました。スクリーンショットを撮る感覚で手軽に実行結果をキャプチャできますのでアニメーション系シェル芸にとっての良きパートナーになるのではないでしょうか。

それでは皆さん、2019年も良いシェル芸ライフを。

Written on January 10, 2019