2016年10月8日

シェル芸スパルタン演習(第01回シェル芸勉強会編)

ここ最近、Unixシェルに触る機会が減っており、CLIを使う能力の低下を感じていた。
また、近日中に、第25回シェル芸勉強会の開催が予定されており、参加するにあたり予習をしたいとも考えていた。
そこで、過去に開催されたシェル芸勉強会の問題を活用し、シェル芸力をつける演習をおこなうことにした。

今回は、問題として、第01回シェル芸勉強会の設問を用いた。


実行環境

  • Arch Linux 4.8.4-1-ARCH

  • GNU bash 4.3.46

  • GNU coreutils 8.25-2

  • GNU diffutils 3.5-1

  • GNU findutils 4.6.0-2

  • util-linux 2.28.2-1

  • grep (GNU grep) 2.26

  • GNU bc 1.06.95

  • sed (GNU sed) 4.2.2

  • gawk 4.1.4


シェル芸演習

問題文および模範解答は、以下のURLから。
http://www.slideshare.net/ryuichiueda/20121027-hbstudy38

問1

[ユーザの抽出(初級)]

/etc/passwd から、ユーザ名を抽出したリストを作ってください。

初歩的な問題。
awkでFSに':'を指定して、ユーザ名だけを出力する。
その後、sortとuniqで重複レコードを除去するだけ。

01 #!/bin/sh
02
03 cat /etc/passwd     |
04 awk -F: '$0=$1'     |
05 sort                |
06 uniq

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

cat /etc/passwd | awk -F: '$0=$1' | sort | uniq

問2

[ユーザの抽出2(中級)]

/etc/passwd から、次を調べてください。
- ログインシェルがbashとshのユーザ、どちらが多い?
(結果を判別できるなら、出力形式は問わない。)

問1を少し発展させた問題。
問1と同様に、awkでFSに':'を指定して、ログインシェルのみを出力する。
その後、grepでsh or bashのみを抽出し、sort, uniq -cでレコード数をカウントして完了。

01 #!/bin/sh
02
03 cat /etc/passwd          |
04 awk -F: '$0=$7'          |
05 grep -E '/(sh|bash)'     |
06 sort                     |
07 uniq -c

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

cat /etc/passwd | awk -F: '$0=$7' | grep -E '/(sh|bash)' | sort | uniq -c

問3

[ファイルの一括変換(中級)]

/etc の下にある全てのbashスクリプト(先頭行が #!/bin/bash で始まるもの)について、以下の操作をおこなってください。
- 該当するファイルを、ディレクトリ ~/hoge にコピーする
- 該当するファイルの先頭行を、 #!/usr/local/bin/bash に変更する

自分の環境には/etc以下にbashスクリプトは存在しなかったので、代わりに/usr/binを用いることにした。
まず、grep -rで、該当するファイル名を抽出する。
次に、awkを使って、grep -rの出力を整形する。
そして、xargsを利用し、cpコマンドでhoge以下にコピー、コピー後にsed -iでシェバン部分を上書きすればOK。

01 #!/bin/sh
02
03 grep -r '#!/bin/bash' /usr/bin 2>/dev/null     |
04 awk -F: '$0=$1'                                |
05 xargs -I@ cp @ ./hoge && sed -i '1s;#!;&/usr/local;' ./hoge/*

ファイルをコピー→sed -iでシェバンを書き換えするよりも、リダイレクトでファイルを生成しつつ、シェバンを書き換える方が、エレガントなやり方だったかも。

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

grep -r '#!/bin/bash' /usr/bin 2>/dev/null | awk -F: '$0=$1' | xargs -I@ cp @ ./hoge && sed -i '1s;#!;&/usr/local;' ./hoge/*

問4

[集計(中級)]

まず、次のようなファイルを作成してください。
(補足: 0~109の範囲において、ランダムな数値を任意の個数分発生させる)

$ while :; do echo $((RANDOM % 110)); done > ages
^C
$ cat ages | head
13
5
25
92
37
57
97
4
78
51

このファイルを、以下のように集計してください。
(補足: 生成した数値に対して、0から10区切りのグループ毎に、出現回数を集計する)

$ cat ans ## 集計結果はあくまで一例、数値は人によって異なる
0~9 8248
10~19 8248
20~29 8280
30~39 8128
40~49 8228
50~59 8040
60~69 8300
70~79 8375
80~89 8182
90~99 8082
100~109 8109

データをグループ毎に分けて集計する問題。
まず、集計する前準備として、データをグループ毎に分ける。
今回集計するグループは、10毎に分けられているため、10で割った値(剰余は切り捨て)を見ればOK。
よって、単純に末尾の数字を除去すれば、グループ毎に分けられる。
そこで、sedを使って、末尾数字を削除する。
ただし、数値が1桁の値は消失してしまうため、5行目のsedで'0’に置換しておく。
後は、sort, uniq -cで、グループ毎にレコード数を数えた後、awkで出力を整形するだけ。

まず、ファイルagesの作成。

01 #!/bin/sh
02
03 seq 0 109                                  |
04 awk '{for(i=1;i<=1000;i++){print $0}}'     |
05 shuf                                       |
06 head -n 10000 > ages

そして、agesの集計。

01 #!/bin/sh
02
03 cat ages          |
04 sed 's/.$//'      |
05 sed 's/^$/0/'     |
06 sort -n           |
07 uniq -c           |
08 awk '{print $2*10"~"$2*10+9, $1}'

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

データの生成。

seq 0 109 | awk '{for(i=1;i<=1000;i++){print $0}}' | shuf | head -n 10000 > ages

データの集計。

cat ages | sed 's/.$//' | sed 's/^$/0/' | sort -n | uniq -c | awk '{print $2*10"~"$2*10+9, $1}'

問5

[FizzBuzz(上級)]

1, 2, 3, 4, 5, ...と順に数えていって、
- 3の倍数だったら、数字の代わりにFizz
- 5の倍数だったら、数字の代わりにBuzz
- 15の倍数だったら、数字の代わりにFizzBuzz
を出力してください。

お馴染みのFizzBuzz問題。
awkのパターンでオーソドックスに求めたり、三項演算子ったり。
シンプルな問題故に、熟練シェル芸人さん方が変態解答をしているので、探してみると面白いかも…​!?

パターンで求める。

01 #!/bin/sh
02
03 seq 1 100     |
04 awk '!($0%15){print "FizzBuzz"; next} !($0%5){print "Buzz"; next} !($0%3){print "Fizz"; next}; 1'

三項演算子で求める。

01 #!/bin/sh
02
03 seq 1 100     |
04 awk '{print ($0%15==0 ? "FizzBuzz" : ($0%5==0 ? "Buzz" : ($0%3==0 ? "Fizz" : $0)))}'

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

seq 1 100 | awk '!($0%15){print "FizzBuzz"; next} !($0%5){print "Buzz"; next} !($0%3){print "Fizz"; next}; 1'
seq 1 100 | awk '{print ($0%15==0 ? "FizzBuzz" : ($0%5==0 ? "Buzz" : ($0%3==0 ? "Fizz" : $0)))}'

問6

[日付の計算(中級)]

1978年2月16日は、 2012年10月27日の何日前でしょう?

日付の差分を求める問題。
coreutilsのdateだけだと、けっこう面倒臭い…​
日付(YYYYMMDD)をUnix時間に変換し、その差分を求めた後、Unix時間から日付(YYYYMMDD)に復元するという流れ。
逆に、Tukubaiの高性能な日付処理コマンド、mdateを使えば、一瞬で終わったり。
Tukubai以外にも、 dateutils[http://www.fresse.org/dateutils/] というツールに含まれるdatediffで、代用可能。
coreutilsだけだと、日付関連の処理は大変なんだよね…​

dateで頑張る。

01 #!/bin/sh
02
03 echo $(date -d 20121027 +%s) '-' $(date -d 19780216 +%s)     |
04 bc                                                           |
05 xargs -I@ echo @ '/ (24*60*60)'                              |
06 bc

Tukubai版。

01 #!/bin/sh
02
03 mdate 19780216 20121027

datediff(dateutils)で。

01 #!/bin/sh
02
03 datediff 1978-02-16 2012-10-27

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

echo $(date -d 20121027 +%s) '-' $(date -d 19780216 +%s) | bc | xargs -I@ echo @ '/ (24*60*60)' | bc
mdate 19780216 20121027
datediff 1978-02-16 2012-10-27

問7

[リストにないものを探す(中級)]

まず以下のように、1から10の数字が書いてあり、そのうち一つの数が欠けているファイルを作りましょう。

$ seq 1 10 | awk '{print rand(), $1}' | sort | head -n 9 | awk '{print $2}' > nums
$ cat nums
10
7
8
2
9
3
5
4
1

欠けた数字を端末に表示してください。(diff, 目grep禁止)

1から10までの数字のうち、欠けている1数字を求める問題。
単純な問題だけれど、2つのリストを比較する場面は多くあるので、有用性の高い問題だとも思う。

数値と行数を比較して求める方法。

01 #!/bin/sh
02
03 cat nums     |
04 sort -n      |
05 awk 'NR!=$0{print NR; exit}'

diffを利用した方法。

01 #!/bin/bash
02
03 cat nums               |
04 sort -n                |
05 diff - <(seq 1 10)     |
06 grep '>'               |
07 sed 's/> //'

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

cat nums | sort -n | awk 'NR!=$0{print NR; exit}'
cat nums | sort -n | diff - <(seq 1 10) | grep '>' | sed 's/> //'

問8

[CPU使用率(上級)]

topの出力から、どのユーザが何%CPUを使用しているか集計してください。

参考:topの出力をパイプに渡す方法
$ top -b -n 1 | command ...

問題文の通り、ユーザ毎にCPU使用率を集計する問題。
sedで余分なヘッダ部分を除去、awkで必要なフィールドを抽出したのち、集計する。
これも、なかなか実践的な問題だと思う。

awkで頑張る。

01 #!/bin/sh
02
03 top -b -n 1             |
04 sed '1,10d'             |
05 awk '{print $2,$7}'     |
06 awk '{user[$1]+=$2} END{for(s in user){print s,user[s]}}'

Tukubaiでシンプルに。

01 #!/bin/sh
02
03 top -b -n 1     |
04 sed '1,10d'     |
05 self 2 7        |
06 sort            |
07 sm2 1 1

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

top -b -n 1 | sed '1,10d' | awk '{print $2,$7}' | awk '{user[$1]+=$2} END{for(s in user){print s,user[s]}}'
top -b -n 1 | sed '1,10d' | self 2 7 | sort | sm2 1 1

問9

[横に並んだ数字のソート(上級)]

次のように、適当に数字が記入されたファイルを作ってください。

$ cat file
2 342 453 1 3 2 55
214 324 532 33
0 32 1 345

各行ごとに、数字を小さい順にソートしてください。

縦方向ではなく、レコード毎に横方向へソートする問題。
まず、各レコードに行番号を付加する。
そして、各フィールド毎に、"行番号 数値"の1レコードに展開する。
その状態で、行番号、数値の順にソートをおこなう。
最後に、1レコード目の行番号に基づいて、フィールドを復元すればOK。

awkで頑張る。

01 #!/bin/bash
02
03 cat file                                    |
04 awk '{for(i=1;i<=NF;i++){print NR,$i}}'     |
05 sort -k 1,1n -k 2,2n                        |
06 awk 'BEGIN{o1=1} {if(o1!=$1){print ""}; printf("%d ",$2); o1=$1} END{print ""}'

Tukubai版。
処理の内容としては、上の方法と同じ。
juniで行番号を付加した後、tarrで行番号をキーとして縦展開。
そして行番号、数値の順にソートした後、yarrで行番号をキーとして横展開すればOK。
最後に、必要無くなった行番号を除去する。

01 #!/bin/sh
02
03 cat file                 |
04 juni                     |
05 tarr num=1               |
06 sort -k 1,1n -k 2,2n     |
07 yarr num=1               |
08 delf 1

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

cat file | awk '{for(i=1;i<=NF;i++){print NR,$i}}' | sort -k 1,1n -k 2,2n | awk 'BEGIN{o1=1} {if(o1!=$1){print ""}; printf("%d ",$2); o1=$1} END{print ""}'
cat file | juni | tarr num=1 | sort -k 1,1n -k 2,2n | yarr num=1 | delf 1

問10

[対戦表を作る(上級)]

まず、次のようなファイルを作成してください。

$ cat teams
阪神
中日
広島

次に、以下の様なCSV形式の表を出力してください。

$ cat ans

,中日,広島,阪神
阪神,,,
中日,,,
広島,,,

問題文の通り、対戦表を作る問題。
対戦表を作るには、レコード・フィールドを共に1つ増やす必要がある。
そこで、まず3行目で、1レコード目(ヘッダ部分)にnullフィールドを追加し、更に全レコードを出力する。
その後、awkを使って、ヘッダ以外のレコードに、nullフィールドをして、データを整形する。

01 #!/bin/bash
02
03 cat <(cat teams |sed '1inull' |tr '\n' ' ' |xargs) <(cat teams) |
04 awk -v OFS="," 'NR==1{MNF=NF; sub(/null/, "", $1); print} NR!=1{printf("%s",$0); for(i=2;i<=MNF;i++){printf(",")}; print ""}'

Tukubai版。
超強力なmapコマンドのお陰で、非常にシンプルな解答になった。

01 #!/bin/sh
02
03 loopx teams teams          |
04 awk '{print $0,"_"}'       |
05 map num=1                  |
06 awk -v OFS="," '$1=$1'     |
07 sed 's/[*_]//g'

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

cat <(cat teams |sed '1inull' |tr '\n' ' ' |xargs) <(cat teams) | awk -v OFS="," 'NR==1{MNF=NF; sub(/null/, "", $1); print} NR!=1{printf("%s",$0); for(i=2;i<=MNF;i++){printf(",")}; print ""}'
loopx teams teams | awk '{print $0,"_"}' | map num=1 | awk -v OFS="," '$1=$1' | sed 's/[*_]//g'

雑記

  • 初回だけあって、流石に変態な問題は無かった。
    → あえて挙げるなら、問題10が少し難しかったかも?

  • シェル芸勉強用としては、Project Euler(現在絶賛放置中)もあるのだけれど…​
    → 今回は数をこなしたかったので、勉強会の過去問を利用することにした。

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