dirname和basename与参数扩展


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使用外部二进制文件。)

Answers:


21

不幸的是,他们俩都有自己的怪癖。

两者都是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

边缘情况是出现斜杠(包括根目录的情况,即所有斜杠)。该basenamedirname命令去掉结尾的斜杠做他们的工作才。如果您坚持使用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

¹ 除非您使用的是Solaris 10及更早版本的POSIX之前的外壳/bin/sh(在仍在生产中的机器上缺少参数扩展字符串操作功能-但是sh安装中始终有一个POSIX外壳被调用,只有/usr/xpg4/bin/sh,而不是/bin/sh)。
² 例如:向foo␤文件上传服务提交一个名为的文件,但不能防止这种情况的发生,然后将其删除并导致foo被删除


哇。因此,听起来(在任何POSIX Shell中)最可靠的方法就是您提到的第二种方法? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}?我正在仔细阅读,但没有注意到您提到任何缺点。
通配符

1
@Wildcard的一个缺点是,它对待方式foo/类似于foo,而不是foo/.,与POSIX兼容实用程序不一致。
吉尔(Gillles)“所以别再邪恶了”

知道了谢谢。我想我还是更喜欢这种方法,因为我会知道我是否要处理目录,/如果需要的话,我可以随便添加(或“重新添加”)尾随。
通配符

“例如find,始终包含目录部分且没有尾随的结果/”不太正确,find ././作为第一个结果输出。
塔维安·巴恩斯

@Gilles换行符示例使我震惊。感谢您的回答
山姆·托马斯

10

两者都在POSIX中,因此“应该”不应该考虑可移植性。应该假定shell替换运行得更快。

但是-这取决于您所说的便携式设备。一些(不是必需的)旧系统没有在它们中实现这些功能/bin/sh(想到的是Solaris 10和更早的版本),而另一方面,前一段时间,开发人员被警告说它dirname不如basename

以供参考:

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


7

还有:

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

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是关于如何使用Shell的一组说明。但是[!charclass]用globs [^class]进行移植的便携式方法是用于正则表达式的,而shell不是用于正则表达式的。关于注释匹配... case过滤器,所以如果我符合其中要包含一个斜线的字符串/ 一个!/那么如果下一个案例模式下面任何尾随的比赛/斜线都只能是所有的斜杠。下面的一个不能有任何结尾/
mikeserv

2

您可以从工艺得到提升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实现basenamedirname添加了一些特殊的命令行开关来处理诸如处理多个参数或后缀剥离之类的事情,但这在外壳程序中添加起来非常容易。)

bash可以通过内置的系统实现将它们构建为内置函数并不难,但是上面的函数不需要编译,它们也可以提供一些帮助。


边缘案例列表实际上非常有帮助。这些都是非常好的观点。该列表实际上看起来很完整。真的还有其他优势吗?
通配符

我以前的实现无法x//正确处理类似问题,但是在回答之前,我已经为您修复了问题。我希望就是这样。
PSkocik

您可以运行脚本来比较这些示例上的功能和可执行文件的功能。我得到100%的比赛。
PSkocik

1
您的dirname函数似乎并没有消除重复出现的斜杠。例如:dirname a///b//c//d////eyields a///b//c//d///
codeforester
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.