2016年8月29日

第24回シェル芸勉強会参加(?)報告

2016-08-27(土)に開催された、第24回シェル芸勉強会(正式名称:第6回もう初心者向けでないなんて言わないよ絶対午前のシェル勉強会/第24回◯◯o◯裏番組シェル芸勉強会)に 参加できませんでした
ただ、主催者様のご厚意で、ライブ配信のアーカイブを公開してくださったため、そちらを利用して自主勉強しました。

勉強会は、午前と午後の2部構成でした。
午前は、シェルに関する勉強会で、gawkへの理解を深める講義と、シェル芸入門・プチ演習という内容でした。
午後は、シェル芸勉強会で、シェル芸力を付けるための問題を解きました。

以下、勉強会の詳細を記します。


実行環境

  • Arch Linux (x86_64, 4.7.1-1-ARCH)

  • bash (4.3.46)

  • GNU coreutils (8.25)

  • gawk (4.1.3)


シェルに関する勉強会

gawkへの理解を深める講義

シェル芸おじさんこと、USP友の会 会長の上田 隆一(@ryuichiueda)さんによる、gawkへの理解を深める講義でした。
…​が、直接は参加できなかったので、詳しい内容は分からずorz

シェル芸入門・プチ演習

ハイパーシェル芸キュアエンジニアこと、ぐれさん(@grethlen)さんによる、初心者向けのシェル芸入門・プチ演習でした。
こちらも参加できませんでしたが、資料を公開してくださったため、そちらを参照して自主勉強しました。
(以下、公開してくださった資料)
https://speakerdeck.com/greymd/mei-ri-kou-keru-sieruyun-wojue-eyou

余談ですが、この資料は、シェル芸初心者向けとして、非常に優れていると思いました。
シェル芸の学ぶうえでの大切な考え方から、(半分冗談も混ぜつつ)シェル芸人としての大まかな実力が把握できる分類、そしてシェル芸の小演習と、分かりやすく纏められています。
また、演習自体も、シェル芸のポイントを押さえて、初心者が少しずつステップアップできるような構成になっています。
パターンマッチやフィルタリング等の簡単なものから、コマンド・プロセス置換や特定レコードの抽出等の有用なテクニックまで、更には実用的な使用例までバランス良く問題が組み立てられていました。
個人的にですが、今後シェル芸を初めたい・初心者を脱出したい方向けとして、決定版の資料ではないかなと思いました。

…​と余談が長くなったけれど、以下プチ演習の解答。

Q1

1 #!/bin/sh
2
3 cat holidays \
4 | # 元データの出力
5 awk '/2016-/'
6   # 2016年度のレコードの抽出

Q2

1 #!/bin/sh
2
3 cat holidays \
4 | # 元データの出力
5 awk '/2016-02/' \
6 | # 2016年2月のレコードの抽出
7 grep -c ''
8   # レコード数の出力

Q3

1 #!/bin/sh
2
3 cat holidays \
4 | # 元データの出力
5 awk '/2016-09-19/ && $0=$3'
6   # 2016年9月19日のイベント名の出力

Q4

すいません、解けませんでしたorz
以下、言い訳w

正答の"2016-08-27"は、シェル芸勉強会の開催日。
でも、僕がこの問題を問いたのは、翌日の"2016-08-28"だった。
dateコマンドをオプション無しで実行した場合、現在の日時が表示される。
つまり、当日に挑戦した場合は、dateコマンドの出力を利用するだけで良かった。
…​という理由で、当日以外でコマンドに数字を一切使わずに解く方法は、思いつかなかった…​のは、きっと修行不足ですよね><

Q5

1 #!/bin/sh
2
3 cat holidays \
4 | # 元データの出力
5 grep '2016-' \
6 | # 2016年度のレコードの抽出
7 awk '{dotw[$2]++} END{for(n in dotw){print dotw[n],n}}'
8   # 2フィールド目(曜日)をキーとして集計

Q6

1 #!/bin/sh
2
3 cat holidays \
4 | # 元データの出力
5 sed -n '/2016-08-03/,/2016-09-30/p'
6   # 2016年8月3日から2016年9月30日までのレコードの抽出

Q7

01 #!/bin/bash
02
03 diff \
04 <(
05     cat holidays \
06     | # 元データの出力
07     grep '2015-' \
08     | # 2015年度のレコードの抽出
09     grep -v '\(平\|休\)日' \
10     | # 祝日以外のレコードの除去
11     awk '$0=$3' \
12     | # 3フィールド目以外の除去
13     sort \
14     | # ソート
15     uniq
16       # 重複レコードの除去
17 ) \
18 <(
19     cat holidays \
20     | # 元データの出力
21     grep '2016-' \
22     | # 2016年度のレコードの抽出
23     grep -v '\(平\|休\)日' \
24     | # 以下同じ
25     awk '$0=$3' \
26     |
27     sort \
28     |
29     uniq
30 ) \
31 | # 片方にしか存在しないレコードの出力
32 sed -n '/>/{s/> //;p}'
33   # 出力の整形

diffを利用して解こうと思ったら、何だかややこしい事になってしまった…​

Q8

01 #!/bin/sh
02
03 cat holidays \
04 | # 元データの出力
05 awk '$0=$1" "$4' \
06 | # 1, 4フィールド目の抽出
07 tateyoko \
08 | # データの転置
09 awk 'NR==1{for(i=1;i<=NF;i++){arr[i]=$i}} NR==2{for(i=1;i<=NF-2;i++){if($i==1 && $(i+1)==0 && $(i+2)==1){print arr[i],arr[i+1],arr[i+2]}}}'
10  # 問題文に該当するフィールドの出力

Tukubaiのtateyokoコマンドを利用してみた。
7行目のtateyokoを実行した時点で、1レコード目が日付、2レコード目が休日フラグになっている。
そこで、9行目のawkでは、まず1レコード目の全フィールドを連想配列に格納する。
そして、2レコード目の休日フラグを3フィールドずつ走査し、条件に一致した3フィールドがあれば、その位置に該当する1レコード目のフィールド(連想配列)を出力している。

以上、プチ演習の解答。
最後は難しかったけれど、難易度が徐々に高くなるように構成されていたので、テンポよく解くことが出来ました!
Q4は無かった事にしてください…​w

第24回シェル芸勉強会

こちらも事情により参加できなかったため、後から解いた解答を、コメントを付加したうえで掲載します。

問題文および解答は、以下のURLから。
https://blog.ueda.asia/?p=8592

問1

各レコード毎に、要素を集計する問題。

1 #!/bin/sh
2
3 cat Q1 \
4 | # 元データの出力
5 gawk '{for(i=1;i<=NF;i++){arr[$i]++}; printf("玉子:%d 卵:%d\n", arr["玉子"], arr["卵"]); delete arr}'
6   # レコード毎に、要素の個数を集計

一問目にしては、やや難しめかも。
とはいえ、awkの連想配列を利用すれば、それほど難しくはない。
ただ、連想配列を削除するdeleteは、nawk等では利用できない点に注意。

以下、コピペ実行用のワンライナーコード。

cat Q1 | gawk '{for(i=1;i<=NF;i++){arr[$i]++}; printf("玉子:%d 卵:%d\n", arr["玉子"], arr["卵"]); delete arr}'

問2

テキストから、繰り返しになっている文字を省く問題。

01 #!/bin/sh
02
03 cat Q2 \
04 | # 元データの出力
05 grep -o . \
06 | # 行列変換
07 nl \
08 | # 行番号の付加
09 sort -k 2,2 \
10 | # 2フィールド目をキーとしてソート
11 uniq -1 \
12 | # 2フィールド目以降をキーとして、重複レコードを除去
13 sort \
14 | # ソート
15 awk '$0=$2' \
16 | # 1フィールド目の除去
17 tr -d '\n' \
18 | # 改行の除去
19 awk NF=NF
20   # データ末尾に改行を付加

これも前半にしては、けっこう難しい問題。
正規表現をうまく利用すれば、実現できそうな気がするけれど…​
ここは単純に、文字列を縦方向に整形した後に、重複文字を省くことにした。
また、文字列を横方向に戻す際、文字の順番が正しくなるように、縦方向で処理する際に出現順も付け加えておいた。

以下、コピペ実行用のワンライナーコード。

cat Q2 | grep -o . | nl | sort -k 2,2 | uniq -1 | sort | awk '$0=$2' | tr -d '\n' | awk NF=NF

問3

テキストを1フィールド目をキーとしてソートし、更に区切り文字を付け加える問題。

1 #!/bin/sh
2
3 cat Q3 \
4 | # 元データの出力
5 sort -k 1,1 \
6 | # 1フィールド目をキーとしてソート
7 awk 'o1!=$1{print "%%"; o1=$1} $0; END{print "%%"}'
8   # 1フィールド目をキーとして、レコードに区切り文字を付加

やっと前半らしい、出題順序と一致した難易度の問題。
awkのBEGIN/ENDブロックを利用すれば、区切り文字の出力は簡単に実現できる。

以下、コピペ実行用のワンライナーコード。

cat Q3 | sort -k 1,1 | awk 'o1!=$1{print "%%"; o1=$1} $0; END{print "%%"}'

問4

Excelファイルの特定のセルから、値を抽出する問題。

まずはシート1のセルA1を値を抽出。

01 #!/bin/sh
02
03 unzip -p Q4.xlsx xl/worksheets/sheet1.xml \
04 | # Excelファイルに含まれるシートデータを出力
05 sed 's/<[^>]*>[^>]\{0,\}<\/[^>]*>/\n&\n/g' \
06 | # XMLタグ毎に改行
07 sed -n '/c r="A1"/{n;p}' \
08 | # A1セルの内容を抽出
09 sed 's/<[^>]*>//g'
10   # vタグの除去

Excelファイル(xlsx)は、実はzipアーカイブである事と、セルの値が格納されているファイルが判明すれば、それほど難しくない問題。
unzipで該当ファイルの中身を出力した後、sedで目的の値を削り出していく。

そして、セルA4の文字列を抽出する方は…​…​
こういった情報(http://blogs.wankuma.com/nagise/archive/2008/01/25/119214.aspx)を参照したのだけれど、無茶苦茶面倒臭そう。
…​なので、Excelファイルの中身をテキスト出力できる必殺コマンド、Tukubaiのrexcelx(商用版のみ利用可)におまかせしてしまったw

01 #!/bin/sh
02
03 rexcelx 1 Q4.xlsx \
04 | # Excelファイルの1シート目をテキスト出力
05 self 1 \
06 | # 1フィールド目の抽出
07 tateyoko \
08 | # データの転置
09 self 1 4
10   # 1, 4フィールド目の抽出

Tukubai、便利すぎる…​

以下、コピペ実行用のワンライナーコード。

unzip -p Q4.xlsx xl/worksheets/sheet1.xml | sed 's/<[^>]*>[^>]\{0,\}<\/[^>]*>/\n&\n/g' | sed -n '/c r="A1"/{n;p}' | sed 's/<[^>]*>//g'

セルA4の値も抽出する方。

rexcelx 1 Q4.xlsx | self 1 | tateyoko | self 1 4

問5

式に値を代入し、計算する問題。

1 #!/bin/sh
2
3 echo -2 \
4 | # 値の出力
5 xargs -I@ sed 's/x/'@'/g' Q5 \
6 | # 各レコードについて、xに値を代入
7 bc -l
8   # 各レコードについて、式を計算

シンプルな問題。
xargsの、任意の文字列を標準出力からの値に置き換える事ができるオプション、-Iを利用して、xに値を代入する。
後はbcに計算させるだけ。
この時、オプション無しで実行すると、小数点以下の値が表示されないため、-lオプションを付けておく。

以下、コピペ実行用のワンライナーコード。

echo -2 | xargs -I@ sed 's/x/'@'/g' Q5 | bc -l

問6

2種類の単語から構成されるテキストを分析し、数が少ない単語を多い単語で置換する問題。

01 #!/bin/sh
02
03 cat Q6 \
04 | # 元データの出力
05 sed 's/\(玉子\|卵\)/&\n/g' \
06 | # 玉子もしくは卵で改行
07 sed '/^$/d' \
08 | # 空行の除去
09 sort \
10 | # ソート
11 uniq -c \
12 | # 玉子・卵の出現回数の集計
13 sort -k 1n,1 \
14 | # 1フィールド目をキーとしてソート
15 awk '$0=$2' \
16 | # 1フィールド目を除去
17 xargs \
18 | # 列行変換
19 awk '{print "sed '\''s/"$1"/"$2"/g'\'' Q6"}' \
20 | # 「少ない方」を「大きい方」で置換するコマンドの生成
21 sh
22   # 置換の実行

ちょっと難しかったかも。
各単語の出現回数を数えるのは簡単だけれど、その結果を用いて置換するのが、ちょっとややこしかった。
あまりエレガントじゃないけれど、sedコマンドを出力し、シェルに実行させることで何とか実現できた。

以下、コピペ実行用のワンライナーコード。

cat Q6 | sed 's/\(玉子\|卵\)/&\n/g' | sed '/^$/d' | sort | uniq -c | sort -k 1n,1 | awk '$0=$2' | xargs | awk '{print "sed '\''s/"$1"/"$2"/g'\'' Q6"}' | sh

問7

ある範囲の数値から、各桁の数字の構成が同じものを除去する問題。

…​なのだけれど、どうやら問題の意図を勘違いしているようで、どうしても正答が出せなかったorz
以下、 誤答 デス…​

01 #!/bin/sh
02
03 seq -w 00000 99999 \
04 | # 元データの出力
05 awk '{print "echo "$0" | grep -o . | sort -n | xargs | awk NF=NF OFS="}' \
06 | # 各レコードの数字1文字毎に、昇順に並び替えるコマンドの生成
07 sh \
08 | # コマンドの実行
09 sort -n \
10 | # 数値ソート
11 uniq -u
12   # 重複の除去

個人的に、今回一番難しかった問題。
誤答なうえに、実行時間は20分近くも掛かってしまう、かなり残念な解答…​orz
各レコードについて、数字1文字1文字ごとに昇順に並べ替えて、その後に重複を除去すれば答えが出…​なかった。

以下、コピペ実行用のワンライナーコード。

seq -w 00000 99999 | awk '{print "echo "$0" | grep -o . | sort -n | xargs | awk NF=NF OFS="}' | sh | sort -n | uniq -u

ちなみに、上記コードの出力は、次のとおり。

seq -w 00000 99999 | awk '{print "echo "$0" | grep -o . | sort -n | xargs | awk NF=NF OFS="}' | sh | sort -n | uniq -u
00000
11111
22222
33333
44444
55555
66666
77777
88888
99999

どういうこったい…​

(2016-08-29 14:09:09追記)
Twitterにて、設問の正しい意図を教えていただきました!

ということで、改めて正しい解答をば。
といっても、最後のuniqコマンドから、-uオプションを取り除いただけ。

01 #!/bin/sh
02
03 seq -w 00000 99999 \
04 | # 元データの出力
05 awk '{print "echo "$0" | grep -o . | sort -n | xargs | awk NF=NF OFS="}' \
06 | # 各レコードの数字1文字毎に、昇順に並び替えるコマンドの生成
07 sh \
08 | # コマンドの実行
09 sort -n \
10 | # 数値ソート
11 uniq
12   # 重複の除去

問8

ラスト。1から7を全て含む7桁の整数を用いて、ある式において素数を生成できる並び順を探す問題。

01 #!/bin/sh
02
03 seq 1234567 7654321 \
04 | # tmpファイルなど要らぬ
05 grep -v '[089]' \
06 | # 該当しない整数の除去(0,8,9を含むもの)
07 grep -v '\(.\).*\1' \
08 | # 該当しない整数の除去(同じ数字が複数回出現しているもの)
09 awk NF=NF FS= \
10 | # 1文字毎にスペース区切りで分割
11 awk '{print $1$2$3$4,$5$6$7,($1*$2*$3*$4 + $5*$6*$7)}' \
12 | # abcd, efg, (abcd + efg)の出力
13 awk '{print "printf '\''%d %d '\'' "$1,$2" ;","factor "$3}' \
14 | # 3フィールド目を素因数分解するコマンドの生成
15 sh \
16 | # 素因数分解の実行
17 awk 'NF==4{print $1,$2,$4}'
18   # 素数レコードの出力

この問題、実はProject EulerのProblem 032(https://projecteuler.net/problem=32)にちょっと似ているような気がする。
なので、PE032をシェル芸で解いたコード(https://github.com/gin135/Project_Euler/blob/master/sh/prob_032.sh)を参考にしてみた(Qiitaに記事としてまだアップしていません^\^;)。

まず、1から7を全て含む7桁の整数を出力する。
seqで生成すれば良いのだけれど、この設問では出力数が比較的大きいため、探索範囲が出来るだけ小さくなるようにする。

まず、1から7を全て含み、かつ整数は7桁であるため、最小の値は"1234567"、最大の値は"7654321"になる。
seqでは、この範囲の値を生成すれば良い。
次に、設問の条件に一致しない整数を除去する。
具体的には、「1から7以外の数字(0,8,9)を含む」、もしくは「ある数字が複数回出現する」ものを除去すれば良い。
前者は、単純にgrepの-vオプションで、後者は、後方参照を利用して除去する。

上記の処理によって、tmpファイルが得られる。
…​が、 シェル芸では一時ファイルなど使わぬ! ので、そのままパイプで繋げる。
後は、各レコードを式に代入して、結果が素数かどうかを判別すれば良い。
ただし、素数判別をawkでおこなうのは面倒臭いので、factorコマンドを利用して判別することにする。
ここで、factorコマンドはレコード全体を入力として取るため、特定フィールドだけに適用できるようなコマンドを生成する。
最後に、生成したコマンドを実行して、素数になるレコードを抽出すれば完了。

以下、コピペ実行用のワンライナーコード。

seq 1234567 7654321 | grep -v '[089]' | grep -v '\(.\).*\1' | awk NF=NF FS= | awk '{print $1$2$3$4,$5$6$7,($1*$2*$3*$4 + $5*$6*$7)}' | awk '{print "printf '\''%d %d '\'' "$1,$2" ;","factor "$3}' | sh | awk 'NF==4{print $1,$2,$4}'

雑記

今回も遠隔で参加…​するつもりだったのだけれど、台風やら家業やらが重なり、泣く泣く見送ることにorz
TLにシェル芸ハッシュタグが流れてくるのを眺めつつ、農作業用特殊車両を弄って癒やされていました…​ コンバイン、カッコいいです…​…​


ただ、それ以上に自分のシェル芸力が低下しているのを感じてしまったり。
コマンドを組み合わせる発想力だけでなく、コマンドの存在すら忘れていたりしました…​
ここ最近、シェル芸は全くやっていなかったので、そろそろProject Euler等を再開せねば。

ちなみに、今回の解答で使用したコマンドは、以下の通りでした。

$ find sh_24 -name '*.sh' | xargs cat | grep '^[a-z]' | awk '$0=$1' | sort | uniq | awk '1; END{print "Total:",NR}'
awk
bc
cat
diff
echo
gawk
grep
nl
paste
rexcelx
sed
self
seq
sh
sort
tateyoko
tr
uniq
unzip
xargs
Total: 20

うーん、Tukubaiのrexcelx、self、tateyokoを除くと、基本的なコマンドばかり。
まあ、逆に言えば、基本的なコマンドだけでも、組み合わせ次第で非常に柔軟な処理が可能なことを、再確認できたとも言えるのかも。


勉強会を開催してくださった、上田会長をはじめとした日本UNIXユーザ会とUSP友の会の皆様、ありがとうございました!!

Tags: シェル芸 Unix
このエントリーをはてなブックマークに追加