全力で怠けたい

怠けるために全力を尽くしたいブログ。

シェル力が圧倒的に足りてないのでシェル力を養うために第13回シェル芸勉強会の問題に挑戦してみた。

シェル力が圧倒的に足りてないのでシェル力を養うために第13回シェル芸勉強会の問題に挑戦してみた。

問題はすごく工夫してあってパズル感覚で解けて楽しいしシェル力を養うのにも役立ちそう。

自分が使ってる zsh のバージョン。

$ zsh --version
zsh 5.8 (x86_64-apple-darwin19.6.0)

はじめに

自分はシェル力が圧倒的に足りてないので勉強のためにシェル芸勉強会の問題に挑戦してみた。

一応自分がシェルを使う用途を書いておくと普段の仕事は Mac を使ってやっててちょっとしたことをやるのにちょろっとシェルを使う程度。

ちなみにシェル芸ってなに? ってところは WikipediaUSP 友の会 から引用しておく。

シェル芸とは、主にUNIXオペレーティングシステムにおいて「マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理を CLI端末へのコマンド入力一撃で終わらせること」(USP友の会会長・上田隆一による定義[9])である。この技術を持つ人物を指すシェル芸人という呼び方も存在する[10]。

古典的には、UNIX系OSの各種CUIシェル上で単一または複数のコマンドを一行のコマンドラインでをつなぎ合わせて処理をおこなう、いわゆる「ワンライナー」系の手順にあたる。

第13回シェル芸勉強会の問題に挑戦してみた

だいぶ前だけど第12回シェル芸勉強会の問題に挑戦してみたので今回は 第13回シェル芸勉強会 の問題に挑戦してみた。

Q1

Question

次のようにShift JISのファイルを作り、Shift JISで「きく」と書いてあるファイルを探すワンライナーを考えてください。(答えは「b」ですね。)

uedambp:q1 ueda$ echo あいうえお | nkf -xLws > a
uedambp:q1 ueda$ echo かきくけこ | nkf -xLws > b
uedambp:q1 ueda$ echo さしすせそ | nkf -xLws > c

Answer

「きく」を検索するファイルは Shift_JISエンコーディングしているのを UTF-8 に変換して grep してみた。

$ ls | while read f; do cat $f | nkf -Sw | grep 'きく' >/dev/null && echo $f; done

grep の出力は /dev/null にリダイレクトして捨てるようにしてるけど、解答例を見たら grep-q オプションを使ってた。 便利そうなので覚えておく。

ls | while read f; do cat $f | nkf -Sw | grep -q 'きく' && echo $f; done
b

Q2

Question

次のようにディレクトリa,b,c,dに1,2,...,9というファイルがあります。各ディレクトリ内のファイル数をワンライナーで数えてください。

uedambp:q2 ueda$ tree
.
├── a
│   ├── 1
│   ├── 2
│   └── 3
├── b
│   ├── 4
│   └── 5
├── c
└── d
 ├── 6
 ├── 7
 ├── 8
 └── 9

Answer

$ ls | while read f; do echo "$f $(ls $f | xargs -n1 | wc -l)"; done
a/        3
b/        2
c/        0
d/        4

Q3

Question

今度は次のような配置でファイル1,2,...,9が置かれているときに、ワンライナーでa、cの下のファイルの総数をカウントしてください(ディレクトリを除く)。つまりaなら5個、cなら4個が正解です。

uedambp:q3 ueda$ tree
.
├── a
│   ├── 1
│   ├── 2
│   ├── 3
│   └── b
│   ├── 4
│   └── 5
└── c
 └── d
 ├── 6
 ├── 7
 ├── 8
 └── 9

Answer

find コマンドを -type f オプションを指定してファイルだけ検索して uniq -c コマンドでディレクトリごとのファイルの数を数える感じ。

$ ls | while read f; do find $f -type f | awk -F/ '{print $1}' | uniq -c; done
   5 a
   4 c

こちらは解答例。自分は ls コマンドを余分に実行しちゃってるけど解答例のほうは無駄がなくて分かりやすくてタイプ数も少ない。

$ find . -type f | awk -F/ '{print $2}' | uniq -c
   5 a
   4 c

Q4

Question

まず、次のように8桁日付のファイルを作ります。 曜日別にディレクトリを作り、その中に当該するファイルを放り込んでください。

uedambp:q4 ueda$ seq -w 1 31 | xargs -I@ touch 201401@
uedambp:q4 ueda$ ls
20140101 20140107 20140113 20140119 20140125 20140131
20140102 20140108 20140114 20140120 20140126
20140103 20140109 20140115 20140121 20140127
20140104 20140110 20140116 20140122 20140128
20140105 20140111 20140117 20140123 20140129
20140106 20140112 20140118 20140124 20140130

Answer

$ ls | while read f; do wd=$(echo $f | LANG=C gdate -f - '+%a'); mkdir $wd; mv $f $wd; done

Q5

Question

以下のようにa,b,cというディレクトリを作り、その下に「{a,b,c}数字」というファイルを作ります。ファイル名の1文字目とディレクトリ名が一致するようにファイルを移動してください。

uedambp:q5 ueda$ tree
.
├── a
│   ├── a01
│   └── b01
├── b
│   ├── a02
│   ├── a03
│   └── c01
└── c
 └── a04

Answer

$ find . -type f | awk -F/ '{print substr($3,1,1), $0}' | while read d f; do mv $f $d; done
$ tree
.
├── a
│   ├── a01
│   ├── a02
│   ├── a03
│   └── a04
├── b
│   └── b01
└── c
    └── c01

Q6

Answer

次のようにディレクトリa, b, cの下に、8桁日付のファイルをいくつか置きます。 各ディレクトリの最新日付のファイルをカレントディレクトリ(a,b,cのあるディレクトリ)にコピーしてください。各ディレクトリの最新ファイルの日付はそれぞれ違い、コピーの際に衝突しないこととします。

uedambp:q6 ueda$ tree
.
├── a
│   ├── 20130120
│   ├── 20140901
│   └── 20141021
├── b
│   ├── 20131011
│   └── 20140202
└── c
 ├── 20110202
 ├── 20130224
 └── 20141224

Answer

ls コマンドはファイル名をソートしてから出力するので各ディレクトリを ls して最後の出力が最新日付のファイル。 tail コマンドで ls コマンドの最後の出力だけを取得してコピーする。

$ ls | while read f; do ls $f | tail -n 1 | xargs -I{} cp "$f{}" .; done
$ ls
20140202  20141021  20141224  a/        b/        c/

Q7

Question

Q6について、適当にファイルをtouchします。今度はタイムスタンプが最新のファイルを、a, b, cそれぞれからカレントディレクトリにコピーしてください。コピーの際にタイムスタンプを変えない事。

Answer

適当にファイルを touch する。

$ touch a/20140901
$ touch b/20131011
$ touch c/20141224

ls コマンドの -t オプションと cp コマンドの -p オプションを使う。 自分はどちらのコマンドもたしかいい感じのオプションがある気がするけどよく分からないので manual を見ながらやった。

$ ls | while read f; do ls -t $f | head -n 1 | xargs -I{} cp -p "$f{}" .; done

Q8

次のように5個ファイルを作ります。file1をfile2, file2をfile3, file3をfile4, file4をfile5, file5をfile1にmvしてください。

uedambp:q8 ueda$ for i in 1 2 3 4 5 ; do echo $i > file$i ; done
uedambp:q8 ueda$ head *
==> file1 <==
1

==> file2 <==
2

==> file3 <==
3

==> file4 <==
4

==> file5 <==
5

Answer

コピー元とコピー先のファイルが循環するのがポイント。

$ ls | sort -r | awk 'BEGIN{d="tmp"}{print $1,d;d=$1}END{print "tmp",d}' | while read s d; do mv $s $d; done

$ head *
==> file1 <==
5

==> file2 <==
1

==> file3 <==
2

==> file4 <==
3

==> file5 <==
4

参考サイト