bash 新手程序员会反复听到这样一条建议:
给变量加引号!
为什么呢?
展开变量(比如$name)时,你几乎总是想把变量的_值_当作单个字符串(或数字)来使用。
你不会希望这个值被意外地当作两个或更多独立字符串来处理。
引用是确保一段文本被当作单个词处理的主要机制。 引号主要有两种。
双引号允许在其中进行参数扩展、算术扩展和命令替换。
echo "This is bash version $BASH_VERSION"
在双引号字符串内部,可以通过在前面加反斜杠来包含一个字面量双引号。
echo "She said \"Let me teach you about bash.\""
单引号_不允许_在其内部进行任何扩展。
echo 'This is bash version $BASH_VERSION'
# ...^..................................^
# prints exactly: This is bash version $BASH_VERSION
在单引号字符串中_无法_嵌入单引号。
echo $name到底有什么问题?
shell 解析一行代码时,会按顺序执行多种扩展。 最后执行的扩展中有两个是_单词拆分_和_文件名扩展_。
参数扩展、命令替换或算术扩展的结果,只要不是在双引号内产生的,都有可能被“单词拆分”。
shell 会取扩展后的结果,并按$IFS值中出现的任意字符序列把它拆开。
默认情况下,这些字符是空格、制表符和换行符。
我们可以用一个 shell 循环来实际看看:
sentence="The quick brown fox jumps."
i=0
for word in "$sentence"; do echo "$((++i)) $word"; done
# ..........^.........^
输出
1 The quick brown fox jumps.
但不加引号时:
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
输出
1 The
2 quick
3 brown
4 fox
5 jumps.
再看看$IFS取不同值的情况:
csv="first,second,third"
IFS=","
for word in $csv; do echo "$word"; done
输出
first
second
third
禁用单词拆分的一种技巧是把$IFS设为空字符串
sentence="The quick brown fox jumps."
IFS= # or explicitly, IFS=""
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
输出
1 The quick brown fox jumps.
不过,除非同时禁用文件名扩展,否则不建议让变量不加引号。 下面我们会看到怎么做。
glob 模式用来简洁地表达匹配一组文件的模式。
例如,你可以用*.md匹配某个目录中所有 Markdown 文件。
glob 模式自 Unix 诞生以来就一直存在。
glob 通配符包括:
* 匹配零个或多个任意字符,
? 恰好匹配一个任意字符,
[...] 恰好匹配方括号内所列字符集合中的一个字符。
[abc] 恰好匹配一个字符,该字符是 a、b 或 c。
^ 之后)
[][] 是一个字符集,能匹配右方括号或左方括号。[[\]]
[x-y] 恰好匹配一个字符,该字符在从 x 到 y 的_范围_内
^ 之后),或者右方括号之前的最后一个位置。
[-xy] 和 [xy-] 都恰好匹配一个字符,该字符是 -、x 或 y
[x\-y]
[0-9A-Fa-f] 匹配一个十六进制数字[^abc] 恰好匹配一个不是 a、b 或 c 的字符,[^x-y] 恰好匹配一个不在从 x 到 y 范围内的字符[[:character_class:]] 匹配一个属于指定“character_class”的字符
lower:小写字母upper:大写字母alpha:字母digit:数字alnum:字母和数字space:空白blank:水平空白(空格和制表符)xdigit:十六进制数字word:标识符中允许的字符(字母、数字、下划线)punct:标点cntrl:控制字符graph:“可见”字符print:可见字符加空格[[:alpha:]] 类由 [[:lower:][:upper:]] 构成[[:alnum:]] 类由 [[:alpha:][:digit:]] 构成[[:word:]] 类由 [[:alnum:]_] 构成[[:word:]] 又是 [[:punct:]]
[[:graph:]] 类由 [[:alnum:][:punct:]] 构成[[:print:]] 类由 [[:graph:] ] 构成,即只有空格(八进制 040),不包括任何其他空白。可以用for循环遍历一组文件
for file in *.csv; do
do_something_with "$file"
done
注意变量"$file"是加了引号的。
glob 模式可能返回文件名中带空格的文件。
我们需要给变量加引号,这样do_something_with命令就只会收到一个参数,也就是文件名。
glob 模式不是正则表达式。
.
?
.*
*
.* 表示“匹配一个文件名,其第一个字符是字面量点号,后面跟零个或多个任意字符”。.+
?*
Bash 提供了扩展模式,更接近正则表达式。
有时候你需要抑制文件名扩展。 处理用户输入就是其中一种情况。 看看这段代码
read -p "Enter something: " user_input
echo $user_input
如果用户输入一个星号(*),会输出什么?
当前目录中的文件列表。
这大概不是你想要的结果。
另一个例子是把模式作为参数传给期望接收模式的程序。
tr程序常用于文本处理。要把变量值转成小写,可以这样做
var="My Puppet"
echo "$var" | tr '[:upper:]' '[:lower:]'
输出
my puppet
一个常见的错误是忘记给tr的参数加引号:tr [:upper:] [:lower:]
为什么会出问题?
不加引号的 glob 模式会被展开成匹配的文件列表。
假设使用你脚本的人在当前目录里有名为p、u和w的文件:
touch p u w
var="My Puppet"
echo "$var" | tr [:upper:] [:lower:]
这会导致错误!
不加引号的[:upper:]模式会匹配:、u、p、e或r中的任意字符。
不加引号的[:lower:]模式会匹配:、l、o、w、e或r中的任意字符。
Bash 为第一个模式匹配到文件p和u,为第二个模式匹配到文件w,于是tr就会像这样被调用
echo "$var" | tr p u w
而对tr来说,这个参数数量是错的。
可以用set命令禁用文件名扩展。
echo * # prints a list of files
set -f # disable filename expansion
echo * # prints a literal asterisk
set +f # enable filename expansion
内置的shopt命令有几个设置可以控制文件名扩展的行为。
其中几个有意思的是:
shopt -s nocaseglob:执行不区分大小写的匹配shopt -s extglob:启用扩展模式shopt -s nullglob:如果没有文件匹配,就用空值替换该模式。
默认行为是把该模式作为字面量字符串保留在原处。