私の知らないbashの世界

samplerで遊ぶにあたり、bashについて調べました*1github.com なんとなく使っていたbashですが奥が深いということがわかりました。

ここからの引用は特にことわらない限りJM projectのbash(1)からのものです。

Man page of BASH

おことわり

以下の文章はPOSIXシェルの仕様とbash拡張を区別しないで書いています。

単純なコマンド

まず、最も小さな単位である「単純なコマンド (simple command) 」から。

単純なコマンド (simple command) とは、 変数の代入を並べたもの (これは省略可能です) の後に、 ブランク区切りの単語とリダイレクションを記述し、 最後に制御演算子を置いたものです。

早くも驚きです。

変数の代入

変数の代入はsimple commandの構成要素であり、しかもそれを実行するコマンドの前に置くのも、最も基本的な使い方ということです。

コマンド名が残らなかった場合には、 変数を代入した結果が現在のシェル環境に効果を及ぼします。 それ以外の場合、変数は実行されるコマンドの環境に追加されるだけで、 現在のシェル環境には影響を与えません。

(現在のシェル環境における)変数を定義するのと、実行するコマンドの前に変数定義を置いてコマンドの環境変数を定義するのは、別々の構文だと思っていましたが、同じsimple commandなのですね。
実行するコマンドの前に変数定義を置いてコマンドの環境変数を定義するのは、なんとなく裏技っぽい(exportが正統な方法)と思っていましたが、そんなこともなし。

リダイレクション(リダイレクト)

リダイレクトはよく使いますが、これもsimple commandの一部なんですね。

パイプライン

パイプライン (pipeline)は、制御演算子 | または |& で区切った 1 つ以上のコマンドの並びです。 パイプラインのフォーマットを以下に示します:

[time [-p]] [ ! ] command [ [|||&] command2 ... ]

またしても驚きです。

timeキーワード

timeはキーワードであり、コマンドではないんですね。正確には、timeコマンドは存在するんですが、bashで普通に書くとコマンドの方は実行されません。

manpages.ubuntu.com

そして、time|, |&と同レベルの要素としてパイプラインを構成するということは、timeによる出力はコマンドの出力とは別であり、直接パイプラインで渡すことはできないということになります。

$ time sleep 1 |& cat > /dev/null 2>&1

real    0m1.007s
user    0m0.004s
sys 0m0.003s

timeによる出力をパイプラインで渡すにはひと工夫必要になります。

パイプラインとサブシェル

パイプライン中の各コマンドは、それぞれ別のプロセスとして (つまりサブシェル内で) 実行されます。

これも見落としがちな点ですね。

$ strace -f bash -c ': | :' |& rg -o "^\[pid\s+.+?\]" | sort | uniq | wc -l
3

うっかりパイプラインを多用するとプロセスがたくさん起動されることになりますが、それよりも問題なのは、もとのシェルと環境が違うことです。

echo hello | read foo
[ -v foo ]
echo $?
1

readコマンドにより"hello"fooに代入されたはずですが、[ -v foo ]は非ゼロ、つまりfooという変数は未セットということになっています。
これはread fooがサブシェルで行われているため、もとのシェルには影響しないからです。
これをどうにかするにもひと工夫必要です。

続く?

疲れたのでここで終わりにします。

*1:samplerが内部で実行するのはsh -cですが