What is shebang (#!/bin/sh
) in POSIX shell script
この記事はPOSIX原理主義Advent Calendarの13日目だ。
シェルスクリプトに限らず,Perl,Python,Rubyといったスクリプト系言語において,プログラム(スクリプト)の1行目に以下のような記述をする。
#!/bin/sh
#!/usr/bin/env python
この#!
の文字列をshebang(シバン)と呼ぶらしい。名前の由来はshell bang(!)などいくつかの説があるとのことだ。Shebangに続けてそのスクリプトを実行するインタープリターを絶対パスで指定することができる。このように記述すると,スクリプトファイルの実行時にshebangで指定したインタープリターによりスクリプトが実行される。
ほぼ全てのシェルスクリプトの1行目にこのような記述があり,もはや暗黙のルールと思えるこの記述だが,恥ずかしながら意味をよく理解していなかった。そこで,POSIXにおけるこのshebangの解釈について調査したのでまとめる。
コマンドの実行の流れ
まず,shebangについて説明する前に,コマンドがどのように実行されるかについてみておく。
POSIXにおけるコマンドの実行の流れは,XCUのCommand Search and Executionに書かれている。自作のシェルスクリプトやコマンドは,1.e.i.bか2のどちらかに分類される。この2種類はPATH変数から探すか,相対パスで実行するかの違いであり,中身はほぼ同じだ。以下に1.e.i.bの第1段落を抜粋する。
ここで書かれている通り,シェルスクリプトやコマンドはexecl()に相当する関数が呼び出されることで実行される。以下にexecl()
の定義示す。
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
シェルスクリプトやコマンド実行時には,pathにシェルスクリプト・コマンドの絶対パス,arg0にシェルスクリプト・コマンドの名前,arg1以降に残りのコマンド引数が適用されて実行される。
execl()
実行後の挙動
execl()
のPOSIXのぺージによると,execl()
が呼び出された後はpathに指定されたファイルによって処理が異なってくる。
execl()
にコマンドとその引数が引き渡されて実行されるとき,正常な実行可能バイナリーファイルであればそのまま実行される。しかし,それ以外ではファイルの種類によって以下2通りの処理が行われる。
- コマンドが認識できない形式(ENOEXEC)か,認識できてもシステムが未対応(EINVAL)の場合
- ファイル先頭の2バイトが文字列
#!
である場合
コマンドが実行できない場合
1.の場合は,単純なテキストファイルに実行権限が付いた場合が該当する。この場合,execl()
はインタープリターとしてsh
コマンドを使って,まるで以下の形式で実行した環境かのようにexeclp()
とexecvp()
を実行する。
execl(<shell path>, arg0, file, arg1, ..., (char *)0);
ここで,<shell path>はsh
の絶対パス,fileはプロセスのイメージファイル(ファイル名?)arg0,arg1は引数となる(arg0はfileと同じ?)。
つまり,実行可能なバイナリーファイル以外はすべてsh
によるシェルスクリプトとみなされる。
この仕様のため,シェルスクリプトの1行目にはshebang(#!/bin/sh
)がなくても,sh
で起動される。試しに以下の内容のhi.sh
というスクリプトを作って実行すると,shebangがなくてもsh
によって実行されていることがわかる。
ps -p $$ -o comm=
ps -p $$ -p args=
sh /bin/sh ./hi.sh
Shebang(#!
)がある場合
2.の場合は,一般的なシェルスクリプトのパターンだ。これは,ファイルの先頭2バイトが#!
であれば,ファイル1行目の#!
以降の残りの文字を実行するコマンドインタープリターとみなす。
つまり,以下のようなシェルスクリプトでは,#!
以降で指定される/bin/sh
によりファイルが実行される。
#!/bin/sh
ps -p $$ -o comm=
ps -p $$ -p args=
ファイルの先頭2バイトをチェックしているので,UTF-8やUTF-16などでBOM(Byte Order Mark)が付いている場合,shebangは認識されず1.のルートでスクリプトが実行される。その場合,BOMがコマンドとみなされて,当然ながらそんなコマンドはないので,line 1: command not foundというエラーが出る。
一応,以下のようにエラー出力を捨てるコードを1行目に書けば,BOMありでも実行できるシェルスクリプトは作れる。しかし,こんなことをするくらいなら最初からBOMなしで書いたほうがよいだろう。
: 2>-
Shebangによりシェルスクリプトを実行する際に,一つだけ問題がある。それは,以下で記載されている通り,sh
側で#!
から始まるファイルを読み込んだときの挙動が不定なことだ。
つまり,大昔のOSなどでは#!/usr/bin/perl
から始まるファイルをsh
で起動した場合,/usr/bin/perl
が起動することもありえる。
そのため,真の意味で可搬性が高いといえるのは,shebangを書かない1.の方法だ。ただ,shebangは広く普及しており,現在の実装では#!
から始まるファイルをsh
が読み込んでも,その行は単にコメントとして無視される場合がほとんどだ。また,POSIXのshコマンドのページにおいても,shebang #!
が書かれたシェルスクリプトを意識した記述もある。
このことから,わかりやすさと実状を考慮して,shebang行に#!/bin/sh
を書いても問題ないだろう。
Shebangに関する問題点
ここまででshebangがPOSIX上でどのように規定されているかを説明した。ここからは,shebangに関する問題点について説明する。
Shebangに指定するパス
まずは,shebang行に何を書けばよいかという議論だ。一般的に,シェルスクリプトでは以下のように/bin/sh
で指定することが多い。
#!/bin/sh
ここで,はたして本当に/bin/sh
と書いて問題ないのだろうか?実のところ,POSIXでは/bin
というディレクトリは定義されていない。
さらに,POSIX規格ではsh
が/bin/sh
または/usr/bin/sh
にあるとせずに,自分でgetconf PATH
コマンドでPATHを取得するように書かれている。
ただ,このようになってしまうと結局shebangの指定に互換性がなくなり,環境ごとにshebangの書き換えが必須となってしまう。
ではどう考えるか。これはFilesystem Hierarchy Standard (FHS)への準拠を考える。FHSはLinuxを含むUNIX形OSでの主なディレクトリとその内容を定めたものである。したがって,FHSで定義されているコマンドのパスは前提としてよいと考える。
FHSで存在することが必須とされているコマンドは/bin
配下のものが大半であり,残りの/usr/bin
配下などに含むコマンドはほとんどオプションだ。/bin
配下に存在することが要求されているコマンドは以下の35コマンドだ。
cat, chgrp, chmod, chown, cp, date, dd, df, dmesg, echo, false, hostname, kill, ln, login, ls, mkdir, mknod, more, mount, mv, ps, pwd, rm, rmdir, sed, sh, stty, su, sync, true, umount, uname, test, [
3.4. /bin : Essential user command binaries (for use by all users) Filesystem Hierarchy Standard
この中に,sh
コマンドが存在している。したがって,FHSに準拠しているOSでは/bin/sh
は存在することが保証されているので,shebangとして#!/bin/sh
と書いても問題ない。
ここで注意したいのはFHSではbash
やcsh
,env
コマンドの存在は必須ではなかったり,定義されていないことだ。したがって,#!/bin/bash
や#!/usr/bin/env
から始まるスクリプトは可搬性が低い。実際のところ,/bin/bash
は存在することが多いのだが保証はできない。
/usr/bin/env
が定義されていないというのは重要だろう。というのも,シェルスクリプト以外のスクリプト言語(Perl,Python,Ruby)では,#!/usr/bin/env python
のようにenv
コマンドを経由して実行させるshebang行がよく書かれるからだ。
スクリプト言語のインタープリターはPOSIX規格でも定義されておらず,どこにインストールされているかは全く不明だ。一応,FHSでは/usr/binにperlとpythonが存在することがオプションで定義されているのだが,あくまでオプションだ。それに,これらのコマンドではシステムにインストールされたものではバージョンが古いため,ユーザーが独自にインストールしたものを優先して使いたい場合がある。
そこで,env
コマンドを経由してユーザーのPATH
を使うことで,インストール場所の問題を回避することがよくなされる。FHSでは/bin
はシステム管理者とユーザーの両方に使われるコマンドが含まれる。env
コマンドはシステム管理者には不要なので,/usr/bin
にenv
コマンドが存在することが多い。
大半の場合/usr/bin/env
は存在するが,存在しない実装もある。そこで,確実に実行させるために/bin/sh
を経由してスクリプト言語のインタープリターを実行する方法がある。
シェルスクリプトのコメントと,スクリプト言語のコメント文字が違うことを利用して,スクリプト言語のコメント内にシェルスクリプトでexec
コマンドを記述して,スクリプト言語のインタープリターを起動するというトリックを使っている。
個人的にはそこまでする必要はないと考える。PerlもPythonもRubyもPOSIXに存在しないし,そもそもインストールされていなくてPATH変数に存在しないかもしれない。あまり気にしなくてもいいのではないか?
基本的には,#!/usr/bin/env
で記述しておき,万が一env
コマンドが無かったり,スクリプト言語のインタープリターが存在しなければ,自分でインストールしたりシンボリックリンクを貼ればよいだろう。
Shebangにおける引数
以下のようにshebang行にオプションや引数を書くことがある。
#!/bin/sh -eu
#!/usr/bin/env python
ここで問題になるのが,これらのオプションや引数がどう解釈されるかだ。
POSIX規格では,ファイルの先頭2バイトが#!
である場合,ファイル1行目のそれ以降の文字は「インタープリター名として扱う」としか記載されておらず,オプションや引数がある場合にどうなるかは書かれていない。
例えば,shebangに以下のように書いた場合を考える。
#!/bin/sh -e -u
この場合,以下の5パターンが考えられる。
ID | Path | Arg1 | Arg2 |
---|---|---|---|
1 | /bin/sh -e -u | ||
2 | /bin/sh | -e | -u |
3 | /bin/sh | -e -u | |
4 | /bin/sh | -e | |
5 | /bin/sh |
期待するのは全てオプションとして認識されること(表の2番目)だ。しかし,Ububntu 16.04では3番目の挙動をした。つまり,引数-e -u
が空白を含めた1個("-e -u"
)として認識されてしまった。
この挙動については,Linux Programmer’s Manualのexecveのマニュアルに書かれている。以下のコマンドでも閲覧できる。
man execve
さらに,上記マニュアルのNOTESを確認すると以下の記載がある。
つまり,Linuxでは引数を全体で1個として扱う(表の3番目)が,他の実装では最初の空白を終端(表の4番目)とみなしたり,複数の引数を受け付ける(表の2番目)こともありえる。
元々,POSIX規格にはshebang行の引数については言及がないので,表の1番目の#!
以降が空白を含めて丸ごとインタープリターとみなしてしまっても問題ないように解釈できる。このため,どの環境でも動作させるには,基本的にはshebangには引数を書かず,書いたとしてもshebang行における引数は1個までにすべきだろう。
ショートオプションであれば,グループ化できるので以下のようにまとめればよい。
#!/bin/sh -eu
スクリプト言語をenv
経由で実行する場合は,env
コマンドの引数としてスクリプト言語のインタープリターを指定してしまっているので,残念だがオプションの指定は諦めるしかない。
#!/usr/bin/env python # OK
#!/usr/bin/env python -v # NG
Shebang直後の空白
シェルスクリプトなどを見ていると,以下のようにshebang#!
の直後に空白が置かれているshebang行を見かけることがある。
#! /bin/sh
あまりみかけない記述なので,ただの打ち間違いか,Webブラウザの表示や書籍の組版の文字間隔の調整でそうみえているだけだと思っていたのだが,どうやらこのように書く流派が存在するらしい。
このことについては以下のページでの議論とそこから辿れるURLからことの経緯がよくわかる。
発端としては,GNU autoconfのチュートリアル文書において,「4.2BSDでは先頭4バイト”#! /”をマジックナンバーとして読むのでスペースが必要」と記載されていたためだ。しかし,この記述は誤りで,実際は空白はオプション扱いであり,新しい版のGNU autoconfの文書では修正されている。
さらに,問題の4.1 BSDにおいてもソースコード上でshebangの直後に空白が必要となる修正は入っておらず,空白がなければ動作しない実装が存在したという事実も確認できない。Shebangの直後に空白を入れなければならないというのは,あくまで噂や都市伝説でしかない。
これらの信頼性の欠ける情報に従うべきでないという理由とともに,POSIX規格の観点からも,#!
の直後に空白をいれるべきでない。
理由は,#!
以降の文字を空白を含めてインタープリターのパスとして認識してもPOSIX定義上の動作としてなんらおかしくないからだ。
まず,POSIXにおけるshebangによるインタープリターの起動メカニズムはexecl()
のマニュアルの以下の1文のみで定義されている。
ここでは,ファイルの1行目の#!
以降の文字をインタプリターのファイル名とみなすとだけ書かれており,POSIXではこれ以上に詳しいルールはなく,引数の扱いがどうなるかなども実装依存となる。
さらに,以下の事実からPOSIXの定義だけだと,Shebangの直後に空白をおくと,相対パス" /bin/sh"
でインタープリターが指定されたと解釈することもできる。
- Shebangは相対パスも有効,実際にshを
./test
に配置してShebangを相対パス#!test/sh
で書いたシェルスクリプトに記述したコマンドps -p $$ -o args=
で確認可能。 - 名前が空白1個のディレクトリも有効なパス(
mkdir ' '
)。
実際には,空白を含むパスを指定するとコマンドが見つからない。
bash: ./test.sh: /test/bin/sh: bad interpreter: No such file or directory
しかし,POSIXの定義上,#!
以降の残りを空白や引数丸ごと単一コマンドとみなしても,挙動として問題ない。
このPOSIX規格の解釈上の曖昧さをなくすために,shebangの直後に空白をいれるべきでない。常に#!/bin/sh
のように#!
の直後に空白を入れずにshebang行を書こう。
シェルスクリプトへのsetuid
最後の問題はセキュリティに関するものだ。ファイルのアクセス権限として,セットユーザーID(setuid)がある。この属性のついたファイルはそのファイルの所有者(通常はroot)の権限で実行できる。
setuidの利点はroot権限のパスワードを教えることなくroot権限でしか行えない作業を行える点だ。例えば,passwdコマンドで自分のログインパスワードを変更する場合や,mountコマンドでデバイスを接続する場合は,本来ならroot権限のファイルを操作する必要がある。しかし,これのコマンドにはsetuid属性があるので,root権限のパスワードをしらなくても通常ユーザーのまま実行できる。
setuidをファイルに設定するには以下のコマンドで行う。
chmod u+s file
処理内容が限定される場合には便利だが,一時的にroot権限を与えてしまうため,セキュリティ上の危険性がある。
実際にシェルスクリプトにsetuid属性がある場合の脆弱性を狙った攻撃が過去には行われたらしい。
このことは,以下で言及されている。
- 「詳解シェルスクリプト」の「15.4 Setuid Shell Scripts: A Bad Idea」
- 「bashクックブック」の「レシピ14.2 インタープリタスプーフィングの回避」
- Unix – Frequently Asked Questions (4/7) [Frequent posting]Section – How can I get setuid shell scripts to work?
- 上記の日本語訳:UNIX FAQ LIST #4.7
- The Whole Story on #! /usr/bin/ksh Perderabo – Tips and Tutorials – Unix Linux Forums
上記のUnix FAQで説明されている2個の事例を紹介する。
ファイル名を-i
にする問題
これは,root権限で対話シェルが起動してしまうという問題だ。
setuidのついた/etc/setuid_script
というスクリプトのshebangが以下であるとする。
#!/bin/sh
ここで以下の順序でコマンドを実行する。
cd /tmp
ln /etc/setuid_script -i
PATH=.
-i
この場合,最終的に以下のコマンドが実行されてしまう。
/bin/sh -i
/etc/setuid_script
の所有者であるroot権限で対話シェルを起動することになってしまう。これは,ファイル名-i
がsh
のオプションと誤認されてしまうことによる問題だ。
これを防ぐのは簡単であり,以下のようにshebang行の最後にオプションの終わりを示す-
をつければよい。
#!/bin/sh -
この問題については,POSIXのshのページでも以下のように言及がある。
ただ,この問題は手元のUbuntu 16.04では再現できなかった。実際に上記の手順を行っても,ファイル名の部分が./-i
となっており,きちんとファイルとして認識されている。
さらに,上記の最後に書いてあるとおり,shebang行に#!/bin/sh -
と書いても他に重要な問題があるのであまり意味がない。例えば,それは次の問題だ。
実行ファイルの差し替え
これは,root権限で任意のコマンドが実行されてしまうという問題だ。
以下の手順でコマンドを実行する。
cd /tmp
ln /etc/setuid_script temp
nice -20 temp &
mv my_script temp
ここで,3番目のコマンドは以下のように解釈される。
nice -20 /bin/sh - temp
nice -20
によりコマンドの実行優先順位を遅くできるので,sh
がtemp
を読み込む前に,temp
ファイルを別のファイルで上書きすることができる。つまり,任意のファイルを管理者権限で実行できてしまう。
対策
こうしたsetuidに関する問題に対応するには以下の2通りのアプローチがある。
- setuidを付与する実行ファイルはC言語などのバイナリーにする。
- シェルスクリプトではsetuidを使わずに,管理者権限でないと実行できないようにする。
1.がより確実だろう。バイナリーにしてしまえば,間にsh
などがファイルを読み込む隙がないので安全だ。
2.の方法について,管理者権限での実行が必要な場合は,setuidではなくそもそも管理者権限でないと実行できないようにしたほうがいいだろう。
具体的には,以下のコードで管理者権限として実行されているか判定できるので,冒頭に記述しておいてroot権限でなければ終了すればよい。
if [ $(id -u) != 0 ]; then
echo "Please run as root user!" >&2
exit 1
fi
My Future Sight for Past: How to check root user in POSIX Shell script
なお,Linuxを始めとした現代のOSではシェルスクリプトへのsetuidは無視するようになっているので,シェルスクリプトにsetuidを付与しても機能しない。
セキュリティ上の問題になるのですべきではないが,参考までにシェルスクリプトに付与したsetuidを有効にして実行したい場合の方法を示す。これは,POSIX範囲外になるが-p
オプションを付けることでできる。
#!/bin/sh -p
Conclusion
ここまででシェルスクリプトのshebangについて説明してきた内容を以下にまとめる。
- シェルスクリプトのshebang行には,FHSで保証される
#!/bin/sh
を書く。 - 基本的にshebang行にオプションや引数を付けない。つけたとしても最大1個までにする。
- シェルスクリプトにsetuid属性は付与させない。
POSIX規格に厳密に従うならば,shebangを書かないほうがいい。shebangを書かなければ,「#!から始まるファイルがshで結果が不定になる」ことについて悩まなくて済む。
このような曖昧さがPOSIXに残ったのは,POSIX自体が既存の実装ありきだったためだろう。既に存在する実装をカバーできるように,こうした曖昧さをあえて残したのだと思われる。
できることなら,次回のPOSIX規格の改定時にここの曖昧さがなくなってくれたら嬉しい。具体的には,現在2.1 Shell Introductionで「If the first line of a file of shell commands starts with the characters “#!”, the results are unspecified.」となっているところを,以下のツイートのように#!
から始まる場合にどのように処理すべきか明記してほしい。
この記事は元々2016-03-23にまとめた以下のTogetterの記事が頭に残っていたので,これをもとにさらに調査したものだ。
今回の記事はシェルスクリプトを書く上ではほとんど役に立たず,教養を身に付けるような内容となった。ただ,今後おそらく一生見ることになるので,シェルスクリプトの1行目のshebangとは何であるか,どう書くべきなのかをはっきりさせておきたかった。
これで,これからは胸を張ってシェルスクリプトを書くことができるだろう。
“What is shebang (
#!/bin/sh
) in POSIX shell script” に対して1件のコメントがあります。