2016年10月9日

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

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

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


実行環境

  • 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

  • nkf 2.1.4


シェル芸演習

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

問1

[文字化けしたファイルの削除]

次のように、空ファイル abc, DEFG と、文字化けした空ファイルを作ってください。

$ touch abc DEFG
$ echo ほげ | nkf -s | xargs touch

文字化けしたファイルを消してください。ただし、日本語の入力は禁止です。

ファイル名が文字化けしたファイルを削除する問題。
文字化けファイル名を指定するのは困難。
そこで、発想を変えて、全ファイルから文字化けしていないファイルを除外することにする。
lsでファイルをリストアップし、そこからsedで文字化けしていないファイルを除外した後、xargsを利用してrmコマンドを実行する。

01 #!/bin/sh
02
03 ls                           |
04 sed '/^[[:print:]].*$/d'     |
05 xargs rm

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

ls | sed '/^[[:print:]].*$/d' | xargs rm

問2

[計算]

以下のファイル中の数字を全部足し算してください。

$ cat num
1
2 3 4
5 6
7 8 9 10

ファイルの中身の数値を全て足し合わせる問題。
1列、もしくは1行に整形した後、加算記号をつけてbcに放り込むなり、awk等で足し合わせるなり。

01 #!/bin/sh
02
03 cat num        |
04 xargs          |
05 tr ' ' '+'     |
06 bc

Tukubai版。

01 #!/bin/sh
02
03 cat num     |
04 yarr        |
05 xargs plus

Tukubai版、その2。

01 #!/bin/sh
02
03 plus $(cat num |yarr)

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

cat num | xargs | tr ' ' '+' | bc
cat num | yarr | xargs plus
plus $(cat num |yarr)

問3

[条件でデータを取り出し]

次のhogeファイルから、aとbについてそれぞれ一番大きな数を求めましょう。

$ cat hoge
a 12
a 13
b 13
a 432
b 111
b 43

キー毎に最大値を求める問題。
sortのフィールド指定オプションを利用し、キー毎にソートした後、最大値を取り出す。
キー毎の最大値を取り出すには、uniqのフィールドスキップオプションを使うと楽ちん。
ただ、uniqのフィールドスキップは、レコードの前(左側)からしか指定できない。
そのため、下準備として、5行目のawkで、予めフィールドを入れ替えておく。

01 #!/bin/sh
02
03 cat hoge                 |
04 sort -k 1,1 -k 2,2nr     |
05 awk '{print $2,$1}'      |
06 uniq -f 1                |
07 awk '{print $2,$1}'

Tukubai版。
こういった少し複雑な集計処理は、Tukubaiを使うと非常に簡単。

01 #!/bin/sh
02
03 cat hoge                 |
04 sort -k 1,1 -k 2,2nr     |
05 juni key=1               |
06 awk '$1==1'              |
07 delf 1

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

cat hoge | sort -k 1,1 -k 2,2nr | awk '{print $2,$1}' | uniq -f 1 | awk '{print $2,$1}'
cat hoge | sort -k 1,1 -k 2,2nr | juni key=1 | awk '$1==1' | delf 1

問4

[計算]

以下のファイル中の数字を、キー(a, b)ごとに全部足し算してください。

$ cat num2
a 1
b 2 3 4
a 5 6
b 7 8 9 10

キー毎の総和を求める問題。
まず、全フィールドを、1レコード"キー 値"となるように展開する。
その後、awk等を用いて、キー毎に集計すればOK。

01 #!/bin/sh
02
03 cat num2                                 |
04 awk '{for(i=2;i<=NF;i++){print $1,$i}}'  |
04 awk '{sum[$1]+=$2} END{for(n in sum){print n,sum[n]}}'

Tukubai版。
考え方は、上の方法と同じ。
キー毎に値を集計するコマンド、smシリーズを利用すれば、非常にシンプル。

01 #!/bin/sh
02
03 cat num2        |
04 tarr num=1      |
05 sort -k 1,1     |
06 sm2 1 1 2 2

Tukubai版、その2。
上記2通りとは逆に、キー毎に横展開した後、キー毎に総和を求める。

01 #!/bin/sh
02
03 cat num2        |
04 sort -k 1,1     |
05 yarr num=1      |
06 ysum num=1      |
07 self 1 NF

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

cat num2 | awk '{for(i=2;i<=NF;i++){print $1,$i}}' | awk '{sum[$1]+=$2} END{for(n in sum){print n,sum[n]}}'
cat num2 | tarr num=1 | sort -k 1,1 | sm2 1 1 2 2
cat num2 | sort -k 1,1 | yarr num=1 | ysum num=1 | self 1 NF

問5

[日付と曜日]

以下のようにファイルを作って、何曜日が何日あるか集計してください。

$ seq 1990 2012 | awk '{print $1 "0101"}' > osyoga2
$ head -n 5 osyoga2
19900101
19910101
19920101
19930101
19940101

曜日毎に日数を集計する問題。
coreutilsだけだと、けっこう大変…​
各レコード毎に、曜日を出力するコマンドを生成・実行し、集計する。

01 #!/bin/sh
02
03 cat osyoga2                    |
04 sed 's/.*/date -d & +%A /'     |
05 sh                             |
06 sort                           |
07 uniq -c

Tukubai版。
Tukubaiは、mdateを始めとし、日時・曜日の扱いに優れたコマンドが多くあるので、非常に楽ちん。

01 #!/bin/sh
02
03 cat osyoga2     |
04 yobi 1 -j       |
05 delf 1          |
06 sort            |
07 count key=1

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

cat osyoga2 | sed 's/.*/date -d & +%A /' | sh  | sort | uniq -c
cat osyoga2 | yobi 1 -j | delf 1 | sort | count key=1

問6

[ダミーデータの作成]

seq 1 100 の出力をランダムに並び替えてください。

問題文の通り。
shufコマンドで一撃。

01 #!/bin/sh
02
03 seq 1 100     |
04 shuf

別解。GNU sort限定の方法。

01 #!/bin/sh
02
03 seq 1 100     |
04 sort -R

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

seq 1 100 | shuf
# GNU sort限定
seq 1 100 | sort -R

問7

[検索]

大文字小文字を区別しない場合、辞書ファイル /usr/share/dict/words から、重複する単語を検索してください。
(asciiコード以外の字がありますが、とりあえず気にしないでください。)

これも問題文の通り。
trで大文字を小文字に変換した後、sort, uniq -dするだけ。

01 #!/bin/sh
02
03 cat /usr/share/dict/words      |
04 tr '[:upper:]' '[:lower:]'     |
05 sort                           |
06 uniq -d

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

cat /usr/share/dict/words | tr '[:upper:]' '[:lower:]' | sort | uniq -d

問8

[ファイルの比較]

file2から、file1にない数字を抽出してください。

$ cat file1
1
3
4
6
9

cat file2
2
3
4
5
9

シンプルそうに見えて、実は少し難しい問題。
まず、joinコマンドで、file1,2に共通するレコードを出力する。
そして、joinの出力結果に対して、file2の内容を結合する。
この結合したデータの中から、重複していないレコードを抽出すれば、結果的にfile1に含まれていないレコードが得られる。
(処理毎に、ベン図を描いて考えてみると、分かりやすいかも。)

01 #!/bin/sh
02
03 join file1 file2     |
04 cat file2 -          |
05 sort                 |
06 uniq -u

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

join file1 file2 | cat file2 - | sort | uniq -u

問9

[形式変換]

まず、以下のファイルを作ってください。

$ cat game
1 表 2
1 裏 2
2 表 0
2 裏 1
3 表 3
3 裏 0
4 表 3
4 裏 0
5 表 0
5 裏 0

次に、作成したファイルを、以下のように整形してください。
   1 2 3 4 5
表 2 0 3 3 0
裏 2 1 0 0 0

野球(?)のスコア表を作る問題。
awkの連想配列を駆使して、ひたすら頑張る…​

01 #!/bin/sh
02
03 cat game         |
04 sort -k 1,1n     |
05 awk '{data[$1,$2]=$3; tb[$2]; last=$1} END{printf("  "); for(i=1;i<=last;i++){printf("%d ",i)}; print ""; for(n in tb){printf("%s ",n); for(i=1;i<=last;i++){printf("%d ", data[i,n])}; print ""}}'

awkであれだけ頑張っていたのが、Tukubaiのmapを使うと、こんなにシンプルに!

01 #!/bin/sh
02
03 cat game                |
04 sort -k 2,2 -k 1,1n     |
05 self 2 1 3              |
06 map num=1

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

cat game | sort -k 1,1n | awk '{data[$1,$2]=$3; tb[$2]; last=$1} END{printf("  "); for(i=1;i<=last;i++){printf("%d ",i)}; print ""; for(n in tb){printf("%s ",n); for(i=1;i<=last;i++){printf("%d ", data[i,n])}; print ""}}'
cat game | sort -k 2,2 -k 1,1n | self 2 1 3 | map num=1

問10

[ファイルの結合]

file1、file2から、下の出力を得てください。

$ cat file1
001 うそ
002 笑止
003 矢追純

$ cat file2
001 800
002 10000000
003 1


001 うそ 800
002 笑止 10000000
003 矢追純 1

SQLでいう、JOINをおこなう問題。
…​SQLと同じく、joinコマンドで一発w

01 #!/bin/sh
02
03 join file1 file2

joinだけだと詰まらないので、頑張ってやってみた。
awkの連想配列を使い、同じキーを持つレコードを結合していく。

01 #!/bin/sh
02
03 cat file1 file2                                                     |
04 awk '{arr[$1]=arr[$1]$2" "} END{for(n in arr){print n,arr[n]}}'     |
05 sort -k 1,1n

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

join file1 file2
cat file1 file2 | awk '{arr[$1]=arr[$1]$2" "} END{for(n in arr){print n,arr[n]}}' | sort -k 1,1n

雑記

  • 第2回目という事もあるのか、まだまだ優しい問題が多い印象。
    → ただ、問題9は、Tukubai無しだと、ちょっと難しかったかも。

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