为什么扩展的 glob */ 不能用 !(*/) 来否定?

为什么扩展的 glob */ 不能用 !(*/) 来否定?

在 bash 中,如果我们激活extglobshell 选项,我们可以做一些奇特的事情,比如否定 glob(来自man bash):

   If  the  extglob shell option is enabled using the shopt builtin, sev‐
   eral extended pattern matching operators are recognized.  In the  fol‐
   lowing  description,  a pattern-list is a list of one or more patterns
   separated by a |.  Composite patterns may be formed using one or  more
   of the following sub-patterns:

          ?(pattern-list)
                 Matches zero or one occurrence of the given patterns
          *(pattern-list)
                 Matches zero or more occurrences of the given patterns
          +(pattern-list)
                 Matches one or more occurrences of the given patterns
          @(pattern-list)
                 Matches one of the given patterns
          !(pattern-list)
                 Matches anything except one of the given patterns

例如:

$ ls
a1file  a2file  b1file  b2file

$ shopt -s extglob
$ ls !(a*)
b1file  b2file

现在,如果我只想对目录执行某些操作,我可以使用*/

$ ls -F
a1file  a2file  adir/  b1file  b2file  bdir/

$ ls -d */
adir/  bdir/

然而,glob*/显然不能被否定:

$ ls -Fd !(*/)
a1file  a2file  adir/  b1file  b2file  bdir/

是什么赋予了?为什么正确仅包含目录时不!(*/)排除目录?*/有没有办法在 bash 中使用 glob 来排除目录?

上述命令是在 Arch Linux 系统上使用 GNU bash 版本 5.1.8(1)-release 进行测试的。

答案1

因为球体不会跨越/边界。除了 的特殊情况**/(最初来自zsh现在在其他一些外壳中也发现了通常在设置选项(shopt -s globstar对于 bash)之后,glob 运算符无法匹配包含 a 的内容,/因为它们应用于目录列表。

shell在s上分裂了一个x/y/z球体。/对于每个组件,如果该组件包含 glob 运算符,则 shell 会列出父目录并再次匹配每个条目的模式,如果没有,则仅查找带有lstat()² 的文件。

你会发现a*b/c不会匹配上a/b/c。 shell 仅匹配a*b当前目录中的条目。 Even[a/b]*被视为[ab]*用 分隔/

*/*且没有任何东西与 分开/。这是一种特殊情况,*/xshell 首先查找*当前目录列表中匹配的所有文件,然后对于每个文件,尝试查看名为 的文件是否file/x存在(lstat()在这种情况下使用,不列出目录,因为x不包含全局运算符)。*/它是相同的,除了它检查 a 是否存在file/(仅当file是目录或目录的符号链接时才是正确的)。

如果您/在 ksh-style @(...), !(...)... 扩展运算符(其中的子集在bash -O extglobor中可用zsh -o kshglob)内使用,则 shell 之间的行为会有所不同,但通常不会执行您想要的操作,因为 glob 中的模式仅与文件匹配目录列表中的名称。在bash,中!(*/)匹配每个(非隐藏)文件名,可能是因为这里 glob 没有在 上拆分/,并且*/根据每个目录条目名称反向检查 ,并且目录条目名称不能包含/.这并不能真正解释为什么!(*[a/b]*)仍然包含包含as 或bs 的文件名,或者为什么!(*[a")"/b])排除包含as 的文件名而不包含包含)s 或bs 的文件名。

如果您想要未确定类型的文件目录在符号链接解析之后,这不是你可以单独使用 glob 完成的事情,你需要使用zsh它的 glob 限定符,它可以真正根据名称以外的属性来选择文件:

print -rC1 -- *(-^/)

在这里,zsh 匹配 glob,然后应用限定符作为 globbing 后的额外步骤。这里-指定在符号链接解析之后应用以下限定符(stat()而不是lstat()),^否定以下限定符,/选择文件类型目录

bash4.4+ 中,您始终可以将作业外包给其他打印 NUL 分隔结果并用于readarray -td ''获取结果的工具,例如:

readarray -td '' files < <(zsh -c 'print -rNC1 -- *(N^-.)')
(( ${#files[@]} )) && ls -Fd -- "${files[@]}"

或者使用 GNUfindsort

readarray -td '' files < <(
  LC_ALL=C find . -mindepth 1 -maxdepth 1 \
    ! -name '.*' ! -xtype d -printf '%P\0' | sort -z)
(( ${#files[@]} )) && ls -Fd -- "${files[@]}"

(这里对 进行排序sort,以获得与 相同的列表zsh,但对于将该列表传递给 的特殊情况ls,它和ls它自己的排序一样是多余的)。

当您有一个 NUL 分隔列表时,您也可以跳过数组步骤并将输出传递给xargs -r0 ls -Fd --,这将避免必须专门处理空列表情况并解决arg 列表太长局限性。


1 不过,另请参阅~扩展 glob 运算符,zsh它可以作为完整 glob 之后的额外步骤应用,以过滤出路径并跨/s 进行匹配。在 中a*/b*/c*~*e*,对 glob 执行文件名生成算法a*/b*/c*,然后使用模式过滤出生成的路径名*e*

² 不区分大小写的通配符可以改变这一点,就像zsh -o nocaseglob

相关内容