便携式检查空目录

便携式检查空目录

使用 Bash 和 Dash,您可以仅使用 shell 检查空目录(为了简单起见,忽略点文件):

set *
if [ -e "$1" ]
then
  echo 'not empty'
else
  echo 'empty'
fi

然而我最近了解到 Zsh 在这种情况下失败了:

% set *
zsh: no matches found: *

% echo "$? $#"
1 0

因此,该命令不仅set失败,而且甚至没有设置$@。我想我可以测试 if $#is 0,但看起来 Zsh 甚至停止执行:

% { set *; echo 2; }
zsh: no matches found: *

与 Bash 和 Dash 比较:

$ { set *; echo 2; }
2

这可以通过在 bash、dash 和 zsh 中工作的方式来完成吗?

答案1

这有几个问题

set *
if [ -e "$1" ]
then
  echo 'not empty'
else
  echo 'empty'
fi

代码:

  • 如果启用该nullglob选项(以前大多数其他 shell 都支持),则变为列出所有 shell 变量(以及某些 shell 中的函数)zshset *set
  • 如果第一个非隐藏文件的名称以-或开头+,则它将被视为选项set。在 zsh 和 ksh 的某些实现/版本中,这甚至使其成为命令注入漏洞,例如当存在名为+A[$(reboot)].这两个问题可以通过使用来解决set -- *
  • *仅扩展非隐藏文件,因此不是测试目录是否为空,而是测试目录是否包含非隐藏文件。对于某些 shell,您可以使用dotgloborglobdot选项或使用FIGNORE特殊变量(具体取决于 shell)来解决该问题。
  • [ -e "$1" ]测试stat()系统调用是否成功。如果第一个文件是指向不可访问位置的符号链接,则将返回 false。您不需要stat()(甚至不需要lstat())任何文件来知道目录是否为空,只需检查它是否有一些内容。
  • *扩展需要打开当前目录,检索所有条目,存储所有非隐藏条目并对其进行排序,这也是相当低效的。

检查目录是否为非空(具有除 和 之外的任何条目)的最有效方法...使用zshglobF限定符(Ffor满的,这里表示非空):

if [ .(NF) ]; then
  print . is not empty
else
  print "it's empty or I can't read it"
fi

Nnullglob全局限定符。因此.(NF)扩展到.if.已满,否则没有任何内容。

lstat()在目录上之后,如果zsh发现它的链接计数大于2,那么这意味着它至少有一个子目录,所以不为空,所以我们甚至不需要打开该目录(这也意味着,在在这种情况下,即使我们没有读取权限,我们也可以知道该目录非空)。否则,zsh 打开目录,读取其内容并停在第一个既不是也不是的条目处,...无需读取、存储或排序所有内容。

使用 POSIX shell(zsh仅在模拟中表现出(更多)POSIXly sh),仅使用 glob 检查目录是否为非空是非常尴尬的。

一种方法是:

set .[!.]* '.[!.]'[*] .[.]?* [*] *
if [ "$#$1$2$3$4$5" = '5.[!.]*.[!.][*].[.]?*[*]*' ]; then
  echo "empty or can't read"
else
  echo not empty
fi

(假设没有与 glob 相关的选项从默认值更改(POSIX 仅指定noglob)并且未设置GLOBIGNORE(for bash) 和FIGNORE(for ) 变量,并且 (for ) 没有任何文件名包含不形成有效字符的字节序列)。kshyash

这个想法是,在 POSIX shell 中,当 glob 不匹配时,它不会展开(这是 70 年代末 Bourne shell 引入的错误功能)。因此set -- *,如果我们得到$1== *,我们不知道是否是因为没有匹配项,或者是否存在名为 的文件*

您解决该问题的(有缺陷的)方法是使用[ -e "$1" ].在这里,我们使用set -- [*] *.这可以消除两种情况的歧义,因为如果没有文件,上面的内容将保留[*] *,如果有一个名为 的文件*,则变为* *.我们对隐藏文件执行类似的操作。这有点尴尬,因为 Bourne shell 的另一个缺陷(也由zsh、Forsyth shell、pdksh 和修复fish),其中 的扩展.*确实包括特殊(伪)条目...当 报告时readdir()

因此,要使其在所有这些 shell 中工作,您可以这样做:

cwd_empty()
  if [ -n "$ZSH_VERSION" ]; then
    eval '! [ .(NF) ]'
  else
    set .[!.]* '.[!.]'[*] .[.]?* [*] *
    [ "$#$1$2$3$4$5" = '5.[!.]*.[!.][*].[.]?*[*]*' ]
  fi

无论如何,zsh默认情况下的语法与 POSIX sh 语法不兼容,因为它以非向后兼容的方式修复了 Bourne shell 中的大多数主要问题(早在 POSIX.2 首次发布之前),包括*当没有匹配时保持未展开(Bourne shell 之前的 shell 没有这个问题,csh并且tcshfish没有),并且.*包括.and..但其他几个如 split+glob 在参数或算术扩展上执行,所以你不能期望代码用 POSIX sh 编写,zsh除非您打开sh仿真,否则始终可以工作。

sh模拟尤其存在,因此您可以在zsh.

如果你想在 POSIX里面source写入一个文件,你可以这样做:shzsh

emulate sh -c 'source ./that-file'

然后该文件将在仿真中进行评估sh,并且其中声明的任何函数都将保留该仿真模式。

答案2

虽然 zsh 的默认行为是给出错误,这是由nomatch选项控制的。您可以取消设置该选项,*以像 bash 和 dash 那样保留原样:

setopt -o nonomatch

虽然该命令在其他任何一个命令中都不起作用,但您可以忽略它:

setopt -o nonomatch 2>/dev/null || true ; set *

它在 zsh 上运行,并抑制其他命令上失败命令的setopt错误输出 ( 2>/dev/null) 和返回代码 ( )。|| true

正如所写,如果有一个文件,例如:-e那么您将运行set -e并更改 shell 选项以在命令失败时终止;如果你有创造力,结果会更糟。set -- *将更安全并防止选项更改。

答案3

Zsh 的语法与 sh 不兼容。它足够接近,看起来像 sh,但还不够接近,您可以获取 sh 代码并不加修改地运行它。

如果您想在 zsh 中运行 sh 代码,例如因为您有一个为 sh 编写的 sh 函数或代码片段并希望在 zsh 脚本中使用,则可以使用emulate内置。例如,要在 zsh 脚本中获取为 sh 编写的文件:

emulate sh -c 'source /path/to/file.sh'

要使用 sh 语法编写函数或脚本并使其可以在 zsh 中运行,请将其放在开头附近:

emulate -L sh 2>/dev/null || true

在 sh 语法中,zsh 支持所有 POSIX 结构(它与bash --posixksh93 或 mksh 一样兼容 POSIX)。它还支持一些 ksh 和 bash 扩展,例如数组(在 ksh、bash 和 下为 0 索引emulate sh,但在本机 zsh 中为 1 索引)和[[ … ]].如果您想要 POSIX sh 加上 ksh glob,请使用,emulate … ksh …代替, 并为 bashemulate … sh …添加(请注意,这对于脚本/函数来说不是本地的)。if [[ -n $BASH ]]; then shopt -s extglob; fi

.枚举目录中除和之外的所有条目的本机 zsh 方法..

set -- *(DN)

这使用了全局限定符 D包含点文件并N在没有匹配项时生成一个空列表。

.枚举目录中除和之外的所有条目的可移植 sh 方法..要复杂得多。您需要列出点文件,如果您在当前目录或不能保证绝对路径中列出文件,则需要小心,以防文件名以破折号开头。这是一种方法,即使用模式..?* .[!.]* *列出除 和 之外的所有文件...并删除未展开的模式。

set -- ..?*
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi
set -- .[!.]* "$@"
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi
set -- * "$@"
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi

如果您只想测试目录是否为空,那么有一种更简单的方法。

if ls -A /path/to/directory/ | grep -q '^'; then
  echo "/path/to/directory is not empty"
else
  echo "/path/to/directory is empty"
fi

答案4

我不确定解决方案set *。这个怎么样?

dir="./empty"
has_file=false

for i in "$dir"/*; do 
  if test ! "$i" = "$dir/*"; then
    has_file=true
    echo "$i"
  if
done

echo "$has_file"

经过测试,这对于zsh, bash, ash, ksh, bourneAKA效果很好heirloom

相关内容