シェルスクリプトを書いていると、パスから「ファイル名だけ」や「置いてあるディレクトリだけ」を取り出したい場面が必ず出てきます。
ログを日付つきの名前でコピーしたいとき、設定ファイルの隣にあるファイルを読みたいとき、スクリプト自身の場所を基準に相対パスを組みたいとき。
そのたびに文字列を切り貼りしていると、末尾のスラッシュひとつで壊れる処理ができあがります。
そこで使うのが basename と dirname、そして相棒の realpath です。
この記事の実行例は、GNU coreutils 8.32 のサンドボックスで実際に叩いた結果をそのまま載せています。
basename はパスの末尾だけを返す
basename は、与えられたパスの最後の要素だけを標準出力へ返すコマンドです。
$ basename /var/log/syslog
syslog
ディレクトリを渡しても同じ動きになります。
$ basename /var/log/
log
末尾のスラッシュは無視される、という挙動をここで覚えておくと後が楽になるでしょう。
拡張子を落とす
第2引数に接尾辞を渡すと、その部分を取り除いてくれます。
$ basename /etc/nginx/nginx.conf .conf
nginx
同じことは -s オプションでも書けました。
$ basename -s .log /var/log/syslog.log
syslog
注意したいのは、接尾辞が一致しなかった場合は黙って元のまま返すという点。
$ basename /var/log/syslog .log
syslog
syslog に .log は付いていないので、何も削られずそのまま返ってきました。
エラーにならないぶん、想定と違う値がそのまま後続へ流れていきます。
複数まとめて処理する
-a を付けると、引数を全部ファイル名として扱ってくれる仕様。
$ basename -a /a/x.txt /b/y.txt /c/z.txt
x.txt
y.txt
z.txt
-s を使ったときは -a を付けなくても複数引数を受け取ります。
dirname はパスの手前だけを返す
dirname は逆に、最後の要素を取り除いた部分を返すコマンドです。
$ dirname /var/log/syslog
/var/log
こちらも末尾スラッシュの扱いが独特で、/var/log/ を渡すと /var が返るという結果になりました。
$ dirname /var/log/
/var
「ディレクトリを渡したのに親が返ってきた」と驚く定番のポイントです。
ディレクトリ部分が無いとき
パス区切りを含まない文字列を渡すと、カレントディレクトリを表す . が返ります。
$ dirname log.txt
.
空文字を渡した場合も同じく . でした。
シェル変数が空のまま dirname を通すと、意図せずカレントディレクトリを指す挙動になるわけです。
複数引数
dirname は引数を並べるだけで複数処理してくれます。
$ dirname /a/b/c /x/y/z
/a/b
/x/y
realpath でシンボリックリンクを解決する
basename と dirname は、渡された文字列を切るだけで、実体があるかどうかを一切見ていません。
実体まで解決したいときに使うのが realpath です。
$ ln -sfn /tmp/d1/d2 /tmp/link1
$ realpath /tmp/link1
/tmp/d1/d2
リンクを追わずに正規化だけしたいなら -s を付けます。
$ realpath -s /tmp/link1
/tmp/link1
相対パスが欲しいときは --relative-to が便利。
$ realpath --relative-to=/tmp /tmp/d1/d2
d1/d2
存在しないパスを渡すと落ちる
既定の realpath は、実体が無いパスに対してエラーを返す仕様。
$ realpath /tmp/nope/x
realpath: /tmp/nope/x: No such file or directory
$ realpath -m /tmp/nope/x
/tmp/nope/x
これから作るファイルのパスを組み立てるときは、-m を付けて存在チェックを外す必要があります。
スクリプト自身の場所を取る定番パターン
この3つを一番よく使うのが、スクリプトの置き場所を基準にする書き方でしょう。
#!/bin/bash
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
echo "SCRIPT_DIR=$SCRIPT_DIR"
これをシンボリックリンク経由で呼ぶと、リンクが置かれている側のディレクトリが返ります。
手元では /tmp/sub/where-link.sh から呼んで /tmp/sub が返りました。
実体のあるディレクトリが欲しいなら、realpath を挟みます。
#!/bin/bash
SCRIPT_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
echo "SCRIPT_DIR=$SCRIPT_DIR"
同じ呼び方をしても、今度は実体側の /tmp が返ってきました。
リンク経由で呼ばれることを想定するかどうかで、使い分けが変わるというわけです。
クォートを忘れると静かに壊れる
最後に、いちばんやりがちな事故を挙げておきます。
$ f="/tmp/my dir/a b.txt"
$ basename $f
basename: extra operand ‘b.txt’
$ basename "$f"
a b.txt
空白を含むパスをクォートせずに渡すと、単語分割されて別々の引数として解釈されてしまいました。
変数を渡すときは必ずダブルクォートで囲む、という原則はここでも効きます。
bash の変数展開で代用する
外部コマンドを呼ばずに済ませたい場面もあるでしょう。
$ p=/var/log/syslog
$ echo "${p##*/}"
syslog
$ echo "${p%/*}"
/var/log
シンプルなパスならこれで十分ですが、末尾スラッシュや空文字の扱いは basename / dirname と違います。
ループの中で何万回も呼ぶような場面だけ、変数展開へ寄せるのが現実的な落としどころ。
最後に
basename と dirname は「文字列を切るだけ」、realpath は「実体まで解決する」。
この線引きさえ持っておけば、末尾スラッシュとシンボリックリンクで挙動が変わる場面も落ち着いて読めます。
接尾辞が一致しないと黙って素通しする、空文字だと . が返る、といった「静かに間違う」挙動も覚えておきたいところ。
そして何より、変数はダブルクォートで囲む。
分解するもとになるパスをどう集めるかは、find の記事にまとめています。
以上です。










コメントを残す