当脚本遍历一组完整路径时,通常只需要每个路径的最后一部分:文件名(通常不带扩展名)。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##*/},从而在每次迭代中省去一个子进程。