玄人もすなる第22回シェル芸勉強会といふものを、素人もしてみむとて、するなり

はじめに

4月30日(土)に開催された「第22回シェル芸勉強会 大阪サテライト - シェル芸勉強会 大阪サテライト」に参加しました。

参加人数は約20名におよび、大阪サテライトとしては今までにない規模の勉強会となりました。

これは朝10時から夜の20時頃までの、約10時間にわたるsh行の記録です。

メイン会場および他サテライト会場:

各会場の様子:

問題ならびに模範解答:

ライブストリーム(YouTube):


午前の部

「シェルがコマンドを処理する前にしていること」

講師:

鳥海 秀一 さん(USP友の会)

内容:

  1. 入力したコマンドラインをシェルがどのように解釈しているのか
  2. evalコマンドの使い方とその応用

感想:

シェル芸を実行、あるいはシェルスクリプトを書く際、コマンドラインが次のように解釈されていくことを知ることができ、今後のシェルライフに大きく役立ちそうです。

  1. トークン分割
  2. エイリアス展開
  3. その他の展開
    • ブレース展開
    • チルダ展開
    • パラメータ展開
    • コマンド置換
    • 算術式展開
    • プロセス置換
    • 単語の分割
    • パス名展開
    • クオートの削除
  4. コマンド検索

そして、evalならびにbcコマンドを使ってネイピア数を求める

$ eval echo 1 '+1/\(1 \*{1..'{1..20}'} \)' | bc -l
2.71828182845904523525

も凄いものがありますが、個人的には

$ alias eval='eval eval'
$ eval
[exited]

でシェルが落ちるのが最も印象に残りました。

「/etc/rc を読んでみる」

講師:

りゅうち てつや さん(USP友の会/日本UNIXユーザ会)

内容:

  1. FreeBSDのおすすめ
  2. ドキュメントの読み方およびmanコマンドの使い方
  3. rc(8)に関連したファイルを読んでみる

感想:

普段使っているDebianがシステム起動にsystemdを利用しているため、各種rcスクリプトを意識することはあまりなかったのですが、もともとLinux系OSもBSD系OSとよく似た起動方法を採っていたわけで、これを機にFreeBSDにもチャレンジしてみようかと思いました。


昼休み

T.Motooka(@t_motooka)さんによる手書き(!)SVG/PDFの実演が行われました。

Base64も手書きするという……おそるべし。


午後の部

今回はAWKの連想配列の使い方をマスターしていないと解けない問題が多く、思いのほかこれが出来ていないことに気付かされました。

Q1

問題:

ファイルaおよびbについてそれぞれの中央値を求めます。データ数が偶数個の場合は中央2つの値の平均とします。

主観的難度/当日の成否:

★★★★☆/解答失敗

解答:

$ for f in a b; do echo -n "$f: "; cat "$f" | sort -n | awk '{n[NR]=$1}END{print(NR%2?n[NR/2+0.5]:(n[NR/2]+n[NR/2+1])/2)}'; done
a: 3.5
b: 3.4

説明:

読み込んだレコード数を格納する特殊変数NRの値をキーとした連想配列にsort -nでソートした各行の値を代入し、パターンENDで総読み込みレコード数が偶数であれば中央の値、奇数であれば中央2つの値の平均を出力しています。

Q2

問題:

$ echo カレーライス 醤油ラーメン | ...
   カ
   レ
醤油ラーメン
   ラ
   イ
   ス

のように「ー」でクロスさせます。

主観的難度/当日の成否:

★★★★☆/解答失敗

解答:

$ echo カレーライス 醤油ラーメン | tr ' ' '\n' | sed '1s/./   &\n/g' | awk '{l[NR]=$0}END{l[3]=l[NR];for(i=1;i<7;i++){print l[i]}}'
   カ
   レ
醤油ラーメン
   ラ
   イ
   ス

説明:

「カレーライス」の部分と「醤油ラーメン」の部分で2行に分割し、1行目の「カレーライス」を1文字ずつ全角スペース3個を加えて行に分割します。

そして、Q1と同じように特殊変数NRの値をキーとした連想配列に各行の値を格納し、パターンENDで3行目の値を「醤油ラーメン」が格納されている最終行の値に入れ替えて出力します。

Q3

問題:

ファイルQ3

$ cat Q3
aaabbb
bababa
aaabbb
aaabbb
bababa
bbbbba

について

$ cat Q3 | ...
bababa  2 5
aaabbb  1 3 4
bbbbba  6

のように行番号を付けてコンパクトに出力します。

また、出力結果を格納したファイルQ3.ansから元のデータに復元します。

主観的難度/当日の成否:

★★★★☆/解答失敗

解答:

$ cat Q3 | awk '{data[$1]=data[$1]" "NR}END{for(key in data){print key,data[key]}}'
bababa  2 5
aaabbb  1 3 4
bbbbba  6
$ cat Q3.ans | awk '{for(i=2;i<=NF;i++){print $1,$i}}' | sort -k2,2 | awk '{print $1}'
aaabbb
bababa
aaabbb
aaabbb
bababa
bbbbba

説明:

前半・後半ともにAWK力が要求される問題でした。

前半では、キーに各行の値を、値に「行の値 行番号の繰り返し」を格納した連想配列を使って、行の値が重複した場合に行番号を追記できるようにして、パターンENDでこの連想配列のキーと値の対を出力します。

後半では、第2フィールド以降にある数値から「行の値 行番号」を格納したテキストデータの行を各レコード行から復元し、ソートした後、行の値のみ出力します。

AWKの連想配列の使い方だけではなく、ファイルの圧縮・解凍の原理も学べる良い問題だと思います。

Q4

問題:

ファイルQ4について、素数行目に存在するりんごとみかんの数を数えます。

$ cat Q4
りんご
りんご
みかん
みかん
りんご
みかん
りんご
りんご

主観的難度/当日の成否:

★★☆☆☆/解答成功

解答:

$ cat Q4 | paste <(seq 8 | factor) - | awk 'NF==3' | sort -k3,3 | uniq -f2 -c | awk '{print $4,$1}'
みかん 1
りんご 3

説明:

まず、pasteコマンドとプロセス置換を用いて、行番号とそれを素因数分解したものを各行の先頭に付けます。

次に、行番号が素数の場合は「行番号: 素因数1つ りんご又はみかん」のようにフィールド数が3つになっていますので、該当する行を抽出します。

そして、第3フィールドの値について出現回数を求めるためにsort -k3,3ならびにuniq -f2 -cを使い、最後に「りんご又はみかん 出現回数」を出力します。

Q5

問題:

ファイルQ5にある数の並びから、足して10になる並びを見つけ出します。

$ cat Q5
1 3 4 4 2 3 5 6 7 9 1 4

主観的難度/当日の成否:

★★★★☆/解答失敗

解答:

$ cat Q5 | tr -d ' ' | sed ':A;h;:B;p;s/.$//;tB;x;s/^.//;tA;' | sed 's/./& /g' | sed '/^\(. \|\)$/d' | awk '{printf($0": ");for(n=1;n<=NF;n++){s+=$n};print s;s=0}' | sed '/: 10$/!d'
4 4 2 : 10
2 3 5 : 10
9 1 : 10

説明:

sedで数の組み合わせを列挙し、それらの和が10になるものを抽出・出力しています。

なお、数の組み合わせを列挙している部分

$ cat Q5 | tr -d ' ' | sed ':A;h;:B;p;s/.$//;tB;x;s/^.//;tA;' | sed 's/./& /g' | sed '/^\(. \|\)$/d'
1 3 4 4 2 3 5 6 7 9 1 4
1 3 4 4 2 3 5 6 7 9 1
1 3 4 4 2 3 5 6 7 9
1 3 4 4 2 3 5 6 7
1 3 4 4 2 3 5 6
(...中略...)
7 9 1
7 9
9 1 4
9 1
1 4

にある「sed ‘:A;h;:B;p;s/.$//;tB;x;s/^.//;tA;’」には、

  1. 外側のループ用のラベルAを設定
  2. パターンスペースに格納されている文字列をホールドスペースにコピーして保存
  3. 内側のループ用のラベルBを設定
  4. 現時点のパターンスペースを出力
  5. パターンスペースから末尾の1文字を削除
  6. パターンスペースにある文字が無くなるまでラベルBに戻って内側のループを繰り返す
  7. パターンスペースにある文字が無くなったら、ホールドスペースに保存してある文字列をパターンスペースに戻す
  8. 戻したパターンスペースの先頭1文字を削除
  9. パターンスペースにある文字が無くなるまでラベルAに戻って外側のループを繰り返す

という意味があります。

そして、その後の「sed ‘s/./& /g’」は各文字の間に半角スペースを挿入、「sed ‘/^(. |)$/d’」は1文字と半角スペースしか存在しない行あるいは空白行を削除するという意味になっています。

Q6

問題:

ファイルQ6_1

$ cat Q6_1
所謂いわゆる「Z」というものにだって、
もっと何か表情なり印象なりがあるものだろうに、
YのからだにXでもくっつけたなら、
こんな感じのものになるであろうか、
とにかく、どこという事なく、見る者をして、
ぞっとさせ、いやな気持にさせるのだ。
私はこれまで、こんな不思議な男の顔を見た事が、
やはり、いちども無かった。

のX・Y・Zに対して、ファイルQ6_2

$ cat Q6_2
X 駄馬の首
Y 人間
Z 死相

の内容に基づいて置き換えます。

主観的難度/当日の成否:

★★☆☆☆/解答成功

解答:

$ cat Q6_2 | awk '{print "sed \"s/"$1"/"$2"/g\""}' | sed -z 's/\n/ | /g' | sed 's/^/cat Q6_1 | /' | sed 's/| $/\n/' | sh
所謂いわゆる「死相」というものにだって、
もっと何か表情なり印象なりがあるものだろうに、
人間のからだに駄馬の首でもくっつけたなら、
こんな感じのものになるであろうか、
とにかく、どこという事なく、見る者をして、
ぞっとさせ、いやな気持にさせるのだ。
私はこれまで、こんな不思議な男の顔を見た事が、
やはり、いちども無かった。

説明:

$ cat Q6_2 | awk '{print "sed \"s/"$1"/"$2"/g\""}' | sed -z 's/\n/ | /g' | sed 's/^/cat Q6_1 | /' | sed 's/| $/\n/'
cat Q6_1 | sed "s/X/駄馬の首/g" | sed "s/Y/人間/g" | sed "s/Z/死相/g"

でシェル芸の文字列を生成できますので、それをパイプでシェルに渡して置換処理しています。

Q7

問題:

明示的に端末を閉じる次のようなコマンド、例えば

  • shutdown
  • reboot
  • exit
  • logout

などを使わずに端末を閉じます。

主観的難度/当日の成否:

★☆☆☆☆/解答成功

解答:

$ ps | grep -v 'PID' | awk '{print $1}' | tac | xargs kill -15

なお、これを短くした別解として

$ ps -o pid= | tac | xargs kill -15

を挙げておきます。

説明:

呼び出し端末と同じ端末に関連付いているプロセスのみを抽出するためにpsをデフォルトで実行し、抽出したプロセスをすべて終了させます。

なお、最後にシェルを終了させないと端末が閉じないため、プロセスIDのリストをxargsに渡す前にtacで並び順を逆にしておく必要があります。

ちなみに、ここで挙げた解答よりもはるかにシンプルかつスマートなシェル芸があります。

$ exec ls

このシェル芸は、指定したコマンドのプロセスでシェルのプロセスを上書きするexecを使うと指定したコマンドの終了と共にシェルも終了してしまうことを利用したものです。

Q8

問題:

C++のソースファイルQ8.ccについて、関数プロトタイプを付け加えます。

具体的には、「using namespace std;」がある行の後に「void aho(void);」と「string nazo(void);」をソースコードからコピーして挿入するということです。

主観的難度/当日の成否:

★★★★★/解答失敗

解答:

$ cat Q8.cc | awk '{l[NR]=$0}/^[a-z]+ [a-z]+\(void\)$/{m[NR]=$0}END{for(i=1;i<=NR;i++){print l[i];if(l[i]=="using namespace std;"){for(j=1;j<=NR;j++){if(m[j]){print m[j]";"}}}}}'
#include <iostream>
#include <string>
using namespace std;
void aho(void);
string nazo(void);

void aho(void)
{
	cout << nazo() << endl;
}

string nazo(void)
{
	return "謎";
}

int main(int argc, char const* argv[])
{
	aho();
	return 0;
}

説明:

この問題もAWKの連想配列についての知識が要求されます。

手順としては、

  1. 行番号をキーとして、各行の値を連想配列lに格納
  2. 同じく行番号をキーとして、プロトタイプ宣言に必要な行であればその値を、そうでなければ空文字列を値として連想配列mに格納
  3. パターンENDの外側のforループで連想配列lに格納した値を出力
  4. 連想配列lの要素の値が「using namespace std;」の場合、次行以降にプロトタイプ宣言を挿入。具体的には内側のforループで連想配列mに格納した値のうち空文字列でないものを出力

のようになっています。


懇親会&LT大会

ライトニングトークの発表者と内容は次の通りでした。

  1. Anubis(@Anubis_369)さん: Windows PowerShellによるCSVの処理について
  2. 日柳 光久(@mikkun_jp): GIMP・Inkscape用のカラーパレット(Pantone/DIC/TOYO)を生成するシェルスクリプト
  3. nmrmsys(@nmrmsys)さん: 「シェル芸で使いたいjqイディオム
  4. T.Motooka(@t_motooka)さん: jqを使ったアマゾンウェブサービス(AWS)の管理用スクリプト・opensslを使った特定サイトの証明書情報を毎日確認するスクリプトなど
  5. くんすと(@kunst1080)さん: 「FreeBSDのススメ」※動画あり
  6. tanishiking(@tanishiking)さん: ターミナルからニコニコ動画の検索ができるnicotermの解説
  7. MSR(@msr386)さん: 「代替grep速度比較」および「CloudAtCostという買い切りVPSってどうなん?

いずれの発表もマニアックでありながら実用的で、特にjqはWebAPIをCLIで扱う際に欠かせないツールになるのではないかと思います。

そして発表が最後であったにもかかわらず、提供サービスの内実のとんでもなさで会場の話題を一気にさらった人柱志願者向け激安VPSのCloudAtCostが実にアヤしくて良い感じです。


おわりに

今回の勉強会では、午前の部での「初心者向け」という言葉の意味はあくまでも講義が取り扱う領域について初心者ということであって、自分のような「普通の人」にとってそれは明らかに詐称であること、そして午後の部ではAWK力の不足、特に連想配列についての知識が乏しいことを思い知らされました。

最後に、

に改めてお礼を申し上げます。44GC44KK44GM44Go44GG44GU44GW44GE44G+44GX44Gf44CCCg==

Written on May 9, 2016