Contents

在shell脚本中处理特殊字符

1. 概述

有时,当我们编写 shell 脚本时,我们必须处理特殊字符,如空格、符号和其他非 ASCII 字符。shell 脚本和其他工具可能无法直接处理这些字符。因此,我们必须采取一些措施来处理这些特殊字符。

在本教程中,我们将介绍有关在 shell 脚本中处理特殊字符的最常见用例。首先,我们将讨论在 shell 脚本中包装命令 和变量替换。

然后,我们将处理包含某些前缀的文件名。之后,我们将介绍read 命令和IFS 变量以逐字读取字符串。

最后,我们将看到Shellcheck 实用程序的运行情况以及我们如何使用它来确保我们的脚本没有任何警告。

2. 用双引号包裹替换

在 shell 中,当我们为mv之类的命令指定文件名时,shell 会将文件名之间的空格视为分隔符。因此,每个文件名将对应于磁盘上的一个单独的文件或目录。

但是当我们有一个包含空格的文件名时会发生什么?好吧,shell 会将文件名视为文件列表。

我们可以通过尝试处理带有空格的文件名来在终端中演示这一点:

$ mv file with spaces /tmp
mv: cannot stat 'file': No such file or directory
mv: cannot stat 'with': No such file or directory
mv: cannot stat 'spaces': No such file or directory

发生这种情况是因为 shell 认为它是由空格分隔的文件列表。为了克服这个问题,我们需要用双引号将文件名括起来

$ mv "file with spaces" /tmp

现在,shell 会将这个文件名视为一个整体。

2.1. 双引号内的变量替换

这对于 shell 内部的变量也有些相同。假设我们有一个变量*$HOME*。用双引号括住这个变量可能意味着三件事:

  • HOME变量的值作为一个整体
  • 使用空格作为分隔符将字符串拆分为字段
  • 将每个空格分隔的字段视为可以由 shell 扩展的 glob

在我们的例子中,我们对字符串 上下文感兴趣——变量周围的双引号产生一个字符串。因此,字符串中任意数量的空格和其他特殊字符(?、[、\)都将成为字符串的一部分:

#!/bin/sh
doc="Reference Manual.pdf"
doc_path="$XDG_DOCUMENTS_DIR/$doc"
echo "$doc_path"
$ sh script.sh
/home/user/Documents/Reference Manual.pdf

另一方面,其他两个用例将在列表上下文中产生输出——列表中的每个单词都是由空格分隔的字段。

例如,如果我们使用“ $@ ”处理位置参数,它将以列表形式生成参数,@0、@1、@2 等等,直到 @#:

#!/bin/sh
# Count lines in each file
for f in "$@"; do
  echo $(wc -l "$f")
done
$ sh script.sh /etc/fstab /etc/hostname
13 /etc/fstab
1 /etc/hostname

2.2. 双引号内的命令替换

同样的概念也适用于命令替换。通常,我们在*$()*符号或反引号内替换命令。但是,我们应该知道,使用反引号进行命令替换不是 POSIX 方式,一些 shell 可能会抱怨它

#!/bin/sh
# Prefer this
result="$(lsblk | grep sda)"
# Not this
result="`lsblk | grep sda`"

在上面的示例中,命令的输出将产生一个字符串,因为我们在字符串 context中使用了双引号。输出的格式将被保留,包括换行符。

但是,如果我们省略引号,格式将不会被保留,因为 shell 将在列表上下文中产生结果:

$ echo "$(lsblk | grep sda)"
sda      8:0    0 119.2G  0 disk 
|-sda1   8:1    0   128M  0 part /boot/efi
|-sda2   8:2    0     8G  0 part [SWAP]
`-sda3   8:3    0 111.1G  0 part /
$ echo $(lsblk | grep sda)
sda 8:0 0 119.2G 0 disk |-sda1 8:1 0 128M 0 part /boot/efi |-sda2 8:2 0 8G 0 part [SWAP] `-sda3 8:3 0 111.1G 0 part /

在此输出中,结果字符串实际上是由空格分隔的字段列表。

3. 处理带有“-”和“+”前缀的文件名

文件名可以包含前导短划线 (-) 或加号 (+)。众所周知,命令行中的破折号 (-) 前缀表示大多数命令的选项。因此,我们的脚本在处理这些文件名时会产生错误。

**幸运的是,我们可以通过在包含破折号或加号前缀的文件名前使用双破折号 (–) 来解决此问题。**它指示命令选项的结束,以便后续参数将被视为文件名:

#!/bin/sh
wc -l -- "$@"
$ sh script.sh -- -text text_file
 2 -text
 1 text_file
 3 total

在上面的脚本中,我们在*$@之前指定了前导双破折号,因此每个带有前导破折号的文件名都将按原样使用。在这种情况下,它会识别“-text”*文件。此外,它不会影响不包含前导破折号或加号的其他文件名。

3.1.处理名为“-”的文件名

我们可能会遇到文件名仅包含一个破折号的文件。但是,某些命令会将其视为标准输入或标准输出。在这些情况下,我们可以对名为“-”的文件使用重定向运算符(<、>)

$ echo "Hello, World!" > -
$ cat < -
Hello, World!

4. read和IFS

4.1. read没有选项

read命令从变量、文件或标准输入中读取输入。当我们在不带任何选项的 shell 脚本中使用read命令时,它会对空格、反斜杠和续行等特殊字符进行一些操作。

例如,让我们在终端中编写一个简单的命令,读取一个字符串,然后打印它的行:

#!/bin/sh
kiss='  Keep \
  It Simple\Stupid'
printf "%s\n" "$kiss" | while read line; do
  printf "%s\n" "$line"
done;
$ sh script.sh
Keep   It SimpleStupid

kiss变量中,我们有一个续行,前导双空格,第二行有一个反斜杠。但是,当我们将此字符串提供给read命令时,它将删除那些出现在换行符和前导空格旁边的反斜杠。

4.2. -r选项

如果我们想覆盖read的默认行为并保留反斜杠怎么办?好吧,在这种情况下,我们需要使用-r*选项*:

...
printf "%s\n" "$kiss" | while read -r line; do
    printf "%s\n" "->$line"
...
$ sh test.sh
->Keep \
->It Simple\Stupid

现在,文本打印成两行,就像我们想要的那样。反斜杠也被保留。

4.3. IFS环境变量

上述输出中缺少的一件事是前导双空格。** read命令会占用前导空格,并且没有合适的选项供我们指定。**

因此,我们需要使IFS(内部字段分隔符)环境变量无效(清空)。默认情况下, IFS变量包含我们可以用来分割字符串的分隔符或定界符。

通过清空IFS变量,我们可以按原样读取行,因为将没有分隔符用于分割字符串

...
printf "%s\n" "$kiss" | while IFS= read -r line; do
...
$ sh script.sh
->  Keep \
->  It Simple\Stupid

5. 用反斜杠转义特殊字符

在 shell 中,转义特殊字符的最常见方法是在字符之前使用反斜杠。这些特殊字符包括 ?、+、$、! 和 [ 等字符。

让我们尝试在终端中打印这些字符:

$ echo \
> 

当我们回显单个反斜杠时,shell 将其视为续行。因此,为了打印反斜杠,我们需要添加另一个反斜杠:

$ echo \\
\

$ 字符是从 shell 变量中读取的前缀:

$ echo $0
/usr/bin/zsh
$ echo $$
2609
$ echo \$0
$0
$ echo \$$
$$

?、! 和 $ 等其他字符在 shell 中也具有特殊含义。因此,请记住,每当我们在字符串中遇到这些字符时,我们都需要在它们之前添加一个反斜杠以获取文字字符。

6. 使用 Shellcheck 编写健壮的脚本

**Shellcheck 是一个简单的实用程序,我们可以针对我们的 shell 脚本运行它来进行分析。Shellcheck 将检查我们脚本中的错误、警告和潜在的安全漏洞。**它支持各种 shell,如dashbashksh

6.1. 安装

默认情况下,Shellcheck 不随主要发行版一起提供。但是,不用担心,因为它在大多数官方软件包存储库中都可用。

我们可以使用yumapt 之类的包管理器来安装shellcheck包。安装后,我们来验证一下:

$ shellcheck --version
ShellCheck - shell script analysis tool
version: 0.8.0

6.2. 用法

我们将编写一个简单的 shell 脚本,将我们的 IP 地址从一个变量打印到屏幕上:

#!/bin/sh
greeting="Hello!
ip_addr=$(curl -s icanhazip.com 2> /dev/null)
echo "$greeting Your IP is $ip_addr"

现在,让我们针对这个脚本运行shellcheck

$ shellcheck script.sh
In test.sh line 3:
greeting="Hello!
^-- SC1009 (info): The mentioned syntax error was in this simple command.
         ^-- SC1078 (warning): Did you forget to close this double quoted string?
In test.sh line 6:
echo "$greeting. Your IP is $ip_addr"
     ^-- SC1079 (info): This is actually an end quote, but due to next char it looks suspect.
                                    ^-- SC1073 (error): Couldn't parse this double quoted string. Fix to allow more checks.

运行shellcheck后,我们可以看到它打印了很多有用的信息。在这种情况下,我们为问候变量留下了结束引号。在第 6 行,我们开始使用双引号,但该工具指出它可能是*“Hello* .

让我们修复这些错误并再次运行shellcheck

...
greeting="Hello!"
ip_addr=$(curl -s icanhazip.com 2> /dev/null)
echo "$greeting. Your IP is $ip_addr"
...
$ shellcheck script.sh
$

由于我们已经修复了错误,我们没有任何警告。

有时,shellcheck会检测到我们甚至可能没有注意到的非常细微的错误。因此,如果我们编写了大量脚本,shellcheck应该在我们的工具箱中,因为它强制我们使用最佳实践,最终让我们更好地编写 shell 脚本