将从 JSON 文档中提取的数据分配给 Zsh shell 参数

将从 JSON 文档中提取的数据分配给 Zsh shell 参数

在日常 shell 会话中,我经常发现自己需要将 JSON 文档中的数据(通过某些 jq 过滤器提取)分配给 Zsh shell 参数:JSON 标量到 Zsh 标量、JSON 数组到 Zsh 数组以及 JSON 对象到 Zsh 关联数组。问题(之前提出的问题似乎没有解决)是数据通常包含换行符(甚至 NUL 字节),这使得这是一项相当重要的任务。

这是我到目前为止所想到的:

function assign-from-json {
    local -A opts && zparseopts -A opts -D -F -M -- a A && typeset -r opts
    if [[ $# -ne 3 || ( -v opts[-a] && -v opts[-A] ) ]] ; then
        >&2 printf 'Usage: %s [-a|-A] NAME FILTER JSON\n' $0
        return 2
    fi
    if [[ -v opts[-a] ]] ; then
        local -a lengths && { lengths=( "${(@f)$( jq -r "$2 | .[] | tostring | length" <<< $3 )}" ) || return $? } && typeset -r lengths
        local data && { data="$( jq -j "$2 | .[] | tostring" <<< $3 )" || return $? } && typeset -r data
        local elem
        local -a elems
        for length in "${lengths[@]}" ; do
            read -u 0 -k $length elem
            elems+=$elem
        done <<< $data
        eval "${(q)1}"='( "${elems[@]}" )'
    elif [[ -v opts[-A] ]] ; then
        local transformed_json && { transformed_json="$( jq "$2 | to_entries | map(.key, .value)" <<< $3 )" || return $? } && typeset -r transformed_json
        assign-from-json -a $1 "." $transformed_json
    else
        eval "${(q)1}"="${(q)$( jq -r $2 <<< $3 )}"
    fi
}

在大多数情况下它工作得很好:

% json='
{
    "scalar": "Hello, world",
    "array": [1, 2, 3],
    "scary_scalar": "\nNewlines\u0000NUL bytes\ttabs",
    "scary_array": [
        "A\nvery\u0000scary\nvalue",
        "A less scary value",
        "eh"
    ]
}
'
% assign-from-json scalar '.scalar' $json && printf '%q\n' $scalar
Hello,\ world
% typeset -a array && assign-from-json -a array '.array' $json && printf '%q\n' "${array[@]}"
1
2
3
% assign-from-json scary_scalar '.scary_scalar' $json && printf '%q\n' $scary_scalar
$'\n'Newlines$'\0'NUL\ bytes$'\t'tabs
% typeset -a scary_array && assign-from-json -a scary_array '.scary_array' $json && printf '%q\n' "${scary_array[@]}"
A$'\n'very$'\0'scary$'\n'value
A\ less\ scary\ value
eh
% typeset -A assoc && assign-from-json -A assoc '.' $json && printf '%q -> %q\n' "${(@kv)assoc}"
array -> \[1,2,3\]
scary_array -> \[\"A\\nvery\\u0000scary\\nvalue\",\"A\ less\ scary\ value\",\"eh\"\]
scary_scalar -> $'\n'Newlines$'\0'NUL\ bytes$'\t'tabs
scalar -> Hello,\ world

不幸的是,它似乎很难处理尾随换行符:

% assign-from-json bad_scalar '.' '"foo\n"' && printf '%q\n$ $bad_scalar
foo
# expected: foo$'\n'
  1. 我认为尾随换行符的问题是由于命令替换删除了它们。你看到一个简单的方法来解决它吗?
  2. assign-from-json -A assoc ...即使assoc不排版为关联数组,也可以这样做。我怎样才能防止这种情况发生?
  3. 您还发现代码还有其他问题吗?

答案1

在命令替换中保留尾随换行符的常见解决方法是附加一个值,以便换行符不再尾随。然后从变量中删除虚拟值:

v1=$'line1\nline2\n\n\n'
v2=$(print $v1)
v3=$(printf $v1; print X);v3=${v3%X}
typeset -p v1 v2 v3

输出:

typeset v1=$'line1\nline2\n\n\n'
typeset v2=$'line1\nline2'
typeset v3=$'line1\nline2\n\n\n'

这确实使处理返回代码变得复杂,因此您可能需要这样的东西:

local data \
  && {data="$( jq -j "$2 | .[] | tostring" <<< $3;
    ret=$?;
    print X;
    return $ret)" \
    || return $? } \
  && data=${data:%X} \
  && typeset -r data

保留尾随换行符的一些其他选项列于这个答案。不幸的是,我认为zsh对于这种情况还没有“命令替换标志”。


参数扩展标志t可用于测试变量的类型并确定名称是否引用关联数组或其他内容:

function vtype {
  tt=${(Pt)1}
  if [[ $tt == association* ]]; then
      print "$1: YES [$tt]"
  else
      print "$1: no  [$tt]"
  fi
}
typeset -A a1; vtype a1
typeset -a a2; vtype a2
integer i1; vtype i1
typeset -AlxRr a3; vtype a3

上面的代码还使用P扩展标志将位置参数值解释$1为另一个参数名称。该测试使用通配符 ( ...*),因为类型标识符t可能具有多个组成部分。输出:

a1: YES [association]
a2: no  [array]
i1: no  [integer]
a3: YES [association-right_blanks-lower-readonly-export]

祝你好运,去做zsh你需要做的事。我经常发现,稍微超出其舒适极限的工具比使用单独的、不太熟悉的工具完全重建效果更好。困难的部分是定义“稍微超出”。

答案2

您应该使用 perl 或 python 等语言而不是 shell(甚至 zsh)。

例如,使用 perlJSON模块:

$ cat json-example.pl 
#!/usr/bin/perl

use strict;
use JSON;

my $json_text;
# slurp the entire input file into a single string.
do { $/=''; $json_text=<> };

my $j = decode_json($json_text);

print $j->{scary_array}->[1], "\n";

示例运行:

$ chmod +x json-example.pl

$ ./json-example.pl input.json 
A less scary value

有关 Perl 数据结构的详细信息,请参阅perldataperllolperldscperlref、的手册页。perlreftut

json 数据实际上非常好地映射到 perl 数据结构 - 无论是在编写代码时还是在转储以使用类似模块进行调试时数据::转储器或者数据::转储,其表示形式与 json 数据几乎相同。

$j例如,如果使用Data::Dumpsdd函数转储,则如下所示:

{
  array => [1, 2, 3],
  scalar => "Hello, world",
  scary_array => ["A\nvery\0scary\nvalue", "A less scary value", "eh"],
  scary_scalar => "\nNewlines\0NUL bytes\ttabs",
}

这实际上就是如何用 Perl 语法定义包含该数据的哈希。您可以将其复制粘贴到脚本中,将其分配给变量(例如my $var = { ... };),并且编译不会出现问题。

my $var = {
  array => [1, 2, 3],
  scalar => "Hello, world",
  scary_array => ["A\nvery\0scary\nvalue", "A less scary value", "eh"],
  scary_scalar => "\nNewlines\0NUL bytes\ttabs",
};

print $var->{array}->[0], "\n";

会打印1.

更重要的是,这个数据在 Perl 中一点也不可怕。这只是数据。


模块注释:

  • Data::Dumper是一个核心 Perl 模块,包含在 Perl 中。
  • Data::Dump不是并且需要单独安装(例如apt install libdata-dump-perl在 Debian 等上,或者使用 perl 进行安装)公用事业)。与核心Data::Dumper模块相比,我更喜欢它。
  • JSON也不是核心 perl 模块(在 Debian 上,使用 . 安装apt install libjson-perl。也可以选择安装libjson-xs-perl,这可以使用编译的 C 代码加速 JSON 模块。

相关内容