日々之迷歩

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

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

「第45回シェル芸勉強会:福岡サテライト」レポート

AIP Cafeで始まったシェル芸勉強会の福岡サテライト会場ですが、今回は久しぶりにAIP Cafeでの開催でした。参加者は2人の方がキャンセルされて3人でした。1人は初参加で、1人は長崎サテライト会場に参加の経験がある方が福岡へやってこられました。2人とも会場の場所が分かりづらかったとのことで、AIP Cafeは場所が初見殺しだったことを忘れておりました。

勉強会の情報

勉強会主催者上田さんが公開されているリンク集をご覧ください。

b.ueda.tech

スライド資料

最初に話す時間が取れないので、イントロと言うより何だろう??

speakerdeck.com

午前の部

午前中はぷるさんの自宅から配信されたJavaScript入門を自宅で拝見いたしました。動画はこちらです。

https://www.pscp.tv/w/1zqKVEXyXAaxBwww.pscp.tv

JavaScript初心者が呟いた小並感溢れるツイート。 そもそも非同期処理とかやる機会が最近は全然無い。 そもそも最近お仕事でコードを書いていない。

twitter.com twitter.com twitter.com twitter.com twitter.com twitter.com twitter.com twitter.com

配信を見た後、会場のAIP Cafeに向かって移動を開始。

午後の部

午後からはいつものように、上田さんが出題された問題に取り組む時間です。自分なりの解答例を記載いたします。今回は難易度がやや下がった感じがありました。個人的には出題者上田さんによるQ6解答例が印象に残りました。

Q1

データを並べ替え、指定したキーの中で最後の行のみを抽出する問題。

日付が固定長じゃないという意地悪付きですが、GNU sortコマンドの-Vというバージョン名で並べ替えるというオプションを利用しました。ebanさんに教えていただきましたが、-Vオプションを使うと数字以外は全部区切りになるようです。

twitter.com

まずは最後の行のみを抽出する関係で-rオプションもつけて逆順に並べ替えます。

$ nkf data.csv | sort -r -V
2019/12/30,トマト,4個
2019/12/21,バナナ,5個
2019/12/9,バナナ,4個
2019/12/6,トマト,3個
2019/12/1,トマト,7個
2019/11/23,バナナ,2個
2019/11/21,ピーマン,32個
2019/11/8,ピーマン,31個
2019/11/2,トマト,1個

次に2行目の野菜をキーにして、1行目の日付が最も新しい(つまり最初の行)のみを抽出します。 uniqコマンドを使う手がありますが、awkの連想配列を使った解答がこちらです。

$ nkf data.csv | sort -r -V | sort -s -t, -k2,2 | awk -F, '!a[$2]++'
2019/12/30,トマト,4個
2019/12/21,バナナ,5個
2019/11/21,ピーマン,32個

なぜ最初の行のみが抽出出来るのか、以下に解説してみます。 まず2列目をキーにした配列aの値を確認します。

$ nkf data.csv | sort -r -V | sort -s -t, -k2,2 | awk -F, '{print $2,a[$2]++}'
トマト 0
トマト 1
トマト 2
トマト 3
バナナ 0
バナナ 1
バナナ 2
ピーマン 0
ピーマン 1

次に配列aの値に対して論理否定を指定します。 awkでは0は偽で0以外は真なので、値が0の時(つまり最初の行)のみ1(真)になります。 この真偽値をパターンで利用して、2列目をキーにして最初の行のみを抽出します。

$ nkf data.csv | sort -r -V | sort -s -t, -k2,2 | awk -F, '{print $2,!a[$2]++}'
トマト 1
トマト 0
トマト 0
トマト 0
バナナ 1
バナナ 0
バナナ 0
ピーマン 1
ピーマン 0

キーにする列を使った連想配列を使ってuniqコマンドと同様の動きを実現するテクニックは、日本GNU AWKユーザー会の会長である斉藤博文さんの著書で知りました。 www.shoeisha.co.jp

ユニケージ開発で利用されるTukubaiコマンドで、キーの列を指定して最初もしくは最後を抽出するコマンド(getfirst、getlast)があるのでこれを利用する手もあります。

$ nkf data.csv | sort -V | sort -s -t, -k2,2 | tr ',' ' ' | getlast 2 2
2019/12/30 トマト 4個
2019/12/21 バナナ 5個
2019/11/21 ピーマン 32個

uec.usp-lab.com

Q2

問題の意図を完全に見誤っていました、論外でしょ私、、、 データの2列目に記載された日毎の株価終値について、月毎に最大値と最小値を出そうという問題。

最大値と最小値を同時に処理するため、moreutils付属のpeeを使った解答例が出ていたので、福岡サテライトではpeeコマンドについて解説をしました。 peeはパイプに流れてきたデータを、2つのコマンドでそれぞれ処理させたい時に使います。

$ seq 1 3 | pee 'cat' 'tac'
1
2
3
3
2
1
$ seq 1 3 > temp
$ sed 's/$/どすえ/' temp > temp #【入出力で同じファイル指定】
$ cat temp #【ファイルの中身が消えちゃった!】
$ seq 1 3 > temp #【再度挑戦】
$ sed 's/$/どすえ/' temp | sponge temp #【sponge使ってみる】
$ cat temp #【中身は消えない】
1どすえ
2どすえ
3どすえ

私はawkで頑張る解答を考えていたが、時間内で完成出来ませんでした。 勉強会終了後下記の解答例を考えましたが、最大値や最小値を地道に選び出す考え方です。 2次元配列を使う意味はあまり無いですね、使ってみたかっただけです。 真の2次元配列はGNU awkの新しいバージョンのみサポートされています。

$ nkf nikkei_stock_average_daily_jp.csv | sed '1d;$d' | tr -d '"' | tr , ' ' | awk '{print substr($1,1,7),$2}' | awk 'pre_month!=$1{min=100000;max=0;}$2>max{amax[$1]=$2;max=$2}$2<min{amin[$1]=$2;min=$2}{pre_month=$1}END{for(i in amax)print i,amax[i],amin[i]}' | sort

#【GNU awkで真の2次元配列を使った例】
$ nkf nikkei_stock_average_daily_jp.csv | sed '1d;$d' | tr -d '"' | tr , ' ' | awk '{print substr($1,1,7),$2}' | awk 'pre_month!=$1{min=100000;max=0;}$2>max{a[$1][1]=$2;max=$2}$2<min{a[$1][2]=$2;min=$2}{pre_month=$1}END{for(i in a){print i,a[i][1],a[i][2]}}' | sort

#【awkで擬似的な2次元配列を使った例】
$ nkf nikkei_stock_average_daily_jp.csv | sed '1d;$d' | tr -d '"' | tr , ' ' | awk '{print substr($1,1,7),$2}' | awk 'pre_month!=$1{min=100000;max=0;}$2>max{a[$1,1]=$2;max=$2}$2<min{a[$1,2]=$2;min=$2}{pre_month=$1}END{for(i in a){split(i,b,SUBSEP);print b[1],b[2],a[b[1],b[2]]}}' | sort | awk 'p==$1{printf " "$3}p!=$1{printf "\n"$1" "$3}{p=$1}' | awk NF

Q3

何行目が違うかではなくて、最初の文字から通算して数える問題。 バイナリデータを比較して異なるバイトのファイル先頭からの位置を出力するcmpコマンドで出来るか?と思いましたが、時間内に出来ませんでした。 後ほど書きの解答を考えてみました。各文字は8バイトであり、少数の切り上げ処理はRubyが楽だったので利用しました。 少数を切り上げているのは、8バイト中いずれかのバイトに差異があった場合でも対応するためです。

$ cmp -l <(tr -d '\n' < flags_a) <(tr -d '\n' < flags_b) | ruby -alne 'puts ($F[0].to_f/8).ceil' | uniq
39
65

twitter.com

Q4

結合文字が混じっているのが意地悪な要因。 単純にgrep -o .とかだと結合文字がバラされて失敗します、さて困りました。 ちょっとズルをした解答例を考えました。 16進数で1バイト毎にダンプすると、文字はe8またはe9で始まっていることを利用しています。 e8e9の直前に改行文字0aを挿入しています。

$ cat nabe | od -t x1 -An | tr -d '\n' | sed -r 's/ e(8|9) / 0a &/g' | xxd -p -r | sed 1d
部
邊
(以下略)

Q5

地道に文字を全パターン切り出し、それぞれが回文になっているか確認しました。 まず確認する文字の全パターン切り出しをawkで頑張ります。

$ cat message | awk -F '' '{for(i=1;i<=NF-3;i++){for(j=1;j<=NF-i;j++)print substr($0,i,j)}}'
き
きつ
きつつ
きつつき
(略)
つ
つつ
つつき
つつきと
(略)
す
すが
すがし
すがしか
が
がし
がしか

後はそれぞれの行が回文になっているかを確認します。確認方法は下記の通りです。

# 回文の場合は表示される
$ echo きつつき | pee cat rev | uniq -d
きつつき

# 回文では無い場合は表示されない
$ echo たけやぶ | pee cat rev | uniq -d

上記の確認方法を使った解答例がこちらです。 ループを使って上記コマンドを何度も呼び出しているので、実行速度がとても遅いです。

$ cat message | awk -F '' '{for(i=1;i<=NF-3;i++){for(j=1;j<=NF-i;j++)print substr($0,i,j)}}' | while read l; do echo $l | pee cat rev | uniq -d; done | sort | uniq | awk 'length>1'
かしがすきすきすがしか
がすきすきすが
(略)
ゆんゆ
んゆん

Q6

最初問題の意図が理解出来ず。 参加者の方に意味を教えていただきました、ありがとうございます。

結局自力では解けず、出題者上田さんの解答解説で考え方を勉強させていただきました。 まずは各行をキーにしてファイル全体をgrep -oで検索して切り出します。 次に複数行表示される場合は部分的な回文なので、1行のみ表示される物だけを選べば良いということですね。

Q7

無限に出力する解答は出来ませんでした。 小数点以下10万桁まで限定のイカサマ的解答がこちらです。 scale=で指定する桁数が大きすぎると、危険なので注意してください。 (bcコマンドが使うメモリ量が膨大になるため)

$ echo 1 7 | sed 's@\(.\) \(.\)@scale=100000;\1/\2@' | bc | tr -d '\\\n'

Q8

ギブアップでした。

終わりに

久しぶりにAIP Cafeで福岡サテライトを開催しました。 会場の広さ的にはこのくらいの方が参加者のサポートがやりやすいと思いました。 持ってきたスピーカーがショボすぎたので、次回AIP Cafeで開催する時はマトモなスピーカーを準備するか、AIP Cafe設置のスピーカー活用を考えてみます。

僕以外の参加者は2人でしたが、1人は長崎サテライトで参加していただいている学生さんでした。 実家に帰ってきてるので福岡サテライトへ参加いただいたようです。もう1人は初参加の方。Linuxサーバ管理とかをやっているが、コマンドの使い方を勉強したいというのがキッカケとのことでした。

自分なりにサポートや解説を頑張ってみたのですが、果たしてどうだったでしょか?かしら??問題の難易度的にはやや落ち着いた感じではありましたが、今後もサポートや解説は出来るだけやってみたいと思いました。