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

pushd / popd に気をつけろ!

More than 1 year has passed since last update.

みんな大好き 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 # => 正しい合計値

なお、pushdpopd の間はインデントしておくとディレクトリが切り替わっているのが分かりやすくてよいと思います。

また、pushd / popd は 現在スタックに積まれているディレクトリを標準出力に出力するので > /dev/null をつけます。

tonluqclml
エムスリーでソフトウェアエンジニアしています。仕事ではRubyもScalaもPythonもBashもなんでもやる雑食系。 Twitter:https://twitter.com/doloopwhile 昔の個人ブログ:http://doloopwhile.hatenablog.com/ 勤務先ブログ: https://www.m3tech.blog/
http://doloopwhile.hatenablog.com/
m3dev
インターネット、最新IT技術を活用し日本・世界の医療を改善することを目指します
https://m3.recruitment.jp/engineer/
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