シェル芸スパルタン演習(第05回シェル芸勉強会編)
ここ最近、Unixシェルに触る機会が減っており、CLIを使う能力の低下を感じていた。
また、近日中に、第25回シェル芸勉強会の開催が予定されており、参加するにあたり予習をしたいとも考えていた。
そこで、過去に開催されたシェル芸勉強会の問題を活用し、シェル芸力をつける演習をおこなうことにした。
今は、問題として、第05回シェル芸勉強会の設問を用いた。
実行環境
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
curl 7.50.3
usp Personal Tukubai 20160402 ( Open usp Tukubaiで代用可能)
シェル芸演習
問題文および模範解答は、以下のURLから。
http://www.slideshare.net/ryuichiueda/20130622-23348634
問1
あるディレクトリで適当にファイルへのシンボリックリンクを作り、リンク先のファイルをコピーして実体のあるファイルに置き換えてください。
シンボリックリンクを実体ファイルに置き換える問題。
ls -lの出力から実体ファイルパスを取り出し、リンクを削除&&実体ファイルをコピーするコマンド列を生成し、実行する。
01 #!/bin/sh
02
03 ls -l |
04 sed '1d' |
05 awk '{print $9,$11}' |
06 awk '{print "rm",$1,"&&","cp",$2,"./"}' |
07 sh
以下、コピペ実行用のワンライナーコード。
ls -l | sed '1d' | awk '{print $9,$11}' | awk '{print "rm",$1,"&&","cp",$2,"./"}' | sh
問2
/etc/hostsと/etc/resolv.confをつなげて、以下のように各行に元のファイル名がついた一つのファイルを作ってください。 /etc/hosts ## /etc/hosts # Host Database /etc/hosts # (略) /etc/resolv.conf # /etc/resolv.conf # Mac OS X Notice /etc/resolv.conf # (略)
複数のファイルの中身を、元のファイル名込みで連結する問題。
awkの特殊変数FILENAMEと、リダイレクトを使えば、簡単。
01 #!/bin/sh
02
03 awk '{print FILENAME,$0}' /etc/hosts /etc/resolv.conf > file
以下、コピペ実行用のワンライナーコード。
awk '{print FILENAME,$0}' /etc/hosts /etc/resolv.conf > file
問3
問題2で作ったファイルを適当なディレクトリの下に復元してください。
問題2の逆版。
ちょっとややこしいけれど、各レコード(ファイル名)に対応したechoコマンドを生成し、リダイレクトで書き込んでいく。
01 #!/bin/sh
02
03 cat file |
04 sed 's;^/.*/;;' |
05 awk 'fname=$1{$1=""; sub(/^ /, "", $0); print "echo '\''"$0"'\''",">> ./"fname}' |
06 sh
以下、コピペ実行用のワンライナーコード。
cat file | sed 's;^/.*/;;' | awk 'fname=$1{$1=""; sub(/^ /, "", $0); print "echo '\''"$0"'\''",">> ./"fname}' | sh
問4
a, bそれぞれについて、一番下にある行を取り出してください。 $ cat data2 a 14 a 5 b 12 b 3 a 20 b 1 b 2
キー毎に、一番最後に出現するレコードを取り出す問題。
行番号を付加して、キー毎のレコードの順序を保持した状態で、ソートするのがポイント。
01 #!/bin/sh
02
03 cat data2 |
04 awk '{print NR,$2,$1}' |
05 sort -k 3,3 -k 1,1nr |
06 uniq -f 2 |
07 awk '{print $3,$2}'
Tukubai版。キー毎に一番最後の出現するレコードを取り出すコマンド、getlastを使う。
大まかな流れは、上の方法と同じ。
01 #!/bin/sh
02
03 cat data2 |
04 juni |
05 sort -k 2,2 -k 1,1 |
06 getlast 2 2 |
07 delf 1
以下、コピペ実行用のワンライナーコード。
cat data2 | awk '{print NR,$2,$1}' | sort -k 3,3 -k 1,1nr | uniq -f 2 | awk '{print $3,$2}'
cat data2 | juni | sort -k 2,2 -k 1,1 | getlast 2 2 | delf 1
問5
下の図のように魔法陣を作って、魔法陣になっているかどうか確認してください。 $ cat data1 2 9 4 7 5 3 6 1 8
問題文の通り。
…面倒くさいので、Web上から取得することにするw
01 #!/bin/sh
02
03 curl -s 'https://ja.wikipedia.org/wiki/%E9%AD%94%E6%96%B9%E9%99%A3' |
04 grep -A 14 'サトゥルヌス魔方陣' |
05 sed '1,/caption/d' |
06 sed 's;</\{0,1\}td[^<]*>;;g' |
07 tr -d '\n' |
08 sed 's/<tr>//g; s;</tr>;\n;g' |
09 awk NF=NF FS=
ちゃんと魔法陣になっているかどうかの確認。
縦・横・斜め*2について、和を求める。
01 #!/bin/sh
02
03 sh cmd_19345.sh |
04 awk '{for(i=1;i<=NF;i++){r[NR]+=$i; c[i]+=$i; arr[NR,i]=$i}} END{for(n in r){print r[n]}; for(n in c){print c[n]}; for(i=1;i<=3;i++){lr+=arr[i,i]; rl+=arr[3-(i-1),3-(i-1)]}; print lr"\n"rl}'
以下、コピペ実行用のワンライナーコード。
curl -s 'https://ja.wikipedia.org/wiki/%E9%AD%94%E6%96%B9%E9%99%A3' | grep -A 14 'サトゥルヌス魔方陣' | sed '1,/caption/d' | sed 's;</\{0,1\}td[^<]*>;;g' | tr -d '\n' | sed 's/<tr>//g; s;</tr>;\n;g' | awk NF=NF FS=
確認。
sh cmd_19345.sh | awk '{for(i=1;i<=NF;i++){r[NR]+=$i; c[i]+=$i; arr[NR,i]=$i}} END{for(n in r){print r[n]}; for(n in c){print c[n]}; for(i=1;i<=3;i++){lr+=arr[i,i]; rl+=arr[3-(i-1),3-(i-1)]}; print lr"\n"rl}'
問6
/usr/share/dict/words 等辞書ファイルから、大文字で始まる単語、小文字で始まる単語の数を数えてください。
awkのパターンマッチと連想配列を使えば、楽勝。
01 #!/bin/sh
02
03 cat /usr/share/dict/words |
04 awk '/^[A-Z]/{u++} /^[a-z]/{l++} END{print "[A-Z]",u"\n[a-z]",l}'
以下、コピペ実行用のワンライナーコード。
cat /usr/share/dict/words | awk '/^[A-Z]/{u++} /^[a-z]/{l++} END{print "[A-Z]",u"\n[a-z]",l}'
問7
$ echo ダァシエリイェス して、ワンライナーで字を反転してください。 (「ダ」がめんどくさい) スェイリエシァダ
問題文の通り、濁点がある「ダ」が面倒臭い。
grep -oで列行変換した後、「ダ」がある1・2行目のみを、sedを使って結合する。
あとは、tacで反転、xargsで行列変換すればOK。
01 #!/bin/sh
02
03 echo ダァシエリイェス |
04 grep -o . |
05 sed '1{h;d}; 2G' |
06 tac |
07 xargs |
08 tr -d ' '
Tukubai版。
Tukubaiには、全角半角を相互変換するコマンドがあるので、それを使えば楽ちん。
いったん全角文字にし、その後反転し、最後に半角に戻すだけ。
01 #!/bin/sh
02
03 echo ダァシエリイェス |
04 zen |
05 rev |
06 han
以下、コピペ実行用のワンライナーコード。
echo ダァシエリイェス | grep -o . | sed '1{h;d}; 2G' | tac | xargs | tr -d ' '
echo ダァシエリイェス | zen | rev | han
問8
以下のような図形を出力してください。 a aaa aaaaa aaaaaaa aaaaaaa aaaaa aaa a
難易度が高い問題。
要求されている図形は、鏡合わせの形になっているため、その性質を利用する。
ちなみに、sedで鏡合わせに出力するスクリプトは、以下の通り。
例:
— ginjiro (@gin_135) 2016年10月5日
$ echo -e 'a\naa\naaa' | sed 'p; 1!G; $!h; $!d'
a
aa
aaa
aaa
aa
a
$ echo -e 'a\naa\naaa' | sed '$!p; 1!G; $!h; $!d'
a
aa
aaa
aa
a
Tukubai版。
Tukubaiにある桁合わせコマンド、ketaを使うと、かなり楽ちん。
01 #!/bin/sh
02
03 yes a |
04 head -n 4 |
05 awk '{for(i=1;i<=NR;i++){printf("%s",$0)}; print ""}' |
06 keta |
07 awk '{print $0$1}' |
08 sed 's/.//5' |
09 sed 'p; 1!G; $!h; $!d'
Tukubai版無し版。途中でtacを使って反転する必要があるので、ちょっと面倒臭い。
01 #!/bin/sh
02
03 yes a |
04 head -n 4 |
05 awk '{for(i=1;i<=NR;i++){printf("%s",$0)}; print ""}' |
06 tac |
07 awk 'NR==1{len=length($0); print} NR!=1{for(i=1;i<=len-length($0);i++){printf " "}; print}' |
08 tac |
09 awk '{print $0$1}' |
10 sed 's/.//5' |
11 sed 'p; 1!G; $!h; $!d'
以下、コピペ実行用のワンライナーコード。
yes a | head -n 4 | awk '{for(i=1;i<=NR;i++){printf("%s",$0)}; print ""}' | keta | awk '{print $0$1}' | sed 's/.//5' | sed 'p; 1!G; $!h; $!d'
yes a | head -n 4 | awk '{for(i=1;i<=NR;i++){printf("%s",$0)}; print ""}' | tac | awk 'NR==1{len=length($0); print} NR!=1{for(i=1;i<=len-length($0);i++){printf " "}; print}' | tac | awk '{print $0$1}' | sed 's/.//5' | sed 'p; 1!G; $!h; $!d'
問9
例のように縦に一つずつランダムにずらして字を出力してください。 o o o o o o o o o o
これもなかなか難しい…
0, 1, 2のいずれかの数字をランダムで出力し、0なら左へ、1ならそのまま、2なら右へずらしていく。
01 #!/bin/sh
02
03 yes |
04 head -n 10 |
05 awk 'BEGIN{srand()} {print int(rand()*3)}' |
06 awk 'BEGIN{co=5} {co+=($0==0 ? -1 : ($0==2 ? 1 : 0)); for(i=1;i<=10;i++){printf("%s", i==co ? "o" : " ")}; print ""}'
以下、コピペ実行用のワンライナーコード。
yes | head -n 10 | awk 'BEGIN{srand()} {print int(rand()*3)}' | awk 'BEGIN{co=5} {co+=($0==0 ? -1 : ($0==2 ? 1 : 0)); for(i=1;i<=10;i++){printf("%s", i==co ? "o" : " ")}; print ""}'
問10
/usr/share/dict/words から抽出した単語で、 this is a pen という出力を得てください。 ただし、数字は一切使わないこと シェル変数、ファイルにも出力しないこと for, while, && 禁止
またまた難しい問題… 数字禁止の制約が無ければ、簡単なのだけれど。
まず、grepでthis, is, a, penの4単語を抜き出す。
ここで、数字は禁止=awkのフィールド指定等が使えないので、sedで頑張って入れ替えていく。
01 #!/bin/sh
02
03 cat /usr/share/dict/words |
04 grep -E '^(this|is|a|pen)$' |
05 sed '/^a$/c*this' |
06 sed '/^pen$/c*a' |
07 sed '/^this$/c*pen' |
08 tr -d '*' |
09 xargs
以下、コピペ実行用のワンライナーコード。
cat /usr/share/dict/words | grep -E '^(this|is|a|pen)$' | sed '/^a$/c*this' | sed '/^pen$/c*a' | sed '/^this$/c*pen' | tr -d '*' | xargs
雑記
前半は比較的簡単なのだけれど… 後半3問は、かなり難しかった。
開催5回目にして、早くも変態な方向性に突入しているような…w