如何处理Shell脚本中的特殊字符

2022-12-26 15:16:42 浏览数 (1)

概述

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

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

然后,我们将处理包含特定前缀的文件名。之后,我们将介绍读取命令和IFS变量以逐字读取字符串。 最后,我们将看到Shellcheck实用程序的运行情况,以及我们如何使用它来确保我们的脚本没有任何警告。

2. 用双引号包裹替换

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

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

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

代码语言:javascript复制
$ 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 认为它是由空格分隔的文件列表。为了克服这个问题,我们需要用双引号将文件名括起来:

代码语言:javascript复制
$ mv "file with spaces" /tmp

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

2.1. 双引号内的变量替换

这对于 shell 内部的变量也有些相同。假设我们有一个变量

整体取HOME变量的值 使用空格作为分隔符将字符串拆分为字段 将每个以空格分隔的字段视为一个可以由 shell 扩展的 glob 在我们的例子中,我们对字符串 上下文感兴趣——变量周围的双引号产生一个字符串。因此,字符串中任何数量的空格和其他特殊字符(?、[、)都将成为字符串的一部分:

代码语言:javascript复制
#!/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 等等,直到@#:

代码语言:javascript复制
#!/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. 双引号内的命令替换

同样的概念也适用于命令替换。通常,我们在HOME。用双引号将此变量括起来可能意味着三件事:¨K19K¨G2G另一方面,其他两个用例将在列表上下文中产生输出——列表中的每个单词都是一个由空格分隔的字段。¨K21K¨G3G¨K29K同样的概念也适用于命令替换。通常,我们在()符号或反引号中替换命令。但是,我们应该知道使用反引号替换命令不是 POSIX 方式,一些 shell 可能会抱怨它:

代码语言:javascript复制
#!/bin/sh

# Prefer this
result="$(lsblk | grep sda)"

# Not this
result="`lsblk | grep sda`"

在上面的示例中,命令的输出将产生一个字符串,因为我们在字符串 上下文中使用了双引号。输出的格式将被保留,包括换行符。 但是,如果我们省略引号,格式将不会保留,因为 shell 将在列表上下文中产生结果:

代码语言:javascript复制
$ 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. 处理带有“-”和“ ”前缀的文件名

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

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

代码语言:javascript复制
#!/bin/sh
wc -l -- "$@"
$ sh script.sh -- -text text_file
 2 -text
 1 text_file
 3 total

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

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

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

代码语言:javascript复制
$ echo "Hello, World!" > -

$ cat < -
Hello, World!
4.阅读和IFS
4.1. 阅读无选项

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

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

代码语言:javascript复制
#!/bin/sh

kiss='  Keep 
  It SimpleStupid'

printf "%sn" "$kiss" | while read line; do
  printf "%sn" "$line"
done;

$ sh script.sh
Keep   It SimpleStupid

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

4.2. -r选项_

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

代码语言:javascript复制
printf "%sn" "$kiss" | while read -r line; do
    printf "%sn" "->$line"

$ sh test.sh
->Keep 
->It SimpleStupid

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

4.3. IFS环境变量

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

因此,我们需要取消(清空)IFS(内部字段分隔符)环境变量。默认情况下, IFS变量包含可用于拆分字符串的分隔符或定界符。 通过清空IFS变量,我们可以按原样读取行,因为没有分隔符可用于拆分字符串:

代码语言:javascript复制
...
printf "%sn" "$kiss" | while IFS= read -r line; do
...

$ sh script.sh
->  Keep 
->  It SimpleStupid
5. 用反斜杠转义特殊字符

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

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

代码语言:javascript复制
$ echo 
> 

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

代码语言:javascript复制
$ echo \

$ 字符是从 shell 变量读取的前缀:
代码语言:javascript复制
$ echo $0
/usr/bin/zsh

$ echo $$
2609

$ echo $0
$0

$ echo $$
$$

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

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

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

6.1. 安装

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

我们可以使用yum或apt等包管理器来安装shellcheck包。安装完成后,我们来验证一下:

代码语言:javascript复制
$ shellcheck --version
ShellCheck - shell script analysis tool
version: 0.8.0
6.2. 用法

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

代码语言:javascript复制
#!/bin/sh

greeting="Hello!
ip_addr=$(curl -s icanhazip.com 2> /dev/null)

echo "$greeting Your IP is $ip_addr"

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

代码语言:javascript复制
$ 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后,我们可以看到它打印了很多有用的信息。在本例中,我们保留了greeting变量的结尾引号。在第 6 行中,我们开始使用双引号,但该工具指出它可能是“Hello .

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

代码语言:javascript复制
...
greeting="Hello!"
ip_addr=$(curl -s icanhazip.com 2> /dev/null)

echo "$greeting. Your IP is $ip_addr"
...

$ shellcheck script.sh
$

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

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

七、结论

在本文中,我们讨论了如何处理 shell 中的特殊字符和空格。我们编写了各种小型 shell 脚本来演示针对不同用例的不同方法。

最后,我们介绍了shellscheck静态分析工具以及它如何帮助我们成为更好的 shell 脚本开发人员。

作者:Haidar Ali

源链接:https://www.baeldung.com/linux/special-characters-in-shell-scripts

格式整理:IT运维技术圈

0 人点赞