Shell 技巧 - 处理文件名中的空格

作者:Dave Taylor

在 UNIX 年轻的旧时代,没有人会想到在文件名中放入空格。这根本不会发生——就像您永远不会在 DOS 或 Windows 系统上这样做一样。文件名简短、简洁且格式良好,例如 HW43.DOC。

大多数 Linux 命令行实用程序和 shell 本身的设计都基于这样一个前提:空格分隔字段值,而不是作为文件名的可接受组成部分。如果您做过任何脚本编写,您已经知道这一点。文件名中的空格会在 shell 脚本中引起很大的麻烦!这是一个简单的例子

for name in $(ls | grep a)
do
  echo "File #$count = $name"
  count=$(( $count + 1 ))
done

为了做好准备,我创建了一个目录,其中包含一些棘手的文件名

$ ls
"quoted" beastly filename      sample2.txt
multi-word file name.pdf           test.sh

是的,为了最大限度地增加麻烦,我有一个文件名,其中包含引号和空格。不要让我开始在名称中包含转义字符或不可打印字符。这是可以做到的,但我会尽快重命名它。

并非所有上面的文件名都包含“a”,所以让我们看看当片段脚本在此目录中运行时会发生什么

$ ./test.sh
File # = "quoted"
File #1 = beastly
File #2 = filename
File #3 = multi-word
File #4 = file
File #5 = name.pdf
File #6 = sample2.txt

哦,那是多么丑陋和错误!

如果文件名足够简单,shell 可以处理这些文件名,并且 for 循环for name in *a*产生三个文件名,而不是六个,但是在您的脚本编写旅程中,您不可避免地会遇到嵌入空格的问题。

最常见的错误是当然是在脚本中的其他地方使用文件名时忘记引用它们。例如,让我们处理一个将文件名中的空格替换为下划线的脚本。

带有 Bug 的文件重命名

这种重命名的显而易见的解决方案是这样的

for name in "* *"
do
  newname="$(echo $name | sed 's/ /_/g')"
  mv $name $newname
done

然而,这不起作用,而且以一种最有趣的方式

mv "quoted" beastly filename multi-word file 
 ↪name.pdf sample2.txt test.sh "quoted" 
 ↪beastly filename multi-word file 
 ↪name.pdf sample2.txt test.sh 
↪"quoted"_beastly_filename_multi-word_file_
↪name.pdf_sample2.txt_test.sh_"quoted"_beastly_
↪filename_multi-word_file_name.pdf_sample2.txt_test.sh

发生的事情是"* *"只是生成两个完整的文件名列表,而不仅仅是那些包含空格的文件名——糟糕。让我们尝试不同的模式

for name in *\ *

这招奏效了,但我们没有考虑到当 shell 看到像这样的行时

mv multi-word file name.pdf multi-word_file_name.pdf

它会抱怨它看到四个文件名参数给mv命令,而不是所需的两个

usage: mv [-f | -i | -n] [-v] source target
       mv [-f | -i | -n] [-v] source ... directory

在这种情况下,解决方案是用引号引用文件名变量

mv "$name" $newname

作为一种习惯,最好始终在任何上下文中引用您引用的文件名,以确保当 shell 将它们作为参数传递给命令时,可以正确处理带有嵌入空格的文件名。

然而,这不是通用的解决方案,因为如果您使用子 shell 和管道,引号很难在多个步骤中幸存下来。

一种可行的方法是将 shell 中的内部字段分隔符 IFS 设置为除空格以外的其他内容,如 Bash 手册页中所述

IFS:内部字段分隔符,用于扩展后进行单词拆分,以及使用 read 内置命令将行拆分为单词。默认值为“<space><tab><new-line>”。

这对于“read”非常有用,特别是如果您正在读取文本行并希望使用不同的字段分隔符(想想平面文件文本数据库文件),但它仍然没有真正解决我们的文件名问题。

我过去使用过的一种方法,虽然它是一种草率、粗糙的解决方案,但首先是将空格更改为一些不太可能的字符序列,运行所有处理,并在最后一秒将它们改回来。例如

safename="$(echo name | sed 's/ /_-_/g')"

并使用以下命令反转

original="$(echo $safename | sed s'/_-_/ /g')"

它解决了问题,但这绝对不是一种非常有效或智能的计算资源使用方式。

我在此处概述了三种可能的解决方案路径:修改 IFS 值,确保始终引用引用的文件名,以及在内部重写文件名以将空格替换为不太可能的字符序列,并在退出脚本时将其反转。

顺便说一句,您是否尝试过将 find|xargs 对与带有空格的文件名一起使用?它非常复杂,以至于这两个命令的现代版本都有特殊的参数来表示空格可能作为文件名的一部分出现find -printxargs -0(通常,它们不是相同的标志,但那是另一个故事)。

在撰写本专栏的这些年中,我不止一次被这个特殊问题绊倒,并收到读者的电子邮件,分享一个示例脚本如何在文件名中出现空格时抛出它的位。他们是对的。

我的防御性反应是“伙计,不要在文件名中使用空格”,但这真的不是一个好的长期解决方案,对吗?

我希望做的是在 Linux Journal 讨论区上开放此讨论:您如何在脚本中解决此问题?或者,您只是虔诚地避免在文件名中使用空格吗?

Dave Taylor 从事 shell 脚本编程已经很长时间了,30 年。他是流行的 Wicked Cool Shell Scripts 的作者,可以在 Twitter 上通过 @DaveTaylor 找到他,更常见的是在 www.DaveTaylorOnline.com

加载 Disqus 评论