「第15回ドキッ!grepだらけのシェル芸勉強会」の復習

「第15回ドキッ!grepだらけのシェル芸勉強会」が去る2月1日(日)に行われました。

残念ながらUstreamからのリモート参加となりましたが(中継Thanks)、 第11回@大阪(実は初めて参加したIT系勉強会)にも劣らない濃い世界でした。

以下、解答例と少しばかりの解説です。

なお、問題の詳細については、 【問題】第15回ドキッ!grepだらけのシェル芸勉強会 | 上田ブログ を参照してください。


Q1: 1という文字を含まないファイルを列挙

$ seq 2 5 > a
$ seq 1 9 > b
$ seq 5 11 > c
$ seq 3 6 > d

A1:

$ grep -FL '1' a b c d

grepの-Lオプション(パターンマッチ文字列を含む行が存在しないファイル名を列挙) を使用しました(-Fは「正規表現ではない単なる文字列」の意)。

$ grep -FL '1' *

上記の様に「a b c d」より「*」の方が良いかも知れません。

Q2: file.[1-9]$、file.[1-9]0$、file.[0-9]+00$のファイルのみ残す

$ seq 1 10000 | xargs -I@ touch file.@

A2:

当日のTwitter上では

$ ls file.* | grep -ve '00$' -ve '\.[1-9]0$' -ve '\.[1-9]$' | xargs rm -f

と解答しましたが、

$ ls | grep -v '^file\.\([1-9]0\?\|[0-9]\+00\)$' | xargs rm

と正規表現パターンを一つにまとめてしまう方法もあります。

Q3: テキスト内の「-v」、「-f」、「awk」の数をカウント

$ cat text1
awk -v v="hoge" 'BEGIN{print v}'
echo 'BEGIN{print 1}' | gawk -f -
nawk 'BEGIN{print " BEGIN{print x}"}' | awk -v x=3 -f -

A3:

問題を見た時は「uniq -c」が思い浮かばなかったため、解答は

$ echo -v:$(grep -ce -v text1);echo -f:$(grep -ce -f text1);echo awk:$(grep -cwe awk text1)

という今ひとつ良くない感じでした。というわけで、その反省を踏まえ

$ grep -owe '-v\|-f\|awk' text1 | sort | uniq -c

という別解を書いてみました。シェル芸としても後者の方が良いでしょう。

Q4: /etc/下の「#!/bin/sh」なファイルで「set -e」の有無を調査

当日の自分にとって、 この問題は全く歯が立たない難問でしたので「宿題」として解いてみました。

A4:

手順としては、

  1. ファイル内に「#!/bin/sh」があるものをリストアップ
  2. リスト内の各ファイルにある「set -e」の数を計上
  3. 最後はAWKで第2フィールドの数値で振り分け

という具合になります。

$ grep -lR '#!/bin/sh' /etc 2> /dev/null | xargs grep -c 'set -e' | awk -F: '{$2!=0?Y+=1:N+=1}END{print "有:"Y"\n無:"N}'

ちなみに、標準エラー出力を捨てておくとsudo無しで実行できるので安全かと。

Q5: 日本語やギリシャ文字のある行を除去

$ cat text2
A pen is a pen?
日本語でおk
ΩΩπ<Ω< na nandatte!!
Randy W. Bass
env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
#危険シェル芸

A5:

以下の様に

$ LANG=C grep -Pv '[^[:print:]]' text2
$ grep -Pv '[\p{Hiragana}\p{Katakana}\p{Han}\p{Greek}]' text2

正規表現パターンをPerlのものとして扱うという-Pオプションを使うのですが、 2つめのUnicodeのスクリプトを使用した解答例で、 頭に「LANG=C」を加えると1つめとは逆に間違った結果になるのが少々謎です。

Q6: ファイルの中の数字を足して10になるものを示す

$ echo 1 2 3 4 > a
$ echo 2 3 4 5 > b
$ echo 1 4 5 > c

A6:

各ファイルの「:」以降をAWKのsystem関数に喰わせる

$ grep -r . | tr ' ' '+' | awk -F: '{printf $1":";system(sprintf("echo %s | bc",$2))}' | grep ':10$'

というTwitter上における解答例の他には、grepを一切使わない

$ awk '{sum=$1+$2+$3+$4;if(sum==10){print FILENAME":"sum}}' *

という(今回のテーマからいえば邪道な)解法もあります。 なお、個人的には後者のほうが好みではありますが。

Q7: psコマンドの行、親プロセスの行、親の親のプロセスの行を表示

この問題もQ4と並ぶ難問でした (そもそも出題の意味の把握に苦労しました)。

A7:

……とはいえ、以下の当日における解答例でも間違いではないようです (少々表示が五月蝿いですが)。

$ ps axjf | grep -B2 -w 'ps' | grep -vw 'grep'

「プロセスツリーを表示すれば何とかなるのでは」と考え、manで調べてみると

プロセスツリーを表示する:
  ps -ejH
  ps axjf

とありましたので後者を有り難く利用した次第です。

Q8: seqとfactor以降はgrep縛りで「素数の一つ前の数で、かつ10以上の数」を列挙

$ seq 10 1000 | factor | ...(grepだけ)

A8:

シェル芸界隈ではもはやおなじみのコマンド、factorの出力フォーマットと、 grepの-Bオプション(直後の「1」が行数指定)で「マッチした行の1行前も出力する」 ことを知っていれば比較的易しい問題だと思われます。

$ seq 10 1000 | factor | grep -B1 '^[0-9]\+: [0-9]\{2,\}$' | grep '[0-9] [0-9]\+$' | grep -o '^[0-9]\+'

別解の

$ seq 10 1000 | factor | grep -B1 '^[0-9]\+: [0-9]\+$' | grep '^[0-9]\+: [0-9]\+ ' | grep -o '^[0-9]\+'

も同じようなものです。


以上、解答例と解説でした(良い復習にはなりましたが疲れた)。

Written on February 13, 2015