dirname 和 basename vs 參數擴展
是否有任何客觀理由偏愛一種形式而不是另一種形式?性能、可靠性、便攜性?
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
邊緣情況是尾隨斜杠(包括根目錄的情況,都是斜杠)。
basename
and命令在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/.
而不是 likefoo
。如果您正在對目錄條目進行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
被刪除