在 shell 程式中拆分字元串的安全且可移植的方法是什麼?
在編寫 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/bin
and/usr/local/sbin
…中,如果PATH
contains/*/*/*/../../../*/*/*/*/../../../*/*/*/*
,你不希望它讓你的機器停機)空
$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
$PATH
is/usr/bin:/bin:
(這是不好的做法,但仍然很常見),這意味著"/usr/bin"
,"/bin"
and""
(即目前目錄),而 shell 分詞(除 之外的所有 POSIX shellzsh
)會將其拆分為/usr/bin
and/bin
only。如果
$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 中,按空格、製表符或換行符拆分),在這些命令之後,它將最終設置但為空(這意味著沒有拆分)。另一個潛在的問題是,如果你推廣這種方法並在許多不同的函式中使用它,那麼如果在上面的
...
部分中,你正在呼叫一個做同樣事情的函式(製作$IFS
in的副本$oldIFS
),那麼你就走了把原來的鬆了$oldIFS
,恢復了錯的$IFS
。相反,您可以盡可能使用子shell:
( IFS=: ... ) # only the subshell's IFS was affected, the parent still has its own IFS
我的方法是在每次需要分詞(這種情況很少見)時設置 $IFS (並打開
set -o noglob
或關閉),而不必費心恢復以前的值。當然,如果您的腳本呼叫了其他人的程式碼,而該程式碼不遵循這種做法並且假定了預設的分詞行為,那麼這將不起作用。