5
3

More than 1 year has passed since last update.

シェルスクリプトで相対パスを絶対パスに変換する正しい方法

Last updated at Posted at 2020-09-28

問題点

某所でファイルの相対パスから絶対パスに変換する方法として以下のようなものが紹介されていました。

path="$(cd $(dirname "$1") && pwd)/$(basename "$1")"

これにはいくつか問題がありますが、わかるでしょうか?

答え

  1. cd の引数がダブルクォートで括られていないので空白が入るパスを正しく扱えない
  2. - で始まるパスをオプションと誤認識する
  3. 改行で終わるパスを正しく扱えない
  4. ルート上にあるファイルを指定すると / ではなく // で始まるパスとなる
  5. zsh の場合、環境変数 PATH が上書きされる
  6. 存在しないディレクトリが含まれていた場合、エラーが出力されるもののエラーにならずに間違ったパスに変換される

1, 2 に関しては簡単なミスレベルなので以下のように書き換えるだけで OK です。

path="$(cd -- "$(dirname -- "$1")" && pwd)/$(basename -- "$1")"

注意 古い zsh 4.0 あたり以前は cd -- に対応していません。Busybox 1.17.0 あたり以前は dirname --, basename -- に対応していません。また(この記事の範囲では考慮する必要はありませんが)cd -- - だと「前にいたディレクトリに戻る」という機能になるので、それらに対応する場合は絶対パス以外の頭に ./ をつけるか、- で始まるパスの頭に ./ をつける回避策が必要です。

3 はコマンド置換を使うと末尾の連続する改行が取り除かれる事によるものです。例えば以下のような挙動です。

$ printf 'foo\n\n\n'
foo


# ↑空行が2行出力される
$ echo "$(printf 'foo\n\n\n')"
foo
# 空行は表示されない

改行で終わるパスはレアケースなのでそんなパスを使うなということで良いとは思いますが、コマンド置換を使って対応するならこのようになります。

$ var="$(printf 'foo\n\n\n' && echo _)"
$ var="${var%_}"
$ printf '%s' "$var"
foo


# ↑空行が2行出力される

4 は pwd の出力が / の場合を考えると // になるのは明らかです。// で始まるパスでもファイルにアクセスすることはできますが、パスを文字列として比較している場合などで問題が出る可能性があります。

5 は知らないとハマってしまいますが zsh では 変数 path は 環境変数 PATH と紐付いる特殊変数です。環境変数 PATH と同じ内容で : 区切りのパスを配列として参照することができます。そのため path=abc と実行してしまうと 環境変数 PATH まで abc となってしまいます。

$ path=abc
$ echo $PATH
abc

$ path=(foo bar baz)
$ echo $PATH
foo:bar:baz

$ date
zsh: command not found: date
# PATH が 書き換えられているためコマンドが実行できなくなる

6 が一番問題でエラーにならずに間違ったパスに変換され処理が進んでしまうため重大なバグを引き起こす可能性があります。こうなる理由は cd で発生したエラーが後続の basename の実行で隠蔽されてしまうからです。

$ cd /tmp

$ cat test.sh # test.sh に以下の内容で作成
#!/bin/sh
set -e # エラーで停止するようにしても効果はありません
abspath="$(cd -- "$(dirname -- "$1")" && pwd)/$(basename -- "$1")"
echo $?
echo "$abspath を削除"

$ sh ./test.sh no-directory/file # 存在しないディレクトリ上のファイルを指定
./test.sh: 3: cd: can’t cd to no-directory # ← エラーメッセージは表示される
0 # ← 終了ステータスがエラーではない
/file を削除 # ← ルートディレクトリ上のファイルを削除しようとしてしまう

解決方法

これらの問題を解決した正しい方法は次のようなものです。(3. 改行で終わるパスへの対応は面倒なので省略します。)

# set -e していれば 下記の || exit $? は不要
dirname="$(cd -- "$(dirname -- "$1")" && pwd)" || exit $?
abspath="${dirname%/}/$(basename -- "$1")"

dirnamebasename はエラーになることはないという前提にしています。ならないですよね・・・?)

別解(シェル関数による実装)

簡易な(行数が少ない)コードを求めているのであれば上記のコードで良いのですがシェルスクリプトにするのであれば行数を気にする必要はないと思うのでシェル関数にしました。上記のすべての問題への対応に加えパフォーマンスも向上させています。また相対パスから絶対パスへの変換だけでなく、その逆も実装ししたので別記事にしています。

また関連記事としてこちらもどうぞ

5
3
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
5
3