Bash

攜帶式檢查空目錄

  • March 19, 2021

使用 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 中工作的方式完成嗎?

有幾個問題

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

程式碼:

  • 如果啟用該nullglob選項(從zsh但現在大多數其他 shell 支持),set *則變為set列出所有 shell 變數(以及某些 shell 中的函式)
  • 如果第一個非隱藏文件的名稱以-or開頭+,它將被視為選項set。這兩個問題可以通過使用set -- *來解決。
  • *僅擴展非隱藏文件,因此不是測試目錄是否為空,而是測試它是否包含非隱藏文件。對於某些 shell,您可以使用dotgloborglobdot選項或FIGNORE根據 shell 使用特殊變數來解決此問題。
  • [ -e "$1" ]測試stat()系統呼叫是否成功。如果第一個文件是指向不可訪問位置的符號連結,則返回 false。您不需要stat()(甚至不需要lstat())任何文件來知道目錄是否為空,只需檢查它是否有一些內容。
  • *擴展涉及打開目前目錄,檢索所有條目,儲存所有非隱藏條目並對其進行排序,這也是非常低效的。

檢查目錄是否為非空(除.and之外的任何條目..)的最有效方法zsh是使用Fglob 限定符(F對於full,此處表示非空):

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

Nnullglob全域限定符。因此.(NF)擴展到.if .is full 而沒有其他內容。

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 ksh) 變數,並且 (for yash) 沒有任何文件名包含不形成有效字元的字節序列)。

這個想法是,在 POSIX shell 中,當一個 glob 不匹配時,它不會被擴展(Bourne shell 在 70 年代後期引入的錯誤功能)。因此set -- *,如果我們得到$1== *,我們不知道是因為沒有匹配項還是有一個名為*.

您解決此問題的(有缺陷的)方法是使用[ -e "$1" ]. 在這裡,我們使用set -- [*] *. 這可以消除這兩種情況的歧義,因為如果沒有文件,上面將保留[*] *,如果有一個名為 的文件*,則變為* *. 我們對隱藏文件做類似的事情。這有點尷尬,因為 Bourne shell 的另一個錯誤功能(也由Forsyth shell zshfishpdksh和.*``.``..``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 沒有那個問題,csh並且tcshfish沒有),並且.*包括...但其他幾個,比如在參數或算術擴展上執行的 split+glob,所以你不能指望程式碼用 POSIX sh 編寫以始終工作,zsh除非您打開sh仿真。

sh仿真尤其如此,因此您可以在zsh.

如果你想在 POSIX裡面source寫一個文件,你可以這樣做:sh``zsh

emulate sh -c 'source that-file'

然後該文件將在sh仿真中進行評估,並且在其中聲明的任何函式都將保留該仿真模式。

雖然 zsh 的預設行為是給出錯誤,但這是由nomatch選項控制的。您可以取消設置選項,以 bash 和 dash 的方式保留*原位:

setopt -o nonomatch

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

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

setopt在 zsh 上執行,並在其他命令上抑制了失敗命令的錯誤輸出 ( 2>/dev/null) 和返回程式碼 ( || true)。

如所寫,如果有文件,例如-e: 那麼您將執行set -e並更改 shell 選項以在命令失敗時終止;如果你有創造力,結果會更糟。set -- *將更安全並防止選項更改。

引用自:https://unix.stackexchange.com/questions/492912