Bash中的双引号与星号文件名扩展有何区别?

5
在目录~/temp/a/foo/~/temp/b/foo foo/中,我有一些名为bar1bar2bar bar1bar bar2等的文件。

我尝试编写一行Bash命令,将所有这些包含"foo"作为名称最后一部分的文件复制到上级"foo"文件夹所在的目录中。

只要文件名中没有空格,这是一个简单的任务,但是以下两个命令在处理foo foo目录时会失败:

for dir in `find . -type d -name '*foo'` ; do cp $dir/* "$(echo $dir|sed 's_foo__g')" ; done

cp命令无法将"foo foo"中的最后一个foo视为同一目录名称的一部分。)
for dir in `find . -type d -name '*foo'` ; do cp "$dir/*" "$(echo $dir|sed 's_foo__g')" ; done

("$dir/*" 没有被展开。)

尝试用 "$(echo $dir/*)" 代替 $dir/* 的方法甚至更加不成功。

是否有一种简单的方法来展开 $dir/*,以便 cp 能够理解?


请参阅http://mywiki.wooledge.org/DontReadLinesWithFor。 - Charles Duffy
3个回答

4

不仅 for 循环是错误的 - sed 也不是完成这项任务的正确工具。

while IFS= read -r -d '' dir; do
  cp "$dir" "${dir/foo/}"
done < <(find . -type d -name '*foo' -print0)

使用-print0find命令中(以及IFS= read -r -d '')可以确保文件名中包含换行符时不会出错。

使用< <(...)结构可以确保循环内部设置变量、更改目录状态或执行类似操作的情况下,这些更改将保留下来(在bash中,管道右侧是子shell,因此将管道传输到while循环中意味着在退出该循环时丢弃对shell状态所做的任何更改)。

使用${dir/foo/}比调用sed要高效得多,因为它在bash内部执行字符串替换。


你确定那里需要 IFS= 吗?顺便说一下,文章写得很好! - janos
尽管Janos建议接受此答案作为解决方案,但事实证明该代码实际上并未移动任何文件。Bash 4.2.24抱怨cp无法统计''。我不确定cp "$dir" "${dir/foo/}"这一行的作用是什么。它是否真的试图将文件复制到位于“foo”目录上方的现有目录中? - uvett
@uvett Dan找到了错误所在。如果这样还是不行的话,请确保你的文件以 #!/bin/bash 开头;因为这段代码不能在 #!/bin/sh 下运行。 - Charles Duffy
@janos IFS= 防止尾随空格被剥离。 (这也可以通过不指定 dir 并允许读取的内容分配给 $REPLY 来避免)。 - Charles Duffy
非常甜美和健壮,再次感谢。在我描述问题的那个问题中,“cp -r”被证明是不必要的。不确定是否应编辑此答案中的代码,因为字符串替换部分可以(应该)省略,但同时它也是一些有趣评论的主题。 - uvett
显示剩余2条评论

2
这里的问题不在于cp,而是在于for,因为默认情况下它按单词而非目录名来分割子shell的输出。
一个简单的解决方法是使用while,逐行处理目录列表,像这样:
find . -type d -name '*foo' | while read dir; do cp "$dir"/* "$(echo $dir | sed 's_foo__g')" ; done

这应该可以解决你在空格方面的问题,但这绝不是一个万无一失的解决方案。 请参考Charles Duffy的答案,以获得更准确的解决方案。

几乎正确,但你真的应该使用NUL分隔管道的内容——否则,一个恶意构造的文件名包含换行符,就可以导致任意文件被复制。 - Charles Duffy
顺便提一下,dir 永远不会包含空格并不准确,这是因为 for 的工作方式而产生的副作用;单词分割不是 for 语义的一部分,如果将 IFS 设置为不包含空格,则字符串分割输出可能包含空格。话虽如此,依赖于字符串分割出错的原因还有其他(例如,因为通配符扩展同时发生)。 - Charles Duffy
谢谢,这似乎适用于我所有的真实文件复制问题。它在我的简化示例上实际上失败了,这是一个有趣的细节。(sed删除了“foo foo”文件夹名称中的两个“foo”)。 - uvett
@CharlesDuffy “几乎正确”……离真相还有一定距离,是吧 :-) 但这正是我为什么加上“绝不是万无一失”的原因。感谢您的建议,我会重新表述我关于for的写法。 - janos
@uvett,我建议你接受Charles Duffy的答案,而不是我的。他的回答更准确、解释更清晰,并且考虑了所有边角情况。 - janos

0
~/temp/b/foo foo/ 重命名为没有空格的名称,例如 ~/temp/b/foo_foo/,然后再尝试您要做的事情。之后,如果您真的需要,可以将其重新命名回原始名称,包含空格。顺便说一句,我从不使用包含空格的文件名,只是为了避免像您现在面临的这种复杂情况。

我试图简化一个涉及数十个文件夹和数百个具有丑陋名称的文件的问题。重命名它们似乎是一个次优选项,特别是因为我之后还需要恢复文件夹名称。 - uvett

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