目录名和基本名与参数扩展

目录名和基本名与参数扩展

是否有任何客观原因让我们更喜欢一种形式而不是另一种形式?性能、可靠性、便携性?

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 使用 shell 参数扩展,v2 使用外部二进制文件。)

答案1

不幸的是,两者都有自己的怪癖。

POSIX 需要两者,因此它们之间的差异不是可移植性问题。

使用这些实用程序的简单方法是

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

请像往常一样注意变量替换周围的双引号,以及--命令后面的双引号,以防文件名以破折号开头(否则命令会将文件名解释为选项)。在一种边缘情况下,这仍然会失败,这种情况很少见,但可能是由恶意用户强制执行的:命令替换会删除尾随换行符。因此,如果调用文件名foo/bar␤,则将base设置为bar而不是bar␤.解决方法是添加一个非换行符并在命令替换后将其删除:

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

通过参数替换,您不会遇到与奇怪字符扩展相关的边缘情况,但斜杠字符存在许多困难。根本不是边缘情况的一件事是,计算目录部分需要针对没有/.

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

边缘情况是尾随斜杠的情况(包括根目录的情况,全部是斜杠)。basenameand命令dirname在完成工作之前会去掉尾部的斜杠。如果您坚持使用 POSIX 结构,则无法一次性去除尾部斜杠,但您可以分两步完成。当输入只包含斜杠时,您需要注意这种情况。

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

1除非您使用 Solaris 10 及更早版本等 POSIX 之前的 shell (在仍在生产的计算机上缺乏参数扩展字符串操作功能,但在安装中/bin/sh始终会调用 POSIX shell ,只是它是,而不是)。sh/usr/xpg4/bin/sh/bin/sh
²例如:提交一个名为foo␤文件上传服务的文件,但该服务无法防范此问题,然后将其删除并导致foo被删除

答案2

两者都采用 POSIX,因此“应该”不关心可移植性。应该认为 shell 替换运行得更快。

但是 - 这取决于您所说的便携式是什么意思。一些(不必要的)旧系统没有在它们的/bin/sh(Solaris 10 及更早版本)中实现这些功能,而另一方面,不久前,开发人员被警告说它dirname不如basename.

以供参考:

在考虑可移植性时,我必须考虑全部我维护程序的系统。并非所有都是 POSIX,因此需要权衡。您的权衡可能会有所不同。

答案3

还有:

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

0

发生这样奇怪的事情是因为当两个进程通信时需要进行大量的解释和解析以及其他操作。命令替换将删除尾随换行符。和 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.

……也许评论让人分心……

答案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了一些特殊的命令行开关,用于处理多个参数或后缀剥离等内容,但这在 shell 中添加起来非常容易。)

将它们制作成bash内置函数也不那么困难(通过利用底层系统实现),但上述函数不需要编译,并且它们也提供了一些提升。

相关内容