假设我有两个可能的路径,我想列出 Linux 计算机上的目录和文件:
/some/path1/
/some/path2/
如果我在 中执行以下操作,如果或至少之一存在,tcsh
我会得到退出代码:0
path1
path2
ls -d /some/{path1,path2}/*
但是,如果我在 中执行完全相同的操作bash
,则会收到2
退出代码,并显示一条stderr
消息报告path1
不存在(如果 path1 是不存在的路径)。
在这种情况下我怎样才能表现bash
得像这样?如果至少存在一条路径,是否有一个我可以要求它返回的tcsh
开关?如果两者都不存在,我确实期望非零代码,这就是返回的内容。ls
0
tcsh
答案1
您的大部分问题已在以下位置得到解答为什么 nullglob 不是默认值?。
需要记住的一件事是:
ls -d /some/{path1,path2}/*
在csh
// tcsh
/ zsh
/ (但不是,见下文bash
)中与:ksh
fish
ls -d /some/path1/* /some/path2/*
当执行支撑扩展时前(不是作为...的一部分) glob 扩展,它是 shell 将这些/some/pathx/*
模式扩展为匹配文件列表,以作为单独的参数传递给ls
.
bash
,ksh
它主要复制的内容继承了 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
,您可以启用extglob
bash 的选项来支持 ksh 的扩展 glob 运算符的子集,包括交替:
(shopt -s extglob; ls -d /some/@(path1|path2)/*)
现在,由于从 Bourne shell 继承的错误功能,如果该 glob 与任何文件都不匹配,/some/@(path1|path2)/*
则会按原样传递到ls
并ls
最终可能列出一个名为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 列上打印路径r
aw(如果与 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 sh
oremulate ksh
或 with 来启用set +o nomatch
² 请注意,当全局不匹配时,具体取消什么行为存在显着差异,该fish
行为通常更明智,也bash -O failglob
可能是最糟糕的
答案2
刚刚读过史蒂芬的回答。
发生这种情况是因为如何bash
处理与任何内容都不匹配的 glob。要获得与您在 中观察到的相同行为tcsh
,您需要启用 bash 的nulglob
选项。从man bash
:
空球
如果设置,bash 允许不匹配任何文件的模式(请参阅上面的路径名扩展)扩展为空字符串,而不是它们本身。
启用nullglob
,shopt -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