web-dev-qa-db-ja.com

dirnameおよびbasenameとパラメーターの展開

ある形式を他の形式よりも好む客観的な理由はありますか?パフォーマンス、信頼性、移植性?

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は外部バイナリを使用します。)

21
Wildcard

残念ながら、どちらにも癖があります。

どちらもPOSIXでは必須であるため、両者の違いは移植性の問題ではありません¹。

ユーティリティを使用する簡単な方法は

base=$(basename -- "$filename")
dir=$(dirname -- "$filename")

いつものように、変数置換を囲む二重引用符と、ファイル名がダッシュで始まる場合はコマンドの後の--に注意してください(それ以外の場合、コマンドはファイル名をオプションとして解釈します)。これは稀なケースですが、悪意のあるユーザーによって強制される可能性があります²:コマンドの置換により、末尾の改行が削除されます。したがって、ファイル名がfoo/bar␤の場合、basebar␤ではなく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以前が思い浮かびます)。一方、しばらく前、開発者はdirnamebasenameほどポータブルではありません。

参考のため:

移植性を検討する際には、プログラムを保守するシステムのallを考慮する必要があります。すべてがPOSIXであるとは限らないため、トレードオフがあります。トレードオフは異なる場合があります。

10
Thomas Dickey

もあります:

mkdir '
';    dir=$(basename ./'
');   echo "${#dir}"

0

このような奇妙なことは、2つのプロセスが対話するときに発生する必要のある多くの解釈と解析と残りの部分があるために発生します。コマンドの置換により、末尾の改行が削除されます。そしてNUL (それは明らかにここでは関係ありませんが)basenamedirnameは、他にどのようにしてそれらに話しかけるので、いずれにしても後続の改行を削除しますか?とにかく、ファイル名の末尾の改行はとにかく嫌悪感の種ですが、あなたは決して知りません。そして、別の方法で実行できる場合に、欠陥のある可能性のある方法を実行することは意味がありません。

まだ... ${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.

...コメントが邪魔になるかもしれません...

7
mikeserv

インプロセスbasenamedirnameからブーストを得ることができます(これらがビルトインではない理由がわかりません。これらが候補でない場合、何がなのかわかりません。 )しかし、実装は次のようなものを処理する必要があります:

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 basenamedirnameの私の最新の実装は、複数の引数の処理や接尾辞の除去などの特別なコマンドラインスイッチを追加しますが、それは非常に簡単ですシェルに追加します。)

これらをbashビルトインにすることも(基礎となるシステム実装を利用することで)それほど難しくはありませんが、上記の関数をコンパイルする必要はありません。

2
PSkocik