当脚本遍历一组完整路径时,通常只需要每个路径的最后一部分:文件名(通常不带扩展名)。basename 命令正是做这件事的:它去掉前导的目录路径和可选的后缀,然后打印剩余的部分。

本指南通过实际示例,讲解如何在命令行和 Bash 脚本中使用 basename。

语法

basename 支持两种语法形式:

basename NAME [SUFFIX]

basename OPTION... NAME...

第一种形式接受一个名称和一个可选的要去除的后缀;第二种形式配合 -a 使用,可一次性接受多个名称。

基本用法

最常见的用途是从完整文件路径中去掉目录路径:

basename /etc/passwd
passwd

basename 还会去掉末尾的 / 字符,因此下面两条命令产生的结果相同:

basename /usr/local/

basename /usr/local
local
local

多路径输入

使用 -a(即 --multiple)选项可以一次性处理多个以空格分隔的路径:

basename -a /etc/passwd /etc/shadow
passwd
shadow

这等同于对每条路径分别运行 basename 并收集其输出。

去除末尾后缀

将后缀作为第二个参数传入,即可从结果中去除它:

basename /etc/sysctl.conf .conf
sysctl

使用 -s(即 --suffix=SUFFIX)选项也可以得到相同的结果:

basename -s .conf /etc/sysctl.conf
sysctl

由于 -s 已经隐含了 -a,因此当需要一次性从多个名称中去除某个后缀时,应优先使用这种形式:

basename -a -s .conf /etc/sysctl.conf /etc/sudo.conf
sysctl
sudo

以 NUL 结尾的输出

默认情况下,每个结果都以换行符结尾。使用 -z(即 --zero)选项可以让每行以 NUL 字符而非换行符结尾。这在将输出传递给支持 NUL 分隔输入的命令(如 xargs -0)时很有用:

basename -az -s .conf /etc/sysctl.conf /etc/sudo.conf | xargs -0 echo
sysctl sudo

在 Bash 脚本中使用 basename

在脚本中,常见的做法是使用命令替换捕获结果,并将其保存在变量中:

path="/var/log/nginx/access.log"

file="$(basename "$path")"

echo "$file"
access.log

请为 $path 加上引号,这样包含空格的路径才会被视为单个参数。如果不加引号,Bash 会把路径拆分成多个操作数,导致 basename 可能得出错误结果或以错误退出。

将后缀作为第二个参数传入,可以同时去掉扩展名:

file="$(basename "$path" .log)"

echo "$file"
access

有一个边界情况值得防范:如果名称以短横线开头,basename 会把它当作选项,从而报错退出而不打印任何内容。只要路径来自用户输入或另一条命令的输出,就应使用 -- 将选项与操作数分隔开:

name="$(basename -- "$user_input")"

用参数展开替代 basename

Bash 自身就能完成同样的任务,而无需启动新进程。使用 ##*/ 的参数展开可以删除直到并包含最后一个斜杠在内的所有内容:

path="/var/log/nginx/access.log"

echo "${path##*/}"
access.log

对该结果再应用 %.* 即可去掉扩展名:

file="${path##*/}"

echo "${file%.*}"
access

这两种方式并不完全等价。每次调用 basename 都会派生一个子进程,因此在遍历成千上万个文件的循环中,参数展开的速度明显更快。另一方面,basename 会替你处理末尾斜杠:basename /usr/local/ 会打印 local,而对同样的值使用 ${path##*/} 则会展开为空字符串。

在脚本中批量重命名文件

下面的示例展示了如何在 Bash 的 for 循环中使用 basename,将当前目录下的文件重命名,把 .jpeg 扩展名替换为 .jpg。它使用了 mv -n,因此不会覆盖已存在的 .jpg 文件:

for file in ./*.jpeg; do

    [ -e "$file" ] || continue

    mv -n -- "$file" "$(basename -- "$file" .jpeg).jpg"

done

./ 开头的通配模式可避免以短横线开头的文件名被解析为选项,而存在性检查则在没有匹配到 .jpeg 文件时跳过整个循环。

对于需要更复杂模式匹配的批量重命名,rename 命令通常是一个更简洁的替代方案。

快速参考

如需可打印的快速参考,请参阅 Linux 命令速查表。

Command
basename /path/to/file
basename /path/to/file.txt .txt
basename -s .txt /path/to/file.txt
basename -a /path/one /path/two
basename -a -s .conf /etc/a.conf /etc/b.conf
basename -z /path/to/file
file="$(basename "$path")"
file="${path##*/}"

常见问题

basename 与 dirname 有什么区别?basename 打印路径的最后一个组成部分(文件或目录名),而 dirname 打印目录部分。二者是互补的工具,但它们的输出可能会对重复的或末尾的斜杠做规范化处理。

basename 会修改任何文件吗?不会。basename 只是解析你传入的字符串并打印结果,它不会读取或写入文件系统。

basename 能去除任意后缀,而不仅限于文件扩展名吗?可以。后缀只是对名称末尾的一段普通字符串匹配。basename report_final.txt _final.txt 会输出 report。后缀必须出现在名称的最末尾才会被去除。

如何在 Bash 脚本中不调用 basename 就去除文件扩展名?使用参数展开:${filename%.*} 会去掉最短的匹配 .* 的末尾后缀,而 ${path##*/} 会去掉目录部分。关于二者权衡,请参阅「用参数展开替代 basename」。

总结

basename 命令会从文件名中去除目录路径和可选的后缀,这使它成为在 Shell 脚本中从完整路径里提取文件名的最快捷方式。当同一脚本需要遍历非常庞大的文件列表时,应改用 ${path##*/},从而在每次迭代中省去一个子进程。