替换命令结果时查找“路径必须先于表达式”

替换命令结果时查找“路径必须先于表达式”

我正在尝试运行find以排除文件中的目录列表。该文件也由不同的命令使用,因此我坚持文件的格式,即每行包含目录名“按原样”后跟斜杠,例如

My Files/

我尝试了各种方法,例如 xargs,但我无法正常工作,但最终决定预处理黑名单以生成要查找的参数,如下所示:

find . -type f $(cat .blacklist.txt | while read -r line; do printf "! -path './%s*' -prune " "$line"; done)

但是,每当我在黑名单中遇到包含空格的目录名称时,都会收到错误:

find: paths must precede expression: `<Last Part Of Name>/*''

我曾经set -x试图弄清楚发生了什么,看起来目录名称的各个部分被单独引用。例如,假设列表中有一个名为“我的文件”的目录名称。我看到set -x文件中这一特定行的输出:

++ printf '! -path '\''./%s*'\'' -prune ' 'My Files/'

到目前为止我认为一切都很好。但是当我在最终组装的命令中看到它的输出时,它看起来像这样:

'!' -path ''\''./My' 'Files/*'\''' -prune 

问题很明显:它正在将我努力保留的目录名称变成两部分!但我不明白为什么它会这样做。我尝试了一些变体,例如:

printf "! -path './%s*' -prune " "$line"

我认为以某种方式我需要确保路径的结果输出在从 printf 中出来时被引用,但我已经尝试了所有这些,但没有一个起作用:

printf "! -path \"./%s*\" -prune " "$line"
printf "! -path "'./$line*'" -prune "
printf "! -path '"./$line*"' -prune "

但这些都无法阻止目录名称中的各个单词被拆分。

我也尝试过这个:

printf "! -path ./%s* -prune " "$line"

这可以避免分割目录名,但由于它不再被引用,它会扩展为该路径下的所有子目录,这是错误的,并且还会破坏命令,因为现在有多个子目录,其中只需要一个目录。

%s如果我使用后跟参数作为 的单独参数,或者直接printf替换%swith ,似乎也并不重要。$line

答案1

并且目录名称的各个部分似乎被单独引用。

我不会这么说。发生的情况是,当您! -path './My Files*' -prune从命令替换中打印 例如 时,它会被分词为!, -path, './My, Files*'-prune因为分词步骤不会处理像引号这样的语法,而只会查看空白字符(或者您已经知道的任何字符)设置IFS)。这Files*'也将充当一个 glob,但您可能没有以单引号结尾的文件名。

您在输出中看到的引号set -x之所以存在,是因为 Bash 使该输出有效作为 shell 的输入进行显示。转义的引用是您从 打印出来的引用printf

在这里,您需要生成!, -path, ./My Files*,-prune作为 的不同参数find。一种方法是将参数收集到数组中。

#!/bin/bash
args=()
filename='./My Files'
args+=( ! -path "$filename*" -prune )
# etc.
find . -type f "${args[@]}"

但你可能真的需要它

find . -type f ! -path './somedir/*' ! -path './otherdir/*'

(没有-prune,遍历整个树,包括列出的目录,只是忽略其中的任何内容),或者像 Stéphane Chazelas 在他们的答案中显示的那样,

find . \( -path ./somedir -o -path ./otherdir \) -prune -o -type f -print

(甚至不能进入列出的目录,您需要转义()shell)。

如果排除的名称可以包含 的find模式匹配 ( *?[) 所特有的字符,则需要使用反斜杠以及任何反斜杠转义这些字符。

如果我们使用前一种模式,并忽略转义:

#!/bin/bash
args=(-type f)
while read -r filename; do
    args+=( ! -path "./$filename/*" )
done < excluded.txt
find . "${args[@]}"

更好的方法是更复杂一些,如下所示:

#!/bin/bash
args=()
first=1
while read -r escaped; do
    if [ "$first" != 1 ]; then
        args+=( -o )
    fi
    args+=( -path "./$escaped" )
    first=0
done < <(sed -e 's/[[?*\]/\\&/g' < excluded.txt)
find . \( "${args[@]}" \) -prune -o -type f -print

(这里,还使用进程替换 ( <(cmd...)) 通过 sed 通过管道传输文件名列表以进行转义)

构建数组时的关键是将参数完全按照将它们放入命令中的方式放入赋值中,而不是添加任何额外的引号。然后,在使用数组时,您必须只关注"${args[@]}"语法,注意引号。

答案2

/如果目的是查找常规文件,请跳过每行列出一个路径后跟 a 的目录中的文件.blacklist.txt,然后该文件包含:

my dir/
 my [other] dir?/sub dir/

例如,您需要使用以下参数调用 find:

  1. find
  2. .
  3. (
  4. -path
  5. ./my dir
  6. -o
  7. -path
  8. ./ my \[other\] dir\?/sub dir[并且?转义,否则它们会被finds特殊对待-path;在大多数实现中转义]不是必需的,find但不会造成伤害)
  9. )
  10. -prune# 修剪上面匹配的
  11. -o# 还要别的吗:
  12. -type
  13. f# 类型为“常规”的文件
  14. -print

所以你需要为每一行:

  • 删除尾随/
  • 转义特殊字符-path\?*[]
  • 前置./
  • 前置-path一行

然后在每个参数之间放置一个-o参数,并将结果行收集到一个列表中以传递给find.

readarray -t要进行这种分割,在 bash shell 中,您可以使用和 inzsh参数扩展标志来完成f。您可以像您一样使用 split+glob ,省略周围的引号$(...),但您需要首先设置$IFS为仅换行符并禁用我们不想要的 glob 部分(在 zsh 中未完成)set -o noglob

bash

readarray -t args < <(
  sed 's|/$||
       s|[][\\?*]|\\&|g
       s|^|./|
       1!i\
-o
       i\
-path' .blacklist.txt
)
find . '(' "${args[@]}" ')' -prune -o -type f -print

在 zsh 中:

find . '(' ${(f)"$(
  sed 's|/$||
       s/[][\\?*]/\\&/g
       s|^|./|
       1!i\
-o
       i\
-path' .blacklist.txt)"} ')' -prune -o -type f -print

zsh具有b参数扩展标志来转义 glob 运算符,并且在该 shell 中,您可以使用带有Nullglob 限定符的 globbing 将黑名单减少到实际存在的目录并P重新附加-o-path参数。

() {
  find . '(' $@[2,-1] ')' -prune -o -type f -print
} ./${(f)^"$(<.blacklist.txt)"}(Ne['REPLY=${(b)REPLY%/}']P[-o]P[-path])

在这里,我们使用 glob 限定符P-o-path放在每个文件前面P,将列表传递给匿名函数,并将倒数第二个参数传递给find( $@[2,-1])。鉴于您可能想.blacklist.txt从结果中省略自身,您也可以这样做:

find . '(' -path ./.blacklist.txt ./${(f)^"$(<.blacklist.txt)"}(Ne['REPLY=${(b)REPLY%/}']P[-o]P[-path]) ')' -prune -o -type f -print

如果您有 GNUfind或兼容版本,则替代方法-path不需要进行转义、删除尾随/或前置的操作./是使用-samefile

find . '(' -samefile .blacklist.txt ${(f)^"$(<.blacklist.txt)"}(NP[-o]P[-path]) ')' -prune -o -type f -print

答案3

我会用 来做到这perl一点文件::查找find因为与仅编写简单的过程脚本相比,构建具有大量谓词的长命令是一个 PITA。

#!/usr/bin/perl

use strict;
use File::Find;
use autodie qw(open);

# load the blacklist into an array
my @blacklist;
open(my $BL,"<","blacklist.txt");
while(<$BL>) {
  # Assume one directory per line. Paths are treated
  # as regular expressions, not as literal strings.
  chomp;
  push @blacklist, $_;
};
close($BL);

# generate a regexp to match all the entries in the array
our $blacklist_re = "^(?:" . join("|",@blacklist) . ")";
#print "$blacklist_re\n";

find { wanted => \&wanted, preprocess => \&prune }, '.';

sub wanted { -f && print "$File::Find::name\n" };
sub prune  { return grep { ! -d || ! m/$blacklist_re/ } @_ };

更新于 2023 年 4 月 24 日 - 此版本实际上通过使用预处理子例程来修剪不需要的目录,这样File::Find就不会下降到它们中。

prune每次find进入目录时都会调用预处理子例程(在本例中包括作为find函数参数给出的任何目录.)。

目录中的文件名(包括目录、套接字、设备节点等)作为数组传递给prune,它返回一个文件名数组,这些文件名要么不是目录,要么与黑名单正则表达式不匹配。这使用了 perl 的内置grep函数 - 这不是外部/bin/grep命令,它对列表/数组而不是文件进行操作。看perldoc -f grep

任何不在 prune 返回的数组中的文件名都会被排除在进一步处理之外find,因此wanted子例程已简化为只是一个-f测试 -wanted甚至从未看到修剪过的目录或文件。

示例运行:

包含blacklist.txt以下内容的文件:

foo bar
bar baz

并创建这些目录和一些虚拟文件:

mkdir 'foo bar' 'bar baz' 
touch 'bar baz/a.txt' 'foo bar/b.txt' 'foo bar/c.txt'
touch d.txt

目录结构如下所示:

.
├── bar baz
│   └── a.txt
├── blacklist.txt
├── d.txt
├── ff.pl
├── foo bar
    ├── b.txt
    └── c.txt

运行脚本(将其另存为,例如,ff.pl并使其可执行chmod +x ff.pl)会产生以下输出:

$ ./ff.pl 
./ff.pl
./blacklist.txt
./d.txt

即 blacklist.txt 中的目录被排除在输出之外。

答案4

我可能会做什么(假设bash和 GNU find):

find . -exec fgrep -qx \{} $(sed 's#/$##' .blacklist.txt) \; -prune -o -type f -print

如果黑名单很长,效率不是很高,但它适用于大多数文件名。 (最多:例如,带有换行符的文件名仍然是一个问题。)

相关内容