bash:用空格移动文件

MrJ*_*s87 13 bash files mv

当我移动文件名中带有空格的单个文件时,它的工作方式如下:

$ mv "file with spaces.txt" "new_place/file with spaces.txt"
Run Code Online (Sandbox Code Playgroud)

现在我有一个可能包含空格的文件列表,我想移动它们。例如:

$ echo "file with spaces.txt" > file_list.txt
$ for file in $(cat file_list.txt); do mv "$file" "new_place/$file"; done;

mv: cannot stat 'file': No such file or directory
mv: cannot stat 'with': No such file or directory
mv: cannot stat 'spaces.txt': No such file or directory
Run Code Online (Sandbox Code Playgroud)

为什么第一个示例有效,而第二个示例无效?我怎样才能让它工作?

ter*_*don 23

永远,永远不要使用for foo in $(cat bar). 这是一个经典的错误,通常被称为bash 陷阱 1 号。你应该使用:

while IFS= read -r file; do mv -- "$file" "new_place/$file"; done < file_list.txt
Run Code Online (Sandbox Code Playgroud)

当您运行for循环,bash将适用wordsplitting什么读取,这意味着a strange blue cloud将被读取astrangebluecloud

$ cat files 
a strange blue cloud.txt
$ for file in $(cat files); do echo "$file"; done
a
strange
blue
cloud.txt
Run Code Online (Sandbox Code Playgroud)

相比于:

$ while IFS= read -r file; do echo "$file"; done < files 
a strange blue cloud.txt
Run Code Online (Sandbox Code Playgroud)

甚至,如果你坚持UUoC

$ cat files | while IFS= read -r file; do echo "$file"; done
a strange blue cloud.txt
Run Code Online (Sandbox Code Playgroud)

因此,while循环将读取其输入并使用read将每一行分配给一个变量。在IFS=将输入字段分隔符为NULL *,和所述-r的选择read停止它从解释反斜杠(使得\t被视为斜线+t而不是作为一个选项卡)。在--mv的意思是“对待一切之后-作为参数,而不是一个选项”,它可以让你处理开始的文件名-正确。


* 这里没有必要,严格来说,在这种情况下唯一的好处是read避免删除任何前导或尾随空格,但是当您需要处理包含换行符的文件名时,这是一个好习惯,或者一般来说,当您需要能够处理任意文件名时。


Sté*_*las 12

$(cat file_list.txt)在 POSIX shell 中,如bash列表上下文中未加引号的是 split+glob 运算符(zsh仅按您的预期执行拆分部分)。

它根据$IFS(默认情况下,SPC, TAB 和 NL)的字符进行拆分,并且除非您完全关闭通配符,否则会进行通配符。

在这里,您只想在换行符上拆分而不想要 glob 部分,所以它应该是:

IFS='
' # split on newline only
set -o noglob # disable globbing

for file in $(cat file_list.txt); do # split+glob
  mv -- "$file" "new_place/$file"
done
Run Code Online (Sandbox Code Playgroud)

这也有优势(在while read循环中)丢弃空行,保留尾随的未终止行,并保留mv的标准输入(例如在提示的情况下需要)。

它也有缺点,虽然该文件的全部内容必须(与像炮弹多次被存储在内存中bashzsh)。

对于某些外壳(kshzsh在较小程度上bash),您可以使用$(<file_list.txt)而不是优化它$(cat file_list.txt)

要使用while read循环执行等效操作,您需要:

while IFS= read <&3 -r file || [ -n "$file" ]; do
  {
    [ -n "$file" ] || mv -- "$file" "new_place/$file"
  } 3<&-
done 3< file_list.txt
Run Code Online (Sandbox Code Playgroud)

或与bash

readarray -t files < file_list.txt &&
for file in "${files[@]}"
  [ -n "$file" ] || mv -- "$file" "new_place/$file"
done
Run Code Online (Sandbox Code Playgroud)

或与zsh

for file in ${(f)"$(<file_list.txt)"}
  mv -- "$file" "new_place/$file"
done
Run Code Online (Sandbox Code Playgroud)

或者使用 GNUmvzsh

mv -t -- new_place ${(f)"$(<file_list.txt)"}
Run Code Online (Sandbox Code Playgroud)

或者使用 GNUmv和 GNUxargs以及 ksh/zsh/bash:

xargs -rd '\n' -a <(grep . file_list.txt) mv -t -- new_place
Run Code Online (Sandbox Code Playgroud)

更多关于在 bash/POSIX shells 中忘记引用变量的安全性含义的不引用扩展意味着什么的更多阅读