日々之迷歩

世の中わからんことだらけ

ITが複雑で難しくなっていく様に翻弄される日々です。微力ながら共著させていただいた「シェル・ワンライナー160本ノック」をよろしくお願い申し上げます。

第24回シェル芸勉強会へ遠隔参加

ということで今回は快挙です!ついに会場の様子を写真に撮ることが出来ました・・・こんなことで感無量な気持ちなれるものですね。今回の会場はいつもの会場とは違って、会社事務所の会議スペースを利用させていただきました。参加者が少なかったのでテレビを使った小ぢんまりな雰囲気です。

問題出題と解説の上田さん、ありがとうございます。参加者の方々もお疲れ様でした。

会場の理想は中立の立場になれる場所の方がいいなあと思っているのですが、今回は会場がなかなか確保出来なかったので。 参加者数は自分も含めて全部で3名。1名は午前中のみでもう1名は午前の途中からと午後にかけての参加でした。 午後に参加された方は熊本から遠路はるばる参加いただきました。

勉強会の情報

勉強会主催者の上田さんが公開されているページをご覧ください。 b.ueda.tech

開始前

今回はすぐに午前の部が始まったので、特にイントロは話しませんでした。一応こんなの準備はしていたのだがお蔵入りです。 speakerdeck.com

午前の部

今回午前の部は初心者向けに偽りなし!?な感じでした。 前半は勉強会主催者上田さんによるawk入門編でした。manの読み方から基本的な使い方の説明がありました。

後半はgreymdさんによる初心者向け問題が展開されていきました。 毎日叩ける シェル芸を覚えよう! - Speaker Deck

午前の部にいらっしゃった方はawkやテキスト処理にあまり慣れていらっしゃらないようでしたので、 私が補足説明をしながらこちらのペースで進めて丁度いい感じだったと思います。 余談でデータベースを使う場合にCUIでは下記のような感じで処理出来る、ってのをお話ししたりしました。 CUIとGUIのそれぞれの良さがあるということが面白いと思っていただけたようです。

$ echo 'SELECT * FROM users;' | mysql 接続先サーバ

お昼は近所の博多ラーメンへ行きました。長いこと行ってなかったので久しぶりに美味しかったですね。 長浜ラーメンはシオカライぜとか、福岡市内で豚骨ラーメン以外はなかなか流行らんのうとか、 そもそもラーメン自体酸化した脂肪だから体にワルイやろ!と身も蓋もないこととかが話題になりました。

午後の部

人数は減って私ともう一人の2名でさしの勝負!?の時間となりました。 ひどい問題という前評判の割には?比較的マトモな印象であったような気がします??

ではここから福岡サテライト会場で解説した内容を記載いたします。

参加者の方がTukubaiコマンドの使い方に興味があるということもあり、Tukubaiコマンドを使った解答をメインに作成してみました。 まあ私の場合Tukubai使った方が楽な場合が多い気がしますが。

解答例はmacOSで作成しました。GNU版のコマンドはguniqなど頭にgが付くコマンドになっている場合がございます。

Q1

横方向つまり行毎での集計な問題。Tukubai的解答がこちらです。まずは行番号付けて縦に並べて整列します。

$ cat Q1 | juni | tarr num=1 | sort
1 卵
1 玉子
1 玉子
1 玉子
1 玉子
...
4 卵
4 玉子
5 卵
5 玉子
5 玉子

次に行毎の重複を数え上げて横に並べ替えます。3列目と5列目が重複の数になっています。

$ cat Q1 | juni | tarr num=1 | sort | count 1 2 | yarr num=1
1 卵 1 玉子 5
2 卵 3 玉子 3
3 卵 2 玉子 4
4 卵 5 玉子 1
5 卵 1 玉子 2

最後はawkで整形して完成です。

$ cat Q1 | juni | tarr num=1 | sort | count 1 2 | yarr num=1 | awk '{print $4":"$5,$2":"$3}'
玉子:5 卵:1
玉子:3 卵:3
玉子:4 卵:2
玉子:1 卵:5
玉子:2 卵:1

次にオーソドックスにawkを使った解答です。しかし最初はうっかりミスで、連想配列を行毎に初期化するのを忘れて累計が出てます。

$ cat Q1 | awk '{for(i=1;i<=NF;i++){a[$i]++};for(v in a){printf v":"a[v]" "};print ""}'
玉子:5 卵:1 
玉子:8 卵:4 
玉子:12 卵:6 
玉子:13 卵:11 
玉子:15 卵:12 

初期化をどうするか?awkにdelete()という関数があるようなので利用しました。

$ cat Q1 | awk '{for(i=1;i<=NF;i++){a[$i]++};for(v in a){printf v":"a[v]" "};print "";delete(a)}'
玉子:5 卵:1 
玉子:3 卵:3 
玉子:4 卵:2 
玉子:1 卵:5 
玉子:2 卵:1 

参考にさせていただいたツイートです。

Q2

2回目以降の重複を削除する問題。これもTukubai芸の解答を考えました。まず縦に並べて行番号を付けておきます。

$ cat Q2 | grep -o . | juni
1 へ
2 の
3 へ
4 の
5 も
6 へ
7 じ

次に列を入れ替えて並べ替えます。そして各文字をキーにして最初の1行目のみを取得します。

$ cat Q2 | grep -o . | juni | self 2 1 | sort -k1,1 | getfirst 1 1
じ 7
の 2
へ 1
も 5

後は2列目をキーにして並べ替え、1列目だけを選べば完成です。

$ cat Q2 | grep -o . | juni | self 2 1 | sort -k1,1 | getfirst 1 1 | sort -k2,2n | self 1 | tr -d '\n'
へのもじ

各文字をキーにして最初の1行目のみを出力する方法ですが、出題者上田さんのawkを使った解答を参考に補足させていただきました。 下記の実行結果を見れば動きが理解しやすいかもしれません。 !a[$1]a[$1]=1の組み合わせがポイントで、!a[$1]が真(つまり1)になった時に出力されています。

$ cat Q2 | grep -o . | awk '{print $1,!a[$1];a[$1]=1}'
へ 1
の 1
へ 0
の 0
も 1
へ 0
じ 1

勉強会終了後、この考え方を更に発展させた解答例を思いつきました。まずはこちらをご覧ください。

$ cat Q2 | grep -o . | awk '{print $1,!a[$1]++}'
へ 1
の 1
へ 0
の 0
も 1
へ 0
じ 1

つまり!a[$1]++が真の時に出力すれば良いということですね。

$ cat Q2 | grep -o . | awk '!a[$1]++'
へ
の
も
じ

この考え方は、シェルスクリプトマガジンvol.39に載っていたAWK入門での記事を参考にさせていただきました。

シェルスクリプトマガジン vol.39

Q3

これは色々ゴニョゴニョやってみたのだがうまくいきませんでした。

GNU版uniqの--group=bothというオプションにびっくりでした。

出題者上田さんのawkを使った解答例に補足をさせていただきました。 下記の動きを見てもらえばif($1!=a)a=$1の組み合わせがどう働いているか分かりやすいと思います。 1列目のデータが変わる行のところで$1!=aが真になるのを利用しています。

$ sort Q3 | awk '{print $1!=a,$0;a=$1}'
1 金 日成
0 金 正日
0 金 正男
1 キム タオル
0 キム ワイプ

Q4

Personal Tukubai の反則芸で、何の苦労もございませんでした、、、 有償ソフトのPersonal Tukubaiでは、xlsx形式のファイルが読み書き出来るコマンドが使えます。

$ rexcelx 1 A1 A1 Q4.xlsx
114514

補足ですが、xlsx形式のファイルはXML形式になった複数のファイルが一つのzipファイルに固められたものです。 fileコマンドで確認するとzip形式のファイルということが確認出来ます。

$ file Q4.xlsx 
Q4.xlsx: Zip archive data, at least v2.0 to extract

unzipコマンドで-lオプションを使うと、zipファイルの中身一覧を出力します。 xl/worksheets/sheet1.xmlというファイルに、シート1の実データが記載されているようです。

$ unzip -l Q4/Q4.xlsx 
Archive:  Q4/Q4.xlsx
  Length     Date   Time    Name
 --------    ----   ----    ----
     1220  01-01-80 00:00   [Content_Types].xml
      733  01-01-80 00:00   _rels/.rels
      698  01-01-80 00:00   xl/_rels/workbook.xml.rels
      745  01-01-80 00:00   xl/workbook.xml
      795  01-01-80 00:00   xl/sharedStrings.xml
     7646  01-01-80 00:00   xl/theme/theme1.xml
     1210  01-01-80 00:00   xl/styles.xml
     1420  01-01-80 00:00   xl/worksheets/sheet1.xml
    22300  01-01-80 00:00   docProps/thumbnail.jpeg
      617  01-01-80 00:00   docProps/core.xml
      803  01-01-80 00:00   docProps/app.xml
 --------                   -------
    38187                   11 files

ということでこれは出題者上田さんのエクシェル芸を使うと良さそうです。 詳しくは下記上田さんのブログや、上田さん著書「シェルプログラミング実用テクニック」をご覧ください。 出題者上田さんの解答例にあるhxselectというコマンドは、html-xml-utils というツールに付属しています。 UbuntuやmacOSな方はパッケージで簡単にインストール出来ます。

b.ueda.tech

Q5

数式が書いてあるテンプレートに値を代入するような問題。まずは下記のように無難にtrコマンドで代入処理をします。

$ tr 'x' '2' < Q5 
2 + 2^2
2 + 1/2
2*2*2

後はbcコマンドに渡して計算します。-lオプションは浮動小数点計算させるためです。

$ tr 'x' '2' < Q5 | bc -l
6
2.50000000000000000000
8

次にecho 2 |で始めた場合の解答例です。これはコマンド列を生成するという発想にすれば良さそうですね。 こんな感じで実行するコマンド列を作ってみます。

$ echo 2 | sed 's/./tr x & < Q5/'
tr x 2 < Q5

出来たコマンド列をシェルに食わせて実行して完成です。

$ echo 2 | sed 's/./tr x & < Q5/' | sh | bc -l
6
2.50000000000000000000
8

Q6

重複カウントと置換の複合問題といったところでしょうか。 まずは前半で重複カウントをします。玉子の方が多いようです。

$ cat Q6 | grep -oE '(卵|玉子)' | sort | uniq -c | sort -k1,1n
  12 卵
  15 玉子

次にここからコマンド列を生成します。sedを使って置換する準備です。

$ cat Q6 | grep -oE '(卵|玉子)' | sort | uniq -c | sort -k1,1n | xargs | awk '{print "sed s/"$2"/"$4"/g Q6"}'
sed s/卵/玉子/g Q6

出来たコマンド列をシェルに渡して実行して完成です。

$ cat Q6 | grep -oE '(卵|玉子)' | sort | uniq -c | sort -k1,1n | xargs | awk '{print "sed s/"$2"/"$4"/g Q6"}' | sh
玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子

最後の処理を変える別解を示します。下記のようにsedスクリプトを生成します。

$ cat Q6 | grep -oE '(卵|玉子)' | sort | uniq -c | sort -k1,1n | xargs | awk '{print "s/"$2"/"$4"/g"}'
s/卵/玉子/g

作成したsedスクリプトを、GNU sedの-f -オプションで標準入力から受け取って処理して完成です。

$ cat Q6 | grep -oE '(卵|玉子)' | sort | uniq -c | sort -k1,1n | xargs | awk '{print "s/"$2"/"$4"/g"}' | gsed -f - Q6
玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子玉子

Q7

さあ難しい領域に突入してきました。色々とこねくり回してたけど敗北でした。

GNU awkにasort()関数があったんですね。出題者上田さんの発想力を勉強させていただきました。

Q8

前半はまあ力技です。出題者上田さんの解答例とほぼ同様になりました。 後半は素数の選択自体はfactorコマンド(macOSの場合はgfactorコマンド)で簡単そうですが、 計算元の数字を残した上で検索する方法が分かりませんでした。 grepの-fオプションを使うのかなあ?というところまでは思いついたのですが。

出題者上田さんが別解として解説されていたgrepの-fオプションを使った解答例を理解するために、 自分なりに解説しながら記載させてもらうことにします。

まずは検索元データの作成です。

$ cat tmp | sed 's/./& /g' | awk '{print $0,$1*$2*$3*$4+$5*$6*$7}'
1 2 3 4 5 6 7  234
1 2 3 4 5 7 6  234
1 2 3 4 6 5 7  234
1 2 3 4 6 7 5  234
1 2 3 4 7 5 6  234
.....
7 6 5 4 1 3 2  846
7 6 5 4 2 1 3  846
7 6 5 4 2 3 1  846
7 6 5 4 3 1 2  846
7 6 5 4 3 2 1  846

計算結果は846以下みたいなので、845以下の素数のリストを作っておきます。正確性を上げるために正規表現にしておきます。

$ seq 1 846 | gfactor | awk 'NF==2{print " "$2"$"}' | head
 2$
 3$
 5$
 7$
 11$
.....
 821$
 823$
 827$
 829$
 839$

後は素数の正規表現リストをgrepの-fオプションで渡してやれば良いですね。ここではbashのプロセス置換を利用しています。

$ cat tmp | sed 's/./& /g' | awk '{print $0,$1*$2*$3*$4+$5*$6*$7}' | grep -f <(seq 1 846 | gfactor | awk 'NF==2{print " "$2"$"}')
2 3 4 6 1 5 7  179
2 3 4 6 1 7 5  179
2 3 4 6 5 1 7  179
2 3 4 6 5 7 1  179
2 3 4 6 7 1 5  179
.....
6 4 3 2 1 7 5  179
6 4 3 2 5 1 7  179
6 4 3 2 5 7 1  179
6 4 3 2 7 1 5  179
6 4 3 2 7 5 1  179

ここで余談です。sedで1文字ずつ分ける処理を使っているが、この処理は意外と負荷が高めなんですよね。 負荷を減らすためにawkを使う事例を紹介します。

awkで列の区切り文字(セパレータ)を表すFSという特殊変数を空文字にすると、連続した文字を一つずつ列として処理してくれます。

$ cat tmp | awk '{print $0,$1*$2*$3*$4+$5*$6*$7}' FS=
1234567 234
1234576 234
1234657 234
1234675 234
1234756 234
.....
7654132 846
7654213 846
7654231 846
7654312 846
7654321 846

これを使っても同様に結果が出せます。

$ cat tmp | awk '{print $0,$1*$2*$3*$4+$5*$6*$7}' FS= | grep -f <(seq 1 846 | gfactor | awk 'NF==2{print " "$2"$"}')
2346157 179
2346175 179
2346517 179
2346571 179
2346715 179
.....
6432175 179
6432517 179
6432571 179
6432715 179
6432751 179

上記のまとめでです。

  • sed 's/./& /g'で1文字ずつ分割する処理は負荷が高め。
  • awkでFS=を指定すると、1文字ずつフィールド分割して処理。

このawkでFS=を使うやり方は、真・マイナンバーシェル芸の記事を書いていた時にTwitterで教えていただいきました。

papiro.hatenablog.jp

終了後

午後も参加いただいた方は、熊本から遠路はるばるいらっしゃいました。 ずいぶん前に福岡の勉強会でお会いしてから大変お久しぶりでした。 TwitterでTukubaiコマンドに興味があるのを聞いていたので、どんなコマンドがあるかなど解説させていただきました。

お話を聞いてみると、仕事での開発はC#でやることが多いとのことですが、データ処理やお仕事の自動化では、シェルやLispを扱うことが多いらしいです。 S式が飛び交うAPIをwgetで叩いてるとか面白いですね。やっぱりシェルやテキスト処理は必要な素養なんだなあと改めて実感した次第です。

またデータからコマンド列を作ってシェルにパイプで渡して実行したり、sedの命令やgrepの検索文字列をデータから作って渡す、という考え方が面白いと感じられたそうです。これが出来るとメタプログラミング的な要素をCUIで実現出来るので重要だと思うし、今回の問題もそういう意図があった気がしています。

1年くらい前から筋トレを本格的にされていて、漢も惚れそなカッチョイイ体をされています。自分も最近少しながら筋トレ始めてるので、今後も色々参考にさせていただきたいですね。