「サーバをバックグラウンドで起こしてから続けて処理する1行を流したら、そのようなファイルやディレクトリはありません と言われた。でもそのファイルは確かにある」 「同じ行の curl は成功しているのに、その次のコマンドだけ落ちる」 「手で1コマンドずつ打つと動く。1行にまとめた途端に落ちる」
こういう落ち方をしたとき、疑うべきはパスの綴りでも権限でもありません。原因は bash の演算子の結合の強さで、`cd` が後続のコマンドに効いていないことです。この記事では、なぜそうなるのかを最小再現で確認し、直し方3つと、そもそも踏まないための書き方をまとめます。
-
& は && より結合が弱いリスト区切り。cd X && cmd & 次のコマンド は2つのリストに割れる - 前半(
cd X && cmd)はバックグラウンドのサブシェルで走るので、cd はそのサブシェルの中でしか起きない - 後半は元のシェル・元のカレントディレクトリで走る。だから相対パスの実行だけが Exit 127 で落ちる
- 絶対URLを叩く
curl は影響を受けないため「一部だけ落ちる」ように見えて原因が見えにくい - 直し方は「括弧で囲む」「後続で
cd し直す」「絶対パスで書く」の3つ。起動と後続処理を同じ行に混ぜないのが予防 目次を開く
症状: あるはずの相対パスが「見つからない」と言われる
サーバを立ち上げてから、起動確認と後処理を続けて書いた1行です。
# 落ちる書き方(ENDPOINT には起動したサーバのURLを入れてある)
cd ~/projects/myapp && setsid nohup ./.venv/bin/python app/serve.py 8777 >> log 2>&1 < /dev/null & \
sleep 3; curl -s "$ENDPOINT"; ./.venv/bin/python -c 'print(1)' 返ってくるのはこれです。
/bin/bash: ./.venv/bin/python: そのようなファイルやディレクトリはありません 終了ステータスは 127(コマンドが見つからない)。ところが ls -l ~/projects/myapp/.venv/bin/python を叩けばファイルは存在し、同じ行の curl は 200 を返しています。「サーバは起きているのにインタプリタが消えた」ように見えるのが、この問題の厄介なところです。
原因: & は && より結合が弱い
&& は「左が成功したら右を実行する」AND リストを作る演算子です。対して & は「ここまでのリストをバックグラウンドで実行する」区切りで、&& より結合が弱い(=より外側で行を割る)。つまり先の1行は、bash からはこう見えています。
# 実際にはこの2つに割れている
( cd ~/projects/myapp && setsid nohup ./.venv/bin/python app/serve.py 8777 >> log 2>&1 < /dev/null ) &
sleep 3; curl -s "$ENDPOINT"; ./.venv/bin/python -c 'print(1)' & で非同期実行されるリストはサブシェルで走ります。cd はそのサブシェルの中だけで効くので、元のシェルのカレントディレクトリは1文字も動きません。後半は起動時のディレクトリのまま実行されるため、絶対URLの curl は通り、相対パスの実行だけが落ちます。
4行で再現する
mkdir -p /tmp/bgdemo
printf '#!/bin/sh\necho ok\n' > /tmp/bgdemo/hello.sh && chmod +x /tmp/bgdemo/hello.sh
cd /tmp
cd /tmp/bgdemo && sleep 30 & pwd 最後の行の pwd が返すのは /tmp/bgdemo ではなく /tmp です。pwd の代わりに ./hello.sh と書けば、ファイルは存在するのに 127 で落ちます。
演算子の効き方を表で押さえる
| 書き方 | どう解釈されるか |
|---|---|
A && B | A が成功したら B。どちらも同じシェル・同じディレクトリで走る |
A & B | A をバックグラウンドへ。B はすぐ元のシェルで走る |
cd X && A & B | 「cd X && A」がまとまってバックグラウンドへ。B は元のディレクトリ |
( cd X && A ) & B | 上とまったく同じ意味。括弧を書くと解釈が目で見える |
直し方は3つ
1. バックグラウンドに投げる部分を括弧で囲む
( cd ~/projects/myapp && setsid nohup ./.venv/bin/python app/serve.py 8777 >/dev/null 2>&1 & ) サブシェルになる範囲が読んで分かるので、書いた意図と実際の挙動がずれません。1行に収めたいときはこれが第一候補です。
2. 後続でもう一度 cd する
cd ~/projects/myapp && sleep 3 && ./.venv/bin/python -c 'print(1)' 後半を独立した行として書き、その中で改めて cd します。前半のバックグラウンド起動とは完全に切り離します。
3. 後続を絶対パスで書く
sleep 3
~/projects/myapp/.venv/bin/python -c 'print(1)' 相対パスをやめてしまえば、カレントディレクトリがどこであっても動きます。CI やエージェント経由の実行のように「どこから走るか分からない」場面では、これが一番壊れにくい書き方です。
予防: 起動と後続処理は同じ行に混ぜない
3つのどれでも直りますが、根本的にはバックグラウンド起動と、その後の処理を1行に詰めないことが予防になります。あわせて次の2点も覚えておくと事故が減ります。
- `$!` を素直に信じない。
cd X && cmd &の形では、$!が返すのは外側のサブシェルのプロセスIDで、中で起動した本体のIDではありません。プロセスIDが必要なら、起動されたプログラム自身にPIDファイルを書かせるほうが確実です。 - 呼び出しごとに作業ディレクトリが初期値へ戻る実行環境に注意。エージェントのシェル実行ツールなどは1回の呼び出しが終わるとカレントディレクトリを戻すため、毎回自分で
cdを書くことになり、この穴を踏みやすくなります。バックグラウンド起動と後続処理は別の呼び出しに分けてしまうのが安全です。
落ちたときの確認手順
原因の切り分けは4手で終わります。
- 終了ステータスを見る: 直後に
echo $?を打つ。127 はコマンド未検出、126 は見つかったが実行できない(権限・改行コードなど) - カレントディレクトリを見る: 落ちた行の直前に
pwdを1つ挟んで流し直す - 絶対パスで存在を確認する:
ls -l <絶対パス>でファイル自体はあると確定させる - 1行を分解する:
&の位置で行を割り、前半と後半を別々に実行してみる
2と3が食い違っていれば(ファイルはあるのに、いる場所が違う)、原因はほぼこの結合の問題です。逆に3で存在しなければ、素直にパスの綴りかインストールの問題です。
まとめ
-
&は&&より結合が弱いので、cd X && cmd & 後続は「バックグラウンドのサブシェル」と「元のシェル」の2つに割れる -
cdはサブシェルの中でしか効かず、後続は元のディレクトリで走る。だから絶対URLのcurlは通り、相対パスだけが Exit 127 で落ちる - 直すなら
( ... )で囲む、後続でcdし直す、絶対パスで書くの3つ。予防はバックグラウンド起動と後続処理を同じ行に混ぜないこと - 迷ったら
pwdを1つ挟む。「ファイルはあるのに見つからない」の答えは、たいていカレントディレクトリ側にあります
よくある質問(FAQ)
&& を ; に変えれば直りますか
直りますが、理由は別です。; も & と同じ強さのリスト区切りなので、cd X ; cmd & 後続 は「cd X」「cmd &」「後続」の3つに割れ、cd X が元のシェルで実行されます。結果として後続にも cd が効きます。ただし cd が失敗しても次に進んでしまうため(存在しないディレクトリでもそのまま実行される)、&& の安全性を捨てることになります。括弧で囲む書き方のほうがおすすめです。
nohup や setsid が原因ではないのですか
違います。原因は演算子の結合だけなので、nohup や setsid を外しても同じことが起きます。逆に cd X && cmd & の cmd を単なる sleep に変えても再現します。上の4行の再現例で確認できます。
( ) と { } はどちらを使うべきですか
& を付けた時点でどちらもサブシェルで走るため、挙動は同じです。読みやすさ優先で ( ) を使うのが無難です。{ } は前後に空白が必要で、閉じ括弧の直前に ; を書かないと構文エラーになるため、1行では書き間違えやすくなります。
sh や zsh でも同じですか
& と ; が AND-OR リストの区切りである点は POSIX の決まりに沿ったシェル(dash・zsh など)で共通なので、同じ落ち方をします。スクリプトの先頭で指定しているシェルが何であっても、pwd を1つ挟んで確認する手順はそのまま使えます。
