みんな大好き pushd と popd は、ターミナルで作業するときは大変便利です。
しかし、スクリプトの中では pushd / popd よりも、サブシェル実行 ( ... )
を使った方が良いかもしれません。
pushd / popd の問題点
最大の問題は**sudo -u
で他のユーザーとして実行しようとした時に失敗する**ことです。
シェルスクリプトは、アプリのデプロイとか、ログの整理とかに使うことが多いと思います。そんな用途では "ディレクトリに移動 → 作業 → 次のディレクトリに移動" という流れになることが多いでしょう。
# update-apps.sh
: 全体の事前準備
for d in /path/to/apps/*; do
pushd "$d"
: ファイルをダウンロードしたり
: 展開したり、削除したり
popd # ←ここでエラーになる(後述)
done
: 全体の事後処理(完了をメールやチャットに通知するとか)
ところで、デプロイ作業は rootやアプリ専用のユーザーで行いたいはずです。ユーザーを変更するには sudo
を使います。
# ssh と sudo を使えば、1コマンドでデプロイを実行できるぞ!
$ ssh myuser@server sudo -u appuser /path/to/update-apps.sh
しかし、ssh myuser@server
を実行するとカレントディレクトリは myuser のホーム(/home/myuser
)になりますがappuser
はそこにアクセスできません。すると、pushd
した後、popd
で/home/myuser
に戻ろうとした時、移動先にアクセス権が無いためエラーになるのです。
代替案: ( ... )
でサブシェルで実行する
popd
に失敗する問題は、sudo
に-i
オプションを付けたり、スクリプトの始めでcd
したりしても回避できます。
しかし、オススメの解決策は( ... )
を使うことです。
# update-apps.sh
# サブシェルを使ったバージョン
: 全体の事前処理
for d in /path/to/apps/*; do
(
cd "$d"
: ファイルをダウンロードしたり
: 展開したり、削除したり
)
# エラーにならない
done
: 全体の事後処理(完了をメールやチャットに通知するとか
( ... )
の中はサブシェルで実行されるのでcd
しても外側のカレントディレクトリは変更されません。つまり「元のディレクトリに戻る」操作自体が発生しないので、アクセス権の問題も起きないのです。
また、( ... )
で囲むと一塊りの処理であることが見た目にも分かりやすくなります。また、実際に独立したサブシェルで実行されているので、処理の影響範囲を見分けやすくなります。
pushd / popd を使わなくてはならないケース
( ... )
の中身はサブシェルで実行されます。これはメリットでもあるのですが、逆に言えば変数を更新しても外側には反映されません。
# Documents/ にあるgitプロジェクトのファイル数を数える(動作しない)
count=0
for d in ~/Documents/*/; do
(
cd "$d"
n="$(git ls-files | wc -l)" # ファイル数を数える
count="$(( $count + $n ))" # 外側には反映されない代入
)
done
echo $count # => 0
この場合こそ pushd / popd を使います。
# Documents/ にあるgitプロジェクトのファイル数を数える(動作する)
count=0
for d in ~/Documents/*/; do
pushd "$d" > /dev/null
n="$(git ls-files | wc -l)" # ファイル数を数える
count="$(( $count + $n ))" # 外側には反映されない代入
popd > /dev/null
done
echo $count # => 正しい合計値
なお、pushd
と popd
の間はインデントしておくとディレクトリが切り替わっているのが分かりやすくてよいと思います。
また、pushd / popd は 現在スタックに積まれているディレクトリを標準出力に出力するので > /dev/null
をつけます。