Help us understand the problem. What is going on with this article?

shellの-cオプションについてUbuntuのsh(dash)、bash、zshはそれぞれ違う挙動をする

More than 1 year has passed since last update.

shellの-cオプションについて、全てのshellで同じ挙動だと思っていたら、shellごとに結構違う挙動を持っていたことが原因でハマったのでメモ

shellの-cオプションについて

manコマンドで見てみるとこんな感じ。

For sh(dash) ubuntuの場合shはdashという軽量シェルにリンクしています

Read commands from the command_string operand instead of from the standard input. Special 0 will be set from the command_name operand and the positional parameters (\$1, \$2, etc.) set from the remaining argument operands.

For bash

If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, they are assigned to the positional parameters, starting with \$0.

For zsh

Take the first argument as a command to execute, rather than reading commands from a script or standard input. If any further arguments are given, the first one is assigned to \$0, rather than being used as a positional parameter.

特段挙動に違いはなさそうです、通常shellプログラムがコマンドを標準入力+改行コードで受け取るのではなく、-cで渡される文字列をコマンドとして認識して実行するということが書かれています。

bash -c './sample.sh' と bash sample.shは違う

たまに、混在してしまう人がいるのですが上記は、全く意味合いが違うので整理しておいた方がいいです。
下記は、新しいbashプロセス上でsample.shという実行可能スクリプトファイルを実行しています。そのため、sample.shのコンテンツを評価するのは、この実行結果からはbashであると判断できません shebangにzshやshが指定されていた場合は、もちろんそちらのプログラムでコンテンツが評価されます

$ bash -c './sample.sh'

下記は、bashにsample.shというコマンドが羅列されているファイルを読み込ませ、bashが評価しています。そのため、sample.shに羅列されているコマンドは全てbashで評価されます

$ bash sample.sh

-cで評価される時にshellプログラムに渡される$0が指定できる

manを見ると1つ面白い挙動としてこんなことは書かれていますね

If there are arguments after the command_string, they are assigned to the positional parameters, starting with \$0.

$0には、通常コマンド名(スクリプト名)が自動挿入されるということをみなさん思われていると思いますので下記の実行結果は誰もが予測できるものだと思います。

$ bash -c 'echo $0'
bash

ただ、manの通り command_string(ここでは echo $0) の後に、引数を複数渡すと \$0 から順にpositional argumentに代入されていくそうなのです。なので、上記の呼び出しに追加で引数を渡してあげると、下記のようになります。

$ bash -c 'echo $0' first_argument
first_argument

ユースケースはイマイチ思いつきませんが、まぁ頭の片隅にでも入れておきます。

dash, bash, zshの-cの違い

とりあえず、上述まではshellの-cオプションの簡単な説明として、ここからは本題に入っていきます。
何が違うか、端的にいうとbash, zshは特定の条件下(コマンドが1で出会った場合)で暗黙的にexecコマンドにより、渡されたコマンドを新しいshellプロセスの実行空間でそのまま実行します

bash, zshでコマンドが1行だった場合

command_string が 1行のコマンドだった場合

vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ bash -c 'sleep 100'
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0  1000  9842  9841  20   0  21308  3936 wait   Ss   pts/0      0:00 -bash
0  1000 10212  9842  20   0   7292   816 hrtime S+   pts/0      0:00 sleep 100
vagrant@vagrant:~$ zsh -c 'sleep 100'
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0  1000  9842  9841  20   0  21308  3936 wait   Ss   pts/0      0:00 -bash
0  1000 10287  9842  20   0   7292   656 hrtime S+   pts/0      0:00 sleep 100

bashもzshも現在のloginシェルの子プロセスとしてsleep 100が実行されています

dashでコマンドが1行だった場合

vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ sh -c 'sleep 100'
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0  1000  9842  9841  20   0  21308  3936 wait   Ss   pts/0      0:00 -bash
0  1000 10394  9842  20   0   4508   752 wait   S+   pts/0      0:00 sh -c sleep 100
vagrant@vagrant:~$ ps axl | grep 10394 | grep -v 'grep --color=auto 10394'
0  1000 10394  9842  20   0   4508   752 wait   S+   pts/0      0:00 sh -c sleep 100
0  1000 10395 10394  20   0   7292   752 hrtime S+   pts/0      0:00 sleep 100

sleep 100は、現在のloginシェルの孫プロセスとして実行されていることがわかります。つまり、dashは単純に新しくdashプログラムを立ち上げインタラクティブシェルと同じように、そこで順順に子プロセスを作成しコマンドを実行している事がわかります。

bashでコマンドが2行以上だった場合

vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ bash -c 'ls; sleep 100'
a.out  devstack  ls  nova  sample.sh  t  t1  tes  test.c  tete
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0  1000  9842  9841  20   0  21308  3936 wait   Ss   pts/0      0:00 -bash
0  1000 10482  9842  20   0  12520  2820 wait   S+   pts/0      0:00 bash -c ls; sleep 100

2行以上の場合は、コマンド情報を保持するためにexecですぐさまプロセス空間を受け渡す事はせず(できません)、すべてのコマンドは、ログインシェルからみて、孫プロセスとして実行されます

zshでコマンドが2行以上だった場合

vagrant@vagrant:~$ echo $$
9842
vagrant@vagrant:~$ zsh -c 'sleep 10;sleep 100'
vagrant@vagrant:~$ date
Sun Sep 10 11:45:21 UTC 2017
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0  1000  9842  9841  20   0  21308  3936 wait   Ss   pts/0      0:00 -bash
0  1000 10806  9842  20   0  26044  3180 sigsus S+   pts/0      0:00 zsh -c sleep 10;sleep 100
vagrant@vagrant:~$ date
Sun Sep 10 11:45:33 UTC 2017
vagrant@vagrant:~$ ps axl | grep 9842 | grep -v 'grep --color=auto 9842'
0  1000  9842  9841  20   0  21308  3936 wait   Ss   pts/0      0:00 -bash
0  1000 10806  9842  20   0   7292   764 hrtime S+   pts/0      0:00 sleep 100

2行以上の場合は、bash同様コマンド情報を保持するためにexecですぐさまプロセス空間を受け渡す事はせず(できません)、しかし最後のコマンドではexecを使いzshのプロセス空間でコマンドを実行している事がわかります。

dashでコマンドが2行以上だった場合

言わずもがな、1行の時と同じく全コマンドは孫プロセスとして実行されます

-cでexecするかどうか、子か孫かどうかで何が問題に?

ぶっちゃけ、そこらへんの挙動どうでもよくね?って僕も思ってたのですが、実はここの理解をきちんと整理する必要のあるシチュエーションが最近出てきたのです。はい、あれです。docker周りです。

ubuntuイメージでのDockerfileのCMDに文字列を渡すとき

DockerfileのCMDには、2種類の値が渡せます配列と文字列。配列の場合は、shellを通さずに実行されるのですが、文字列の場合はshellのコマンドとして解釈されるので(sh -cの引数として評価)、AppacheやNginxなどの常駐プログラムを実行する時は、注意が必要です。

kill時SIGTERMがCMDに書かれているエントリの実行プロセスに送られる

なぜ注意がいるかというと、docker killした際、dockerデーモン(docker-containerd)は、CMDで実行されたプロセスに対して、安全に終了できるように、SIGTERMシグナルを送ってくれます。なのでそれを受け取って安全に終了プロセスに入れるんですが、もちろん世の中SIGTERMをきちんとハンドルしてくれる行儀のよいソフトウェアばかりでないので、デフォルト10秒待ってもプロセスが死なない場合は、SIGKILLで強制的にプロセスを終了させようとします。
なので、SIGTERMをきちんとハンドルできるようにする事はかなり重要なのです。

shellはsignalの伝搬なんてしない

なので、docker containerを立ち上げる際に、コンテナから見て最上位プロセスがSIGTERMをきちんとハンドルできるようにしないといけないのです。
もちろんshellはsignalを子プロセスに伝搬なんぞしないので、例えばubuntuイメージで下記みたいなCMDを設定すると

CMD "nginx -g 'daemon off'"

文字列が指定されているので、sh -c の引数として評価され
docker containerでは下記のような形になります。先ほどのセクション通りUbuntuのsh(dash)は、-cで渡されたコマンドをexecしないので、コンテナ内のPID1はshellになります。

        pid:1 sh -c "nginx -g 'daemon off;'"
ppid:1  pid:2 nginx: master process nginx -g daemon off;
ppid:2  pid:3 nginx: worker process

この状態で、docker killをすると、pid1のshellにSIGTERMが送られるので当たり前のように無視され、タイムアウト後SIGKILLで強制的に殺されるという結末になってしまいます。
dockerを使い始めたばかりのプロジェクトなどでは、たまにこういうのを見かけるので注意が必要です。結構この手の間違いをしてしまう人多いはずです。また、centosのshはbashのシンボリックリンクになっているので、PID1がnginxになります。手元のOSがCentosで手元のshではちゃんとexecしてくれているように見えるけどコンテナの中は違うとか(ええ、実際そういうのありました)

あとがき

実は、このshellごとに-cの処理の仕方が違う事についてはdockerで問題になってから調べました。
一部コンテナが正常終了しないという状況で調べて見たら、PID1がshやbashになっていることに気づき、調べ始めたところ思いの外、違う挙動をする事がわかりました。
みんな使うshellを統一しようなんてしないので、この手の微妙な違いから生まれるこういう問題は少し厄介だなと・・・・
zshだと、最後のコマンドが自動的にexecされるのでsedなどで実行時configをいじって、最後に常駐プログラムを実行みたいなこともできるのですが、bashでは明示的にしないといけないとか。
当たり前のことですが、きちんと確認することは大事ですね。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした