ある形式を他の形式よりも好む客観的な理由はありますか?パフォーマンス、信頼性、移植性?
filename=/some/long/path/to/a_file
parentdir_v1="${filename%/*}"
parentdir_v2="$(dirname "$filename")"
basename_v1="${filename##*/}"
basename_v2="$(basename "$filename")"
echo "$parentdir_v1"
echo "$parentdir_v2"
echo "$basename_v1"
echo "$basename_v2"
生成する:
/some/long/path/to
/some/long/path/to
a_file
a_file
(v1はシェルパラメータ展開を使用し、v2は外部バイナリを使用します。)
残念ながら、どちらにも癖があります。
どちらもPOSIXでは必須であるため、両者の違いは移植性の問題ではありません¹。
ユーティリティを使用する簡単な方法は
base=$(basename -- "$filename")
dir=$(dirname -- "$filename")
いつものように、変数置換を囲む二重引用符と、ファイル名がダッシュで始まる場合はコマンドの後の--
に注意してください(それ以外の場合、コマンドはファイル名をオプションとして解釈します)。これは稀なケースですが、悪意のあるユーザーによって強制される可能性があります²:コマンドの置換により、末尾の改行が削除されます。したがって、ファイル名がfoo/bar
の場合、base
はbar
ではなくbar
に設定されます。回避策は、非改行文字を追加し、コマンド置換後にそれを取り除くことです。
base=$(basename -- "$filename"; echo .); base=${base%.}
dir=$(dirname -- "$filename"; echo .); dir=${dir%.}
パラメーター置換を使用すると、奇妙な文字の拡張に関連するEdgeケースに遭遇することはありませんが、スラッシュ文字にはいくつかの困難があります。まったくEdgeケースではないことの1つは、/
がない場合、ディレクトリ部分の計算に異なるコードが必要になることです。
base="${filename##*/}"
case "$filename" in
*/*) dirname="${filename%/*}";;
*) dirname=".";;
esac
エッジの場合は、末尾にスラッシュがある場合です(ルートディレクトリの場合はすべてスラッシュです)。 basename
およびdirname
コマンドは、ジョブを実行する前に末尾のスラッシュを取り除きます。 POSIX構造に固執する場合、末尾のスラッシュを一度に削除する方法はありませんが、2つのステップで実行できます。入力がスラッシュのみで構成されている場合は、注意する必要があります。
case "$filename" in
*/*[!/]*)
trail=${filename##*[!/]}; filename=${filename%%"$trail"}
base=${filename##*/}
dir=${filename%/*};;
*[!/]*)
trail=${filename##*[!/]}
base=${filename%%"$trail"}
dir=".";;
*) base="/"; dir="/";;
esac
Edgeケースではないことを偶然に知った場合(たとえば、開始点以外のfind
結果に常にディレクトリパーツが含まれ、末尾に/
がない場合)、パラメーター拡張文字列操作は簡単です。すべてのEdgeケースに対処する必要がある場合、ユーティリティは使いやすく(ただし遅く)なります。
場合によっては、foo
ではなくfoo/
としてfoo/.
を扱いたいことがあります。ディレクトリエントリを操作している場合、foo/
はfoo
ではなくfoo/.
と同等であると見なされます。これは、foo
がディレクトリへのシンボリックリンクである場合に違いがあります。foo
はシンボリックリンクを意味し、foo/
はターゲットディレクトリを意味します。その場合、末尾にスラッシュが付いたパスのベース名は.
であり、パスは独自のdirnameにすることができます。
case "$filename" in
*/) base="."; dir="$filename";;
*/*) base="${filename##*/}"; dir="${filename%"$base"}";;
*) base="$filename"; dir=".";;
esac
高速で信頼性の高い方法は、zshに history modifiers を使用することです(これにより、最初にユーティリティのように末尾のスラッシュが削除されます)。
dir=$filename:h base=$filename:t
¹ Solaris 10以前の/bin/sh
などのPOSIX以前のシェルを使用している場合を除いて(まだ稼働中のマシンにはパラメーター拡張文字列操作機能がありませんでしたが、インストールには常にsh
というPOSIXシェルがあり、 /usr/xpg4/bin/sh
ではなく/bin/sh
のみです)。
² 例:foo
というファイルを、これに対して保護されていないファイルアップロードサービスに送信してから削除し、代わりにfoo
を削除します
どちらもPOSIXに含まれているため、移植性は問題ではありません。シェルの置換はより高速に実行されると推定されます。
しかし、それはあなたがポータブルで何を意味するかに依存します。一部の(必要ではない)古いシステムは、それらの機能を/bin/sh
に実装していません(Solaris 10以前が思い浮かびます)。一方、しばらく前、開発者はdirname
がbasename
ほどポータブルではありません。
参考のため:
dirname-パス名のディレクトリ部分を返す (POSIX)
Dirnameユーティリティは、システムIIIで開発されました。 System Vリリースを通じて、System Vリリース3のこの説明で指定された要件に一致するバージョンに進化しました。4.3BSD以前のバージョンには、dirnameが含まれていませんでした。
Solaris 10のshマニュアルページ (Oracle)
マニュアルページには、##
または%/
についての記述はありません。
移植性を検討する際には、プログラムを保守するシステムのallを考慮する必要があります。すべてがPOSIXであるとは限らないため、トレードオフがあります。トレードオフは異なる場合があります。
もあります:
mkdir '
'; dir=$(basename ./'
'); echo "${#dir}"
0
このような奇妙なことは、2つのプロセスが対話するときに発生する必要のある多くの解釈と解析と残りの部分があるために発生します。コマンドの置換により、末尾の改行が削除されます。そしてNUL (それは明らかにここでは関係ありませんが)。 basename
とdirname
は、他にどのようにしてそれらに話しかけるので、いずれにしても後続の改行を削除しますか?とにかく、ファイル名の末尾の改行はとにかく嫌悪感の種ですが、あなたは決して知りません。そして、別の方法で実行できる場合に、欠陥のある可能性のある方法を実行することは意味がありません。
まだ... ${pathname##*/} != basename
および同様に${pathname%/*} != dirname
。それらのコマンドは、指定された結果に到達するためにほとんど明確に定義された一連のステップを実行するように指定されています。
仕様は以下ですが、最初はより簡潔なバージョンです:
basename()
case $1 in
(*[!/]*/) basename "${1%"${1##*[!/]}"}" ${2+"$2"} ;;
(*/[!/]*) basename "${1##*/}" ${2+"$2"} ;;
(${2:+?*}"$2") printf %s%b\\n "${1%"$2"}" "${1:+\n\c}." ;;
(*) printf %s%c\\n "${1##///*}" "${1#${1#///}}" ;;
esac
これは、POSIXに完全に準拠したbasename
であり、単純なsh
です。難しいことではありません。結果に影響を与えずにできるので、以下で使用するいくつかのブランチをマージしました。
仕様は次のとおりです。
basename()
case $1 in
("") # 1. If string is a null string, it is
# unspecified whether the resulting string
# is '.' or a null string. In either case,
# skip steps 2 through 6.
echo .
;; # I feel like I should flip a coin or something.
(//) # 2. If string is "//", it is implementation-
# defined whether steps 3 to 6 are skipped or
# or processed.
# Great. What should I do then?
echo //
;; # I guess it's *my* implementation after all.
(*[!/]*/) # 3. If string consists entirely of <slash>
# characters, string shall be set to a sin‐
# gle <slash> character. In this case, skip
# steps 4 to 6.
# 4. If there are any trailing <slash> characters
# in string, they shall be removed.
basename "${1%"${1##*[!/]}"}" ${2+"$2"}
;; # Fair enough, I guess.
(*/) echo /
;; # For step three.
(*/*) # 5. If there are any <slash> characters remaining
# in string, the prefix of string up to and
# including the last <slash> character in
# string shall be removed.
basename "${1##*/}" ${2+"$2"}
;; # == ${pathname##*/}
("$2"|\
"${1%"$2"}") # 6. If the suffix operand is present, is not
# identical to the characters remaining
# in string, and is identical to a suffix of
# the characters remaining in string, the
# the suffix suffix shall be removed from
# string. Otherwise, string is not modi‐
# fied by this step. It shall not be
# considered an error if suffix is not
# found in string.
printf %s\\n "$1"
;; # So far so good for parameter substitution.
(*) printf %s\\n "${1%"$2"}"
esac # I probably won't do dirname.
...コメントが邪魔になるかもしれません...
インプロセスbasename
とdirname
からブーストを得ることができます(これらがビルトインではない理由がわかりません。これらが候補でない場合、何がなのかわかりません。 )しかし、実装は次のようなものを処理する必要があります:
path dirname basename
"/usr/lib" "/usr" "lib"
"/usr/" "/" "usr"
"usr" "." "usr"
"/" "/" "/"
"." "." "."
".." "." ".."
^ From basename(3) から
その他のEdgeケース。
私は使用しています:
basename(){
test -n "$1" || return 0
local x="$1"; while :; do case "$x" in */) x="${x%?}";; *) break;; esac; done
[ -n "$x" ] || { echo /; return; }
printf '%s\n' "${x##*/}";
}
dirname(){
test -n "$1" || return 0
local x="$1"; while :; do case "$x" in */) x="${x%?}";; *) break;; esac; done
[ -n "$x" ] || { echo /; return; }
set -- "$x"; x="${1%/*}"
case "$x" in "$1") x=.;; "") x=/;; esac
printf '%s\n' "$x"
}
(GNU basename
とdirname
の私の最新の実装は、複数の引数の処理や接尾辞の除去などの特別なコマンドラインスイッチを追加しますが、それは非常に簡単ですシェルに追加します。)
これらをbash
ビルトインにすることも(基礎となるシステム実装を利用することで)それほど難しくはありませんが、上記の関数をコンパイルする必要はありません。