Bash变量替换与dirname和basename

39
下一个脚本。
str=/aaa/bbb/ccc.txt
echo "str: $str"
echo ${str##*/} == $(basename $str)
echo ${str%/*} == $(dirname $str)

产生:

str: /aaa/bbb/ccc.txt
ccc.txt == ccc.txt
/aaa/bbb == /aaa/bbb

问题是:

  • 在bash脚本中,何时建议使用命令dirnamebasename,何时使用变量替换?为什么?

主要是因为想知道:

str="/aaa/bbb/ccc.txt"
count=10000

s_cmdbase() {
let i=0
while(( i++ < $count ))
do
    a=$(basename $str)
done
}

s_varbase() {
let i=0
while(( i++ < $count ))
do
    a=${str##*/}
done
}

s_cmddir() {
let i=0
while(( i++ < $count ))
do
    a=$(dirname $str)
done
}

s_vardir() {
let i=0
while(( i++ < $count ))
do
    a=${str%/*}
done
}

time s_cmdbase
echo command basename
echo ===================================
time s_varbase
echo varsub basename
echo ===================================
time s_cmddir
echo command dirname
echo ===================================
time s_vardir
echo varsub dirname

在我的系统上产生的结果:

real    0m33.455s
user    0m10.194s
sys     0m18.106s
command basename
===================================

real    0m0.246s
user    0m0.237s
sys     0m0.007s
varsub basename
===================================

real    0m30.562s
user    0m10.115s
sys     0m17.764s
command dirname
===================================

real    0m0.237s
user    0m0.226s
sys     0m0.007s
varsub dirname

调用外部程序(分叉)需要时间。问题的主要点是:

  • 使用变量替换代替外部命令是否存在一些陷阱?
  • 使用变量替换代替外部命令是否存在一些风险?

1
我会说:dirnamebasename是非常精确的工具,适用于像这样的情况。变量替换则适用于更一般的情况。因此,当我需要目录名称时,我会使用dirname,当我需要文件名时,我会使用basename,而当我需要更一般的东西而没有特定的工具可用时,则使用变量替换。 - fedorqui
2
@fedorqui 我认为dirnamebasename更容易阅读,特别是对于不经常编写shell代码的人来说(这样就可以维护+1),但性能差异是一个公正的观点。我会认为,一旦你需要在循环内部使用它们(而不仅仅是在$0上),你将想要考虑使用参数替换。 - Adrian Frühwirth
如果考虑到性能(即花费大量时间来执行“dirname”/“basename”),则使用参数扩展。但是,如果可读性/健壮性更重要,则使用更简单/更容易阅读的“basename”/“dirname”。通常更需要可读性…所以通常最好坚持使用“basename”/“dirname”。 - Trevor Boyd Smith
3个回答

33

外部命令进行了一些逻辑修正。请检查下一个脚本的结果:

doit() {
    str=$1
    echo -e "string   $str"
    cmd=basename
    [[ "${str##*/}" == "$($cmd $str)" ]] && echo "$cmd same: ${str##*/}" || echo -e "$cmd different \${str##*/}\t>${str##*/}<\tvs command:\t>$($cmd $str)<"
    cmd=dirname
    [[ "${str%/*}"  == "$($cmd $str)" ]] && echo "$cmd  same: ${str%/*}" || echo -e "$cmd  different \${str%/*}\t>${str%/*}<\tvs command:\t>$($cmd $str)<"
    echo
}

doit /aaa/bbb/
doit /
doit /aaa
doit aaa
doit aaa/
doit aaa/xxx

结果为

string   /aaa/bbb/
basename different ${str##*/}   ><          vs command: >bbb<
dirname  different ${str%/*}    >/aaa/bbb<  vs command: >/aaa<

string   /
basename different ${str##*/}   ><  vs command: >/<
dirname  different ${str%/*}    ><  vs command: >/<

string   /aaa
basename same: aaa
dirname  different ${str%/*}    ><  vs command: >/<

string   aaa
basename same: aaa
dirname  different ${str%/*}    >aaa<   vs command: >.<

string   aaa/
basename different ${str##*/}   ><  vs command: >aaa<
dirname  different ${str%/*}    >aaa<   vs command: >.<

string   aaa/xxx
basename same: xxx
dirname  same: aaa

最有趣的结果之一是 $(dirname "aaa")。外部命令 dirname 正确地返回 .,但变量扩展 ${str%/*} 返回了错误的值 aaa

其他表示方法

脚本:

doit() {
    strings=( "[[$1]]"
    "[[$(basename "$1")]]"
    "[[${1##*/}]]"
    "[[$(dirname "$1")]]"
    "[[${1%/*}]]" )
    printf "%-15s %-15s %-15s %-15s %-15s\n" "${strings[@]}"
}


printf "%-15s %-15s %-15s %-15s %-15s\n" \
    'file' 'basename $file' '${file##*/}' 'dirname $file' '${file%/*}'

doit /aaa/bbb/
doit /
doit /aaa
doit aaa
doit aaa/
doit aaa/xxx
doit aaa//

输出:

file            basename $file  ${file##*/}     dirname $file   ${file%/*}     
[[/aaa/bbb/]]   [[bbb]]         [[]]            [[/aaa]]        [[/aaa/bbb]]   
[[/]]           [[/]]           [[]]            [[/]]           [[]]           
[[/aaa]]        [[aaa]]         [[aaa]]         [[/]]           [[]]           
[[aaa]]         [[aaa]]         [[aaa]]         [[.]]           [[aaa]]        
[[aaa/]]        [[aaa]]         [[]]            [[.]]           [[aaa]]        
[[aaa/xxx]]     [[xxx]]         [[xxx]]         [[aaa]]         [[aaa]]        
[[aaa//]]       [[aaa]]         [[]]            [[.]]           [[aaa/]]       

1
请注意,$(dirname /) 的结果是 /,但 var=/; echo "${var%/*}" 的结果为空行。同样地:$(dirname abc/) 的结果是 .,但 var=abc/; echo "${var%/*}" 的结果是 abc,而 $(dirname abc//) 的结果也是 .,但 var=abc//; echo "${var%/*}" 的结果是 abc/ - Jonathan Leffler
@JonathanLeffler :) 你的前两个例子(/aaa/)已经被涵盖了,双斜杠abc//(最后一个例子)是一个不错的补充。顺便说一下,感谢你的编辑。 - clt60
1
我已经添加了一个表格展示——代码和数据。如果您不喜欢它,请将其删除(回滚编辑)。根据自己的需要进行微调。我发现它比您的输出更易读(除了选择用[[]]来包围字符串),但它没有标记来指示结果是相同还是不同。 - Jonathan Leffler
我注意到如果文件路径指向文件而不是目录,则参数扩展似乎完全正常。因此,我使用 [ -f "${file}" ] && path="${file##*/}" || path="${file}"。即使包括额外的条件,速度也快了约350倍!对于我的用例(在.lessfilter中),我只关心文件(和由||条件覆盖的URL),所以我非常愿意采用更快的选项。 - Shaun Mitchell

11
  1. 如果dirname的参数中不含斜杠/,它将输出.,因此使用参数替换模拟dirname取决于输入,结果可能不相同。

  2. basename接受一个后缀作为第二个参数,这也会从文件名中删除该组件。您也可以使用参数替换来模拟此行为,但由于不能同时进行两者,所以当使用basename时,它不像参数替换那样简洁。

  3. 使用dirnamebasename都需要子shell,因为它们不是shell内建命令,因此参数替换将更快,特别是在循环调用它们时(正如您已经展示的一样)。

  4. 我在不同系统上的不同位置看到过basename(例如/usr/bin/bin),因此如果您必须在脚本中使用绝对路径,由于找不到可执行文件,它可能会出错。

因此,是的,有些事情需要考虑,根据情况和输入,我会使用两种方法。

编辑:实际上,dirnamebasename都作为bash可加载的builtin在源树中的examples/loadables目录下提供,并且可以使用以下命令(一旦编译完成)启用。

enable -f /path/to/dirname dirname
enable -f /path/to/basename basename

7
使用变量替换的主要陷阱在于它们可能难以阅读和支持。当然,这是主观的!个人而言,我到处使用变量替换。我使用 read、IFS 和 set 替代 awk。我使用 bash 正则表达式和 bash 扩展 globbing 替代 sed。但这是因为:
a) 我需要性能
b) 这些脚本只有我一个人会看到
令人悲哀的是,许多必须维护 shell 脚本的人对该语言了解甚少。你必须做出平衡决策:哪个更重要,性能还是可维护性?在大多数情况下,你会发现可维护性胜出。
你必须承认,basename $0 相当明显,而 ${0##*/} 相当晦涩。

1
+1 你在我的上面评论中扩展了我的观点,所以我同意,尽管我仍然更喜欢使用参数替换来获得明显的性能影响。在我看来,可读性(以及如果需要行为,则为dirname foo-no-slash的边缘情况)是使用外部变量的唯一论据,因此我仍然反对使用它们。 - Adrian Frühwirth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接