第63回ビッグハンドタウンシェル芸勉強会の問題と解説

はじめに

こんにちは。デジタル改革推進部データドリブンマネジメント推進部門の江川尋喜 (Hiroki Egawa / @yabeenico) です。

第63回ビッグハンドタウンシェル芸勉強会が2023年02月25日に開催されました。
今回は NTT Com のオフィスビル、大手町プレイスを会場提供させていただきました。
このブログエントリでは、勉強会で出題された問題の解答と解説をします。

大手町会場の様子

シェル芸勉強会について

まずシェル芸について、提唱者の上田隆一 (@ryuichiueda) 先生がこう定義しています。

[シェル芸とは]
シェル芸の定義バージョン1.1
マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。あるいはそのときのコマンド入力のこと。
要は
Unix系OSのシェル上でのワンライナーのことです。勝手に名前つけてすんません。
https://b.ueda.tech/?page=01434

シェル芸勉強会はシェル芸を極めたい人、すなわちシェルスクリプトやコマンドの愛好家が参加し、交流します。
参加者のバックグラウンドはコマンド開発者、インフラエンジニア、研究者、学生等様々です。

シェル芸勉強会は次の流れで進行します。

  • 上田先生が作ってきた問題を出題
  • 参加者が Twitter に #シェル芸 を付けて解答を投稿
  • 上田先生が解答を解説

#シェル芸 をつけてつぶやくとシェル芸bot (@minyoruminyon) が実行結果を表示してくれます。
また、入力に使うデータは ryuichiueda/ShellGeiData/vol.63 にあります。

今回紹介する解答は私が考えたものと、他の人の考えを取り入れたものがあります。
また、文字数がなるべく少なくなる解答を用意しました。
それでは問題、解答及び解説を見ていきましょう。

問題と解説

Q1(@butackle66さんから)

1x1〜9x9の九九の答えをすべて足し合わせてください。
https://twitter.com/ryuichiueda/status/1629339996028018688?s=20

解答例1

文字数が少ないです。

$ echo {1..9}*{1..9}+ 0|bc
2025

解説

bash のブレース展開で計算式を出力し、bc で計算しています。
検索ワード: ブレース展開 / brace expantion / sequence expantion

$ echo {1..9}
1 2 3 4 5 6 7 8 9

$ echo {1..9}{1..9}
11 12 13 14 15 16 17 18 19 21 22 23 24 25 26 27 28 29 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 47 48 49 51 52 53 54 55 56 57 58 59 61 62 63 64 65 66 67 68 69 71 72 73 74 75 76 77 78 79 81 82 83 84 85 86 87 88 89 91 92 93 94 95 96 97 98 99

$ echo {1..9}*{1..9}+ 0
1*1+ 1*2+ 1*3+ 1*4+ 1*5+ 1*6+ 1*7+ 1*8+ 1*9+ 2*1+ 2*2+ 2*3+ 2*4+ 2*5+ 2*6+ 2*7+ 2*8+ 2*9+ 3*1+ 3*2+ 3*3+ 3*4+ 3*5+ 3*6+ 3*7+ 3*8+ 3*9+ 4*1+ 4*2+ 4*3+ 4*4+ 4*5+ 4*6+ 4*7+ 4*8+ 4*9+ 5*1+ 5*2+ 5*3+ 5*4+ 5*5+ 5*6+ 5*7+ 5*8+ 5*9+ 6*1+ 6*2+ 6*3+ 6*4+ 6*5+ 6*6+ 6*7+ 6*8+ 6*9+ 7*1+ 7*2+ 7*3+ 7*4+ 7*5+ 7*6+ 7*7+ 7*8+ 7*9+ 8*1+ 8*2+ 8*3+ 8*4+ 8*5+ 8*6+ 8*7+ 8*8+ 8*9+ 9*1+ 9*2+ 9*3+ 9*4+ 9*5+ 9*6+ 9*7+ 9*8+ 9*9+ 0

$ echo 1 + 2 | bc
3

解答例2

ブレース展開と bc のどちらも利用しない別解です。
ブレース展開では変数が使えないので、変数が使いたシチュエーションで利用できます。

$ join -j9 <(seq 9) <(seq 9) | awk '$0=a+=$1*$2' | tail -n1
2025

解説

join -j9 <(seq 9) <(seq 9)
-> 列結合を利用して 1 1 から 9 9 までの組を作っています。

### サンプルデータ
$ cat <(printf '1 a\n2 b\n3 c\n4 d\n') <(printf '1 e\n2 f\n3 g\n4 h')
1 a
2 b
3 c
4 d
1 e
2 f
3 g
4 h

### 1列目で結合
$ join -j1 <(printf '1 a\n2 b\n3 c\n4 d\n') <(printf '1 e\n2 f\n3 g\n4 h')
1 a e
2 b f
3 c g
4 d h

### 存在しない列で結合すると full join となる
$ join -j9 <(printf '1 a\n2 b\n3 c\n4 d\n') <(printf '1 e\n2 f\n3 g\n4 h')
 1 a 1 e
 1 a 2 f
 1 a 3 g
 1 a 4 h
 2 b 1 e
 2 b 2 f
 2 b 3 g
 2 b 4 h
 3 c 1 e
 3 c 2 f
 3 c 3 g
 3 c 4 h
 4 d 1 e
 4 d 2 f
 4 d 3 g
 4 d 4 h
 
 ### 1 1 から9 9 までの組を出力 (途中は sed で削除)
 $ join -j9 <(seq 9) <(seq 9) | sed 4,78d
 1 1
 1 2
 1 3
 9 7
 9 8
 9 9

awk '$0=a+=$1*$2'
-> 累積和を計算しています。
参考: awk 基礎

### a に累積和を保存
$ seq 4 | awk '{a+=$1; print a}'
1
3
6
10

### (a+=$1) は代入後の a の値に評価される
$ seq 4 | awk '{print a+=$1}'
1
3
6
10

### $0 に a を代入し、間接的に a を print
### pattern = $0=(a+=$1), action = 暗黙的に {print $0} となる
$ seq 4 | awk '$0=(a+=$1)'
1
3
6
10

tail -n1
-> 最後の1行を抽出しています。

Q2(@butackle66さんから)

9132円の支払いに10000円札で払ったときのおつりの出し方をひとつ(できるひとはたくさん)画面に出力してみてください。
https://twitter.com/ryuichiueda/status/1629340985389125632?s=20

解答例1

文字数が少ないです。
また、出すコインの枚数が最小になります。

$ printf %s\\n 500 100 50 10 5 1|awk 'BEGIN{t=10000-9142}{t-=$0*(a=int(t/$0))}$0=$0" "a'
500 1
100 3
50 1
10 0
5 1
1 3

解説

printf %s\\n 500 100 50 10 5 1
-> 6種類のコインを出力しています。

### `printf` は第1引数のフォーマットを第2引数以降の全てに適用する
$ printf '%s\n' 500 100 50 10 5 1
500
100
50
10
5
1

$ printf '%s/' 500 100 50 10 5 1
500/100/50/10/5/1/

awk 'BEGIN{t=10000-9142}{t-=$0*(a=int(t/$0))}$0=$0" "a'
-> 以下の python3 コードをワンライナーにしています。
参考: awk 基礎

t = 10000 - 9142
for d0 in [500, 100, 50, 10, 5, 1]: # $0
    a = int(t / d0)             # a=int(t/$0)
    t = t - d0 * a              # t-=$0*(...)
    d0 = str(d0) + " " + str(a) # $0=$0" "a
    print(d0)

解答例2

ネタです。
コインの枚数の組み合わせを金額が858円になるまでランダムに探索しています。

$ tr -dc 0-9</dev/urandom|fold -w6|awk -F '' '500*$1+100*$2+50*$3+10*$4+5*$5+1*$6==10000-9142'|head -n1
071928

出力の見方: 500円玉0枚, 100円玉7枚, ..., 1円玉8枚

解説

tr -dc 0-9</dev/urandom|fold -w6
-> 6桁のランダムな数字を生成しています。

### /dev/urandom はランダムなデータを返す
$ cat /dev/urandom | head -c10
ȱ�KH$繀

### tr -d: 削除, -c: 以外を -> 0-9 以外を削除 -> 0-9 のみ抽出
$ cat /dev/urandom | tr -dc 0-9 | head -c10
0597488882

### fold で6桁に切り詰める
$ cat /dev/urandom | tr -dc 0-9 | fold -w6 | head -n3
764711
039900
491372

awk -F '' '500*$1+100*$2+50*$3+10*$4+5*$5+1*$6==10000-9142'
-> 入力の6桁の数字のうち金額が858円となるものを抽出しています。

### -F '' で文字単位にフィールド分割
$ echo abc | awk -F '' '{print $2}'
b

また、pattern/action が以下となります。

  • pattern: 500*$1+100*$2+50*$3+10*$4+5*$5+1*$6==10000-9142
  • action: 暗黙的に {print $0}

Q3

次のLaTeXの原稿で、\labelと\refがペアになっていない(参照していない/されていない)ものを抽出してください。
https://twitter.com/ryuichiueda/status/1629347379219689473?s=20

題意としては、参照されていない図表、図表の無い参照を見つけたいということです。
また、今回は \label が0個で \ref が2個のように片方が0個で片方がn個のようなシチュエーションの考慮は不要です。

  • 参照されていない図表の例
    • \label{hoge} はあるが \ref{hoge} がない
  • 図表のない参照の例
    • \ref{hoge} はあるが \label{hoge} がない

解答例1

文字数が少ないです。

$ grep -oP '(label|ref)\K[^}]*' genkou.tex|sort|uniq -u
{eq:state_equation_linear
{eq:state_equation_nonlinear
{fig:typhoon

解説

grep -oP '(label|ref)\K[^}]*}' genkou.tex
-> ファイルから \label{...}\ref{...} の部分を抽出しています。

### -o: マッチした部分のみ抽出
$ echo 12344321 | grep -o .3
23
43

### lable または ref から } が現れる直前までを抽出 (-P は後述)
$ grep -oP '(label|ref)[^}]*' genkou.tex | head -n3
label{eq:state_transition_model
label{eq:state_transition_model2
ref{eq:state_transition_model

### -P: perl 拡張正規表現を利用 -> \K と | を使うため
### \K: \K より左をマッチとして出力しない
### ..3 にマッチさせて...
$ echo 12344321 | grep -oP '..3'
123
443
### 最初の1文字を出力したくない場合
$ echo 12344321 | grep -oP '.\K.3'
23
43

### \K を使って label と ref を出力から除外
$ grep -oP '(label|ref)\K[^}]*' genkou.tex| head -n3
{eq:state_transition_model
{eq:state_transition_model2
{eq:state_transition_model

sort|uniq -u
-> 重複のないユニークな行のみを抽出しています。

### サンプルデータ
$ seq 1 3; seq 2 4
1
2
3
2
3
4

### ユニークな行のみを抽出
### なお、uniq は直前の行との比較のみを行うので、あらかじめ sort が必須
$ (seq 1 3; seq 2 4) | sort | uniq -u
1
4

uniq の便利なオプション

### 重複排除
$ (seq 1 3; seq 2 4) | sort | uniq
1
2
3
4

### ユニークな行のみを抽出 (再掲)
$ (seq 1 3; seq 2 4) | sort | uniq -u
1
4

### 重複した行のみを抽出
$ (seq 1 3; seq 2 4) | sort | uniq -D
2
2
3
3

### 重複した行のみを抽出し、重複排除して出力
$ (seq 1 3; seq 2 4) | sort | uniq -d
2
3

### 重複個数をカウント
$ (seq 1 3; seq 2 4) | sort | uniq -c
      1 1
      2 2
      2 3
      1 4

解答例2

勉強会で解説された方法です。
sortuniq のフィールド機能を活用しています。

$ cat genkou.tex | grep -oE '(ref|label){.*}' | tr '{}' ' ' | sort -k2,2 | uniq -u -f1
label eq:state_equation_linear
label eq:state_equation_nonlinear
ref fig:typhoon

解説

### `grep -oE` までは基本的に解答例1と同じ。| を使うために -E を指定
$ cat genkou.tex | grep -oE '(ref|label){.*}' | head -n3
label{eq:state_transition_model}
label{eq:state_transition_model2}
ref{eq:state_transition_model}

### tr で { と } をスペースに変換
$ cat genkou.tex | grep -oE '(ref|label){.*}' | tr '{}' ' ' | head -n3
label eq:state_transition_model 
label eq:state_transition_model2 
ref eq:state_transition_model 

### sort -k2,2 で2フィールド目で並べ替え
$ cat genkou.tex | grep -oE '(ref|label){.*}' | tr '{}' ' ' | sort -k2,2
label eq:state_equation_linear 
label eq:state_equation_nonlinear 
label eq:state_transition_model 
ref eq:state_transition_model 
ref eq:state_transition_model 
ref eq:state_transition_model 
label eq:state_transition_model2 
ref eq:state_transition_model2 
ref eq:state_transition_model2 
ref eq:state_transition_model2 
ref eq:state_transition_model2 
ref eq:state_transition_model2 
ref eq:state_transition_model2 
label fig:motion 
ref fig:motion 
ref fig:typhoon 

### uniq -u -f1 で1フィールド目を無視してユニークな行を抽出
$ cat genkou.tex | grep -oE '(ref|label){.*}' | tr '{}' ' ' | sort -k2,2 | uniq -u -f1
label eq:state_equation_linear 
label eq:state_equation_nonlinear 
ref fig:typhoon 

Q4

次のファイルから、「東西南北」がワンセットになっている部分を探してください。東西南北の順番は問いません。 https://twitter.com/ryuichiueda/status/1629355288145920001?s=20

  • 例: 東西南北西東 という入力文字列から 1 東西南北, 3 南北西東 を出力する。
  • 何文字目から始まるかの情報も出力する。

解答例1

愚直に解くとこうなるでしょう。

$ awk '{while(a=substr($0,++i,4)){print i,a}}' tonnan.txt|awk '/東/*/西/*/南/*/北/'
3 西南北東
12 南北西東
33 南東西北
41 西北南東
43 南東北西
54 東北南西
55 北南西東
56 南西東北
57 西東北南
58 東北南西
63 南西東北
64 西東北南
68 東南西北
71 北西南東
72 西南東北
97 西北東南

awk '{while(a=substr($0,++i,4)){print i,a}}'
-> 入力文字列を4文字ずつ切り出しています。

a=substr($0, ++i, 4)a (東西南北の文字列) と i (インデックス) を以下のように設定しています。

南西西南北東東西東... (入力)
南西西南 == a, i == 1
 西西南北 == a, i == 2
  西南北東 == a, i == 3
   南北東東 == a, i == 4
    北東東西 == a, i == 5
     東東西東 == a, i == 6

実行結果

$ awk '{while(a=substr($0,++i,4)){print i,a}}' tonnan.txt | head -n5
1 南西西南
2 西西南北
3 西南北東
4 南北東東
5 北東東西

awk '/東/*/西/*/南/*/北/'
-> 東西南北が4つとも含まれている行のみ抽出しています。

pattern の /regex/$0 ~ /regex/ と等価です。
すなわち、入力行を正規表現マッチした結果を 0 or 1 の数値に評価します。
よって、以下の式は等価です。

/東/*/西/*/南/*/北/
($0 ~ /東/) && ($0 ~ /西/) && ($0 ~ /南/) && ($0 ~ /北/)

解答例2

eban さんの解答 を解説します。
文字数が少ないです。
「東西南北が含まれている」の判定を「1行に同じ文字が2回出現しない」で行っています。

$ sed ':a;p;s/.//;ta' tonnan.txt|grep -o ^....|grep -vnE '(.).*\1'
3:西南北東
12:南北西東
33:南東西北
41:西北南東
43:南東北西
54:東北南西
55:北南西東
56:南西東北
57:西東北南
58:東北南西
63:南西東北
64:西東北南
68:東南西北
71:北西南東
72:西南東北
97:西北東南

解説

sed ':a;p;s/.//;ta'
-> パターンスペースの1文字目の削除と print を繰り返しています。

パターンスペースの解説はここではやりませんが、tonnan.txt の中身で初期化された文字列型の変数と考えて下さい。
この sed は以下4つのコマンドから成り立っています。

:a    # ラベル
p     # パターンスペースを print
s/.// # パターンスペースの最初の1文字を削除
ta    # 直前の置換が成功したら :a に戻る

実行結果

$ sed ':a;p;s/.//;ta' tonnan.txt | tail
東南東東西北東南
南東東西北東南
東東西北東南
東西北東南
西北東南
北東南
東南
南

grep -o ^....
-> 先頭4文字を切り出しています。

$ sed ':a;p;s/.//;ta' tonnan.txt | grep -o ^.... | tail
南東東北
東東北南
東北南東
北南東南
南東南東
東南東東
南東東西
東東西北
東西北東
西北東南

grep -vnE '(.).*\1'
-> 同じ文字が含まれていない行のみを抽出しています。

\1(.) と同じ文字にマッチします (後方参照といいます)。
例えば (.)(.)(.)\3\2\1abccba にマッチします。

### (.).*\1 -> 同じ文字が少なくとも2文字含まれている行
### -v -> 以外を
### -n -> 行番号付きで表示
### -E は () を使うために指定
$ sed ':a;p;s/.//;ta' tonnan.txt|grep -o ^.... | grep -vnE '(.).*\1'
3:西南北東
12:南北西東
33:南東西北
41:西北南東
43:南東北西
54:東北南西
55:北南西東
56:南西東北
57:西東北南
58:東北南西
63:南西東北
64:西東北南
68:東南西北
71:北西南東
72:西南東北
97:西北東南

Q5

つぎのリバーシの盤面について、E6とG7に❌を打ちたいです。

$ cat reversi.txt  
 12345678  
A          
B          
C   ⚪      
D   ⚪⚫     
E   ⚫⚫     
F    ⚪⚫    
G          
H          

つぎのワンライナーを完成させてください。(Perl使える人もこの問題はsedでお願いします。)

$ cat reversi.txt | sed あるsedのコード | sed 最初のsedのコードと同じコード  

https://twitter.com/ryuichiueda/status/1629363762879758336?s=20

題意はこんな感じです。

  • sed でリバーシの実装をしたい。
  • が置ける場所に印をつけたい。
  • 今回のスコープは左斜上に探索して判定する処理。

期待する出力

 12345678
A        
B        
C   ⚪    
D   ⚪⚫   
E   ⚫⚫❌  
F    ⚪⚫  
G      ❌ 
H        

解答例

勉強会で解説されたものです。
別解の余地は無いでしょう。

$ cat reversi.txt | sed -Ez 's/(⚪.{10}(⚫.{10})+) /\1❌/g' | sed -Ez 's/(⚪.{10}(⚫.{10})+) /\1❌/g'
 12345678
A        
B        
C   ⚪    
D   ⚪⚫   
E   ⚫⚫❌  
F    ⚪⚫  
G      ❌ 
H        

解説

  • -z オプションで改行を無視しています。
  • C 言語で2次元配列を扱うのと同じ要領で探索しています。
  • reversi.txt の横幅が10文字であることを利用しています。
  • こんなパターンを見つけたら全角スペースを❌に変換しています。
    • ⚪(任意の10文字)⚫(全角スペース)
    • ((任意の10文字)⚫) を+で繰り返すことで、⚫が2個続くパターンを検知

sed -z の実行例

$ printf 'abc\ndef\nghi'
abc
def
ghi

### & はマッチ全体 -> マッチした部分の後ろに x を追記
$ printf 'abc\ndef\nghi' | sed -z 's/b.../&x/'
abc
dxef
ghi

Q6

Q5のワンライナーに続けて、⚪と❌の間の⚫を⚪にしてください。
https://twitter.com/ryuichiueda/status/1629369189843750912?s=20

解答例

勉強会で解説されたものです。
合理的な別解は恐らくありません。

$ cat reversi.txt | sed -Ez 's/(⚪.{10}(⚫.{10})+) /\1❌/g' | sed -Ez 's/(⚪.{10}(⚫.{10})+) /\1❌/g' | tr -d '\n' | perl -CSD -Mutf8 -pe 's/⚫(?=.{9}(⚫.{9})*❌)/⚪/g' | fold -b27
 12345678
A        
B        
C   ⚪    
D   ⚪⚪   
E   ⚫⚪❌  
F    ⚪⚪  
G      ❌ 
H        

解説

perl を使いますが、改行無視は sed -z のような感じではなく、tr と fold で行っています。

perl -CSD -Mutf8 -pe 's/⚫(?=.{9}(⚫.{9})*❌)/⚪/g'
-> コンセプトは Q5 とだいたい同じ。

-CSD -Mutf8
-> マルチバイト対応

⚫(?=.{9}(⚫.{9})*❌)
-> (?=...) は肯定先読み。

  • 検索対象だがマッチしたとはみなさない。
  • 今回の場合、検索対象だが置換対象でなくなる。
    • -> 先頭の⚫のみが⚪への置換対象となる。

LT

LT (Long Talk) が2件ありました。

LT1 curlでTelegram botを操作 (やべえ @yabeenico)

私が登壇しました。
初めてのシェル芸勉強会での LT 発表でした。
チャットアプリ Telegram を curl で操作する方法を紹介しました。

LT2 音声合成してみよう (たいちょー @xztaityozx_001)

自作初音ミクみたいなことをしていました。
たいちょーさんの音声を返す REST API エンドポイントが立ち上がっていたので、みんなでたいちょーさんの音声を合成しました。

おわりに

Q1~Q4 は試行錯誤できてが楽しかったです。
Q5 Q6 は難しいのに加えて別解の余地がなかったので厳しかったです。

シェル芸勉強会の問題を解くときは、厳密な解答より、シチュエーションをイメージしてそれにマッチするものを考えると良いです。
例えば Q3 では、私は次のシチュエーションをイメージしました。
「図表の参照のアンマッチが存在することを知ったのでとりあえずその一覧が欲しい」
そのため、Q3 解答例1では { が先頭に残っていたり、reflabel を区別できませんが、シチュエーションにマッチするのでそれでいいのです。

また、勉強会でも言及されていますが、問題が解けなくても解くまでに試行錯誤したこと自体に学びがあります。
問題を解くまでのプロセスを大切にしていきましょう。

以上、第63回ビッグハンドタウンシェル芸勉強会の問題と解説でした。

© NTT Communications Corporation 2014