2017年6月30日

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

2017-04-22(土)に開催された、第28回シェル芸勉強会(正式名称:jus共催 第10回初心者向けなのかと百条委員会化する午前のシェル勉強会/第28回基準値を超えるシェル芸勉強会 - USP友の会)に参加しました。
ただ、会場へは直接伺わなかったため、Youtubeで配信してくださったライブ中継を利用して、遠隔で参加しました。

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

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


実行環境

  • Arch Linux 4.10.10-1

  • GNU bash 4.4.12

  • GNU coreutils 8.26-1

  • GNU findutils 4.6.0-2

  • grep (GNU grep) 3.0

  • sed (GNU sed) 4.4

  • gawk 4.1.4

シェルに関する勉強会

シグナル

USP友の会 今泉 光之( @bsdhack)さんによる、シグナルについて理解を深める内容でした。

man 7 signal などを適時参照しつつ、POSIX標準シグナルの各種シグナルハンドラについて、丁寧な解説がおこなわれました。
普段、 kill コマンド等で頻繁に使うにも関わらず、実はハンドラ毎の違いをよく理解していなかったため、非常に勉強になる内容でした。
また、一部のシグナル名は、通信に電話回線を使用していた際の名残である(SIGHUPは電話が切断された)等、興味深い話も多く聞くことが出来ました。

第28回シェル芸勉強会

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

参加しなかった方にも、地獄をおすそ分け❤

問1

問1.1

LaTeXソースから、 figure 環境部分を抽出する問題。

初めの問題だけあって、まだ簡単な難易度。
sed の範囲指定アドレスを使って、 figure 環境に該当する部分のみを出力するだけ。

#!/bin/sh

cat contents.tex                            | # 元ファイルの出力
sed -n '/\\begin{figure}/,/\\end{figure}/p'   # figure環境レコードの抽出

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

cat contents.tex | sed -n '/\\begin{figure}/,/\\end{figure}/p'

問1.2

LaTeXソースから、画像ファイル名とパスの組み合わせを列挙する問題。

1問目の副問題なのに、いきなり難易度が上がったような…​
LaTeXでは、画像ファイルパスは caption 、ファイル名は caption マクロで管理している。
なので、上記2つのマクロを含むレコードを抽出し、余分な部分を除去していけばOK。
除去が完了したら、問題指定のフォーマットに沿うように、出力を整形してやる。

#!/bin/sh

cat contents.tex                               | # 元ファイルの出力
grep -e '\\includegraphics' -e '\\caption'     | # includegraphics, captionマクロを含むレコードの抽出
grep -o '{[^{}]*}'                             | # マクロ内部の抽出
tr -d '{}'                                     | # 波括弧の除去
xargs -n 2                                     | # 1レコード2フィールドに変換
awk '{print $2,$1}'                              # 第1・第2レコードの入れ替え

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

cat contents.tex | grep -e '\\includegraphics' -e '\\caption' | grep -o '{[^{}]*}' | tr -d '{}' | xargs -n 2 | awk '{print $2,$1}'

問2

第2章の第1文目を抽出する問題。

この回で一番簡単だった問題。
sed を使って、該当するレコードを元データから少しずつ削り出していく。

#!/bin/sh

cat contents.tex               | # 元ファイルの出力
sed '/^$/d'                    | # 空行の除去
sed '/^%/d'                    | # コメント行の除去
sed -n '/\\section/{n; p}'     | # sectionの次行の抽出
sed -n '2p'                      # 第2章 第1文の抽出

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

cat contents.tex | sed '/^$/d' | sed '/^%/d' | sed -n '/\\section/{n; p}' | sed -n '2p'

問3

注釈部分のみを抽出する問題。
当日の回答は間違ってた…​

ホールドスペースの練習のような問題。
3行目の内容をホールドスペースに入れる&&出力されないように削除して、7行目のパターンスペース処理時に追記するだけ。

#!/bin/sh

cat contents.tex                     | # 元ファイルの出力
sed 's/。}[、。]/\n&/g'              | # footnoteマクロ末端部分に改行を挿入(sedの範囲指定処理のため)
sed -n '/\\footnote/,/。}[、。]/p'   | # footnoteマクロ該当レコードの抽出
tr -d '\n'                           | # 改行の除去
sed 's/\\footnote/\n&/g'             | # footnoteマクロ直前に改行を挿入
grep -o '\\footnote{.*。}[、。]'     | # footnoteマクロ部分の抽出
sed 's/.$//'                           # 末尾の句読点を除去

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

cat contents.tex | sed 's/。}[、。]/\n&/g' | sed -n '/\\footnote/,/。}[、。]/p' | tr -d '\n' | sed 's/\\footnote/\n&/g' | grep -o '\\footnote{.*。}[、。]' | sed 's/.$//'

問4

章毎に個別のファイルに分割する問題。

比較的簡単だった問題。
awk でカウンタ変数を使いつつ、リダイレクトでセクションごとにファイル出力していく。

#!/bin/sh

cat contents.tex                                                        | # 元ファイルの出力
awk '{if(match($0,/\\section{/)){s++}; print $0 > "contents_"s".tex"}'    # セクション毎にファイルに出力

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

cat contents.tex | awk '{if(match($0,/\\section{/)){s++}; print $0 > "contents_"s".tex"}'

問5

ソースに含まれる「○○座標系」という用語を列挙する問題。

GNU grep に依存しまくりの解答。
ただ、他の方の解答を拝見すると、どうやらPerlの正規表現を使うと、よりスマートに出来るらしい。

#!/bin/sh

cat contents.tex                       | # 元ファイルの出力
grep -o '[^[:punct:]ぁ-ん]*座標系'     | # ○○座標系の抽出
sort                                   | # レコードのソート
uniq                                     # 重複レコードの除去

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

cat contents.tex | grep -o '[^[:punct:]ぁ-ん]*座標系' | sort | uniq

問6

各段落の先頭に全角スペースを挿入する問題。

当日に解答したコードは、段落開始部分以外にも全角スペースを入れてしまっており、微妙に間違ってたorz
まず、候補レコード全てに全角スペースを付加してやり、その後不要なレコードから全角スペースを除去してやると、ええ感じにできた。

#!/bin/sh

cat contents.tex                                                                        | # 元ファイルの出力
sed -r '/\\(section|sub|begin|end)/!{/^[ \t]{2,}\\/!{/^[^%]/s/^/ /}}'                  | # section・環境以外のレコード先頭に全角スペースを追加
gawk '/^ *$/{s=0} {if(index($0, " ")){s++}; print (s>1) ? gensub(/ /,"","g",$0) : $0}'  # 段落開始以外のレコードから全角スペースを除去

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

cat contents.tex | sed -r '/\\(section|sub|begin|end)/!{/^[ \t]{2,}\\/!{/^[^%]/s/^/ /}}' | gawk '/^ *$/{s=0} {if(index($0, " ")){s++}; print (s>1) ? gensub(/ /,"","g",$0) : $0}'

問7

本文部分の改行を除去する問題。
この問題も、当日のコードは間違っていた。というか、文意を間違えていて、適切に改行を除去していなかった…​

単純に初めに改行を全て除去してしまうと、後に復元のしようが無くなってしまう。
そのため、初めに必要な部分に、俺俺改行マーク <:LF:> を付加しておく。
付加した後は、テキストの加工をおこなっていき、最後に俺俺改行マークを改行に変換し、復元すればOK。

#!/bin/sh

cat contents.tex                       | # 元ファイルの出力
tac                                    | # 入力を反転出力
sed '/^$/{n; /。$/s/.*/&<:LF:>/}'      | # 段落末端に改行マークを付加
tac                                    | # 入力を反転出力
sed '/^%/s/.*/&<:LF:>/'                | # コメントレコードに改行マークを付加
sed '/^\\begin/,/^\\end/s/.*/&<:LF:>/' | # LaTeX環境レコードに改行マークを付加
sed '/^\\begin/s/.*/<:LF:>&/'          | # LaTeX環境レコード開始前に改行マークを付加
sed '/section{/s/.*/&<:LF:>/'          | # sectionマクロレコードに改行マークを付加
sed '/^$/s/.*/&<:LF:>/'                | # 空行に改行マークを付加
tr -d '\n'                             | # 改行の除去
sed 's/<:LF:>/\n/g'                      # 改行マークを改行に変換

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

cat contents.tex | tac | sed '/^$/{n; /。$/s/.*/&<:LF:>/}' | tac | sed '/^%/s/.*/&<:LF:>/' | sed '/^\\begin/,/^\\end/s/.*/&<:LF:>/' | sed '/^\\begin/s/.*/<:LF:>&/' | sed '/section{/s/.*/&<:LF:>/' | sed '/^$/s/.*/&<:LF:>/' | tr -d '\n' | sed 's/<:LF:>/\n/g'

問8

LaTeXソースから、目次を作成する問題。

最終問題は、それほど難しくは無かった。
目次を作成するために必要な、 \section, \subsection, \subsubsection マクロを含んだレコードを抽出し、 不要な文字を除去する。
その後、 awk で章・節・小節のカウンタ変数を作り、章節番号と各タイトルを出力してやる。

cat contents.tex                       | # 元ファイルの出力
grep -E '^\\(|sub|subsub)section{'     | # section, subsection, subsubsectionレコードの抽出
sed 's/\\label.*//'                    | # labelマクロの除去
sed 's/}$//'                           | # *sectionマクロ終了文字'}'の除去
sed 's/{/<FS>/'                        | # *sectionマクロ開始文字'{'をフィールドスペースに変換
awk -F'<FS>' '
    /\\sec/{s++; ss=0; print s,$2}
    /\\subsec/{ss++; sss=0; print s"."ss,$2}
    /\\subsub/{sss++; print s"."ss"."sss, $2}
'                                        # 出力の整形

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

cat contents.tex | grep -E '^\\(|sub|subsub)section{' | sed 's/\\label.*//' | sed 's/}$//' | sed 's/{/<FS>/' | awk -F'<FS>' '/\\sec/{s++; ss=0; print s,$2} /\\subsec/{ss++; sss=0; print s"."ss,$2} /\\subsub/{sss++; print s"."ss"."sss, $2}'

雑記

…​今回の問題、難しすぎィ!!
ここ最近、シェル芸勉強会の問題の難易度が、某バトル漫画並にインフレを起こしている気がする…​w
難易度が高いのは、頭をフル回転させられるし、ベテランの方の様々なテクニックも拝見できるので、それはそれで面白いのだけれど…​
さすがに高難易度が続くと少し疲れてしまうので、たまには簡単な問題も混ぜてもらえますと、個人的にありがたいですorz

あと、回を重ねるごとに、LTの変態度もアップしている気がする。


今回の勉強会で一番印象に残ったのは、上田会長が、お子さんが宿題に取り組んでいる際、「検算をちゃんとやろう」と指導しているというエピソード。

今回の午後演習でもそうだったけれど、シェル芸で真面目な処理をおこなう際、実は出力結果が間違っていたというケースが、ここ最近多くあったので…​
そもそも、とりあえず解答が得られたら、それで満足してしまう僕自身の態度が、あまり宜しくないのだと思うorz

こういった自体を防ぐために、簡単で良いので、シェル芸でも検算が必要だと改めて考えさせられた。
たとえば、シェル芸で検算をやるなら、以下のような操作をしてみると、良いかもしれない。

  • 出力結果に対して、 grepgrep -v を実行し、意図した出力が得られているか、意図しない出力が得られていないかを確認する

  • フィールド区切り形式データの場合、 awk '$0=NF' や Tukubaiの retu 等で、全レコードでフィールド数が揃っているかどうかを確認する

  • フィールド数1のデータの場合、 sort | uniq -c を実行してみて、大雑把に集計してみる

これらを実行してみるだけでも、意図した出力がきちんと得られているかどうか、判別できるようになるはず。
幸い、シェル芸で上記の検算をおこなうには、お尻にパイプとコマンドをくっ付けるだけとお手軽なので、積極的にやる癖を付けていきたい。


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

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