まえがき
- Bashの組み込み機能のevalを使って可読性を犠牲に文字数を可能な限り削ってシェル芸する方法を書く
- 実務でやると絶対に怒られるので仕事ではなるべく使わないほうがよいかと
evalってなんぞや
man eval
の日本語訳は以下。
evalユーティリティは、引数を連結し、それぞれを文字で区切ってコマンドを作成します。 構築されたコマンドは、シェルによって読み取られて実行されます。
ようは文字列をシェルのコードとして評価してくれる組み込みコマンドです。
evalで何ができるの
正直「eval使って嬉しい!」と感じたシーンが今までなかったのと、
だいたい別のもっとより良いシンプルなアプローチがあるので良い使用例が浮かびませんでした。
あえてやるなら以下のように文字列で書いた算術演算式を実行できることでしょうか。
#!/bin/bash
eval echo "$1"
$ ./eval.sh '$(( 10 + 20 ))'
30
ただし、こんなこともできるので、evalはなるべく使わないほうがよいと思います。脆弱性の元です。
$ ./eval.sh '$(( 10 + 20 )); whoami'
30
jiro4989
evalでシェルを短くしてみる
本題です。evalでシェルのコードを短くしてみます。
Case1 同じコマンドのサブコマンド実行を短くする
docker-compose
とか使ってるとサブコマンドをいろいろ連続で実行したくなることがあります。
$ docker-compose down
$ docker-compose build --no-cache
$ docker-compose up -d
$ docker-compose logs service1
途中のコマンドでエラーになったら中断してほしいので &&
でつないで
コマンドが成功したときだけ後続の処理を実行してほしいです。
するとコマンドは以下のようになります。
$ docker-compose down && docker-compose build --no-cache && docker-compose up -d && docker-compose logs service1
長い。こんなの手打ちしてられない。そんな時に、eval。
evalを使うと以下のように短く表現できます。
$ eval 'docker-compose '{down,"build --no-cache","up -d","logs service1"}' && ' :
{}
とカンマで区切っている箇所はブレース展開というBashの機能です。
こんな風に使います。
$ echo 2019/{1..12}
2019/1 2019/2 2019/3 2019/4 2019/5 2019/6 2019/7 2019/8 2019/9 2019/10 2019/11 2019/12
この挙動を踏まえて前述のdocker-composeのコマンドを読み解くと、以下のように展開されてevalに渡されています。
$ docker-compose down && docker-compose build --no-cache && docker-compose up -d && docker-compose logs service1 && :
最後の:
は何もしないコマンドです。末尾が&&
で終了するとシェルの式としては不正なので、
何もしないコマンドを最後に配置することでシェルの式として完成させて、且つ何もしないようにしています。
Case2 forループを使わない
同じコマンドを10回実行したい、というケース、以下のように表現できると思います。
# forを使う
for i in {1..10}; do
echo "test"
done
# seqを使う
seq 10 | xargs -I __ echo "test"
とにかく文字数をケチりたいときに、evalを使って以下のように表現できます。
$ eval 'echo test $(: '{1..10}');'
test
test
test
test
test
test
test
test
test
test
これは以下のように文字列が展開されてevalで評価されています。
$ eval test $(: 1); echo test $(: 2); echo test $(: 3); echo test $(: 4); echo test $(: 5); echo test $(: 6); echo test $(: 7); echo test $(: 8); echo test $(: 9); echo test $(: 10);
算術演算式と同様に、Bashのコマンド置換の文字列もevalで評価できます。
コマンド置換とは、$()
で括られた範囲に書かれたコマンドを実行し、$()
をそのコマンドの標準出力の出力結果で置換します。
以下は今日の日付のディレクトリの作成をコマンド置換で行う例です。
$ date +%Y%m%d
20191205
$ mkdir $(date +%Y%m%d)
$ ls -d 20191205
20191205
dateの実行結果がmkdir
の引数として実行されています。この機能を利用しています。
docker-composeの時に何もしないコマンドとして:
を使用していましたが、ここも同様の使い方をしています。
$(: 1)
とかは結果的には何も出力しないので、コマンド置換結果の文字列は空になります。
結果的にechoの引数にもならず、testという文字列だけが残ります。
Case3 パイプ処理の連続
以下のような市松模様のテキストが存在します。
$ eval 'eval "echo "{"* "," *"}"{,,,,,,};"'{,,,,} | tr -d " "
* * * * * * *
* * * * * * *
* * * * * * *
* * * * * * *
* * * * * * *
* * * * * * *
* * * * * * *
* * * * * * *
* * * * * * *
* * * * * * *
この空白部分に以下のようにテキストを埋めたいです。
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
この目的を達成したいときに、sedを使って素直に書くと以下のように表現できます。
$ eval 'eval "echo "{"* "," *"}"{,,,,,,};"'{,,,,} | tr -d " " |
sed "s/ /あ/" |
sed "s/ /い/" |
sed "s/ /う/" |
sed "s/ /え/" |
sed "s/ /お/" |
sed "s/ /か/" |
sed "s/ /き/"
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
でもいちいち毎回毎回sedを書いてられないです。
そんな時にevalを使って以下のように表現できます。
$ eval 'eval "echo "{"* "," *"}"{,,,,,,};"'{,,,,} | tr -d " " |
eval 'sed "s/ /'{あ,い,う,え,お,か,き}'/" | ' cat
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
*あ*い*う*え*お*か*き
あ*い*う*え*お*か*き*
これも考え方はdocker-compose
の時と同じです。
sed
のコマンド列の最後を|
で終わらせる文字列にすることで、
ブレース展開時にパイプでの処理の継続を表現します。
最後に、標準入力のテキストをそのまま出力するcat
で終わらせることでコマンド列を完成させます。
応用
以上のevalの使い方を踏まえて、図形生成を短くしてみます。
以下のような図形を生成したいです。
********************
*********プレ*********
********ミアムキ********
*******ムチチゲカル*******
******ビ丼肉カレーうど******
*****んミニ牛豆腐キムチチ*****
****ゲ膳(プレミアム牛肉使用****
***)セット担々うどんきつねうど***
**んとろたま(プレミアム牛肉使用)**
*ミニ(関西風だし)ミニ牛丼セットチキ*
*ングリルセットキムカル丼牛めしとろた*
**ま旨辛ネギたっぷりネギたまうどん**
***(関西風だし)プレミアム牛ス***
****タミナ豚バラ生姜焼定食オ****
*****リジナルカレーうどん*****
******プレミアム牛とじ******
*******たまうどんミ*******
********ニ牛めし********
*********セッ*********
********************
ひし形の図形の中央に松屋のメニューを埋め込みたいです。これをevalを使って短く表現します。
まず、こんなひし形をevalで作ってみます。
$ ((eval paste -d '""' '<(seq 10 -1 1 | awk '"'"'{for (i=0;i<$1;i++) { printf "%s", "*" } for (i=0;i<10-$1;i++) {printf "%s", " "} print ""}'"'"{," | rev"}") ") | tee >(tac))
********************
********* *********
******** ********
******* *******
****** ******
***** *****
**** ****
*** ***
** **
* *
* *
** **
*** ***
**** ****
***** *****
****** ******
******* *******
******** ********
********* *********
********************
順に実装していきます。
まず左上の三角形を生成します。
$ seq 10 -1 1 | awk '{for (i=0;i<$1;i++) { printf "%s", "*" } for (i=0;i<10-$1;i++) {printf "%s", " "} print ""}'
**********
*********
********
*******
******
*****
****
***
**
*
これを左右反転したものをpaste
して上半分の図形を生成したいです。
そこで、eval。
paste -d "" <(seq 10 ~~~) <(seq 10 ~~~ | rev)
という文字列をブレース展開で生成してevalで評価しています。
$ (eval paste -d '""' '<(seq 10 -1 1 | awk '"'"'{for (i=0;i<$1;i++) { printf "%s", "*" } for (i=0;i<10-$1;i++) {printf "%s", " "} print ""}'"'"{," | rev"}") ")
********************
********* *********
******** ********
******* *******
****** ******
***** *****
**** ****
*** ***
** **
* *
あとはこれを上下反転したものが欲しいです。
tee >(tac)
で実現します。
$ ((eval paste -d '""' '<(seq 10 -1 1 | awk '"'"'{for (i=0;i<$1;i++) { printf "%s", "*" } for (i=0;i<10-$1;i++) {printf "%s", " "} print ""}'"'"{," | rev"}") ") | tee >(tac))
********************
********* *********
******** ********
******* *******
****** ******
***** *****
**** ****
*** ***
** **
* *
* *
** **
*** ***
**** ****
***** *****
****** ******
******* *******
******** ********
********* *********
********************
できました。
次に中の空白を松屋のメニューで埋めます。
松屋のメニューを生成するのにはmatsuya
コマンドを使用します。
まず、matsuyaで生成した文字列からsedの置換式を生成します。
$ matsuya | grep -o . | sed -E "s/./sed -es, ,&, | /" | tr -d \\n
sed -es, ,鉄, | sed -es, ,皿, | sed -es, ,ス, | sed -es, ,タ, | sed -es, ,ミ, | sed -es, ,ナ, | sed -es, ,豚, | sed -es, ,バ, | sed -es, ,ラ, | sed -es, ,シ, | sed -es, ,ャ, | sed -es, ,ン, | sed -es, ,ピ, | sed -es, ,ニ, | sed -es, ,オ, | sed -es, ,ン, | sed -es, ,ソ, | sed -es, ,ー, | sed -es, ,ス, | sed -es, ,定, | sed -es, ,食, |
これだけだとひし形を全て埋めるには文字が足りないので、evalで増殖します。適当に30回くらいで埋まりました。
$ eval 'eval $(matsuya | grep -o . | sed -E "s/./sed -es, ,&, | /" | tr -d \\n)$(: '{1..30}')' cat
前述のシェル芸で生成したひし形から改行を取り除いて、そこに上記matsuyaシェル芸をひっつけます。
最後に取り除いた改行を復元して完成。
$ ((eval paste -d '""' '<(seq 10 -1 1 | awk '"'"'{for (i=0;i<$1;i++) { printf "%s", "*" } for (i=0;i<10-$1;i++) {printf "%s", " "} print ""}'"'"{," | rev"}") ") | tee >(tac)) | tr \\n Q | eval 'eval $(matsuya | grep -o . | sed -E "s/./sed -es, ,&, | /" | tr -d \\n)$(: '{1..30}')' cat | tr Q \\n
********************
*********プレ*********
********ミアムキ********
*******ムチチゲカル*******
******ビ丼肉カレーうど******
*****んミニ牛豆腐キムチチ*****
****ゲ膳(プレミアム牛肉使用****
***)セット担々うどんきつねうど***
**んとろたま(プレミアム牛肉使用)**
*ミニ(関西風だし)ミニ牛丼セットチキ*
*ングリルセットキムカル丼牛めしとろた*
**ま旨辛ネギたっぷりネギたまうどん**
***(関西風だし)プレミアム牛ス***
****タミナ豚バラ生姜焼定食オ****
*****リジナルカレーうどん*****
******プレミアム牛とじ******
*******たまうどんミ*******
********ニ牛めし********
*********セッ*********
********************
改行するとこんなかんじ。
$ ((eval paste -d '""' '
<(seq 10 -1 1 |
awk '"'"'
{for (i=0;i<$1;i++) { printf "%s", "*" }
for (i=0;i<10-$1;i++) {printf "%s", " "} print ""}'"'"{," | rev"}") ") |
tee >(tac)) |
tr \\n Q |
eval 'eval $(matsuya |
grep -o . |
sed -E "s/./sed -es, ,&, | /" |
tr -d \\n)$(: '{1..30}')' cat |
tr Q \\n
改行しても見辛い。
まとめ
以下の内容について書きました。
- evalで似たようなコードをまとめる方法
- ブレース展開との組み合わせ方
無理くり似たような処理をまとめたりするのにevalは便利ですけれど、業務コードには絶対に使わないほうがいいです。
ジョークに使うくらいにとどめるのをおすすめします。