2017年7月1日

第29回シェル芸勉強会参加報告

2017-07-01(土)に開催された、第29回シェル芸勉強会(jus共催 第29回激しいシェル芸勉強会)に参加しました。
ただ、当日は体調不良で遠隔での参加もできなかったため、Youtubeで配信してくださったライブ中継のアーカイブを利用して、自主勉強をしました。

勉強会は、午前と午後の2部構成でした。
午前は、シェルに関する勉強会で、Perlについて学習する内容でした。
午後は、シェル芸勉強会で、シェル芸力を付けるための問題を解きました。

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


実行環境

  • Arch Linux 4.9.34-1-lts

  • GNU bash 4.4.12

  • GNU coreutils 8.27-1

  • GNU diffutils 3.6-1

  • grep (GNU grep) 3.1

  • sed (GNU sed) 4.4

  • gawk 4.1.4

  • w3m 0.5.3+git20170102


シェルに関する勉強会

Perlワンライナー入門

USP友の会 鳥海 秀一( @hid_tori )さんによる、Perlワンライナーの書き方や代表的なオプションなど学ぶ内容でした。
資料はこちらから。
https://umidori.github.io/shellgei-29th-am/index.html

題目のとおり、Perlの基本から、Perlをワンライナーで動かす際に有用な情報まで、様々なテクニックが紹介されていました。
個人的には、Perlには、AWKの RS のような、有用な組込変数が多く存在することが、非常に印象的でした。

ちなみに、本Perl講義は、次回も引き続き開催されるそうで、次回の内容は「Perl正規表現」だそうです。
Perlの正規表現は、前回の第28回シェル芸勉強会で、非常に有用だったので、とても気になる…​!!

第29回シェル芸勉強会

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

問1

複数の得点リストを結合する問題。

第一問だけあって、まだ簡単な問題。
awk なり Tukubaiの sm* なりで集計・ID順にソートするだけ。

#!/bin/bash

cat kadai{1,2}                                                  | # 元データの出力
                                                                  ### 1:ID 2:名前 3:得点
awk '{sum[$1" "$2]+=$3} END{for(n in sum){print n,sum[n]}}'     | # 1:ID, 2:名前をキーとして集計
sort -k 1,1                                                       # 出力のソート

Tukubai版。

#!/bin/bash

cat kadai{1,2}     | # 元データの出力
                     ### 1:ID 2:名前 3:得点
sort               | # ソート
sm2 key=1/2          # 1,2フィールドをキーとして集計

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

cat kadai{1,2} | awk '{sum[$1" "$2]+=$3} END{for(n in sum){print n,sum[n]}}' | sort -k 1,1

Tukubai版。

cat kadai{1,2} | sort | sm2 key=1/2

問2

出欠簿の出欠フィールドに、最新の出欠状況を反映させる問題。

6回目の出欠状況が記録されているデータ attend6 を、「1:出席番号 2:第6回目の出席」の書式に変換し、 attend と結合してやる。
両ファイルを結合した出力が得られたら、不足している「第6回目の欠席」データを補って、出力を整形すれば良い。
また、Tukubaiの loopj を使う場合、結合する際に不足フィールドを補う機能があるため、処理内容が少しシンプルになる。

#!/bin/sh

cat attend6                                             | # 元データの出力
tr ',' '\n'                                             | # 行列変換
                                                          ### 1:ID
sed 's/$/ 出/'                                          | # 出席データフィールドを追加
                                                          ### 1:ID 2:第6回目の出欠状況
sort -k 1,1                                             | # 第一フィールドをキーとしてソート
join -1 1 -2 1 -a 1 attend -                            | # 入力1,2をそれぞれ第一フィールドをキーとして結合(結合できないレコードも表示)
                                                          ### 1:ID 2:名前 3:出席状況 4:第6回目の出欠状況(欠席者は無し)
awk 'NF==4{print $1,$2,$3$4} NF==3{print $1,$2,$3"欠"}'   # 欠席データを付与&出力を整形
                                                          ### 1:ID 2:名前 3:出席状況

Tukubai版。

#!/bin/sh

cat attend6                     | # 元データの出力
tr ',' '\n'                     | # 行列変換
                                  ### 1:ID
sed 's/$/ 出/'                  | # 出席データフィールドを追加
                                  ### 1:ID 2:第6回目の出席状況
sort -k 1,1                     | # 第一フィールドをキーとしてソート
loopj -d'欠' num=1 attend -     | # 入力1,2をそれぞれ第一フィールドをキーとして結合(不足フィールドを'欠'として補う)
                                  ### 1:ID 2:名前 3:出欠状況 4:第6回目の出欠状況
sed 's/ //3'                      # 余分な空白の除去
                                  ### 1:ID 2:名前 3:出欠状況

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

cat attend6 | tr ',' '\n' | sed 's/$/ 出/' | sort -k 1,1 | join -1 1 -2 1 -a 1 attend - | awk 'NF==4{print $1,$2,$3$4} NF==3{print $1,$2,$3"欠"}'

Tukubai版。

cat attend6 | tr ',' '\n' | sed 's/$/ 出/' | sort -k 1,1 | loopj -d'欠' num=1 attend - | sed 's/ //3'

問3

これまでの出欠状況に応じて、試験の得点を出力する問題。

スマートな方法を思いつかなかったので、出席回数・開講回数を出力して、愚直に出席回数が満たしているかどうかを判定する。
判定処理は一度にやるのではなく、出席・開講回数を一度フィールドとして追加し、後に awk 等で判別処理をする形式にすると、若干シンプルかも。

#!/bin/sh

cat attend                                                      | # 元データの出力
                                                                  ### 1:ID 2:名前 3:出欠状況
awk '{print $0, gsub(/出/, ".", $3), length($NF)}'              | # 4:出席回数 5:開講回数フィールドを追加
                                                                  ### 1:ID 2:名前 3:出欠状況 4:出席回数 5:開講回数
join -11 -21 -a1 - test                                         | # それぞれ第一フィールドをキーとして結合(結合できないレコードも表示)
                                                                  ### 1:ID 2:名前 3:出欠状況 4:出席回数 5:開講回数 6:得点(不参加者は無し)
awk '$4<=$5/2{$NF=0; print; next} NF!=6{print $0, 0} NF==6'     | # 出席回数および得点の有無に応じて、6:得点フィールドの出力を変更
                                                                  ### 1:ID 2:名前 3:出欠状況 4:出席回数 5:開講回数 6:得点
awk '{print $1, $2, $NF}'                                         # 不要なフィールドの除去
                                                                  ### 1:ID 2:名前 3:試験得点

Tukubai版。

#!/bin/sh

loopj -d'0' num=1 attend test                                | # 第一フィールドをキーとして結合(不足フィールドは0で埋める)
                                                               ### 1:ID 2:名前 3:出欠状況 4:得点
awk 'atd=$3{print $0, length(atd), gsub(/欠/, "", atd)}'     | # レコード末尾に5:開講回数 6:欠席回数 フィールドを追加
                                                               ### 1:ID 2:名前 3:出欠状況 4:得点 5:開講回数 6:得点
awk '$6/$5 <= 0.5{print $1,$2,$4} 0.5 < $6/$5{print $1,$2,0}'  # 6:欠席回数 / 5:開講回数 の割合に応じて、出力を整形
                                                               ### 1:ID 2:名前 3:得点

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

cat attend | awk '{print $0, gsub(/出/, ".", $3), length($NF)}' | join -11 -21 -a1 - test | awk '$4<=$5/2{$NF=0; print; next} NF!=6{print $0, 0} NF==6' | awk '{print $1, $2, $NF}'

Tukubai版。

loopj -d'0' num=1 attend test | awk 'atd=$3{print $0, length(atd), gsub(/欠/, "", atd)}' | awk '$6/$5 <= 0.5{print $1,$2,$4} 0.5 < $6/$5{print $1,$2,0}'

問4

数列に対して、正負の有無・桁数毎にレコードに分けて出力する問題。

この問題は小問1,2と分かれているけれど、どちらも正答を出せる解答を考えてみた。
正負記号の有無、数値長毎に、 awk の連想配列を使って集計・結合していく。
問3と同様に、一変に集計・結合処理をするのではなく、一度正負フラグや数値長を別フィールドとして出力してやると、 処理がわかりやすくなると思う。

#!/bin/sh

### Q4.1
echo -1 4 5 2 42 421 44 311 -9 -11                                        | # 数列の出力
tr ' ' '\n'                                                               | # 1レコード1フィールドに整形
sort -n                                                                   | # 数値順でソート
awk '{print $0,sub(/-/, "", $0),gsub(/[0-9]/, "", $0)}'                   | # 1:数値, 2:正負フラグ 3:数値長 を出力
                                                                            ### 1:数値 2:正負フラグ 3:数値長
awk '{arr[$2,$3] = arr[$2,$3]" "$1} END{for(n in arr){print arr[n]}}'     | # 2:正負フラグ,3:数値長 をキーとして、1:数値を集計
sort -k 1,1n                                                                # カテゴリ順にソート


### Q4.2
echo -1 +4 5 2 42 421 44 311 -9 -11                                       |
tr ' ' '\n'                                                               |
sort -n                                                                   |
awk '{print $0,sub(/-/, "", $0),gsub(/[0-9]/, "", $0)}'                   |
awk '{arr[$2,$3] = arr[$2,$3]" "$1} END{for(n in arr){print arr[n]}}'     |
sort -k 1,1n

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

echo -1 4 5 2 42 421 44 311 -9 -11 | tr ' ' '\n' | sort -n | awk '{print $0,sub(/-/, "", $0),gsub(/[0-9]/, "", $0)}' | awk '{arr[$2,$3] = arr[$2,$3]" "$1} END{for(n in arr){print arr[n]}}' | sort -k 1,1n
echo -1 +4 5 2 42 421 44 311 -9 -11 | tr ' ' '\n' | sort -n | awk '{print $0,sub(/-/, "", $0),gsub(/[0-9]/, "", $0)}' | awk '{arr[$2,$3] = arr[$2,$3]" "$1} END{for(n in arr){print arr[n]}}' | sort -k 1,1n

問5

テキストで表記された正三角形を、時計回りに回転させる問題。

Tukubaiの tateyoko コマンドを用いたら、かなりシンプルに出来た!
解説は長くなるので後述。

まずはTukubai版から。

#!/bin/sh

cat triangle     | # 元データの出力
tateyoko -i      | # 転置処理(不足フィールドは補う)
tr -d '_'        | # 余分な文字の除去
tac              | # レコードの反転
tateyoko -i      | # 転置処理(不足フィールドは補う)
tr -d '_'        | # 余分な文字の除去
tac                # レコードの反転

処理過程をコマンド毎に表すと、以下のようになる。
まず、元データの状態。

$ cat triangle
   1
  3 9
 7 a 6
8 4 2 5

この元データに対して、 tateyoko -i で不足フィールドを補って転置処理を実行すると、以下の出力が得られる。

$ cat triangle | tateyoko -i
1 3 7 8
_ 9 a 4
_ _ 6 2
_ _ _ 5

次に、 tateyoko -i で転置した出力から、転置時に補った不足分レコードを削除する。 この処理の結果、各段の幅合わせがおこなわれ、 1 9 6 5 を軸として反転させたような、逆三角形の出力が得られる。

$ cat triangle | tateyoko -i | tr -d '_'
1 3 7 8
 9 a 4
  6 2
   5

この逆正三角形を、 tac で上下反転してみる。 すると、元の正三角形を反時計回りに一回転させた状態になった。

$ cat triangle | tateyoko -i | tr -d '_' | tac
   5
  6 2
 9 a 4
1 3 7 8

ということで、正三角形に対して tateyoko -i | tr -d _ | tac を実行すると、反時計回りに一回転出来る事が分かった。
後は、設問で求められている出力になるように、 tateyoko -i | tr -d _ | tac でもう一回、反時計回りに回転させればOK。

通常版。
時計回りに一回転させた際の各段を、 awk の連想配列を駆使して結合し、出力していく。
$NF は最下段、 $NF-1 は下から2段目、 $NF-2 は上から2段目、 $1 は最上段と考えて、それぞれ連想配列に入れていく。
最後に、幅合わせの処理と、正三角形になるようにレコードの反転を実行する。

#!/bin/sh

cat triangle                                                                          | # 元テキストの出力
awk '{for(i=NF;i>=1;i--){st[NF-i]=$i" "st[NF-i]}} END{for(n in st){print st[n]}}'     | # 右回転時の各段の出力
awk '{for(i=1;i<NR;i++){printf " "}; print $0}'                                       | # 幅合わせの処理
tac                                                                                     # 上下の反転

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

Tukubai版。

cat triangle | tateyoko -i | tr -d '_' | tac | tateyoko -i | tr -d '_' | tac

通常版。

cat triangle | awk '{for(i=NF;i>=1;i--){st[NF-i]=$i" "st[NF-i]}} END{for(n in st){print st[n]}}' | awk '{for(i=1;i<NR;i++){printf " "}; print $0}' | tac

問6

特定の数が欠けている素数列に対して、欠けている数値部分で改行を実行する問題。

元の素数列に対して、1から100までの全ての素数を列挙した素数列と、 diff で差分を取ってみる。
差分を取った結果から、改行すべき箇所を特定し、出力を整形してやればOK。

#!/bin/sh

cat prime                     | # 元データの出力
tr ' ' '\n'                   | # 列行変換
diff -y - <(primes 1 100)     | # 1~100までの素数数列との差分を表示
awk '$0=$1'                   | # 第一フィールドのみの出力
xargs                         | # 行列変換
sed 's/\(> \)\{1,\}/\n/g'       # 欠番素数部分の改行

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

cat prime | tr ' ' '\n' | diff -y - <(primes 1 100) | awk '$0=$1' | xargs | sed 's/\(> \)\{1,\}/\n/g'

問7

AAを表示するHTMLを、CLI上でAAとして出力する問題。

w3m-dump オプションを使って、ブラウザ上で表示されるそのままの出力を、端末上に出力してやるだけ。
他の方の解答を見ていると、 nkf--numchar-input オプションを使うのが、正当な正答らしい。
というか、 nkf って、実体参照の復号化も出来るのか…​ 知らんかった。

#!/bin/sh

cat nyaan.html          | # 元データの出力
w3m -T text/html -dump    # HTMLコードの解釈結果の出力

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

cat nyaan.html | w3m -T text/html -dump

問8

AAから、空行・空列を除去する問題。

基本的な考え方は、通常版・Tukubai版どちらもほぼ同じ。
シェル上でのテキスト処理は、原則として、入力先頭からの逐次処理かつレコード単位 → フィールド単位の順番の処理となる。
(要するに、基本的に「上から下へ → 左から右へ」の流れでしか処理できない。)
したがって、縦方向に何らかの判定をおこなう(ある列が全て空白だったら○をおこなう○)といった処理は、非常に難しい。
そこで、縦方向に判定処理をするのではなく、出力を一度転置(縦横の入替)をしてやり、横方向として判定処理できるようにすると良い。
横方向での判定処理が完了したら、再度転置をして元の形に戻してやればOK。

福岡の著名シェル芸人・ぱぴろん( @papiron )さんも、同様の考え方で解答していらっしゃった。

#!/bin/sh

cat shellgei                                                                                | # 元AAの出力
grep -v '^ *$'                                                                              | # 余分な列の除去
awk -vFS= '{for(i=1;i<=NF;i++){str[i]=str[i]""$i}} END{for(i=1;i<=NF;i++){print str[i]}}'   | # 行列転置
grep -v '^ *$'                                                                              | # 余分な列(この処理時点では行)の除去
awk -vFS= '{for(i=1;i<=NF;i++){str[i]=str[i]""$i}} END{for(i=1;i<=NF;i++){print str[i]}}'     # 行列転置

Tukubai版。

#!/bin/sh

cat shellgei          | # 元データの出力
grep -v '^ *$'        | # 空行の除去
tr ' ' '@'            | # スペースを未使用文字に変換
awk -vFS= NF=NF       | # 1文字1フィールドに変換
tateyoko              | # 転置処理
grep -v '^[@ ]*$'     | # 空行(転置前は空列)の除去
tateyoko              | # 転置処理
tr -d ' '             | # 余分なスペースの除去
tr '@' ' '              # 未使用文字をスペースに変換

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

cat shellgei | grep -v '^ *$' | awk -vFS= '{for(i=1;i<=NF;i++){str[i]=str[i]""$i}} END{for(i=1;i<=NF;i++){print str[i]}}' | grep -v '^ *$' | awk -vFS= '{for(i=1;i<=NF;i++){str[i]=str[i]""$i}} END{for(i=1;i<=NF;i++){print str[i]}}'

Tukubai版。

cat shellgei | grep -v '^ *$' | tr ' ' '@' | awk -vFS= NF=NF | tateyoko | grep -v '^[@ ]*$' | tateyoko | tr -d ' ' | tr '@' ' '

雑記

今回は、暑さやら低気圧やらのためか、当日の朝から酷い頭痛になってしまい、遠隔ですら参加出来なかったorz
前回は、オーバーキルな難易度だったけれど、今回は比較的易しい問題が多かった印象。
最近、テキスト処理をする機会が少なくて、とても飢えていたので、シェル芸勉強会の問題は、とても良い気分転換になった (^q^)


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

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