日々之迷歩

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

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

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

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

勉強会募集サイト

本家東京会場募集

usptomo.doorkeeper.jp

大阪サテライト会場募集

atnd.org

福岡サテライト会場募集

atnd.org

長崎サテライト会場募集

shell-nagasaki.connpass.com

イベント報告関連

jus共催 第45回シェル芸勉強会リンク集 | 上田ブログ

horo1717.hatenablog.com

スライド資料

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

speakerdeck.com

午前の部

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

www.pscp.tv

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

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

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

午後の部

第45回シェル芸勉強会(午後の部) www.youtube.com

問題と解答はこちら。

【問題と解答】jus共催 jus共催 第45回せんせいがAIとかしんぎゅらりてぃーってタイトルにつけとくとべんきょうかいにひとがあつまるよっていってたかんけいないけどシェル芸勉強会 | 上田ブログ

今回の問題はこんな感じらしい。上田会長、毎回の事ながら問題作成と運営ありがたや。

twitter.com

参加者の方へサポートをしながら考える。毎度の事ながら結構頭を使う。 自分なりの解答例がこちら。今回は難易度がやや下がったか? 個人的にはQ6の上田会長による解答例が印象に残った、は〜なるほどと関心。

Q1

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

日付が固定長じゃないという意地悪付きだが、GNU sortコマンドには-Vというバージョン名で並べ替えるというオプションがあるので利用する。ちなみに-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サーバ管理とかをやっているが、コマンドの使い方を勉強したいというのがキッカケとのこと。

自分なりにサポートや解説を頑張ってみたのだが、どうだったかしら??問題の難易度的にはやや落ち着いた?感じではあるが、慣れない方にとっては果てしなく高難度に感じると思う。今後もサポートや解説は大事だと思った次第。