-
发生时间:2023-05-17
-
问题描述:
- 我在产线客户端中实现了一个 check_no_less_than 模块,它的作用是通过 SSH 登录设备 bash 并执行命令,并检查返回的数据是否不小于期望值。
- 在测试 check_no_less_than 模块时,我构造了一个测试用例:发送一条命令给设备,让设备返回的 data 为20,而我配置的期望值为 30,看此函数是否能报错。(代码如下所示)
// check_no_less_than_test.js const cmd = `echo "{\"code\": 0, \"data\": 20}"` // ssh_client.js const hostCmd = `${this.excutor} root@${this.options.ip} '${cmd}'`- 当运行此测试用例时,会报 JSON 解析错误:
Uncaught SyntaxError: Unexpected token c in JSON at position 1 -
问题类别:软件开发
-
原因分析:
- 错误信息表明返回的数据不符合 JSON 格式,于是先打印返回的数据,结果为
{code: 0, data: 20},原来双引号不见了。 - 很容易想到是转义问题,因此先在终端环境下进行测试,最终发现需要进行两次转义(不仅要转义双引号,还要把转义双引号的转义符也转义)。
- 经过耐心查阅 bash 手册,分析出原因:这条命令实际上先被本地的 bash 解析一次,然后又被远端的 bash 再解析一次。(详见下面「bash 解析引号原理」一章)
- 错误信息表明返回的数据不符合 JSON 格式,于是先打印返回的数据,结果为
-
解决方案:将命令修改为
echo {\\\"code\\\": 0, \\\"data\\\": 20} -
实施结果:使用修改后的命令,能够正常跑通测试用例。
-
经验总结:
- SSH 登录远端设备执行 echo 命令返回双引号时要转义两次,因为命令实际上被(本地和远端的 bash)解析了两次。
- bash 和 JavaScript 两门语言中单引号的用法的不同的:bash 里面单引号可以保留反斜杠的字面含义,而 JavaScript 里面单引号没有这种用法。
- 字符串转义问题特别容易出错,建议最好提前在终端实测一下,由简到繁逐步调试。
bash 解析引号原理
首先我们需要理解单引号和双引号在 bash 里面的用法,然后再来分析一些案例。
单引号
先看 bash 手册对单引号的介绍:
Enclosing characters in single quotes (‘’’) preserves the literal value of each character within the quotes. A single quote may not occur between single quotes, even when preceded by a backslash.
意思很简单,单引号会保留被单引号所括住的任意字符的字面含义,包括反斜杠。这意味着在 bash 里面无法使用反斜杠来转义单引号。
$ echo '\'
\
$ echo '$$'
$$
$ echo '`echo hi`'
`echo hi`
$ echo '"'
"
# 单引号里面的反斜杠没有转义效果,因此这里 bash 在等待用户输出另一个单引号。
$ echo '\''
>
从上面测试可以看到,单引号可以保留反斜杠、美元符号、反引号和双引号等特殊字符的字面含义,同时无法转义自身。
双引号
再看 bash 手册对双引号的介绍(这里删掉一些对本文主题关系不大的描述,对那些内容感兴趣的同学可自行点击链接去阅读):
Enclosing characters in double quotes (‘"’) preserves the literal value of all characters within the quotes, with the exception of ‘$’, ‘
’, ‘\’. The characters ‘$’ and ‘’ retain their special meaning within double quotes (see Shell Expansions). The backslash retains its special meaning only when followed by one of the following characters: ‘$’, ‘`’, ‘"’, ‘\’, or newline. Within double quotes, backslashes that are followed by one of these characters are removed. Backslashes preceding characters without a special meaning are left unmodified. A double quote may be quoted within double quotes by preceding it with a backslash.
这里可以看到 bash 里面双引号与单引号的区别:
- 单引号会保留所有字符的字面含义,双引号也会保留大部分字符的字面含义,但有三个例外:
$、反引号和反斜杠。 - 单引号里面,反斜杠没有转义效果;双引号里面,当反斜杠后面紧跟
$、单引号、双引号、反斜杠和换行符时,会有转义效果,bash 解析的时候会将反斜杠删除;否则不会删除。
# "$$" 在 bash 里面代表当前 shell 的 PID
$ echo "$$"
3735
# 反斜杠在 bash 里面代表命令替换(Command Substitution)
$ echo "`echo hi`"
hi
# 当双引号里面的反斜杠后面紧跟的字符不可转义时,会保留反斜杠
$ echo "\a"
\a
$ echo "'"
'
# 双引号里面的反斜杠把紧跟的双引号转义了,因此这里 bash 在等待用户输入另一个双引号。
$ echo "\"
>
案例分析
掌握以上基础知识后,我们便可以进行一些案例分析了。
-
在本地终端打印不带任何引号的
hello world$ echo hello world hello world -
在本地终端打印被双引号括起来的
hello world# bash 里面双引号被用于保留字符的字面含义,因此不能直接打印,需要转义或用单引号括起来。 $ echo "hello world" hello world -
在本地终端打印被双引号括起来、且双引号被转义的
hello world# bash 里面反斜杠后面紧跟双引号,会保留双引号的字面含义,因此双引号能被打印。 $ echo \"hello world\" "hello world" -
在本地终端打印被双引号括起来、且双引号又被单引号括起来的
hello world# bash 里面单引号会保留任意字符(包括双引号)的字面含义,因此双引号能被打印。 $ echo '"hello world"' "hello world" -
SSH 远端登录设备(下文的 back 是我个人的一台 Linux 主机)打印不带任何引号的
hello world:$ ssh back echo hello world hello world -
SSH 远端登录设备打印带有双引号的
hello world:# 本地的 bash 解析命令时便把双引号去掉了,因此发给远端的 bash 时就已经没有双引号了。 $ ssh back echo "hello world" hello world -
SSH 远端登录设备打印带有双引号、且双引号被转义的
hello world:# 本地的 bash 解析命令时把转义符去掉,发给远端的 bash 时是带有双引号的,但远端的 bash 解析时会把双引号去掉,最终还是没有双引号。 $ ssh back echo \"hello world\" hello world -
SSH 远端登录设备打印带有双引号、且双引号被转义、且转义符也被转义的
hello world:# 本地的 bash 解析命令时去掉一层转义符,`\\\"` 变成 `\"`,发给远端的 bash 时仍带有转义符和双引号,因此最终能打印双引号。 $ ssh back echo \\\"hello world\\\" "hello world" -
SSH 远端登录设备打印带有双引号、且双引号被转义、且双引号和转义符被单引号括起来的
hello world:# 本地的 bash 解析命令时会保留 `\"` 的字面含义,发给远端的 bash 时仍带有转义符和双引号,因此最终能打印双引号。 $ ssh back echo '\"hello world\"' "hello world"
最后提一个问题:check_no_less_than 的测试用例里面,能否使用案例9的命令呢?这样看上去比案例8要短一点点。
答案是不能。这是因为 JavaScript 和 bash 对单引号的解析是不一样的: 前面说到,bash 里面单引号可以保留反斜杠的字面含义,但 JavaScript 里面单引号和双引号是完全相同的,不能保留反斜杠的字面含义。
以下例子可以证明这一点:
> let cmd = `'\"'`
undefined
> let utf8Encode = new TextEncoder();
undefined
> utf8Encode.encode(cmd)
Uint8Array(3) [ 39, 34, 39 ]
从 node 控制台的输出可以发现,变量 cmd 里面并不会保留转义符——也就是说,在 JavaScript 里面单引号是无法保留转义符的字面含义的。