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


20

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

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

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

両方ともPOSIXで必要とされるため、両者の違いは移植性の問題ではありません¹。

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

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

--ファイル名がダッシュで始まる場合は、変数置換を囲む二重引用符と、コマンドの後も注意してください(そうでない場合、コマンドはファイル名をオプションとして解釈します)。これは1つのエッジケースでは依然として失敗しますが、これはまれですが、悪意のあるユーザー²によって強制される可能性があります。コマンド置換は末尾の改行を削除します。したがって、ファイル名が呼び出されるとfoo/bar␤、の代わりににbase設定されbarますbar␤。回避策は、改行以外の文字を追加し、コマンド置換後に削除することです。

base=$(basename -- "$filename"; echo .); base=${base%.}
dir=$(dirname -- "$filename"; echo .); dir=${dir%.}

パラメータ置換を使用すると、奇妙な文字の展開に関連するエッジケースに遭遇することはありませんが、スラッシュ文字には多くの困難があります。エッジケースではないことの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

エッジケースではないことがわかっている場合(たとえばfind、開始点以外の結果には常にディレクトリパーツが含まれ、末尾はありません/)、パラメーター展開文字列の操作は簡単です。すべてのエッジケースに対処する必要がある場合は、ユーティリティの方が使いやすい(ただし遅い)。

場合によっては、のfoo/ようにfoo/.ではなく、のように扱いたい場合がありますfoo。あなたはディレクトリエントリに作用している場合、その後foo/に相当することになっているfoo/.、ではありませんfoo。これは、fooディレクトリへのシンボリックリンクである場合に違いを生じます。fooシンボリックリンクをfoo/意味し、ターゲットディレクトリを意味します。その場合、スラッシュが付いたパスのベース名はであることが有利.であり、パスはそれ自体のディレクトリ名にすることができます。

case "$filename" in
  */) base="."; dir="$filename";;
  */*) base="${filename##*/}"; dir="${filename%"$base"}";;
  *) base="$filename"; dir=".";;
esac

高速で信頼性の高い方法は、zshとその履歴修飾子を使用することです(これは、ユーティリティと同様に、最初に末尾のスラッシュを取り除きます)。

dir=$filename:h base=$filename:t

¹Solaris 10以前のようなPOSIX以前のシェル/bin/sh(まだ運用中のマシンにはパラメーター拡張文字列操作機能がありませんshが、インストールでは常にPOSIXシェルが呼び出されますが、それだけで/usr/xpg4/bin/shはありません/bin/sh)を使用している場合を除きます。
² 例:foo␤これに対して保護されていないファイルアップロードサービスに呼び出されたファイルを送信し、それを削除して、foo代わりに削除する


ワオ。(どのPOSIXシェルでも)最も堅牢な方法は、2番目の方法です。 base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}?私は注意深く読んでいたが、欠点に言及していることに気付かなかった。
ワイルドカード

1
@Wildcard Aの欠点は、それが扱うことにあるfoo/ようfooでないように、foo/.POSIX準拠のユーティリティと一致していないしています、。
ジル 'SO-悪である停止

了解、ありがとう。私はまだこの方法を好むと思う。なぜなら、ディレクトリを処理しようとしているどうかがわかり/必要に応じて末尾にたどり着くことができるからだ。
ワイルドカード

「たとえば、findディレクトリ部分が常に含まれ、末尾にない結果」などとは限り/ません。最初の結果としてfind ./出力さ./れます。
タビアンバーンズ

@Gilles改行文字の例に驚いた。答えてくれてありがとう
サムトーマス

10

どちらもPOSIXに含まれているため、移植性は「問題ありません」。シェル置換は、より高速に実行されると推定される必要があります。

しかし、それはあなたがポータブルという意味に依存します。一部の(必ずしも必要ではない)古いシステムはそれらの機能を実装していませんでした/bin/sh(Solaris 10以前が思い浮かぶ)一方で、しばらく前に、開発者はdirnameほどポータブルではないことを警告されましたbasename

参考のために:

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


7

もあります:

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

これbasenameはsimpleで完全にPOSIXに準拠してい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.

...おそらくコメントは気を散らします...


1
うわー、ファイル名の末尾の改行についての良い点。ワームの缶。しかし、私はあなたのスクリプトを本当に理解しているとは思わない。私は[!/]前に見たことがありません、そのようなもの[^/]ですか?しかし、あなたのコメントはそれと一致していないようです
。...-ワイルドカード

1
@Wildcard-まあ..それは私のコメントではありません。それが標準です。POSIX仕様basenameは、シェルでそれを行う方法に関する一連の指示です。しかし[!charclass]、グロブ[^class]でそれを行うための移植可能な方法は正規表現です-シェルは正規表現のために仕様化されていません。...コメントをマッチングについてcaseフィルタので、私は最後のスラッシュを含む文字列と一致する場合/ !/、次の場合のパターンであれば、以下のマッチ任意の末尾の/すべてのスラッシュは、彼らが唯一の可能すべてのスラッシュを。そして、その下には末尾がありません/
-mikeserv

2

あなたは、プロセス内からブーストを得ることができますbasenameし、dirname(これらは組み込みコマンドでない理由を私は理解していない-これらは候補ではない場合、私が何であるかを知らない)が、のようなハンドル物事への実装のニーズ:

path         dirname    basename
"/usr/lib"    "/usr"    "lib"
"/usr/"       "/"       "usr"
"usr"         "."       "usr"
"/"           "/"       "/"
"."           "."       "."
".."          "."       ".."

^ basename(3)から

その他のエッジケース。

私は使用しています:

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ビルトインにすることも難しくはありませんが(基礎となるシステム実装を利用することにより)、上記の関数をコンパイルする必要はなく、ある程度のブーストも提供します。


エッジケースのリストは実際には非常に役立ちます。これらはすべて非常に良い点です。リストは実際にはかなり完全に見えます。本当に他のエッジケースはありますか?
ワイルドカード

私の以前の実装ではx//、正しく処理できませんでしたが、答える前に修正しました。それでいいのですが。
PSkocik

スクリプトを実行して、これらの例で関数と実行可能ファイルが実行することを比較できます。100%一致しています。
PSkocik

1
dirname関数は、繰り返し発生するスラッシュを除去しないようです。例:dirname a///b//c//d////eyields a///b//c//d///
コードフォレスター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.