Shell

dirname 和 basename vs 參數擴展

  • March 19, 2019

是否有任何客觀理由偏愛一種形式而不是另一種形式?性能、可靠性、便攜性?

filename=/some/long/path/to/a_file

parentdir_v1="${filename%/*}"
parentdir_v2="$(dirname "$filename")"

basename_v1="${filename##*/}"
basename_v2="$(basename "$filename")"

echo "$parentdir_v1"
echo "$parentdir_v2"
echo "$basename_v1"
echo "$basename_v2"

產生:

/some/long/path/to
/some/long/path/to
a_file
a_file

(v1 使用 shell 參數擴展,v2 使用外部二進製文件。)

不幸的是,兩者都有自己的怪癖。

兩者都是 POSIX 所必需的,因此它們之間的區別不是可移植性問題¹。

使用實用程序的簡單方法是

base=$(basename -- "$filename")
dir=$(dirname -- "$filename")

請注意變數替換周圍的雙引號,以及--命令之後的雙引號,以防文件名以破折號開頭(否則命令會將文件名解釋為選項)。這在一種極端情況下仍然失敗,這種情況很少見,但可能是惡意使用者強制執行的²:命令替換會刪除尾隨的換行符。因此,如果呼叫文件名,foo/bar␤則將base設置為bar而不是bar␤. 一種解決方法是添加一個非換行符並在命令替換後將其剝離:

base=$(basename -- "$filename"; echo .); base=${base%.}
dir=$(dirname -- "$filename"; echo .); dir=${dir%.}

使用參數替換,您不會遇到與奇怪字元擴展相關的極端情況,但斜線字元存在許多困難。根本不是邊緣情況的一件事是,對於沒有/.

base="${filename##*/}"
case "$filename" in
 */*) dirname="${filename%/*}";;
 *) dirname=".";;
esac

邊緣情況是尾隨斜杠(包括根目錄的情況,都是斜杠)。basenameand命令在dirname執行它們的工作之前會去掉尾部的斜杠。如果您堅持使用 POSIX 結構,則無法一次性去除尾部斜杠,但您可以分兩步完成。當輸入只包含斜杠時,您需要注意這種情況。

case "$filename" in
 */*[!/]*)
   trail=${filename##*[!/]}; filename=${filename%%"$trail"}
   base=${filename##*/}
   dir=${filename%/*};;
 *[!/]*)
   trail=${filename##*[!/]}
   base=${filename%%"$trail"}
   dir=".";;
 *) base="/"; dir="/";;
esac

如果您碰巧知道您不是處於邊緣情況(例如,find除起點之外的結果始終包含目錄部分並且沒有尾隨/),那麼參數擴展字元串操作很簡單。如果您需要處理所有邊緣情況,這些實用程序更易於使用(但速度較慢)。

有時,您可能想要對待foo/likefoo/.而不是 like foo。如果您正在對目錄條目進行foo/操作,則應該等效於foo/.,而不是foo; 當foo是一個目錄foo的符號連結時,這會有所不同:表示符號連結,foo/表示目標目錄。在這種情況下,帶有斜杠的路徑的基本名稱最好是.,並且路徑可以是它自己的目錄名。

case "$filename" in
 */) base="."; dir="$filename";;
 */*) base="${filename##*/}"; dir="${filename%"$base"}";;
 *) base="$filename"; dir=".";;
esac

快速可靠的方法是使用 zsh 及其歷史修飾符(這首先去除尾部斜杠,如實用程序):

dir=$filename:h base=$filename:t

¹除非您使用的是 Solaris 10 和更早版本的 pre-POSIX shell /bin/sh(在仍在生產的機器上缺少參數擴展字元串操作功能——但sh在安裝過程中總會呼叫一個 POSIX shell,只是它是/usr/xpg4/bin/sh,不是/bin/sh)。

²例如:送出一個呼叫foo␤文件上傳服務的文件,該文件不對此提供保護,然後將其刪除並導致foo被刪除

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