在 shell 编程中拆分字符串的安全且可移植的方法是什么?

rah*_*hmu 4 shell shell-script split portability

在编写shell脚本时,我经常想拆分一个字符串。这是一个非常简单的例子:

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

这将在 $PATH 中的每个目录中搜索与$1. 非常简单,它运行良好,但如果 $PATH 中的目录在其名称中包含空格,则会中断。

在出现循环分隔符时拆分字符串的推荐方法是什么?

理想情况下,该解决方案将能够在(相当)旧的 shell 上运行,即 ksh88。

Sté*_*las 12

显而易见的解决方案是使用 shell 分词,但要注意一些问题:

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

您需要,set -o noglob因为当变量未加引号时,会对其执行分词文件名生成globbing),在这里您只需要分(例如,在不太可能$PATH包含 的情况下/usr/local/*bin*,您希望它确实在/usr/local/*bin*文件夹中查找, not in /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 行为)。

因此,如果$PATH/usr/bin:/bin:(这是不好的做法,但仍然很常见),这意味着"/usr/bin","/bin"""(即当前目录),而 shell 单词拆分(除 外的所有 POSIX shell zsh)会将其拆分为/usr/bin/bin

如果$PATH设置但为空,则表示:“仅在当前目录中查找”。而外壳(包括那些$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
Run Code Online (Sandbox Code Playgroud)

基本上,在您的脚本中,您必须猜测默认搜索路径在对您很重要的上下文中是什么。

请注意,POSIX 在未$PATH设置或为空时未指定行为,因此不会帮助您。这也意味着我上面所说的可能不适用于某些过去、现在或未来的 POSIX/Unix 系统。

简而言之,解析$PATH以尝试找出从何处运行命令是一项棘手的工作。

有一个标准命令,它是command

ls_path=$(command -v ls)
Run Code Online (Sandbox Code Playgroud)

但人们可能会问:你为什么想知道?

现在将 IFS 恢复到其默认值:

oldIFS=$IFS
IFS=:
...
IFS=$oldIFS
Run Code Online (Sandbox Code Playgroud)

在大多数情况下将在实践中工作,但不保证 POSIX 工作。

原因是如果$IFS之前未设置这意味着默认拆分行为(即在 POSIX shell 中,在空格、制表符或换行符上拆分),在这些命令之后,它将最终设置但为空(这意味着没有拆分)。

另一个潜在的问题是,如果您概括该方法并在许多不同的函数中使用它,那么如果在...上面的部分中,您正在调用一个执行相同操作的函数(复制$IFSin $oldIFS),那么您将把原来的松了$oldIFS,恢复错了$IFS

相反,您可以在可能的情况下使用子外壳:

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

我的方法是set -o noglob每次需要分词(这种情况很少见)时设置 $IFS(并打开或关闭),而不用费心恢复以前的值。当然,如果您的脚本调用了不遵循该做法并假定默认分词行为的其他人的代码,那么这将不起作用。


man*_*ork 10

只需IFS根据您的需要进行设置,让shell执行分词:

IFS=':'
for dir in $PATH; do
    [ -x "$dir"/"$1" ] && echo $dir
done
Run Code Online (Sandbox Code Playgroud)

这适用于bash,dashksh,但仅使用最新版本进行测试。