Bash 引用
在 Bash 中引用内容很简单……直到它变得不简单。我写过一些脚本,我敢说 90% 的精力都花在了让 Bash 正确引用内容上。
引用的基础知识很简单,当一个值包含空格时,使用单引号或双引号。
a="hello world"
a='hello world'
但是单引号和双引号是不同的。单引号不支持任何类型的变量替换或转义引用字符串中的特殊字符。另一方面,双引号支持两者。
a="hello \"there\" world"
a='hello "there" world'
a='hello \'there\' world' # causes an error
a="hello 'there' world"
b="there"
a='hello \"$b\" world' # a is >>hello \"$b\" world
a="hello \"$b\" world" # a is >>hello "there" world
解决单引号不支持变量替换的一个方法是引用字符串的各个文字部分,然后将变量替换放在它们之间。
b='"there"'
a='"hello" '$b' "world"' # a is: >>"hello" "there" "world"<<
请注意,"$b" 实际上不在字符串的引用部分内。由于字符串的两个文字部分和 "$b" 之间没有空格,因此 Bash 会将最终结果放入单个字符串,然后将其分配给a.
引用问题经常出现的一个地方是,当您处理文件名中包含空格的文件时。例如,如果我们有一个名为 "file with spaces (包含空格的文件)" 的文件,并且我们运行以下脚本
#/bin/bash
for i in $(find .)
do
echo $i
done
我们没有得到我们想要的结果
$ bash t1.sh
.
./file
with
spaces
./t2.sh
./t1.sh
...
一种解决方案是将文件名放入 Bash 数组中,此时 内部字段分隔符 仅设置为换行符。然后我们使用值"${array[@]}"作为for循环的列表。
#/bin/bash
newline='
'
OIFS=$IFS
IFS=$newline
files=($(find .))
IFS=$OIFS
for i in "${files[@]}"
do
echo $i
done
这会产生正确的输出
$ bash t2.sh
.
./file with spaces
./t2.sh
./t1.sh
...
之间的区别"${array[*]}"和"${array[@]}"类似于"$*"和"$@"之间的区别,即在后一种情况下,结果是每个 单词 都被单独引用,而不是整个内容作为一个字符串被引用。
另一种选择是避免特殊引用,只需在for循环运行之前设置 内部字段分隔符 变量,并在循环体中的第一个语句中重置它。
#/bin/bash
newline='
'
OIFS=$IFS
IFS=$newline
for i in $(find .)
do
IFS=$OIFS
echo $i
done
另一个引用问题是当您有一个字符串,您希望将其分解成单独的 单词,但您也希望尊重字符串中任何带引号的子字符串。考虑字符串
a="hello \"there big\" world"
假设您想将其分解为三个 部分
- hello
- there big
- world
如果您运行此命令
#!/bin/bash
a="hello \"there big\" world"
for i in $a
do
echo $i
done
它会产生这个结果
$ bash t4.sh
hello
"there
big"
world
这里的技巧是使用set命令的特殊形式来重置$*(因此,$1, $2等等)。我的第一个版本总是像这样
#!/bin/bash
a="hello \"there big\" world"
set -- $a
for i in "$@"
do
echo $i
done
当然,它不起作用
$ bash t5.sh
hello
"there
big"
world
然后我通常会记得你必须eval该set语句
#!/bin/bash
a="hello \"there big\" world"
eval set -- $a
for i in "$@"
do
echo $i
done
此时它才起作用
$ bash t6.sh
hello
there big
world
此时,我通常已经厌倦了引用内容。