Bash

使用多種模式執行ls時避免非零退出程式碼

  • March 5, 2022

假設我有兩個可能的路徑,我想在 Linux 機器上列出目錄和文件:

/some/path1/
/some/path2/

如果我在 中執行以下操作tcsh,我會得到0退出程式碼,如果至少有一個path1path2存在:

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

但是,如果我在 中執行完全相同的操作bash,我會得到2退出程式碼,並顯示stderr消息報告path1不存在(如果 path1 是不存在的那個)。

在這種情況下,我該如何bash表現tcsh?如果至少存在一條路徑,ls我可以要求它回饋嗎?0如果兩者都不存在,我確實期望非零程式碼,這就是tcsh回饋。

您的大部分問題已經在為什麼 nullglob 不是預設值?.

要記住的一件事是:

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

其中csh/ tcsh/ zsh/ bash/ ksh(但不是fish,見下文)與以下內容相同:

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

由於大括號擴展是在glob 擴展之前執行的(不是作為./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)執行。該助手將在呼叫命令之前執行 glob 擴展,如果所有glob 模式都無法匹配任何文件,則在No match不執行命令的情況下報告錯誤。否則,只要至少有一個匹配的 glob,所有不匹配的都會被簡單地刪除。

因此,在我們上面的範例中,使用csh/tcsh或古老的 Thompson shell 及其/etc/glob助手,ls將使用-d,/some/path1/file1/some/path1/file2only 呼叫,並且可能會成功(只要/some/path1是可搜尋的)。

zsh既是 Korn-like 又是 csh-like shell。它沒有 Bourne shell 的錯誤功能,即不匹配的 glob 按原樣傳遞¹,但預設情況下比csh這更嚴格,所有失敗的 glob 都被視為致命錯誤。因此zsh,在預設情況下,如果其中一個/some/path1/*/some/path2/*(或兩者)不匹配,則該命令被中止。可以使用option²在bashshell中啟用類似的行為。failglob

這使得行為更加可預測/一致,但意味著當您想要將多個 glob 擴展傳遞給命令並且不希望它失敗時,只要其中一個 glob 成功,您就會遇到該問題。但是,您可以設置cshnullglob選項以獲得類似 csh 的行為(或emulate csh)。

這可以通過使用匿名函式在本地完成:

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

或者只是使用一個子shell:

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

但是,在這裡,您可以使用交替 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’s ,尚未複製)nullglob在每個 glob 的基礎上啟用它,並再次使用匿名函式而不是數組:(N)``~(N)``bash

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

shell 現在的fish行為類似於zsh失敗的 glob 導致錯誤的地方,除非 glob 與for, set(用於分配數組)一起使用或count它以某種nullglob方式表現。

此外,在 中fish,雖然大括號擴展本身不是 glob 運算符,但它是作為通配的一部分完成的,或者當大括號擴展與通配相結合併且至少可以返回一個元素時,至少一個命令不會中止。

所以,在fish

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

最終會表現得像 in csh

甚至:

{ls,-d,/xx*}

如果不匹配而不是失敗(在這種情況下與 csh 表現不同),將導致單獨ls呼叫。-d``/xx*

無論如何,如果只是print匹配文件路徑,則不需要ls. 在zsh中,您可以使用其print內置的列列印:

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

將在 3 列上列印路徑raw(如果與Nullglob glob 限定符不匹配,則不列印任何內容)。

相反,如果您想檢查這兩個目錄中是否至少有一個非隱藏文件,您可以執行以下操作:

# 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


¹ Bourne 行為(由 POSIX 指定)可以zsh通過 doemulate shemulate ksh或 with啟用set +o nomatch

² 請注意,當 glob 不匹配時,關於取消的確切內容存在顯著差異,這種fish行為通常更明智,並且bash -O failglob可能是最糟糕的

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