日々之迷歩

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

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

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

2021-04-24(土)「第53回シェル芸勉強会」にオンライン参加し、久しぶりに参加レポートを書きました。コロナ禍の影響で、シェル芸勉強会の福岡サテライト会場は長いこと開催しておりません。前回は「第45回シェル芸勉強会」2019年12月末だったようです。問題作成と解説の上田さん、ありがとうございます。参加者の皆様もお疲れ様でした。

シェル芸勉強会はここ1年くらいオンライン参加の形態になっています。問題出題者の上田さんが自宅からYoutube配信されるのを見聞きしながら、Twitterで問題を見たり解答したりするようなやり方です。最近では雑談やツッコミなどの目的でDiscodeも併用もしています。二次会もオンライン飲み会になっています。

勉強会の情報

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

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

勉強会の内容について

主催者の上田さんがYoutube配信で出題と解説をされている環境はこちらです。

twitter.com

今回の問題は、数学的な問題をAWKでゴリ押しする系のようです。

問題と解答は、出題者上田さんが公開されているページをご覧ください。

問題と解答

【問題と解答】jus共催 第53回シェル芸が好きです。でもゾウさんのほうがもっと好きですシェル芸勉強会 | 上田ブログ

私なりに考えた解答例を以下に掲載いたします。

Q1

1から100万までがランダムに並んだデータで、5列先の数を足して2列目に出力する問題。

$ seq 1000000 | shuf > a
$ head a
742505
19482
511202
49851
406516
617563
227367
821010
753384
906981

pasteコマンドを使った解答がこちら。tailコマンドで6行目以降のデータを作って、横に並べて足し算します。

$ paste a <(tail -n +6 a)
742505  617563
19482   227367
511202  821010
49851   753384
406516  906981
617563  115432
227367  45000
821010  922566
753384  11506
906981  10750
(以下省略)

あとはデータが2列並んだ行のみ、横に並んだ2つを足し算した結果を2列目に表示して完成です。

$ paste a <(tail -n +6 a) | awk 'NF==2{print $1,$1+$2}' > b
$ head b
742505 1360068  <- 742505 + 617563
19482 246849    <- 19482 + 227367
511202 1332212
49851 803235
406516 1313497
617563 732995
227367 272367
821010 1743576
753384 764890
906981 917731
(以下省略)

twitter.com

AWK単体での別解はこちらです。各行のデータを配列に保存し、6行目以降から現在行のデータと5つ前のデータを足し算します。

$ awk '{a[NR]=$0}NR>=6{print a[NR-5],$0+a[NR-5]}' a > b

Q2

数値計算ではなく、有理数として約分する問題です。最初は素因数分解したのを縦に並べてどうにか出来ないか?と考えたがダメでした。

$ cat frac | factor | rs -T | sed 1d
2                        3
3                        3
3                        7
3                        13
5                        633543227197
347                      
54601                    
633543227197             

出題者上田さんの解答例では、各素因数の数を計算し、分子の数から分母の数を引き算していました。難しい。

別解として、Rubyの有理数を扱う機能を使う例が出ていました。RubyのPEPL環境irbで試してみます。

$ irb
irb(main):001:0> 3240933263267302464930/518871903074343r 【最後にrを付ける】
=> (568396410/91) 【約分された結果が表示される】
irb(main):002:0> Rational(3240933263267302464930,518871903074343)
=> (568396410/91)

解答例では末尾にrを記載したやり方が出ていましたが、Rationalクラスを明示的に書いた解答を作ってみました。

$ cat frac | awk '{printf "puts Rational(%s, %s)\n",$1,$2}' | ruby | tr / ' '
568396410 91

twitter.com

RubyのRationalクラスのマニュアルはこちらです。 docs.ruby-lang.org

Q3

ややこしい四捨五入をする問題。Q2でRubyが便利だったので、こちらもRubyで楽をする解答を作ってみました。

まずAWKでRubyのコードを作成します。

$ cat nums | awk '{print "puts "$0".round("length($2)-1")"}' FS=.
puts -0.327.round(2)
puts 2.33333.round(4)
puts 4.0000000000999995.round(15)

次にrubyコマンドでコードを実行して完成です。

$ cat nums | awk '{print "puts "$0".round("length($2)-1")"}' FS=. | ruby
-0.33
2.3333
4.0000000001

Q4

連続して処理を続けて、数が収束するのを確認する問題。一旦処理を行う関数を作成し、関数をパイプで数珠つなぎ実行するというやり方で考えました。 出力を標準エラー出力に出せば、パイプで連続接続しても確認出来るというアイデアを使わせていただきました。

$ f(){ awk '{print > "/dev/stderr"; printf "%04d\n",$0}' | grep -o . |  pee 'sort -r' 'sort' | tr -d '\n' | sed 's/....$/-&/' | awk NF | bc; }; eval $(yes f | head | xargs | tr ' ' '|' | sed 's/^/echo 9467|/') |& xargs
9467 5085 7992 7173 6354 3087 8352 6174 6174 6174 6174

出題者上田さんが作成された、コマンドをパイプで数珠つなぎして実行するjuzというコマンドがあるのを思い出し、使ってみようとしたがうまく動きません。数珠つなぎするのが外部コマンドはなく関数なのが原因だと思われます。

$ f(){ awk '{print > "/dev/stderr"; printf "%04d\n",$0}' | grep -o . |  pee 'sort -r' 'sort' | tr -d '\n' | sed 's/....$/-&/' | awk NF | bc; }; echo 9467 | juz 10 f

twitter.com

関数ではなくシェルスクリプトとして保存(つまり外部コマンドにした)して実行すれば動きました。

$ echo "awk '{print > \"/dev/stderr\"; printf \"%04d\n\",\$0}' | grep -o . |  pee 'sort -r' 'sort' | tr -d '\n' | sed 's/....$/-&/' | awk NF | bc" > f; echo 497 | juz 10 sh f |& xargs
497 9261 8352 6174 6174 6174 6174 6174 6174 6174 6174

上田さん作のjuzコマンドなどのglueutilsはこちらです。juzコマンドは残念ながらmacOSではコンパイル出来ませんでした。(ヘッダーファイルprctl.hがmacOSには付属していないため) github.com

Q5

小問1

2つの素数を並べて、その差で等差数列を作ります。とりあえずはこんな感じでしょうか。

$ seq 1000 | factor | awk 'NF==2{print $2}' | shuf | head -n 2 | sort -n | xargs | awk '{printf "%d %d ",$1,$2;a=$2;d=$2-$1;for(i=1;i<=4;i++){a=a+d;printf a" "};print ""}'
7 823 1639 2455 3271 4087 

小問2

こちらは上記のワンライナーを無限ループさせれば良いです。

$ while true; do seq 1000 | factor | awk 'NF==2{print $2}' | shuf | head -n 2 | sort -n | xargs | awk '{printf "%d %d ",$1,$2;a=$2;d=$2-$1;for(i=1;i<=4;i++){a=a+d;printf a" "};print ""}'; done
557 593 629 665 701 737 
631 733 835 937 1039 1141 
2 19 36 53 70 87 
41 593 1145 1697 2249 2801 
167 193 219 245 271 297 
(以下省略)

Q6

出力結果の各列を素因数分解し、素数の数が6つになる行を探せば良いです。具体的には、Q5小問1のワンライナーで出力される結果を各列素因数分解し、素数の数が6個になる場合を探します。

$ Q5小問1のワンライナー  | factor | awk '{$1="";print}' | tr '\n' ' '
 41  127  3 71  13 23  5 7 11  3 157 【この出力が6列ならば求める結果になる】

whileで無限ループを作った解答がこちらです。

$ while true; do seq 1000 | factor | awk 'NF==2{print $2}' | shuf | head -n 2 | sort -n | xargs | awk '{printf "%d %d ",$1,$2;a=$2;d=$2-$1;for(i=1;i<=4;i++){a=a+d;printf a" "};print ""}' | factor | awk '{$1="";print}' | tr '\n' ' ' | awk 'NF==6'; done
 7  157  307  457  607  757 
 229  619  1009  1399  1789  2179 
(以下省略)

出題者上田さんの解答例でteipコマンドを使ったものがありました。teipコマンドは「ぐれさん」の名前でコミュニティ活動されている方が開発されました。 github.com

teipコマンドは、特定の列に対してフィルタコマンドの処理を適用出来るのがポイントです。先日の業務でもこのコマンドが欲しい事例がありました。例えば2列目のデータのみにsedコマンドの処理を適用する例はこちらです。

$ echo a b c | teip -f 2 sed 's/.*/(&)/'
a (b) c

juzコマンドと合わせ技だとこんなことが出来ます。

$ echo a b c | juz 10 teip -f 2 sed 's/.*/(&)/'
a ((((((((((b)))))))))) c

この問題でのteipコマンドの使い道は次の通りです。横に並んだ数列に対してfactorコマンドを適用すると縦に並んでしまいますが、teipコマンドを併用すると横に並べたまま出力します。

$ seq 2 5 | xargs | factor
2: 2
3: 3
4: 2 2
5: 5

$ seq 2 5 | xargs | teip -f 1-4 factor
2: 2 3: 3 4: 2 2 5: 5

teipコマンドを使うと、素数の数を数える部分を下記のように書くことが出来ます。1列目と2列目は素数なのが分かっているため、3列目以降のみ適用しています。

$ Q5小問1のワンライナー | teip -f 3-6 factor
383 571 759: 3 11 23 947: 947 1135: 5 227 1323: 3 3 3 7 7 【この出力が10列なら求める結果になる】

Q7

これは勉強会の時間内ではギブアップでした。後日トライした結果がこちらです。

twitter.com

2つの座標について、距離と角度を使って追加する中間点を3つ計算し、座標データを追加していく処理をおこないます。 2つの座標を(x1,y1)、(x5,y5)とし、3つの中間点を(x2,y2)(x3,y3)(x4,y4)とします。 (x2,y2)と(x4,y4)は、(x1,y1)と(x5,y5)間を3等分した位置にあります。

座標の位置説明図

下記計算処理の手順を、2から6まで毎行毎に繰り返していけば良いです。

  1. 1列目のデータの場合は、データをx1とy1に入力。
  2. 現在の行のデータを(x5,y5)に代入
  3. (x2,y2)を計算。(x1,y1)と(x5,y5)間で1:2の場所にある。
  4. (x3,y3)を計算。(x2,y2)を基準にする。(x1,y1,)(x5,y5)間の距離の3分の1を半径とし、60度(π/3ラジアン)回転した位置にある。
  5. (x4,y4)を計算。(x1,y1)と(x5,y5)間で2:1の場所にある。
  6. (x5,y5)の値を(x1,y1)へ代入して次の行へ移動。手順2から繰り返す。

上記の処理をAWKの1行プログラミングで書いて関数として定義しました。関数をたくさん通すほどコッホ曲線の深度が深くなっていきます。。

【関数定義】
$ f(){ awk 'NR==1{x1=$1;y1=$2}NR!=1{print x1,y1;x5=$1;y5=$2;a=atan2(y5-y1,x5-x1);r=sqrt((x5-x1)^2+(y5-y1)^2)/3;x2=x1+r*cos(a);y2=y1+r*sin(a);print x2,y2;x3=x2+r*cos(a+3.141592/3);y3=y2+r*sin(a+3.141592/3);print x3,y3;print x1+2*r*cos(a),y4=y1+2*r*sin(a);x1=x5;y1=y5}END{print x1,y1}'; }

$ cat triangle | f | gnuplot -e 'set terminal png;set size ratio -1;set output "./hoge.png";plot "-" w l'
【画像が作成される】

$ cat triangle | f | f | f | gnuplot -e 'set terminal png;set size ratio -1;set output "./hoge.png";plot "-" w l'
【深度がより深い画像が作成される】

TLと二次会

LightningTalkは2つ発表がありました。シェル(シェルスクリプトでは無い!)を書いていて付随するコマンド作った話題と、コマンドを使う勉強のモチベーションをアップ!(某〇〇ではない!)する話題でした。二次会は初めてのgatherを体験しました。ゲーム感覚で面白いですね。

終わりに

久しぶりにシェル芸勉強会の参加記事を書きました。色々書こうとすると結構時間がかかってしまいますが、アウトプットしておくと気分的にも良いですね。 コロナ禍の影響はいつまで続くのか分からないけど、リモート参加の経験値を上げておく機会だと思うことにしましょうか。