シェル力が圧倒的に足りてないのでシェル力を養うために第9回シェル芸勉強会の問題に挑戦してみた。
問題はすごく工夫してあってパズル感覚で解けて楽しいしシェル力を養うのにも役立ちそう。
自分が使ってる zsh のバージョン。
$ zsh --version zsh 5.8 (x86_64-apple-darwin19.6.0)
はじめに
自分はシェル力が圧倒的に足りてないので勉強のためにシェル芸勉強会の問題に挑戦してみた。
一応自分がシェルを使う用途を書いておくと普段の仕事は Mac を使ってやっててちょっとしたことをやるのにちょろっとシェルを使う程度。
ちなみにシェル芸ってなに? ってところは Wikipedia の USP 友の会 から引用しておく。
シェル芸とは、主にUNIX系オペレーティングシステムにおいて「マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理を CLI端末へのコマンド入力一撃で終わらせること」(USP友の会会長・上田隆一による定義[9])である。この技術を持つ人物を指すシェル芸人という呼び方も存在する[10]。
古典的には、UNIX系OSの各種CUIシェル上で単一または複数のコマンドを一行のコマンドラインでをつなぎ合わせて処理をおこなう、いわゆる「ワンライナー」系の手順にあたる。
第9回シェル芸勉強会の問題に挑戦してみた
少し前に第10回シェル芸勉強会の問題に挑戦してみたので今回は1つ前の第9回シェル芸勉強会の問題に挑戦してみた。
Q1
Question
まず、次のようにファイルを作ってください。
$ touch apple avocado banana cinnamon melon $ ls apple avocado banana cinnamon melon
「a,b,c,m」というディレクトリを作って、1文字目が対応するファイルをそれぞれのディレクトリに移動してください。
###こうなったらOK### $ ls * a: apple avocado b: banana c: cinnamon m: melon
Answer
cut -c1
で単語の1文字目を強引に抜き出してるけどあとで解答例を見たら ${f:0:1}
みたいに Bash のパラメーター展開を使っててスマートな感じだった。
$ ls | while read f; do d=$(echo $f | cut -c1); mkdir -p $d; mv $f $d; done $ ls * a: apple avocado b: banana c: cinnamon m: melon
Q2
Question
まず、次のように名前にスペースが入ったファイルを作ります。
$ touch "私は 蟹" "オシャレな 蟹" "足が 10本" $ ls -l total 0 -rw-r--r-- 1 ueda staff 0 2 14 11:22 私は 蟹 -rw-r--r-- 1 ueda staff 0 2 14 11:22 足が 10本 -rw-r--r-- 1 ueda staff 0 2 14 11:22 オシャレな 蟹
このままでは何かと扱いづらいので、間にアンダーバーを入れて次のように名前を変更してください。
$ ls -l total 0 -rw-r--r-- 1 ueda staff 0 2 14 11:25 私は_蟹 -rw-r--r-- 1 ueda staff 0 2 14 11:25 足が_10本 -rw-r--r-- 1 ueda staff 0 2 14 11:25 オシャレな_蟹
Answer
$ ls | while read f; do mv "$f" "$(echo $f | tr ' ' '_')"; done
Q3
Question
ディレクトリを適当に作って、20140101から20141231まで、日付に対応したファイルを作って下さい。各ファイルの中には各日付に対応するdateコマンドの出力を書き込んで下さい。
(ワンライナーが思いつかない場合は、とりあえず手作業でやってみてください。)
###こんな感じでどうぞ### uedambp:20140214USPSTUDY ueda$ ls -l | head total 1460 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140101 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140102 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140103 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140104 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140105 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140106 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140107 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140108 -rw-r--r-- 1 ueda staff 28 2 14 10:23 20140109 uedambp:20140214USPSTUDY ueda$ cat 20140101 水 1 1 00:00:00 JST 2014
Answer
Mac の date コマンドはつらみがあるので Virtual Box 上の Ubuntu 20.04.2 LTS で解いた。
これ date コマンドで日付を相対指定するのがスマートだと思うんだけど 20140101 から 20141231 まで数値のまま繰り返し処理して強引に日付を作ってみた。
$ seq 0 $(echo '20141231-20140101' | bc) | awk '{print 20140101+$1}' | while read d; do date -d $d && date -d $d > $d; done
Q4
Question
次のように4個ファイルを作って、a_ramenとa_curry、b_appleとb_tomatoのファイルの中身を入れ替えてください。
$ echo カレー > a_ramen $ echo ラーメン > a_curry $ echo トマト > b_apple $ echo リンゴ > b_tomato ###(余談)各ファイルと中身は次のようにgrepで確認できる### $ grep "" * a_curry:ラーメン a_ramen:カレー b_apple:トマト b_tomato:リンゴ
Answer
少しちょろっと考えたけど分からなかったので解答例を見た。 解答例を分解しながら少しずつ書いてみる。
grep "" *
でそれぞれのファイルとファイル内容の組み合わせを作って、
$ grep "" * a_curry:ラーメン a_ramen:カレー b_apple:トマト b_tomato:リンゴ
tr ':' ' '
で :
を削除して、
$ grep "" * | tr ':' ' ' a_curry ラーメン a_ramen カレー b_apple トマト b_tomato リンゴ
xargs -n 4
で a_*
のファイル同士の組み合わせを作って、
$ grep "" * | tr ':' ' ' | xargs -n 4 a_curry ラーメン a_ramen カレー b_apple トマト b_tomato リンゴ
awk '{print $1,$4,$3,$2}'
でファイルとファイル内容の組み合わせを入れ替えて、
$ grep "" * | tr ':' ' ' | xargs -n 4 | awk '{print $1,$4,$3,$2}' a_curry カレー a_ramen ラーメン b_apple リンゴ b_tomato トマト
awk '{print "echo",$2,">",$1}'
でコマンド文字列を作って、
$ grep "" * | tr ':' ' ' | xargs -n 4 | awk '{print $1,$4,$3,$2}' | xargs -n 2 | awk '{print "echo",$2,">",$1}' echo カレー > a_curry echo ラーメン > a_ramen echo リンゴ > b_apple echo トマト > b_tomato
コマンド文字列を sh
に流し込んで実行する。
$ grep "" * | tr ':' ' ' | xargs -n 4 | awk '{print $1,$4,$3,$2}' | xargs -n 2 | awk '{print "echo",$2,">",$1}' | sh $ grep "" * a_curry:カレー a_ramen:ラーメン b_apple:リンゴ b_tomato:トマト
Q5
Question
各月ごとにtar.gzファイルにしてください。
###こんな感じで### uedambp:20140214USPSTUDY ueda$ ls *.tar.gz 201401.tar.gz 201404.tar.gz 201407.tar.gz 201410.tar.gz 201402.tar.gz 201405.tar.gz 201408.tar.gz 201411.tar.gz 201403.tar.gz 201406.tar.gz 201409.tar.gz 201412.tar.gz
Answer
これ質問は元ネタのファイルはどこにあるのかまったく書いてないけど問題の流れから Q3 で作った日付のファイルをやってみた。
$ ls | cut -c1-6 | uniq | xargs -I{} sh -c "tar cvzf {}.tar.gz {}*"
Q6
Question
次のようなディレクトリ・ファイル操作を行って下さい。
###小問1: ディレクトリを作る### ~/a/a/a/.../a/a/ (aが百個) ###小問2: ファイルを作る### ~/a/a/a/.../a/a/b (aが百個、bはファイル) ###小問3: ~/a/a/a/.../a/a/の底に移動### uedambp:a ueda$ pwd /Users/ueda/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
Answer
小問1
$ seq 100 | sed 's/.*/a/' | tr '\n' '/' | xargs mkdir -p
小問2
$ seq 100 | sed 's/.*/a/' | tr '\n' '/' | xargs -I{} touch {}b
小問3
$ find . -name a | tail -n 1 | while read f; do cd $f; done
Q7
Question
先ほど作ったファイルbを、50番目のaディレクトリに移動して下さい。
↓うまくできたかどうかの確認方法
###~から50番目のaに移動### uedambp:~ ueda$ for a in {1..50} ; do cd a ; done ###bがあるか確認### uedambp:a ueda$ pwd /Users/ueda/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a uedambp:a ueda$ ls a b ###さらに50個aを下る### uedambp:a ueda$ for a in {1..50} ; do cd a ; done uedambp:a ueda$ pwd /Users/ueda/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a /a/a ###なにもない### uedambp:a ueda$ ls uedambp:a ueda$
Answer
少しちょろっと考えたけど分からなかったので解答例を見た。
こういう問題を考える人って頭がいいなーって思う。
$ seq 150 | sed 's/100/&\nb ./' | sed -E 's/^[0-9][0-9]*/a/' | tr '\n' '/' | sed 's/^/mv /' | sh $ for a in {1..50} ; do cd a ; done $ pwd /path/to/shell-gei-study/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a $ ls a/ b $ for a in {1..50} ; do cd a ; done $ pwd /path/to/shell-gei-study/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a $ ls
Q8
Question
先ほど作ったディレクトリについて、rm -rを使わずに~/a以下のディレクトリを消去してください。
Answer
問題自体はそんなに難しくないけどディレクトリをガーッと消す操作をやるのはちょっと怖かった。
$ seq 100 | tail -r | awk '{for(i=0;i<$1;i++){printf("a/")}; print ""}' | sed 's/.*/rm -f &b; rmdir &/' | sh