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

ter*_*don 7 bash wildcards

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

\n
   If  the  extglob shell option is enabled using the shopt builtin, sev\xe2\x80\x90\n   eral extended pattern matching operators are recognized.  In the  fol\xe2\x80\x90\n   lowing  description,  a pattern-list is a list of one or more patterns\n   separated by a |.  Composite patterns may be formed using one or  more\n   of the following sub-patterns:\n\n          ?(pattern-list)\n                 Matches zero or one occurrence of the given patterns\n          *(pattern-list)\n                 Matches zero or more occurrences of the given patterns\n          +(pattern-list)\n                 Matches one or more occurrences of the given patterns\n          @(pattern-list)\n                 Matches one of the given patterns\n          !(pattern-list)\n                 Matches anything except one of the given patterns\n
Run Code Online (Sandbox Code Playgroud)\n

例如:

\n
$ ls\na1file  a2file  b1file  b2file\n\n$ shopt -s extglob\n$ ls !(a*)\nb1file  b2file\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
$ ls -F\na1file  a2file  adir/  b1file  b2file  bdir/\n\n$ ls -d */\nadir/  bdir/\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
$ ls -Fd !(*/)\na1file  a2file  adir/  b1file  b2file  bdir/\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

Sté*_*las 12

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

\n

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

\n

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

\n

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

\n

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

\n

如果您想要在符号链接解析后未确定为目录类型的文件那么您不能单独使用 glob 来完成此操作,您需要使用zsh及其 glob 限定符,它可以真正根据其他属性选择文件比他们的名字:

\n
print -rC1 -- *(-^/)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
readarray -td \'\' files < <(zsh -c \'print -rNC1 -- *(N^-.)\')\n(( ${#files[@]} )) && ls -Fd -- "${files[@]}"\n
Run Code Online (Sandbox Code Playgroud)\n

或者使用 GNUfindsort

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

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

\n

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

\n
\n

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

\n

\xc2\xb2 不区分大小写的通配可以改变这一点,尽管像zsh -o nocaseglob

\n