Shell

在 shell 程式中拆分字元串的安全且可移植的方法是什麼?

  • August 2, 2020

在編寫 shell 腳本時,我經常想拆分一個字元串。這是一個非常簡單的例子:

for dir in $(echo $PATH | tr : " "); do
   [[ -x "$dir"/"$1" ]] && echo $dir
done

這將在 $PATH 中的每個目錄中搜尋與$1. 非常簡單,它執行良好,但如果我的 $PATH 中的目錄在其名稱中包含空格,則會中斷。

在出現重複分隔符時拆分字元串的推薦方法是什麼?

理想情況下,該解決方案將能夠在(相當)舊的 shell 上執行,即 ksh88。

只需IFS根據你的需要設置,讓shell進行分詞:

IFS=':'
for dir in $PATH; do
   [ -x "$dir"/"$1" ] && echo $dir
done

這適用於和bash,但僅在最新版本中進行了測試。dash``ksh

顯而易見的解決方案是使用 shell 分詞,但要注意一些陷阱:

IFS=:
set -o noglob
for dir in $PATH''; do
   dir=${dir:-.}
   [ -x "${dir%/}/$1" ] && printf "%s\n" "$dir"
done

您需要set -o noglob,因為當一個變數不加引號時,會對其執行分詞文件名生成globbing),在這裡您只需要分詞(例如,在不太可能的情況下$PATH包含/usr/local/*bin*,您希望它在/usr/local/*bin*文件夾中查找,不在/usr/local/binand /usr/local/sbin…中,如果PATHcontains /*/*/*/../../../*/*/*/*/../../../*/*/*/*,你不希望它讓你的機器停機)

$PATH組件表示目前目錄 ( .),而不是/$dir/$1在那種情況下是不正確的。在這種情況下,解決方法是編寫$dir${dir:+/}$1或更改$dir.(當使用printf '%s\n' "$dir".

//foo不一定與 相同/foo,所以如果/在 中$PATH,你不想要$dir/$1,那就是//$1。因此${dir%/}去除尾部斜杠。

然後,還有一些其他問題:

For $PATH,":"是一個欄位分隔符,而 for $IFS, 它是一個欄位終止符(是的,我知道,S是用於S分隔符,責備 ksh 和 POSIX 來標準化 ksh 行為)。

所以 if $PATHis /usr/bin:/bin:(這是不好的做法,但仍然很常見),這意味著"/usr/bin", "/bin"and ""(即目前目錄),而 shell 分詞(除 之外的所有 POSIX shell zsh)會將其拆分為/usr/binand /binonly。

如果$PATH已設置但為空,則表示:“僅在目前目錄中查找”。而 shell(包括那些被$IFS視為分隔符的)會將其擴展為一個空列表。

將上述內容附加''$PATH上面可以解決這兩個問題。

最後但並非最不重要的。如果$PATH未設置,則具有特殊含義,即:查看系統預設搜尋列表,不幸的是,這取決於您詢問的對象(什麼命令)而有所不同。

$ env -u PATH bash -c 'type usbipd'
usbipd is /usr/local/sbin/usbipd
$ env -u PATH ksh -c 'type usbipd'
ksh: whence: usbipd: not found

基本上,在您的腳本中,您必須猜測在對您很重要的上下文中預設搜尋路徑是什麼。

請注意,當未設置或為空時,POSIX 未指定行為$PATH,因此不會幫助您。這也意味著我上面所說的可能不適用於某些過去、現在或未來的 POSIX/Unix 系統。

簡而言之,解析$PATH以嘗試找出從何處執行命令是一項棘手的工作。

有一個標準命令,即command

ls_path=$(command -v ls)

但有人可能會問:你為什麼想知道?

現在將 IFS 恢復為其預設值:

oldIFS=$IFS
IFS=:
...
IFS=$oldIFS

在大多數情況下將在實踐中工作,但不保證 POSIX 可以工作。

原因是 if$IFS之前未設置,這意味著預設的拆分行為(即在 POSIX shell 中,按空格、製表符或換行符拆分),在這些命令之後,它將最終設置但為空(這意味著沒有拆分)。

另一個潛在的問題是,如果你推廣這種方法並在許多不同的函式中使用它,那麼如果在上面的...部分中,你正在呼叫一個做同樣事情的函式(製作$IFSin的副本$oldIFS),那麼你就走了把原來的鬆了$oldIFS,恢復了錯的$IFS

相反,您可以盡可能使用子shell:

(
 IFS=:
 ...
)
# only the subshell's IFS was affected, the parent still has its own IFS

我的方法是在每次需要分詞(這種情況很少見)時設置 $IFS (並打開set -o noglob或關閉),而不必費心恢復以前的值。當然,如果您的腳本呼叫了其他人的程式碼,而該程式碼不遵循這種做法並且假定了預設的分詞行為,那麼這將不起作用。

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