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

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

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

/some/path1/
/some/path2/

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

ls -d /some/{path1,path2}/*

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

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

答案1

您的大部分问题已在以下位置得到解答为什么 nullglob 不是默认值?

需要记住的一件事是:

ls -d /some/{path1,path2}/*

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

ls -d /some/path1/* /some/path2/*

当执行支撑扩展时(不是作为...的一部分) glob 扩展,它是 shell 将这些/some/pathx/*模式扩展为匹配文件列表,以作为单独的参数传递给ls.

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

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

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

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

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

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

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

() { set -o localoptions -o cshnullglob; ls -d /some/{path1,path2}/*; }

或者只使用子外壳:

(set -o cshnullglob; ls -d /some/{path1,path2}/*)

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

ls -d /some/(path1|path2)/*

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

ls -d /some/path[12]/*

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

(shopt -s extglob; ls -d /some/@(path1|path2)/*)

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

(shopt -s extglob failglob; ls -d /some/@(path1|path2)/*)

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

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

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

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

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

() {
  (( $# > 0 )) && ls -d -- "$@"
} /some/path1/*(N) /some/path2/*(N)

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

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

所以,在fish

ls -d /some/{path1,path2}/*

最终会表现得像csh.

甚至:

{ls,-d,/xx*}

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

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

print -rC3 /some/{path1,path2}/*(N)

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

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

# bash
if (shopt -s nullglob; set -- /some/path1/* /some/path2/*; (($#))); then
   echo yes
else
   echo no
fi

或者使用一个函数:

has_non_hidden_files() (
  shopt -s nullglob
  set -- "$1"/*
  (($#))
)
if has_non_hidden_files /some/path1 || has_non_hidden_files /some/path2
then
  echo yes
else
  echo no
fi
# zsh
if ()(($#)) /some/path1/*(N) /some/path2/*(N); then
  echo yes
else
  echo no
fi

或者用一个函数:

has_non_hidden_files() ()(($#)) $1/*(NY1)
if has_non_hidden_files /some/path1 || has_non_hidden_files /some/path2
then
  echo yes
else
  echo no
fi

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

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


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

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

答案2

刚刚读过史蒂芬的回答


发生这种情况是因为如何bash处理与任何内容都不匹配的 glob。要获得与您在 中观察到的相同行为tcsh,您需要启用 bash 的nulglob选项。从man bash

空球

如果设置,bash 允许不匹配任何文件的模式(请参阅上面的路径名扩展)扩展为空字符串,而不是它们本身。

启用nullglobshopt -s nullglob您将获得预期的行为:

$ ls
bar
$ ls {bar,foo}/*
ls: cannot access 'foo/*': No such file or directory
 bar/ff
$ echo $?
2


$ shopt -s nullglob 
$ ls {bar,foo}/*
bar/ff
$ echo $?
0

相关内容