基本上我有一个 file.log 如下
blah blah
blah blah
Hello world | {"foo": "bar"}
blah blah
Hello earth | {"foo1": "bar1"}
现在我的目标是编写一些 shell 命令以获得如下所需的输出:
Hello earth | "bar"
Hello earth | "bar1"
目前,这就是我所拥有的:
grep Hello file.log | awk -F "|" '{print $1, system("jq " $2)}'
但是调用 jq 给了我这个错误:
jq: error: syntax error, unexpected ':', expecting $end (Unix shell quoting issues?) at <top-level>, line 1:
bin:application
jq: 1 compile error
我想这是因为在 system() 内部,我的 $12 被去掉了所有引号字符 ("),因此 JQ 无法识别它的 json。有什么建议吗?
答案1
你这里有几个问题
system
不返回要打印的内容,它返回您执行的命令的退出值(如果一切正常则返回 0)。您将看到 JSON 解码数据,然后是一行Hello earth 0
- JSON 字符串中的双引号会被 shell 吞掉。您正在执行的结果命令是
jq {foo: bar}
(两个参数,JSON 不再引用) - 如果
$2
包含特殊字符$
,例如 ,您的 shell 将解释它们 - 即使使用正确的引用,也
jq
不会像这样调用,它需要一个过滤器作为第一个参数(例如“.
”),并且它希望从文件或标准输入读取 JSON 输入 - 从日志构建命令并执行它具有巨大的安全含义(如果
$2
是的话怎么办; rm -rf ~
?)。如果可以的话最好避免它。
抛开安全问题不谈,下面是一个awk
在大多数情况下都可以工作的代码:
awk -F "|" '{ printf "%s", $1; system("echo \x27" $2 "\x27 | jq .")}'
它的作用是通过$2
单引号 ( \x27
) 发送到jq
stdin。
但问题依然存在
- 如果
$2
包含单引号,它将破坏整个命令 - 如果
$2
以破折号开头(不太可能),它将被解释为选项echo
(我们可以使用printf
命令而不是echo
) - 已经提到的安全问题(例如,如果
$2
包含...'; rm -r ~; : ' ...
在字符串中的任何位置)
现在有更好的awk
代码
awk -F "|" '{ printf "%s", $1; print $2 | "jq ."; close("jq ."); }'
由于是通过 stdin$2
发送到jq
进程,但现在使用awk
管道,因此 shell 不再解释它,解决了上述所有问题。该jq
命令必须在每一行关闭(终止),因此调用close()
.
答案2
另一种解决方案,不使用 awk,仅使用杰克
诀窍是使用--原始输入,这会将文件读取为 string 数组。
因此,对于每一行,测试符号是否|这里将字符串切入 ,并将该部分解析为 json 字符串
jq -j --raw-input '
. as $line |
if index("|") >= 0
then
[ .[:index("|")-1] ,.[index("|")+2:] ]
else
empty
end |
[ .[0] , ( .[1] | fromjson | to_entries | .[0].value ) ] |
.[0] , " | \"" ,.[1] , "\"\n" ' /tmp/file.log
答案3
xhienne 提供了很好的概述现有代码的问题,以及您想要完成的任务的一个很好的替代方案。
以下是另一种选择:根本不要尝试调用jq
,awk
而是让awk
脚本创建正确的 JSON 输出。
$ awk -F '|' 'BEGIN { print "[" } $2 != "" { if (t != "") print t ","; t = $2 } END { print t, "]" }' file | jq .
[
{
"foo": "bar"
},
{
"foo1": "bar1"
}
]
该awk
代码本身将从找到的 JSON 对象生成以下 JSON 数组(给出问题中的示例):
[
{"foo": "bar"},
{"foo1": "bar1"} ]
这使您可以更自由地工作,jq
而不会使您的脚本太难以维护和理解。
在脚本中使用变量t
只是确保我们不会在最后一个 JSON 对象后面出现尾随逗号。
答案4
长话短说:
jq -r -R '
select(contains(" | ")) |
split(" | ") |
.[0] as $text |
(.[1] | fromjson | to_entries | .[0].value ) as $json_obj_value |
"\($text) | \($json_obj_value)"
' yourlogfile.log
完整答案
大多数人并没有意识到它有多么强大jq
(尽管也可以这么说awk
)。
作为库萨罗南达在他们的回答中深思熟虑地指出,你最好的朋友是旗帜-R
,它将逐行读取输入作为 json 字符串而不是 json 对象。这样我们就可以自由地只处理内部的字符串jq
,根本不需要awk
。
这是文档如何描述它的版本1.6:
--raw-input
/-R:
不要将输入解析为 JSON。相反,每行文本都会作为字符串传递到过滤器。如果与 结合使用
--slurp
,则整个输入将作为单个长字符串传递到过滤器。
对于您想要的输出,您还需要旗帜-r
,这使得它在终端中打印裸字符串而不是 json 字符串。
再次来自文档
--raw-output
/-r
:使用此选项,如果过滤器的结果是字符串,那么它将直接写入标准输出,而不是格式化为带引号的 JSON 字符串。这对于使 jq 过滤器与非基于 JSON 的系统对话非常有用。
因此,解决这个问题后,有几种方法可以解决这个问题jq
。
作为EchoMike444 已经用更命令式的方式回答了,我尝试使用一种不同的方法,这种方法更加流水线化。
jq -r -R '
select(contains(" | ")) |
split(" | ") |
.[0] as $text |
(.[1] | fromjson | to_entries | .[0].value ) as $json_obj_value |
"\($text) | \($json_obj_value)"
' yourlogfile.log
基本上我们
- 丢弃任何不含“|”的行
- 将每一行分成两部分
- 将左侧部分装订起来
$text
以便于阅读 - 将正确的部分解析为 json,获取它的第一个值并将其放入
$json_obj_value
绑定中以方便阅读 - 打印一个字符串
"$text | $json_obj_value"
(\(foo)
这是你如何进行插值jq
)
如果你想让它尽可能紧凑,你可以使用
jq -Rr 'select(contains(" | "))|split(" | ")|"\(.[0]) | \(.[1]|fromjson|to_entries|.[0].value)"' yourlogfile.log
这会更小,但也更难阅读。哪一个最好取决于品味和用例。