it-swarm.cn

循环浏览名称中带有空格的文件?

我编写了以下脚本来区分两个目录中包含相同文件的两个导演的输出,例如:

#!/bin/bash

for file in `find . -name "*.csv"`  
do
     echo "file = $file";
     diff $file /some/other/path/$file;
     read char;
done

我知道还有其他方法可以实现这一目标。但是奇怪的是,当文件中有空格时,此脚本将失败。我该如何处理?

Find的示例输出:

./zQuery - abc - Do Not Prompt for Date.csv
160
Amir Afghani

简短答案(最接近您的答案,但可以处理空格)

OIFS="$IFS"
IFS=$'\n'
for file in `find . -type f -name "*.csv"`  
do
     echo "file = $file"
     diff "$file" "/some/other/path/$file"
     read line
done
IFS="$OIFS"

更好的答案(还处理文件名中的通配符和换行符)

find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

最佳答案(基于 吉尔斯的答案

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

甚至更好,以避免每个文件运行一个sh

find . -type f -name '*.csv' -exec sh -c '
  for file do
    echo "$file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
  done
' sh {} +

长答案

您有三个问题:

  1. 默认情况下,命令行管理程序在空格,制表符和换行符上拆分命令的输出
  2. 文件名可以包含通配符,这些通配符将被扩展
  3. 如果有一个名称以*.csv结尾的目录怎么办?

1。仅在换行符上分割

为了弄清楚将file设置为什么,Shell必须获取find的输出并以某种方式对其进行解释,否则file只是find的整个输出。

Shell读取IFS变量,该变量默认情况下设置为<space><tab><newline>

然后,它查看find输出中的每个字符。一旦看到IFS中的任何字符,它就会认为标记了文件名的末尾,因此它将file设置为到目前为止所看到的任何字符,然后运行循环。然后,它从中断处开始获取下一个文件名,并运行下一个循环等,直到到达输出末尾。

因此,它有效地做到了:

for file in "zquery" "-" "abc" ...

要告诉它仅在换行符上拆分输入,您需要执行

IFS=$'\n'

在您的for ... find命令之前。

这会将IFS设置为单个换行符,因此它仅在换行符上分割,而不是空格和制表符。

如果您使用shdash而不是ksh93bashzsh,则需要这样写IFS=$'\n'

IFS='
'

这可能足以使您的脚本正常工作,但是如果您有兴趣适当处理其他一些极端情况,请继续阅读...

2。扩展$file而不使用通配符

在循环中进行

diff $file /some/other/path/$file

shell将尝试扩展$file(再次!)。

它可以包含空格,但是由于我们已经在上面设置了IFS,所以在这里就不会有问题了。

但是它也可能包含通配符,例如*?,这将导致不可预测的行为。 (感谢吉尔斯指出这一点。)

要告诉命令行管理程序不要扩展通配符,请将变量放在双引号中,例如.

diff "$file" "/some/other/path/$file"

同样的问题也可能咬我们

for file in `find . -name "*.csv"`

例如,如果您有这三个文件

file1.csv
file2.csv
*.csv

(极不可能,但仍然可能)

好像你已经跑步了

for file in file1.csv file2.csv *.csv

它将扩展到

for file in file1.csv file2.csv *.csv file1.csv file2.csv

导致file1.csvfile2.csv被处理两次。

相反,我们必须做

find . -name "*.csv" -print | while IFS= read -r file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

read从标准输入中读取行,根据IFS将行拆分为单词,并将它们存储在您指定的变量名称中。

在这里,我们告诉它不要将行拆分为单词,并将行存储在$file中。

另请注意,read line已更改为read line </dev/tty

这是因为在循环内部,标准输入是通过管道从find输入的。

如果我们只做read,它将消耗一部分或全部文件名,并且将跳过某些文件。

/dev/tty是用户从中运行脚本的终端。请注意,如果脚本是通过cron运行的,则会导致错误,但是我认为在这种情况下这并不重要。

然后,如果文件名包含换行符怎么办?

我们可以通过将-print更改为-print0并在管道末尾使用read -d ''来解决此问题:

find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read char </dev/tty
done

这使得find在每个文件名的末尾放置一个空字节。空字节是文件名中唯一不允许使用的字符,因此,无论多么奇怪,它都应处理所有可能的文件名。

要获取另一端的文件名,我们使用IFS= read -r -d ''

在上面使用read的地方,我们使用了换行符的默认行定界符,但是现在,find将空值用作行定界符。在bash中,您不能在参数中将NUL字符传递给命令(甚至是内置的),但是bash理解-d ''的意思NUL分隔 。因此,我们使用-d ''使read使用与find相同的行定界符。注意,-d $'\0'也可以正常工作,因为不支持NUL字节的bash会将其视为空字符串。

为了正确起见,我们还添加了-r,它表示不要专门处理文件名中的反斜杠。例如,在没有-r的情况下,删除了\<newline>,并将\n转换为n

不需要bashzsh或记住所有上述有关空字节的规则的更便携式方法(再次感谢Gilles):

find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' {} ';'

3。跳过名称以* .csv结尾的目录

find . -name "*.csv"

还将匹配名为something.csv的目录。

为避免这种情况,请将-type f添加到find命令中。

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

正如 glenn jackman 所指出的那样,在这两个示例中,要为每个文件执行的命令都在子shell中运行,因此,如果在循环内更改任何变量,它们将被忘记。

如果您需要设置变量并在循环结束时仍设置它们,则可以重写它以使用过程替换,如下所示:

i=0
while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
    i=$((i+1))
done < <(find . -type f -name '*.csv' -print0)
echo "$i files processed"

请注意,如果您尝试在命令行上复制并粘贴此内容,则read line将消耗echo "$i files processed",因此该命令将不会运行。

为避免这种情况,您可以删除read line </dev/tty并将结果发送到_less之类的寻呼机。


[〜#〜]笔记[〜#〜]

我在循环内删除了分号(;)。您可以根据需要将它们放回去,但不需要。

如今,$(command)`command`更常见。这主要是因为写$(command1 $(command2))`command1 \`command2\``容易。

read char并未真正读取字符。它读取整行,因此我将其更改为read line

218
Mikel

如果任何文件名包含空格或Shell globb字符_\[?*_,则此脚本将失败。 find命令每行输出一个文件名。然后,命令行管理程序将命令替换_`find …`_评估如下:

  1. 执行find命令,获取其输出。
  2. find输出拆分为单独的单词。任何空格字符都是单词分隔符。
  3. 对于每个Word,如果它是一种遍历模式,请将其扩展到其匹配的文件列表。

例如,假设当前目录中有三个文件,分别称为_`foo* bar.csv_,_foo 1.txt_和_foo 2.txt_。

  1. find命令返回_./foo* bar.csv_。
  2. 命令行管理程序在空格处拆分此字符串,生成两个单词:_./foo*_和_bar.csv_。
  3. 由于_./foo*_包含全局元字符,因此将其扩展到匹配文件的列表:_./foo 1.txt_和_./foo 2.txt_。
  4. 因此,_for循环依次使用_./foo 1.txt_,_./foo 2.txt_和_bar.csv_执行。

您可以通过减小Word拆分并关闭Globlob来避免此阶段的大多数问题。要减少单词拆分,请将IFS变量设置为单个换行符;这样,find的输出将仅在换行符处分割,并且将保留空格。要关闭通配符,请运行_set -f_。然后,只要没有文件名包含换行符,这部分代码将起作用。

_IFS='
'
set -f
for file in $(find . -name "*.csv"); do …
_

(这不是您的问题的一部分,但我建议在_`…`_上使用$(…)。它们具有相同的含义,但是反引号版本具有奇怪的引号规则。)

下面还有另一个问题:_diff $file /some/other/path/$file_应该是

_diff "$file" "/some/other/path/$file"
_

否则,将_$file_的值拆分为多个单词,并将这些单词视为全局模式,就像上面的命令替换一样。如果您必须记住有关Shell编程的一件事,请记住以下几点:除非您知道,否则始终在变量扩展(_$foo_)和命令替换($(bar)周围使用双引号)你想分裂。 (上面,我们知道我们想将find输出分成几行。)

调用find的一种可靠方法是告诉它为找到的每个文件运行一个命令:

_find . -name '*.csv' -exec sh -c '
  echo "$0"
  diff "$0" "/some/other/path/$0"
' {} ';'
_

在这种情况下,另一种方法是比较两个目录,尽管您必须显式排除所有“无聊”文件。

_diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path
_
22

我很惊讶没有看到readarray。与<<<运算符结合使用时,这非常容易:

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

使用<<<"$expansion"构造还可以将包含换行符的变量拆分为数组,例如:

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

readarray已经在Bash中使用了多年,因此这可能是在Bash中进行此操作的规范方法。

6
blujay

完全安全的查找 循环浏览任何文件(any特殊字符)(请参阅文档链接):

exec 9< <( find "$absolute_dir_path" -type f -print0 )
while IFS= read -r -d '' -u 9
do
    file_path="$(readlink -fn -- "$REPLY"; echo x)"
    file_path="${file_path%x}"
    echo "START${file_path}END"
done
6
l0b0

Afaik查找具有您所需的一切。

find . -okdir diff {} /some/other/path/{} ";"

find会自动节省程序调用。 -okdir将在差异之前提示您(确定是/否)。

不涉及壳牌,不涉足,开玩笑,pi,pa,po。

附带说明:如果将find与for/while/do/xargs结合使用,在大多数情况下,这样做是错误的。 :)

4
user unknown

令我惊讶的是,现在还没有人提到明显的zsh解决方案:

for file (**/*.csv(ND.)) {
  do-something-with $file
}

(D)还包含隐藏文件(N)以避免错误(如果没有匹配项,(.)限制为常规文件。)

bash4.3及以上版本现在也部分支持它:

shopt -s globstar nullglob dotglob
for file in **/*.csv; do
  [ -f "$file" ] || continue
  [ -L "$file" ] && continue
  do-something-with "$file"
done
4
Stéphane Chazelas

如果不加引号,则文件名中带有空格的文件在命令行上看起来像多个名称。如果您的文件名为“ Hello World.txt”,则差异行将扩展为:

diff Hello World.txt /some/other/path/Hello World.txt

看起来像四个文件名。只需在引号周围加上引号即可:

diff "$file" "/some/other/path/$file"
2
Ross Smith

双引号是您的朋友。

diff "$file" "/some/other/path/$file"

否则,变量的内容将被Word拆分。

1
geekosaur

使用bash4,您还可以使用内置的mapfile函数来设置包含每行的数组,并在该数组上进行迭代。

$ tree 
.
├── a
│   ├── a 1
│   └── a 2
├── b
│   ├── b 1
│   └── b 2
└── c
    ├── c 1
    └── c 2

3 directories, 6 files
$ mapfile -t files < <(find -type f)
$ for file in "${files[@]}"; do
> echo "file: $file"
> done
file: ./a/a 2
file: ./a/a 1
file: ./b/b 2
file: ./b/b 1
file: ./c/c 2
file: ./c/c 1
1
kitekat75