シェル芸スパルタン演習(第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
usp Personal Tukubai 20160402 ( Open usp Tukubaiで代用可能)
シェル芸演習
問題文および模範解答は、以下の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(現在絶賛放置中)もあるのだけれど…
→ 今回は数をこなしたかったので、勉強会の過去問を利用することにした。