ディレクトリ名、デフォルト名、およびパラメータ拡張

ディレクトリ名、デフォルト名、およびパラメータ拡張

ある形式を他の形式より好む客観的な理由はありますか?性能、信頼性、携帯性?

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

答え1

残念ながら、どちらにも独自の特徴があります。

POSIX には両方が必要なので、2 つの違いは移植性の問題ではありません。

これらのユーティリティを使用する簡単な方法は次のとおりです。

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

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

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

パラメータ置換を使用すると、奇妙な文字拡張に関連する極端なケースは発生しませんが、スラッシュ文字には多くの困難があります。まったくエッジケースではないことの1つは、noに対してディレクトリ部分計算を実行する必要があることです/

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

エッジケースは後ろにスラッシュがあるケースです(すべてスラッシュであるルートケースを含む)。basenameand コマンドは、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/.foofoo/foo/.foofoofoofoo/.

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

迅速で信頼性の高い方法は、zshとその機能を使用することです。歴史的修正(ユーティリティのように末尾のスラッシュを最初に削除します):

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

1 Solaris 10 以前などの事前 POSIX シェルを使用しない限り(まだ生産中のシステムにはパラメータ拡張文字列操作機能はありませんが、POSIX シェルはインストール時に常に呼び出されますが、そうではありませ/bin/shん)。sh/usr/xpg4/bin/sh/bin/sh
²はいfoo␤ファイルアップロードサービスにファイルを送信しましたが、サービスがこの問題から保護していない場合は、そのファイルを削除して結果をfoo削除します。

答え2

どちらもPOSIXなので、移植性は「心配する必要はありません」。より速く実行するには、シェルの交換を検討する必要があります。

しかし - ポータブルが何を意味するかによって異なります。一部の(不必要に)古いシステムではこれらの機能を実装していません/bin/sh(Solaris 10以前)、一方、少し前まで開発者dirnamebasename

参考までに:

移植性を考慮する際に考慮すべき事項みんな私はプログラムのシステムを維持しています。どちらもPOSIXではないので、長所と短所があります。あなたの長所と短所は異なる場合があります。

答え3

さらに:

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

0

このような奇妙なことが起こる理由は、2つのプロセスが通信するときに発生しなければならない多くの解釈、解析、およびその他のタスクがあるためです。コマンド置換は末尾の改行を削除します。そして板(明らかにここでは関係ありませんが)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.

...コメントが邪魔になる可能性があります...

答え4

プロセス内でブーストを取得できますがbasenamedirnameこれが組み込まれていない理由を理解できません。候補でない場合は何がわかりません)、実装は次のことを処理する必要があります。

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

^から基本名(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(基本システムの実装を利用して)組み込み関数を作成することもそれほど難しくありませんが、上記の関数はコンパイルを必要とせず、いくつかの改善を提供します。

関連情報