日々之迷歩

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

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

「第61回シェル芸勉強会」リモート参加レポート

2022-09-24(土)「第61回シェル芸勉強会」が開催されました。久しぶりに参加レポートを書きました。まずは会長の上田さん、問題作成と勉強会ワンオペありがとうございます。感謝。参加者の皆様もお疲れさあでした。今回はコロナ禍の影響でリモート参加にしました。東京や福岡、大阪の会場は開催されませんでした。次回はオフラインで会場に集まることが出来るのでしょうか?

勉強会の情報

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

b.ueda.tech

勉強会の内容について

今回はデータ処理の問題。題意の理解が難しい問題もありました。

問題と解答

問題に使うデータファイルは、上田さんが公開されている下記Gitリポジトリから取得してください。

$ git clone https://github.com/ryuichiueda/ShellGeiData.git
$ cd ShellGeiData/vol.61

上田さんによる問題や解答例解説は、TogetterまとめやYoutubeライブ配信の録画をご覧ください。以下、私なりに考えた解答を記載いたします。

Q1

要するに、入や出が2回以上連続で出現する行が矛盾しているということ。

sortコマンドで並べ替えのキーを1列目で指定し、重複する行をuniqで探せば良いさそうです。しかしsortコマンドは指定されていない列でも並べ替えをしてしまうという落とし穴があります。

# B君の出入り記録に注目
$ grep B inout
B 入  
B 出
B 入
B 出

# 2列目でも並べ替えが発生してしまう
$ sort -k1,1 inout | grep B
B 出
B 出
B 入
B 入

sortコマンドの-sオプションを使うと、指定した列以外では並びをそのままにします(安定ソート)。あとはuniq -dで重複した行を探して完成です。

$ sort -s -k1,1 inout 
A 入  
A 出
B 入  
B 出
B 入  
B 出
C 入
C 入
C 出

$ sort -s -k1,1 inout | uniq -d
C 入

Q2

これは題意を勘違いして、3行以上連続と思い込んでいました。以下題意を間違えた解答だが、一応載せておきます。 データを@で連結して一行にし、(@以外+@)が3回以上出現する箇所を正規表現で抜き出します。

# データを@で連結して一行にする
$ cat data | sed '$d' | tr '\n' @
abc de@fg@hi@@あいうえお かきくけこ さしす@せ@そ@@UN@KO@@@上田@山田@上田@山田@@不労@所得@@@わうぃううぇを@うぇうぇうぇうぇw@

# grepの-oオプションを使って、(@以外+@)が3回以上出現する箇所を正規表現で抜き出す。
$ cat data | sed '$d' | tr '\n' @ | grep -oE '([^@]+@){3,}' 
abc de@fg@hi@
あいうえお かきくけこ さしす@せ@そ@
上田@山田@上田@山田@

# @を改行に戻して、空行を消す(awk NF)。
$ cat data | tr '\n' @ | grep -oE '([^@]+@){3,}' | tr '@' '\n' | awk NF
abc de
fg
hi
あいうえお かきくけこ さしす
せ
そ
上田
山田
上田
山田
わうぃううぇを
うぇうぇうぇうぇw
0000

Q3

これはギブアップでした。ちょっと難しく考えすぎたかもしれません。下記のように桁数が同じ数字の並びで別の行に分割すればよかったですね。

123456789 #1桁の数字
101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 #2桁の数字
100 #3桁の数字

Q4

いろいろな解答があると思いますが、最初に思いついた解答がこちらです。改行を@に変換して一行にし、末尾の@を削除します。

$ cat file1 | tr '\n' @ | sed 's/@$//' | tr @ '\n'
abc
def
ほげ

結果が分かりにくいのでodコマンドを使って確認します。

$ cat file1 | od -t x1c
0000000  61  62  63  0a  64  65  66  0a  e3  81  bb  e3  81  92  0a
          a   b   c  \n   d   e   f  \n 343 201 273 343 201 222  \n
0000017

$ cat file1 | tr '\n' @ | sed 's/@$//' | tr @ '\n' | od -t x1c
0000000  61  62  63  0a  64  65  66  0a  e3  81  bb  e3  81  92
          a   b   c  \n   d   e   f  \n 343 201 273 343 201 222 【←末尾の改行'\n'が消えている】
0000016

他にはhead -c -1 file1など思いつきました。headコマンドに-cオプションを指定すると、ファイルの先頭から引数で指定したバイト数表示します。引数の前に-があると、指定した引数を末尾から削除して表示します。

余談ですが、この問題を見た時に考えたことがこちらです。 * Vimは行末に改行を必ず入れて保存される。(末尾に改行が無いファイルの場合でも) * 最近大量のテキストデータを扱うことが多いのだが、たまに末尾の行に改行が入っていないデータがあり、catコマンドでファイルを連結する時に問題が発生して困る。

Q5

最初に考えた解答がこちらです。最初にデータを縦に並べ替え、行毎に奇数なら数字 @偶数なら@ 数字と表示します。

$ cat nums2 | grep -o . | awk '!($1%2){$2=$1;$1="@"}($1%2){$2="@"}{print}'
1 @
@ 2
@ 4
@ 2
3 @
@ 2
@ 8
5 @
@ 2
3 @
@ 0
9 @
7 @
5 @
9 @
@ 4
3 @

次にrsコマンドの-Tオプションで、行列を並べ替えます。rsコマンドはUbuntuならsudo apt install rsでインストール出来ます。

$ cat nums2 | grep -o . | awk '!($1%2){$2=$1;$1="@"}($1%2){$2="@"}{print}' | rs -T
1  @  @  @  3  @  @  5  @  3  @  9  7  5  9  @  3
@  2  4  2  @  2  8  @  2  @  0  @  @  @  @  4  @

後は無用な空白を削除し@を空白に戻して完成です。

$ cat nums2 | grep -o . | awk '!($1%2){$2=$1;$1="@"}($1%2){$2="@"}{print}' | rs -T | tr -d ' ' | tr '@' ' '
1   3  5 3 9759 3
 242 28 2 0    4 

上記の解答例とほぼ同じ処理内容で、awkと数字を使わない(ファイル名は使っている)解答がこちらです。

cat nums2 | grep -o .| while read d; do [ $(($d%($$/$$+$$/$$))) -eq 0 ] && echo "@ "$d || echo $d" @"; done | rs -T | tr -d ' ' | tr @ ' '
1   3  5 3 9759 3
 242 28 2 0    4 

上記のポイントは下記の通りです。

bashは$((算術式))の書式で、算術式展開が利用可能です。例えば3に対する2の剰余を計算する例がこちら。

$ echo $((3%2))
1

ここで数字の2を得るために、シェルの特殊変数$$を利用します。(現在のシェルのプロセスIDを参照)

$ echo $(($$/$$))
1

$ echo $(($$/$$+$$/$$))
2

Q6

最初に考えた解答がこちらです。まずawkをつかって、データを1つズラしながら4つずつ折り返します。

$ cat nums3 | awk '{for(i=1;i<=NF-3;i++){print $i,$(i+1),$(i+2),$(i+3)}}'
759 355 639 954
355 639 954 640
639 954 640 980
954 640 980 946
...(略)

更に1列目の3倍が4列目と同じ行のみ、1列目を表示して完成です。

$ cat nums3 | awk '{for(i=1;i<=NF-3;i++){print $i,$(i+1),$(i+2),$(i+3)}}' | awk '$1*3==$4{print $1}'
237
284
9

次にawkを使わない方法です。と言ってもbashのwhileループを使っていますが、考え方は上記と同じです。

まずconvコマンドを使って、データを1つズラしながら4つずつ折り返します。

$ cat nums3 | conv 4
759 355 639 954
355 639 954 640
639 954 640 980
954 640 980 946
...

convコマンドは、@grethlenさん作のegzactに含まれるコマンドです。

次にbashの算術式展開を使って1列目の3倍が4列目と同じか比較し、同じ場合のみ1列目を表示して完成です。

$ cat nums3 | conv 4 | while read a b c d; do [ $(($a*3)) -eq $d ] && echo $a; done
237
284
9

Q7

小問1

題意の理解に時間がかかってしまい、ギブアップしました。勉強会終了後に考えてみました。

表の場合は1列目と末尾列の積、裏の場合は(1-1列目の値)と末尾列の積を計算し、列を追加して末尾列に書き足します。しかし全ての合計が1になるように正規化する必要があります。要するに追加する末尾列の合計を計算し、その合計で末尾列を割り算して再計算します。データを2回舐めています。

まず追加する末尾列の合計を計算します。

# 表の場合
$ awk '{sum+=$1*$NF}END{print sum}' prior 
0.5

# 裏の場合
$ awk '{sum+=(1-$1)*$NF}END{print sum}' prior
0.5

次に再度末尾列を計算しますが、前回計算した合計で割り算します。末尾列の合計値はxargsコマンドの-Iオプションを使って@の位置に指定します。

# 表の場合
$ awk '{sum+=$1*$NF}END{print sum}' prior | xargs -I@ awk '{print $0,$1*$NF/@}' prior
0 0.00990099 0
0.01 0.00990099 0.00019802
0.02 0.00990099 0.00039604

# 裏の場合
$ awk '{sum+=$1*$NF}END{print sum}' prior | xargs -I@ awk '{print $0,$1*$NF/@}' prior
0 0.00990099 0.019802
0.01 0.00990099 0.019604
0.02 0.00990099 0.0194059

最終的に考えた解答例がこちらです。

# 表の場合
$ echo 表 | while read c; do awk '{sum+=$1*$NF}END{print sum}' prior | xargs -I@ awk '{print $0,$1*$NF/@}' prior; done
0 0.00990099 0
0.01 0.00990099 0.00019802
0.02 0.00990099 0.00039604
0.03 0.00990099 0.000594059
0.04 0.00990099 0.000792079
0.05 0.00990099 0.000990099
0.06 0.00990099 0.00118812
0.07 0.00990099 0.00138614
0.08 0.00990099 0.00158416
0.09 0.00990099 0.00178218
...(略)

# 裏の場合
$ echo 裏 | while read c; do awk '{sum+=(1-$1)*$NF}END{print sum}' prior | xargs -I@ awk '{print $,(1-$1)*$NF/@}' prior; done
0 0.00990099 0.019802
0.01 0.00990099 0.019604
0.02 0.00990099 0.0194059
0.03 0.00990099 0.0192079
0.04 0.00990099 0.0190099
0.05 0.00990099 0.0188119
0.06 0.00990099 0.0186139
0.07 0.00990099 0.0184158
0.08 0.00990099 0.0182178
0.09 0.00990099 0.0180198

小問2

勉強会終了後に考えた解答例がこちらです。表か裏かで計算内容を変更しています。出来たpriorファイルは、表裏のデータが1つずつ変わる毎に上書きします。

$ cat coin | grep -o . | while read c; do awk -v c=$c 'c=="表"{sum+=$1*$NF}c=="裏"{sum+=(1-$1)*$NF}END{print sum}' *a/*61/prior | xargs -I@ awk -v c=$c 'c=="表"{print $0,$1*$NF/@}c=="裏"{print $0,(1-$1)*$NF/@}' *a/*61/prior > temp ; mv temp ShellGeiData/vol.61/prior; done ; tail *a/*61/prior
0.91 0.00990099 0.0180198 0.00491449 0.00894437 0.0135665
0.92 0.00990099 0.0182178 0.00441644 0.00812625 0.0124611
0.93 0.00990099 0.0184158 0.00390638 0.00726587 0.0112628
0.94 0.00990099 0.0186139 0.00338435 0.00636258 0.00996871
0.95 0.00990099 0.0188119 0.00285029 0.00541555 0.00857519
0.96 0.00990099 0.0190099 0.00230423 0.00442412 0.00707906
0.97 0.00990099 0.0192079 0.00174617 0.00338757 0.00547694
0.98 0.00990099 0.0194059 0.00117612 0.0023052 0.00376541
0.99 0.00990099 0.019604 0.000594061 0.00117624 0.00194093
1 0.00990099 0.019802 0 0 0

終わりに

久しぶりにシェル芸勉強会の参加記事を書きましたが、しばらく書いてなかったためか「書き慣れ」のレベルが落ちて時間がかかりました。LTや二次会も面白かったです。Lightning Talkって5分か10分程度だと思うのだが、もはや単なるTalkでは?と思う程に皆さん力の入れ方がすごいです。 次回はオフラインで開催出来るでしょうか?コロナ禍の影響が心配です。