はじめに

プロセス置換等、/bin/shでは利用できず/bin/bashを使う必要がある場合、シェルスクリプトを書くときに、shebangを気にする必要がある。しかし、shebangで/bin/bashを指定しても、シェルスクリプト実行時にsh script.shとしてしまうと問題が発生する。

/bin/shと/bin/bashとプロセス置換の問題

shebangが/bin/shの問題点

以下のスクリプトを例にする。

$ cat sh_test.sh
#!/bin/sh
paste <(echo 1) <(echo 2)

このスクリプトをshで実行すると失敗する。
実行権限がついているので、./でファイル名を書いて実行しても、shebangで#!/bin/shが指定しているので失敗する。
bashで実行すると成功する。

$ sh sh_test.sh
sh_test.sh: line 2: syntax error near unexpected token `('
sh_test.sh: line 2: `paste <(echo 1) <(echo 2)'
$ ./sh_test.sh
./sh_test.sh: line 2: syntax error near unexpected token `('
./sh_test.sh: line 2: `paste <(echo 1) <(echo 2)'
$ bash sh_test.sh
1       2

shebangが/bin/bashの問題点

shebangを#!/bin/bashにするといいと一般的に言われている。

$ cat bash_test.sh
#!/bin/bash
paste <(echo 1) <(echo 2)

bashでの実行だけでなく、./で実行できるようになる。

$ ./bash_test.sh
1       2
$ bash bash_test.sh
1       2

しかしshebangを変えても、shで実行すると失敗してしまう。

$ sh bash_test.sh
bash_test.sh: line 2: syntax error near unexpected token `('
bash_test.sh: line 2: `paste <(echo 1) <(echo 2)'

解決策

この問題を解決するために考えたのは二通りの方法。
一つはsh script.shとしてしまった場合に、誤りに気付かせること。
もう一つはプロセス置換等をする場合は必ずbashを使うように強制すること。

スクリプト起動コマンドの検知

一つ目のshebangと異なるシェルで実行していることに気付けるようにするのは可能だが、記述が若干冗長になる。
$$で自身のPIDをとれるので、ps -eの結果とPIDを照らし合わせて、スクリプトの起動コマンドを取得する。また自身の1行目を読んでshebangを見る。起動コマンドがスクリプト名と同じであれば./で実行しているので問題なく、また起動コマンドとshebangが等しくても問題ない。

$ cat exe_check.sh
#!/bin/bash

exe=`ps -e | awk -v"pid=$$" '$1==pid {print $NF}'`
shebang=$(head -1 `readlink -f $0` | awk -F/ '{print $NF}')
test $exe != `basename $0` -a $exe != $shebang && 
  echo "bash固有機能を使用しています。shで実行しないでください。" && 
  exit 1

## 以下、本来実行したいプログラム

paste <(echo 1) <(echo 2)
exit 0

$ sh exe_check.sh
bash固有機能を使用しています。shで実行しないでください。
$ bash exe_check.sh
1       2
$ ./exe_check.sh
1       2
$

全体的にbashの機能を使っていて、他人がそのスクリプトを使うときにshで起動しないように、スクリプトの冒頭に書いて制御するには使えるのではないかと思う。
しかしほとんどが/bin/shで問題ないコマンドで作成されていて、一部だけbash機能を必要とするときには、もう一つの方策であるbashの使用強制がいいと思う。

bash利用の強制

bashの利用を強制することで、/bin/bashで起動されているのか/bin/shで起動されているのかを気にしなくてもいいようにできる。
実行したいコマンドをbash -cで囲うことで、全体が/bin/shで起動されていても、問題のコマンドは/bin/bashで実行されるため、エラーが発生しない。

$ cat force_bash_test.sh
#!/bin/sh
bash -c "paste <(echo 1) <(echo 2)"

$ sh force_bash_test.sh
1       2
$ bash force_bash_test.sh
1       2
$ ./force_bash_test.sh
1       2

もしbash -cで囲うときにエスケープ等が複雑になる場合は、ヒアドキュメントを使用して実行したいコマンドを出力させ、標準入力をbashが読み込むようにするといい。

$ cat force_bash_test.sh
#!/bin/sh

cat <<'CMD' | bash
paste <(echo 1) <(echo 2)
CMD

以下のようにエスケープが複雑な場合は簡単に書ける。他にはawkを使う場合もエスケープが面倒になりがちなので、ヒアドキュメントを使うといいだろう。

# 複雑なエスケープ
bash -c "paste <(echo '\"1\"') <(echo '\"2\"')"

# ヒアドキュメントで簡潔に
cat <<'CMD' | bash
paste <(echo '"1"') <(echo '"2"')
CMD