69
77

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

どこからでも実行できるシェルスクリプトの正しい書き方 ~ 冒頭でディレクトリを移動するな!

Last updated at Posted at 2022-11-04

はじめに

どこからでもシェルスクリプトを実行できるようにと、冒頭でカレントディレクトリを移動するコードをベストプラクティスかのように書いてある記事がいくつかありますが、それは違います。例えば以下のようなコードは良くないコードです。

# スクリプトのある場所にカレントディレクトリを移動してはいけない
cd "$(dirname "$0")"

# 上記のやたらと面倒な書き方
WORKDIR=$(cd "$(dirname "$0")" && pwd)
cd "$WORKDIR"

絶対に書いたらだめなのか?と聞かれるなら、理由をわかった上で「手抜きとして」なら書いても良いと思いますが、ベストプラクティスではありません。

補足 上記のコードは問題点を示すサンプルコードです。他にも cd が失敗した場合などの別の問題がありますが、この記事の内容とは無関係なので意図的に省略しています。私は set -e を使っているのでそのままで問題ありませんし ShellCheck で指摘される程度の常識的な話です(まさか ShellCheck 使ってないなんて事ありませんよね?)。気になる方は SC2164 – ShellCheck Wiki- で始まるパスの問題などについては私の過去記事(ここここ)を参照してください。

なぜこの書き方がダメなのか?

もし、このシェルスクリプトが /usr/local/bin ディレクトリにインストールされたらどうなるのか?を考えてみればわかると思います。そうです。/usr/local/bin ディレクトリ以下にファイルを書き込もうとするからです。通常このディレクトリは root でなければ書き込めないので不便ですし、何よりプログラム用の場所にデータを書き込んではいけません。(注意 データを書き込む前提の話をしています)

OS 標準コマンドやその他の CLI コマンドがどうなっているかを考えてみてください。コマンドがあるディレクトリ(/bin/usr/bin)にデータを書き込んだりしませんよね?例えば touch foo を実行するとどこにファイルが作られるのか?

カレントディレクトリです。

OS の標準コマンドの多くは、プログラム実行前のカレントディレクトリを基準に、指定されたパスのファイルの読み書きをします。例外はありますが、一般論としてプログラム実行中にカレントディレクトリを移動したりしていません。

シェルスクリプトの場所とデータディレクトリを共用するな!

説明を簡単にするためにディレクトリを移動するなという話にしていましたが、正しく説明するなら、シェルスクリプトがある位置をデータディレクトリとして使用するなということです。シェルスクリプトがあるディレクトリには書き込みができないかもしれないのだから、データディレクトリは別の場所にするというのは当然の考え方でしょう。

スクリプトの場所とデータディレクトリが分離されているのであればディレクトリを移動しても構いません。具体的には次のようなパターンです。

ディレクトリを移動しても構わない例

指定されたディレクトリに移動する場合

シェルスクリプトの場所に移動するのではなく、指定されたディレクトリに移動する場合は問題ありません。例えば以下のようなものです。

$ mycmd --flag ./datadir
mycmd
#!/bin/bash

# オプションの処理(手抜き): ハイフンで始まる引数を飛ばす
while [[ "${1:-}" = -* ]]; do shift; done

if [ $# -gt 0 ]; then
  cd "$1"
fi

これはコマンドの引数で指定されたディレクトリ (./datadir) に移動するという使い方です。このシェルスクリプトのその他の使い方です。

$ ./script/mycmd ./script # スクリプトがあるディレクトリをデータディレクトリとして使う場合
$ mycmd .                 # カレントディレクトリをデータディレクトリとして使う場合
$ mycmd                   # 省略した場合はカレントディレクトリをデータディレクトリとして使う

1 番目の使い方は面倒だと思うかもしれません。スクリプトがあるディレクトリをデータディレクトリとして使いたいのだから省略したいと思う気持ちはわかります。私も「手抜きで」行います。手抜きで行う分には構わないと思います。しかしこの記事はそれがベストプラクティスかどうかの話しており、手抜きで行う分には構いませんが良くないやり方であるということです。

2 番目の使い方は、現在のカレントディレクトリをデータディレクトリとして使う方法の一つです。代わりに ./ と書く方法もあるでしょう。このような使い方は docker build . の例として見られます。

3 番目の使い方はデータディレクトリが省略された場合は、カレントディレクトリを使うという方法で、2 の省略形のようなものです。このような使い方は git に見られます。例えば git init はカレントディレクトリを git ディレクトリとして初期化します。

このパターンの派生で、出力ディレクトリをオプションとして指定する方法もあります。このようなパターンは tar コマンドが使っており、以下のコマンドはアーカイブを展開する時に -C オプションで指定したディレクトリに移動してから展開処理を行います。

$ tar xzf archive.tar.gz -C /tmp/

その他のパターンとして、環境変数や設定ファイルでデータディレクトリを指定して、そのディレクトリへ移動するという方法も考えられます。いずれにしろ重要なのは「シェルスクリプトがあるディレクトリをデータディレクトリとして使うのではなく、別の方法で指定しろ」ということです。

サブコマンド実行やライブラリ読み込みのために移動する場合

シェルスクリプトが一つのファイルで完結せず、例えば次のような構造になっているとします。myprog からサブコマンドの myprog-subcommon.sh を相対パスで読み込むものとします。

/home/user/bin/myprog
├ libexec/
│   └ myprog-sub
└ lib/
    └ common.sh

この時、次のように実行できるようにするためには、

user@hostname:~$ bin/myprog

カレントディレクトリを /home/user/bin/myprog に移動するという方法が考えられます。そうすると以下のようにしてサブコマンドの実行やライブラリの読み込みができます。

myprog
cd "$(dirname "$0")"
libexec/myprog-sub
. lib/common.sh

(この時点では)スクリプトの場所とデータディレクトリを共用していないので問題ありません。

この時点では問題ないのですが、ここでカレントディレクトリ(からの相対パスに)にファイルを出力しようとしたら問題になります。例えば以下のような場合です。

user@hostname:~$ bin/myprog -o ./datadir

この場合、出力先はホームディレクトリ以下の /home/user/datadir であると期待するはずです。しかしカレントディレクトリが /home/user/bin/myprog 移動されてしまっているのでそのようにはなりません。シェルスクリプトがパスを参照しないのであれば、スクリプトの場所に移動しても構わないのですが、そうではない場合はカレントディレクトリが移動されたら困ります。頑張ればカレントディレクトリを移動しても動くように作ることは出来ますがあまり意味がありません。

結局このやり方は条件次第ではやっても良いと思いますが、筋が悪い方法で、やっぱりカレントディレクトリを移動せずに次のように書くのが良いという話です。

myprog
script_dir="$(dirname "$0")"
"$script_dir/libexec/myprog-sub"
. "$script_dir/lib/common.sh"

あまりおすすめしませんが、環境変数 PATH を書き換えることでも一応あります。おすすめしない理由は、この程度で グローバルな環境変数 PATH を変えるのはどうかと思うし、該当ディレクトリに余計なファイルがあった時に予期せぬ動作を引き起こしかねないなという懸念からです。十分な理由があるのであれば PATH を変更するのもありだと思います。

myprog
script_dir="$(dirname "$0")"
PATH="$script_dir/libexec:$script_dir/lib:$PATH"
myprog-sub
. common.sh

補足 シンボリックリンクからの実行に気をつけよう

cd "$(dirname "$0")" すれば、どこからでも実行できるように思えるかもしれませんが、実はシンボリックリンク経由で起動する場合、想定とは違うディレクトリになるかもしれません。例えば以下のような場合です。スクリプトとファイルを git で管理していて、それを ~/bin/myprog から実行する場合を想定しています。

/home/user/bin/myprog ・・・ /home/user/workspace/myprog/myprog へのシンボリックリンク

/home/user/workspace/myprog
├ myprog ・・・ 本体
└ README.md

この時に環境変数 PATH/home/user/bin/ が含まれている場合、myprog はどこからでも実行できるように思うかもしれません。しかし、この場合はシェルスクリプトがあるディレクトリは、本体があるディレクトリではなく、シンボリックリンクがあるディレクトリである /home/user/bin/ となります。

データファイルが /home/user/bin/ 以下で構わないのであれば問題ありませんが、一般的には /home/user/workspace/myprog を想定するでしょう。特にサブコマンドを実行する場合yライブラリを読み込む場合、それらのファイルは場合 /home/user/workspace/myprog 以下にあるはずです。

シンボリックリンク経由で実行した場合に適切なディレクトリを見つけるには readlink -f などを使って、シンボリックリンクから実際の本体の場所を突き止める必要があります。難しい話ではないと思うので、その書き方についてはここでは省略します。

どこからでも実行できるシェルスクリプトのベストプラクティス

実行場所を選ばないシェルスクリプトの書き方の基本は簡単です。次のようにデータを読み書きするディレクトリを引数などで渡すように CLI インターフェースを設計するだけです。読み書きするディレクトリは引数として与えられるため、シェルスクリプトがある場所にカレントディレクトリを移動する必要はありません。

$ myprog ./datadir

どこからでも実行できないシェルスクリプトが出来てしまう本当の原因は、シェルスクリプトがあるディレクトリに移動してないからではなく、シェルスクリプトがあるディレクトリをデータディレクトリとして使っているからです。シェルスクリプトがある場所に書き込もうとする考え方自体がアンチパターンなのです。シェルスクリプトの冒頭でカレントディレクトリを移動するというのは、アンチパターンに対しての手抜きの回避策であるということです。シェルスクリプトの場所とデータディレクトリを分離すると git などのバージョン管理も上手く行えることに気づくでしょう。

さいごに

最初に書きましたが手抜きしたい時は「手抜きの回避策」を使っても良いと思います。しかしそれはベストプラクティスではありません。なぜ使ってもいいと思っているのにベストプラクティスではないと指摘しているのかと言うと、他の言語などで CLI コマンドを作ったりして「正しい設計を知っている人が、首を傾げてしまうから」です。

変だなと思っていても、周りが「冒頭でスクリプトの場所に移動するのがベストプラクティスだと」言いまくっていたら、そういうものなのか?と勘違いしてしまいます。冒頭でディレクトリを移動するのがアンチパターンであると書いている記事がなければ、間違った考え方が広まってしまい正しい理解の妨げになるので、こうやって指摘しています。自分または他人が「何をやっているか」を正しく理解しておくのは重要なことです。

繰り返しますが、ベストプラクティスは「シェルスクリプトがある場所にカレントディレクトリを移動しない」ことです。そもそも「カレント」ディレクトリの使い方は、現在いるディレクトリを参照するためだったはずです。その他のコマンドがどういう設計になっているのかをよく観察しましょう。シェルスクリプトがある場所にカレントディレクトリを移動するというのは「手抜きの回避策」です。

69
77
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
77

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?