在Bash中转义字符(用于JSON)

99
我正在使用git,然后将提交消息和其他信息作为JSON负载发布到服务器。
目前我有:
MSG=`git log -n 1 --format=oneline | grep -o ' .\+'`

这会将 MSG 设置为类似以下内容的东西:

Calendar can't go back past today

那么

curl -i -X POST \
  -H 'Accept: application/text' \
  -H 'Content-type: application/json' \
  -d "{'payload': {'message': '$MSG'}}" \
  'https://example.com'

我的真实JSON还有另外几个字段。

这样做很好,但当我有类似上面的提交消息并带有撇号时,JSON会无效。

如何转义bash所需的字符?我不熟悉该语言,因此不确定从哪里开始。至少将'替换为\'应该可以解决问题。


9
额外说明,JSON 应该在值周围使用双引号(而不是单引号),因此即使它在结构上正确并且正确转义,许多(但不是全部)解析器仍然会拒绝上述内容。 - polm23
不是问题的解决方案,但其他人可能会考虑这个:http://dwaves.de/tools/escape/,在我的最小测试中似乎有效。 - Robert Lugg
14个回答

117

jq可以做到这一点。

轻量级、免费且用C语言编写的jqGitHub上拥有超过25k个星标,得到了广泛的社区支持。我个人觉得它在我的日常工作流程中非常快速和有用。

将字符串转换为JSON

echo -n '猫に小判' | jq -Rsa .


"\u732b\u306b\u5c0f\u5224"

解释一下,
- -R 表示 "原始输入" - -s 表示 "包含换行符"(助记: "吞咽") - -a 表示 "ASCII 输出"(可选) - . 表示 "输出 JSON 文档的根部"
Git + Grep 使用案例
要修复 OP 给出的代码示例,只需通过 jq 进行管道处理即可。
MSG=`git log -n 1 --format=oneline | grep -o ' .\+' | jq -Rsa .`

4
对于包含换行的文本,应该添加 -s 选项以获得单一字符串的结果。 - Konard
1
这应该是正确且被接受的答案! - Ismoh

84

使用Python:

这个解决方案不是纯的bash,但它是非侵入性的并且可以处理Unicode。

json_escape () {
    printf '%s' "$1" | python -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
}

请注意,JSON是标准Python库的一部分,并且长期以来一直如此,因此这是一种相当简单的Python依赖项。

或者使用PHP:

json_escape () {
    printf '%s' "$1" | php -r 'echo json_encode(file_get_contents("php://stdin"));'
}

使用方法如下:

$ json_escape "ヤホー"
"\u30e4\u30db\u30fc"

1
第一个参数应该只是一个字符串,它将成为输出JSON中的简单值,而不是一个复杂的对象本身,就像在原始问题中一样。如果您想插入一个复杂的值,使用bash几乎肯定会带来更多麻烦,不值得。 - polm23
2
我喜欢这个。将其更改为简单的一行代码并不难:alias json_escape="python -c 'import json,sys; print json.dumps(sys.stdin.read())'" - Mike D
4
我认为你需要引用 $1,这样就不会丢失空格。 - JW.
如果它调用python,它怎么是纯bash?XD - Tyguy7
2
jq will do the same thing, e.g. jq -aR <<< 'ヤホー' - jchook
显示剩余6条评论

67

不必担心如何正确引用数据,只需将其保存到文件中,并使用curl允许的--data选项中的@构造。为了确保git输出正确转义以供作为JSON值使用,请使用类似jq的工具生成JSON,而不是手动创建它。

jq -n --arg msg "$(git log -n 1 --format=oneline | grep -o ' .\+')" \
   '{payload: { message: $msg }}' > git-tmp.txt

curl -i -X POST \
  -H 'Accept: application/text' \
  -H 'Content-type: application/json' \
  -d @git-tmp.txt \
  'https://example.com'

你也可以直接从标准输入读取,使用-d @-; 我将这留给读者作为练习,构建从git读取并生成正确的有效载荷消息上传到curl的管道流程。

(提示:是jq ... | curl ... -d@- 'https://example.com')


1
没错,我在写这个答案的时候没有考虑到那一点。我现在会进行更新。 - chepner
2
为什么不在bash中跳过文件,直接使用data="$(jq --arg...)"将数据存储到变量中呢? - jchook
1
@jchook回答的结尾提到了如何在没有任何中间存储的情况下完成此操作;curl可以通过管道直接从jq读取,而不是将jq的输出存储在内存中。 - chepner
问题是“如何在bash中转义字符”,所以我必须为那些想要做到这一点并来到这里寻找答案的人们维护正义 :) 问题在于将某些动态内容(来自格式良好的JSON文件或命令输出,或者只是任何文本)编码为JSON的字符串字段,以使用Curl发送到API。 - tishma
我正在尝试将路径作为字符串传递给JSON文件,但它自动更改为绝对路径:(不知道为什么。 - shzyincu
显示剩余2条评论

23
我也在尝试使用JSON进行转移时,试图在Bash中转义字符时遇到了这个问题。我发现实际上有更多需要转义的字符列表——特别是如果您正在尝试处理自由格式文本。
我发现两个有用的提示:
  • 使用在此线程中描述的Bash ${string//substring/replacement} 语法。
  • 使用制表符、换行符、回车等实际控制字符。在vim中,您可以通过键入Ctrl+V,然后是实际的控制代码(例如,Ctrl+I表示制表符)来输入这些字符。
我想出的结果Bash替换如下:
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//\\/\\\\} # \ 
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//\//\\\/} # / 
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//\'/\\\'} # ' (not strictly needed ?)
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//\"/\\\"} # " 
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//   /\\t} # \t (tab)
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//
/\\\n} # \n (newline)
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//^M/\\\r} # \r (carriage return)
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//^L/\\\f} # \f (form feed)
JSON_TOPIC_RAW=${JSON_TOPIC_RAW//^H/\\\b} # \b (backspace)

目前我还不知道如何正确地转义Unicode字符,这似乎也是必需的。如果我解决了这个问题,我会更新我的答案。


1
关于在其他地方建议您使用curl的-d参数和@修饰符,这并不能解决问题。事实上,我已经在使用它,并发现文件的内容仍然需要按照JSON所期望的方式进行正确编码。 - xsgordon
我需要替换所有转义字符(八进制:033)。我发现你可以使用单引号语法,像这样:JSON_TOPIC_RAW=${JSON_TOPIC_RAW//$'\033'/_}。控制字符列表:https://unicodelookup.com/#ctrl。 - illnr

16

好的,我找到该怎么做了。Bash自然支持此功能,尽管语法并不容易猜测!

基本上${string//substring/replacement}返回你所想要的结果,因此你可以使用:

MSG=${MSG//\'/\\\'}
为了做到这一点,下一个问题是第一个正则表达式不再起作用,但可以用以下内容代替。
git log -n 1 --pretty=format:'%s'

最终,我甚至不需要对它们进行转义。相反,我只是将JSON中的所有'替换为\"。嗯,你每天都能学到一些新东西。


9
这绝不是完全符合JSON转义的方式。真正的转义需要用 \t 替换制表符,用 \n 替换换行符,将字面意义的反斜杠加倍等等。 - Charles Duffy

9
git log -n 1 --format=oneline | grep -o ' .\+' | jq --slurp --raw-input

上述代码对我有效。有关更多jq工具,请参见https://github.com/stedolan/jq

5
我发现了类似这样的东西:
MSG=`echo $MSG | sed "s/'/\\\\\'/g"`

MSG最终是没问题的 - 我想我只需要像MSG = MSG.replace("'","'")这样做,但不确定如何在bash中实现。 - Rich Bradshaw
这里有一些关于编程的内容:http://www.cyberciti.biz/faq/unix-linux-replace-string-words-in-many-files/。也许你可以更快地掌握它。 - Dmitry Koroliov

4
最简单的方法是使用jshon,这是一个命令行工具,用于解析、读取和创建JSON。 jshon -s '您的数据放在这里。' 2>/dev/null

3
有时候在你的环境中添加一个支持JSON的工具是不可行的,所以这里提供了一个POSIX解决方案,以shell函数的形式呈现,应该适用于每个UNIX/Linux系统。
json_stringify() {
    LANG=C command -p awk '
        BEGIN {
            ORS = ""

            for ( i = 1; i <= 127; i++ )
                tr[ sprintf( "%c", i) ] = sprintf( "\\u%04x", i )

            for ( i = 1; i < ARGC; i++ ) {
                s = ARGV[i]
                print "\""
                while ( match( s, /[\001-\037\177"\\]/ ) ) {
                    print substr(s,1,RSTART-1) tr[ substr(s,RSTART,RLENGTH) ]
                    s = substr(s,RSTART+RLENGTH)
                }
                print s "\"\n"
            }
        }
    ' "$@"
}

旁白:您可能更喜欢使用广泛可用的(但非POSIX)perl

json_stringify() {
    LANG=C perl -le '
        for (@ARGV) {
            s/[\x00-\x1f\x7f"\\]/sprintf("\\u%04x",ord($&))/ge;
            print "\"$_\""
        }
    ' -- "$@"
}
例子:
json_stringify '"foo\bar"' 'hello
world'

每个参数都会被转换为一个 JSON 字符串,并按行输出。
"\u0022foo\u005cbar\u0022"
"hello\u000aworld"
限制:
  • 无法处理NUL字节。

  • 不验证UNICODE输入;它只转义RFC 8259指定的强制ASCII字符。

  • 输入大小有限(当输入过大时,会出现“参数列表太长”错误)。


回答OP的问题:

以下是您可以使用json_stingify函数构建有效的JSON对象的方法:

MSG=$(git log -n 1 --format=oneline | grep -o ' .\+')

curl -i -X POST \
  -H 'Accept: application/text' \
  -H 'Content-type: application/json' \
  -d '{"payload": {"message": '"$(json_stringify "$MSG")"'}}' \
  'https://example.com'

1
我认为 JSON RFC 在转义列表中错误地忘记了包括 x7F \177 - 但很幸运,你的解决方案已经包含了它。 - RARE Kpop Manifesto

2
中译英:
[...] 如果其中有一个撇号,JSON 就无效了。
根据 https://www.json.org 的说法并非如此。JSON 字符串中允许使用单引号。
如何转义 bash 中所需的字符?
您可以使用 正确地准备要 POST 的 JSON。
由于无法测试 https://example.com,因此我将以 https://api.github.com/markdown(请参见 this 答案)作为示例。
假设 'çömmít' "mêssågè"git log -n 1 --pretty=format:'%s' 的奇异输出。
创建(序列化)带有正确转义的 "text" 属性值的 JSON 对象:
$ git log -n 1 --pretty=format:'%s' | \
  xidel -se 'serialize({"text":$raw},{"method":"json","encoding":"us-ascii"})'
{"text":"'\u00E7\u00F6mm\u00EDt' \"m\u00EAss\u00E5g\u00E8\""}

Curl(变量)

$ eval "$(
  git log -n 1 --pretty=format:'%s' | \
  xidel -se 'msg:=serialize({"text":$raw},{"method":"json","encoding":"us-ascii"})' --output-format=bash
)"

$ echo $msg
{"text":"'\u00E7\u00F6mm\u00EDt' \"m\u00EAss\u00E5g\u00E8\""}

$ curl -d "$msg" https://api.github.com/markdown
<p>'çömmít' "mêssågè"</p>

Curl(管道)

$ git log -n 1 --pretty=format:'%s' | \
  xidel -se 'serialize({"text":$raw},{"method":"json","encoding":"us-ascii"})' | \
  curl -d@- https://api.github.com/markdown
<p>'çömmít' "mêssågè"</p>

实际上,如果您已经在使用xidel,则无需使用curl。
Xidel(管道)
$ git log -n 1 --pretty=format:'%s' | \
  xidel -s \
  -d '{serialize({"text":read()},{"method":"json","encoding":"us-ascii"})}' \
  "https://api.github.com/markdown" \
  -e '$raw'
<p>'çömmít' "mêssågè"</p>

Xidel(管道,查询内)

$ git log -n 1 --pretty=format:'%s' | \
  xidel -se '
    x:request({
      "post":serialize(
        {"text":$raw},
        {"method":"json","encoding":"us-ascii"}
      ),
      "url":"https://api.github.com/markdown"
    })/raw
  '
<p>'çömmít' "mêssågè"</p>

Xidel(全查询)
$ xidel -se '
  x:request({
    "post":serialize(
      {"text":system("git log -n 1 --pretty=format:'\''%s'\''")},
      {"method":"json","encoding":"us-ascii"}
    ),
    "url":"https://api.github.com/markdown"
  })/raw
'
<p>'çömmít' "mêssågè"</p>

在 .sh 文件中,我传递了一个路径,例如 /notebooks/folder/test.py,但它自动转换为 C:/Program Files/Git/notebooks/folder/test.py,不知道为什么 :( - shzyincu
@shzyincu 这里提供的信息太少了,无法对其进行有用的评论。请您提出一个新问题或者查看http://videlibri.sourceforge.net/xidel.html#contact。 - Reino

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接