Shell脚本如何生成JSON格式输出

wen 实用脚本 1

Shell脚本高效生成JSON格式输出的完整指南

目录导读

  • 为什么需要Shell脚本生成JSON?
  • JSON基础与Shell兼容性分析
  • 手动拼接JSON的陷阱与最佳实践
  • 使用jq命令:专业JSON处理利器
  • 轻量级方案:printfecho安全技巧
  • 处理特殊字符与转义逻辑
  • 从结构化数据生成JSON数组
  • 实战:API响应生成与日志格式化
  • 常见错误排查与问答集锦
  • 选择最适合你的方案

为什么需要Shell脚本生成JSON?

在微服务架构、DevOps自动化与云原生工具链中,JSON已成为数据交换的事实标准,Shell脚本作为服务器运维的核心工具,经常需要生成JSON格式的配置、API响应或日志,Shell对数据结构的原生支持薄弱,手动拼接JSON极易引入语法错误或安全漏洞。

Shell脚本如何生成JSON格式输出

核心场景

  • 自动化部署脚本生成配置JSON
  • 监控系统采集指标后输出结构化数据
  • CI/CD管道中生成测试报告
  • 与REST API交互时构造请求体

JSON基础与Shell兼容性分析

JSON格式要求严格:

  • 键必须用双引号包围
  • 字符串值必须用双引号
  • 不能有尾随逗号
  • 特殊字符(如引号、换行符)需转义

Shell变量默认是字符串,其特殊字符(如空格、换行)会直接破坏JSON结构。

name="Bob's \"Tool\""
echo '{"name":"'$name'"}'
# 输出:{"name":"Bob's "Tool""}  → 语法错误

这揭示了手动拼接的根本问题:Shell将双引号视为字符串边界,而JSON要求它们作为值的一部分。


手动拼接JSON的陷阱与最佳实践

致命陷阱

  1. 变量内嵌引号{$var} 无法处理包含双引号的值
  2. 空格问题echo {"key":"value"} 在严格模式下会报错
  3. 尾随逗号:循环输出数组时容易残留
  4. 多行值:换行符直接破坏JSON结构

最佳实践(仅限简单场景)

使用printf%s格式化字符,避免Shell解释:

name="简单文本"
printf '{"name":"%s"}' "$name"
# 输出:{"name":"简单文本"}

但此方法仍无法处理值内部的引号或Unicode字符。


使用jq命令:专业JSON处理利器

jq是Linux下最强大的JSON处理器,它能从Shell变量、文件甚至管道生成合法JSON。

安装方式

# Ubuntu/Debian
sudo apt-get install jq
# CentOS/RHEL
sudo yum install jq
# macOS
brew install jq
# 验证安装
jq --version

基本生成模式

# 从字符串变量生成
name="Bob"
jq -n --arg name "$name" '{ "name": $name }'
# 输出:{"name":"Bob"}
# 从数值变量生成(无需引号)
age=30
jq -n --argjson age "$age" '{ "age": $age }'
# 从布尔值生成
is_active=true
jq -n --argjson active "$is_active" '{ "active": $active }'
# 多字段组合
jq -n \
  --arg name "Alice" \
  --argjson age 28 \
  --argjson verified false \
  '{ "user": $name, "age": $age, "verified": $verified }'

处理数组与嵌套

# 生成数组
items=("apple" "banana" "cherry")
jq -n --arg items "$(printf '%s\n' "${items[@]}")" ' $items | split("\n") '
# 更安全的数组传递:
jq -n --argjson items "$(printf '%s\n' "${items[@]}" | jq -R -s -c 'split("\n") | map(select(length > 0))')" '{ "fruits": $items }'
# 嵌套对象
jq -n \
  --arg name "Server1" \
  --arg ip "192.168.1.10" \
  --argjson port 8080 \
  '{ 
    "host": $name, 
    "connection": { 
      "ip": $ip, 
      "port": $port 
    } 
  }'

为什么jq更安全?

  • 自动处理所有特殊字符(引号、换行、反斜杠)
  • 保证输出格式严格合法
  • 支持--arg--argjson区分字符与数值类型
  • 内置@json过滤器确保值正确转义

轻量级方案:printfecho安全技巧

当无法安装jq时,可以使用printf配合工具函数。

安全拼接函数

# 导出JSON字符串(自动转义双引号与反斜杠)
json_escape() {
    local raw="$1"
    # 转义反斜杠和双引号
    printf '%s' "$raw" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
# 生成键值对
build_json() {
    local key="$1"
    local value="$2"
    printf '{"%s":"%s"}' "$key" "$(json_escape "$value")"
}
# 使用示例
name='Bob said "Hello"'
echo $(build_json "name" "$name")
# 输出:{"name":"Bob said \"Hello\""}

多行值处理

# 将多行文本转为JSON字符串
multiline_to_json() {
    local raw
    raw=$(cat)
    printf '"%s"' "$(echo "$raw" | sed ':a;N;$!ba;s/\n/\\n/g; s/"/\\"/g')"
}
# 测试
echo 'Line1
Line2
Line3' | multiline_to_json
# 输出:"Line1\\nLine2\\nLine3"

数组生成

# 从空分隔列表生成JSON数组
array_to_json() {
    local delimiter="$1"
    shift
    local items=("$@")
    local json="["
    local first=true
    for item in "${items[@]}"; do
        if $first; then
            first=false
        else
            json+=","
        fi
        json+="\"$(json_escape "$item")\""
    done
    json+="]"
    echo "$json"
}
# 使用
items=("apple" "banana" "cherry")
array_to_json "," "${items[@]}"
# 输出:["apple","banana","cherry"]

处理特殊字符与转义逻辑

JSON中的特殊字符包括:

  • 双引号
  • 反斜杠
  • 换行符\n
  • 制表符\t
  • Unicode字符\uXXXX

通用转义函数

strict_json_escape() {
    local raw="$1"
    printf '%s' "$raw" | \
        sed 's/\\/\\\\/g; 
             s/"/\\"/g; 
             s/\t/\\t/g; 
             s/\r/\\r/g; 
             s/\n/\\n/g; 
             s/\x00//g; 
             s/\x08/\\b/g; 
             s/\x0c/\\f/g'
}
# 测试包含所有危险字符
dangerous="Tab\tNewline\nBackslash\\Quote\""
echo "{\"data\":\"$(strict_json_escape "$dangerous")\"}"

注意事项

  • 永远不要使用eval处理JSON,否则可能导致代码注入
  • 在循环中拼接JSON时,优先使用临时文件或管道传递
  • 对不可信输入(如用户输入、文件路径)务必转义

从结构化数据生成JSON数组

从CSV文件生成

# 假设data.csv内容:
# name,age,city
# Alice,28,New York
# Bob,35,Los Angeles
awk -F',' 'NR>1 {
    printf "{ \"name\": \"%s\", \"age\": %s, \"city\": \"%s\" },\n", $1, $2, $3
}' data.csv | sed '$ s/,$//' | sed '1s/^/[/; $s/$/]/'

从命令输出生成

# 将ps命令输出转为JSON数组
ps aux | awk 'NR>1 {
    user=$1; pid=$2; cpu=$3; mem=$4; command=$11;
    printf "{\"user\":\"%s\",\"pid\":%s,\"cpu\":%s,\"mem\":%s,\"command\":\"%s\"},\n", user, pid, cpu, mem, command
}' | sed '$ s/,$//' | sed '1s/^/[/; $s/$/]/' | jq '.'

使用jq从文件直接加载

# 假设有user.txt,每行一个用户名
cat users.txt | jq -R -s -c 'split("\n") | map(select(length > 0)) | { "users": . }'

实战:API响应生成与日志格式化

场景:监控脚本生成健康检查JSON

#!/bin/bash
# 系统监控指标采集并输出JSON
# 采集数据
hostname=$(hostname)
uptime_seconds=$(cat /proc/uptime | awk '{print int($1)}')
memory_usage=$(free | grep Mem | awk '{printf "%.2f", $3/$2 * 100}')
cpu_load=$(uptime | awk -F'load average:' '{print $2}' | cut -d, -f1 | tr -d ' ')
# 使用jq构建最终JSON
jq -n \
  --arg host "$hostname" \
  --argjson uptime "$uptime_seconds" \
  --argjson mem "$memory_usage" \
  --argjson cpu "$cpu_load" \
  --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
  '{
    "type": "system_check",
    "host": $host,
    "timestamp": $timestamp,
    "metrics": {
      "uptime_seconds": $uptime,
      "memory_percent": $mem,
      "cpu_load_15m": $cpu
    },
    "status": (if $mem > 90 then "critical" elif $mem > 70 then "warning" else "ok" end)
  }'

场景:日志行转为结构化JSON

# 假设日志格式:2024-01-15 ERROR Failed to connect
log_line="2024-01-15 ERROR Failed to connect"
# 解析并生成JSON
read -r date level message <<< "$log_line"
jq -n \
  --arg date "$date" \
  --arg level "$level" \
  --arg msg "$message" \
  '{ "timestamp": $date, "level": $level, "message": $msg }'

常见错误排查与问答集锦

Q1:为什么我的JSON总是缺少逗号或多出逗号?

A:最常见的原因是循环拼接时忘记处理边界,建议:

  • 使用jq--slurp--argjson自动管理数组
  • 若手动拼接,先构建完整字符串再输出
  • 在数组最后一个元素后不加逗号(使用if first逻辑)

Q2:如何安全地将用户输入(如文件名)放入JSON?

A:永远使用jq--arg传递字符串,它自动转义所有特殊字符,示例:

unsafe_filename="my file \"path\".txt"
jq -n --arg fname "$unsafe_filename" '{ "file": $fname }'
# 输出:{"file":"my file \"path\".txt"}

Q3:生成大型JSON数组时,内存不足怎么办?

A:使用流式处理。jq支持管道流:

# 逐行读取并输出JSON,最后用jq聚合
seq 1 100000 | xargs -I {} echo '{"index":{}}' | jq -s -c '.'

Q4:为什么我的JSON布尔值变成了字符串?

A:使用--argjson传递布尔值,而非--arg--arg始终生成字符串:

# 错误
jq -n --arg is_ok true '{ "flag": $is_ok }'
# 输出:{"flag":"true"}  → 字符串
# 正确
jq -n --argjson is_ok true '{ "flag": $is_ok }'
# 输出:{"flag":true}    → 布尔值

Q5:如何测试我的JSON是否合法?

A:使用jq .格式化检查,或echo ... | python3 -m json.tool

echo '{"name":"test"}' | jq .   # 成功则输出格式化JSON
echo '{"name"}' | jq .         # 失败则报错parse error

Q6:在shell脚本中如何复用JSON模板?

A:使用envsubstjq的模板能力:

# 模板文件 template.jq
{
  "service": $service_name,
  "port": $port_number,
  "config": {
    "timeout": $timeout
  }
}
# 调用时注入变量
jq -n --arg service_name "web" --argjson port_number 8080 --argjson timeout 30 -f template.jq

选择最适合你的方案

方案 适用场景 安全性 性能 可维护性
jq命令 任何生产环境
printf+转义函数 无jq的嵌入式环境
heredoc直接拼接 极简单行输出
awk/sed文本处理 从CSV/日志生成

黄金法则:只要有条件,优先使用jq,它由Stedolan开发,在GitHub有20k+星,是Shell生成JSON的事实标准,对于无法安装jq的场景,务必使用严格的转义函数,并避免将变量直接嵌入JSON字符串。

jq官网(访问stedolan.github.io/jq)提供了完整的教程和过滤器参考,强烈建议收藏。

抱歉,评论功能暂时关闭!