使用多种模式运行“ls”时避免非零退出代码

shi*_*shu 8 bash tcsh wildcards brace-expansion pattern-matching

假设我有两个可能的路径,我想列出 Linux 计算机上的目录和文件:

/some/path1/
/some/path2/
Run Code Online (Sandbox Code Playgroud)

如果我在 中执行以下操作,如果或至少之一存在,tcsh我会得到退出代码:0path1path2

ls -d /some/{path1,path2}/*
Run Code Online (Sandbox Code Playgroud)

但是,如果我在 中执行完全相同的操作bash,则会收到2退出代码,并显示一条stderr消息报告path1不存在(如果 path1 是不存在的路径)。

在这种情况下我怎样才能表现bash得像这样?如果至少存在一条路径,是否有一个我可以要求它返回的tcsh开关?如果两者都不存在,我确实期望非零代码,这就是返回的内容。ls0tcsh

Sté*_*las 25

您的大多数问题已经在Why is nullglob not default?中得到了解答。。

\n

需要记住的一件事是:

\n
ls -d /some/{path1,path2}/*\n
Run Code Online (Sandbox Code Playgroud)\n

在csh// tcsh/ zsh/ bash(ksh但不是fish,见下文)中与:

\n
ls -d /some/path1/* /some/path2/*\n
Run Code Online (Sandbox Code Playgroud)\n

由于大括号扩展是在 glob 扩展之前执行的(不是作为 的一部分),因此 shell 将这些/some/pathx/*模式扩展为匹配文件列表,以作为单独的参数传递给ls。

\n

bash,ksh它主要复制的内容继承了 Bourne shell 引入的一个错误功能,即当 glob 模式与任何文件都不匹配时,它会按原样传递,实际上是作为命令的参数。

\n

因此,在这些 shell 中,如果/some/path1/*匹配至少一个文件且不/some/path2/*匹配任何文件,ls则将使用-d、/some/path1/file1和/some/path1/file2文字/some/path2/*作为参数进行调用。由于/some/path2/*文件不存在,ls会报错。

\n

csh其行为类似于早期的 Unix 版本,其中 glob 不是由 shell 执行的,而是由/etc/glob辅助实用程序执行的(该实用程序将其命名为 glob)。该帮助程序将在调用该命令之前执行 glob 扩展,并且如果所有glob 模式均无法与任何文件匹配,则报告No match错误而不运行该命令。否则,只要至少有一个匹配的球体,所有不匹配的球体就会被简单地删除。

\n

因此,在上面的示例中,使用csh/tcsh或古老的 Thompson shell 及其/etc/glob助手,ls将使用-d,/some/path1/file1并且/some/path1/file2仅进行调用,并且很可能会成功(只要/some/path1可搜索)。

\n

zsh既是类似 Korn 的 shell,又是类似 csh 的 shell。它没有 Bourne shell 的缺陷,即不匹配的 glob 按 is\xc2\xb9 传递,但默认情况下比csh这更严格,所有失败的 glob 都被视为致命错误。因此zsh,在默认情况下,如果其中一个/some/path1/*或/some/path2/*(或两者)无法匹配,则命令将中止。bash可以使用选项\xc2\xb2 在shell中启用类似的行为failglob。

\n

这使得行为更加可预测/一致,但意味着当您想要将多个 glob 扩展传递给命令并且只要其中一个 glob 成功就不希望它失败时,您可能会遇到该问题。不过,您可以设置cshnullglob选项以获得类似 csh 的行为(或emulate csh)。

\n

这可以通过使用匿名函数在本地完成:

\n
() { set -o localoptions -o cshnullglob; ls -d /some/{path1,path2}/*; }\n
Run Code Online (Sandbox Code Playgroud)\n

或者只使用子外壳:

\n
(set -o cshnullglob; ls -d /some/{path1,path2}/*)\n
Run Code Online (Sandbox Code Playgroud)\n

然而,在这里,您可以使用交替 glob 运算符来使用与所有这些匹配的一个 glob,而不是使用两个 glob:

\n
ls -d /some/(path1|path2)/*\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,你甚至可以这样做:

\n
ls -d /some/path[12]/*\n
Run Code Online (Sandbox Code Playgroud)\n

在 中bash,您可以启用extglobbash 的选项来支持 ksh\ 的扩展 glob 运算符的子集,包括交替:

\n
(shopt -s extglob; ls -d /some/@(path1|path2)/*)\n
Run Code Online (Sandbox Code Playgroud)\n

现在,由于从 Bourne shell 继承的错误功能,如果该 glob 与任何文件都不匹配,/some/@(path1|path2)/*则会按原样传递到ls并ls最终可能列出一个名为literally 的文件/some/@(path1|path2)/*,因此您还需要启用failglob防止这种情况的选项:

\n
(shopt -s extglob failglob; ls -d /some/@(path1|path2)/*)\n
Run Code Online (Sandbox Code Playgroud)\n

或者,您可以使用该nullglob选项(bash从 复制zsh)将所有不匹配的 glob 扩展为空。但:

\n

(shopt -s nullglob; ls -d /some/path1/* /some/path2/*)

\n

在命令的特殊情况下将是错误的ls,如果没有传递任何参数列表.。但是,您可以使用nullglob将 glob 扩展存储到数组中,并且仅ls在数组成员非空时将其作为参数进行调用:

\n
(\n  shopt -s nullglob\n  files=( /some/path1/* /some/path2/* )\n  if (( ${#files[@]} > 0 )); then\n    ls -d -- "${files[@]}"\n  else\n    echo >&2 No match\n    exit 2\n  fi\n)\n
Run Code Online (Sandbox Code Playgroud)\n

在zsh中,您可以使用glob 限定符(它启发了 ksh\'s ,尚未复制)nullglob在每个 glob 的基础上启用它,而不是全局启用它,并再次使用匿名函数而不是数组:(N)~(N)bash

\n
() {\n  (( $# > 0 )) && ls -d -- "$@"\n} /some/path1/*(N) /some/path2/*(N)\n
Run Code Online (Sandbox Code Playgroud)\n

shellfish现在的行为类似于zsh失败的 glob 导致错误的情况,除非 glob 与for, set(用于分配数组)一起使用或count它以某种nullglob方式表现。

\n

另外,在 中fish,虽然大括号扩展本身不是通配符,但它是作为通配符的一部分完成的,或者当大括号扩展与通配符组合时至少不会中止命令并且可以返回至少一个元素。

\n

所以,在fish:

\n
ls -d /some/{path1,path2}/*\n
Run Code Online (Sandbox Code Playgroud)\n

最终会表现得像csh.

\n

甚至:

\n
{ls,-d,/xx*}\n
Run Code Online (Sandbox Code Playgroud)\n

如果不匹配,将导致单独ls调用而不是失败(在这种情况下,行为与 csh 不同)。-d/xx*

\n

无论如何,如果只是print匹配文件路径,则不需要ls. 在 中zsh,您可以使用其print内置函数按列进行打印:

\n
print -rC3 /some/{path1,path2}/*(N)\n
Run Code Online (Sandbox Code Playgroud)\n

将在 3 列上打印路径raw(如果与 ullglob glob 限定符不匹配,则不打印任何内容N)。

\n

如果您想检查这两个目录中的任何一个是否至少有一个非隐藏文件,您可以执行以下操作:

\n
# bash\nif (shopt -s nullglob; set -- /some/path1/* /some/path2/*; (($#))); then\n   echo yes\nelse\n   echo no\nfi\n
Run Code Online (Sandbox Code Playgroud)\n

或者使用一个函数:

\n
has_non_hidden_files() (\n  shopt -s nullglob\n  set -- "$1"/*\n  (($#))\n)\nif has_non_hidden_files /some/path1 || has_non_hidden_files /some/path2\nthen\n  echo yes\nelse\n  echo no\nfi\n
Run Code Online (Sandbox Code Playgroud)\n
# zsh\nif ()(($#)) /some/path1/*(N) /some/path2/*(N); then\n  echo yes\nelse\n  echo no\nfi\n
Run Code Online (Sandbox Code Playgroud)\n

或者用一个函数:

\n
has_non_hidden_files() ()(($#)) $1/*(NY1)\nif has_non_hidden_files /some/path1 || has_non_hidden_files /some/path2\nthen\n  echo yes\nelse\n  echo no\nfi\n
Run Code Online (Sandbox Code Playgroud)\n

(Y1作为找到第一个文件后停止的优化)

\n

has_non_hidden_files请注意,对于用户无法读取的目录(无论它们是否有文件),这些会(默默地)返回 false。在 中zsh,您可以通过其特殊变量来检测这种情况$ERRNO。

\n
\n

\xc2\xb9 Bourne 行为(由 POSIX 指定)可以通过zsh执行emulate shoremulate ksh或 with来启用set +o nomatch

\n

\xc2\xb2 请注意,当 glob 不匹配时,具体取消的行为存在显着差异,该fish行为通常更明智,也bash -O failglob可能是最糟糕的

\n

  • 这个回应的质量令人震惊!感谢您花时间写出如此精美详细的答案。喜欢读它! (2认同)
  • @TooTea,然后你在某人(也许是去年的你)制作了一个名为“kernel-versions.txt”的文件的目录中执行此操作,并开始想知道为什么它不起作用。只要我们没有关于程序期望的参数(文件名或其他东西)的元信息,我们所能做的就是手动完成。(那里也只有一个反斜杠)按原样保留 glob 会让您收到诸如“ls:无法访问'*.foobar':没有这样的文件或目录”之类的错误消息,这看起来不错,但即使这样也是误导性的,因为它隐藏了事实上,shell 执行通配符操作,而不是 ls。 (2认同)