はじめに
シェルスクリプトを書いていると、スクリプトを起動したディレクトリから実行できない状態によくなります。
複雑な処理を書いているときによくハマります。
これをなんとかするために、どこから起動しても同じ振る舞いをするシェルスクリプトを書くにはどうすればよいかを考えてみました。
問題の原因
特定のディレクトリからしか起動できなくなる最大の原因は、カレントディレクトリに依存した処理を書いてしまうことが一番多いと思います。
cd .. # 相対パスでディレクトリを移動
cp file1.txt file2.txt # 相対パスでファイルを指定
実現したい要件
- ディレクトリやパスを指定する場合は、原則として相対パスでなく絶対パスを使う
- スクリプトのファイルがあるフォルダを起点にディレクトリをたどる
コードを書く前に、カレントディレクトリに関するシェルスクリプトの振る舞いについて実験してみる。
実験: サブプロセスの中で cd した場合、もとのプロセスのカレントディレクトリに影響するか?
カレントディレクトリが /some/deep/dir
の状態で、 /some/deep/dir/scriptA.sh
を起動した場合にどのような振る舞いをするかを確認する。
#!/usr/bin/env bash
pwd
# スクリプトを起動したディレクトリ
# /some/deep/dir
dirname $0
# $0 で渡ってきたスクリプトの起動パス
# ./scriptA.sh
fuga=$(cd ../ && dirname $(pwd))
echo $fuga
# サブプロセスで起動したコマンド内で cd した後の状態のカレントディレクトリ(サブプロセス)
# /some
pwd
# サブプロセスで起動したコマンド内で cd した後の状態のカレントディレクトリ(親プロセス)
# /some/deep/dir
このことから以下のことがわかる。
- サブプロセスの中で実行したカレントディレクトリの変更は親プロセスには影響しない。
-
$0
には呼び出されたプログラムのパスが相対パスで格納されている
スクリプトの起動パス $0
と pwd
を組み合わせれば要件を満たせせそう。
実装
スクリプトを起動したパスを絶対パスで取得するコードは以下のようになる。
scriptDir=$(cd $(dirname $0) && pwd)
実際にコードを書くときは以下のように書く。
scriptDir=$(cd $(dirname $0) && pwd)
workDir=$(cd ${scriptDir}/.. && pwd)
sh ${scriptDir}/scriptA.sh
cd ${workDir}
cp ${workDir}/file1.txt ${workDir}/file2.txt
ファイルパスやディレクトリパスを指定する箇所は、すべてフルパスにする。
一部のコマンド(zip など)でカレントディレクトリによって振る舞いが変わるものもあるが、その場合は仕方ないので直前に cd する(ただし、フルパスで指定)。
参考
オープンソースで公開されているCLIツールのソースコードを確認してみても、似たような方法をとっていることがわかる。
#!/bin/bash
set -e
initialCwd=`pwd -P`
scriptDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
apmPath=$0
...
終わりに
このやり方で、起動したパスの問題で悩まされることがなくなった。
もっと良いやり方があればコメントで教えてください。