bashの間接展開(indirect expansion)

/ bash

bashの間接展開(indirect expansion)についての解説が不足しており大変苦労したため書き残しておく。

間接展開とは、シェル変数を参照するときにエクスクラメーションマークが使われる記法のことである。

https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html

parameter の最初の文字が感嘆符ならば、変数間接展開が行われます。 bash は残りの parameter からなる変数の値を変数の名前と見なします。 そしてそこで得られた名前の変数を展開した値を、置換処理の続きで使います。 これが 間接展開 です。

不足しているとは言っても実はこうやってmanに書かれている。

始まりはgetoptsのスニペットで見かけた${!OPTIND}というexpressionなのだがまずはこれを例に説明する。

set -- first second third
optind=1
echo "${1}" "${!optind}"
optind=2
echo "${2}" "${!optind}"
optind=3
echo "${3}" "${!optind}"

結果は以下である。

first first
second second
third third

見るだけで理解できるスニペットを用意したつもりだがお分かりいただけただろうか?exclamationをシェル変数の先頭につけるとそのそのシェル変数をブレースの中で一度展開し、exclamationは削除される。

set --は位置パラメータを再設定するビルトインだが雰囲気でわかるだろう。あと、$1${1}は数字が2桁にならない限り意味するところに違いはない。

ちなみに、このルールを用いるとexclamationを何個もつけるといった構文も考えられそうだがそれはあまりに病的だろう。確かめてはいないがmanに書かれていないから実装はされていないはず。

もう一つ異なるユースケースのスニペットをあげておく。

pid_cat=100
pid_tail=502
pid_head=555

for name in ${!pid_*}; do
    kill ${!name}
done

これを実行するとPID100, 502, 555のプロセスはkillされるので試しに動かす場合は注意するように。あくまでkillコマンドにはPIDっぽいものを渡すことが予想されるというスニペットの工夫だ。

事実、この例はbashのmanに書かれている間接展開の用例全部乗せになっている。${!pid_*}部分は次のように展開されている。

echo ${!pid_*}
pid_cat pid_head pid_tail

変数名のワイルドカードととらえるとわかりやすいだろう。${!pid_@}を用いたとしても多くの場合同じ動作となる。多くの、というのはダブルクォートした際の動作の違いだが通常のIFSを用いる限り考える必要はない。

これらがfor毎にnameという変数に代入され、それをさらに間接参照することによって最終的に100, 502, 555という実際のPIDをkillコマンドに渡せるというわけだ。ちなみに、間接参照はevalによっても行える。

for name in ${!pid_*}; do
    eval kill '$'$name
done

まれに間接展開をしらずに間接展開と同等の操作、いわゆるある変数に入っている変数名の値を参照する、という操作行う方法としてevalが語られているのを見かけるが明らかに冗長である。ただ間接展開の間接展開をしたい場合にはevalが使えるだろう。evalが威力を発揮するのはコマンドに対する可変個のプロセス置換などをシェルで動的に生成したい場合などがあるのだがここでは取り上げないでおく。

シェル芸は以上。

bashに関してすべてはmanにある。他を見る必要はない。そう思っていたがどうしても${!OPTIND}を説明する内容はmanから落ちていると思っている時期が一時期あったが、結局当初の考えに帰着することとなった。