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

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

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

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

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

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

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

答案1

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

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

你需要set -o noglob因为当变量不加引号时,两者分词文件名生成通配)对其执行,在这里你只需要分词(例如,在不太可能出现的情况下$PATHcontains /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,所以如果/是 in $PATH,则您不需要$dir/$1,这将是//$1。因此${dir%/}要去掉尾部斜线。

然后,还有其他一些问题:

对于$PATH,":"是一个字段分隔器而对于$IFS,它是一个字段终结者(是的,我知道,S是为了S分离器,归咎于 ksh 和 POSIX 标准化了 ksh 行为)。

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

如果$PATH已设置但为空,则意味着:“仅在当前目录中查找”。而 shell(包括那些视为$IFS分隔符的 shell)会将其扩展为空列表。

将上述内容附加''$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 可以工作。

原因是如果$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或关闭)每次我需要分词(这很少见)并且不费心恢复以前的值。当然,如果您的脚本调用其他人的代码,而该代码不遵循该实践并采用默认的分词行为,那么这将不起作用。

答案2

根据你的需要设置即可IFS,让shell进行分词:

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

这适用于bashdashksh,但仅在最新版本中进行了测试。

答案3

如果需要读取固定数量的字段到变量中,可以使用这个方法:

input="age:30"

IFS=':' read -r first_field second_field <<< "$input"

echo "$first_field"
echo "$second_field"

我发现它在格雷格的维基

告诉-r我们read不应该将反斜杠视为特殊的。

相关内容