学习路径
/
Bash
Bash
/
教学大纲
/
引号的重要性
引号

引号的重要性 属于 Bash

{other: "%{count} 个练习"}

关于 引号的重要性

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 通配符包括:

  • * 匹配零个或多个任意字符,

  • ? 恰好匹配一个任意字符,

  • [...] 恰好匹配方括号内所列字符集合中的一个字符。

    Advanced
    字符集有很多细节。 点击这里查看。
    • [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:]_] 构成
        • 下划线(八进制 137)既是 [[: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 模式不是正则表达式

glob 模式不是正则表达式。

  • 恰好匹配一个字符
    • 正则 .
    • glob ?
  • 匹配零个或多个字符
    • 正则 .*
    • glob *
      • glob 模式 .* 表示“匹配一个文件名,其第一个字符是字面量点号,后面跟零个或多个任意字符”。
  • 匹配一个或多个字符
    • 正则 .+
    • glob ?*
  • 正则字符集和 glob 字符集的用法相同。
Advanced

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:]

Caution

为什么会出问题? 不加引号的 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:如果没有文件匹配,就用空值替换该模式。 默认行为是把该模式作为字面量字符串保留在原处。
通过 GitHub 编辑 该链接会在新窗口或标签页中打开