BASH - REST-API - JSON封装(中)
BASH语言本身作为脚本语言, 功能比较强大.但是语法又特别弱.
有时候我们需要对程序的接口函数进行标准封装. 比如一般的 shell函数的返回值只有 exit code
和 stdout
两种. 但是我们想要得到一般意义的函数返回 ApiResponse的时候. 这个时候怎么办呢. 这篇文章就是一个基本的技巧.
REST风格JSON API返回封装
如果说,我们想要一个标准JSON结构的返回, 这样我们就可以像JAVA API一样把return code和msg以及data一起从标准返回(对于shell来说就只有标准的输出了). 比如:
返回数据的标准Json:
{
"ret": true,
"errmsg": "this is error msg",
"errcode": 0,
"data": {
"A": {
"a": "b"
},
"B": "2y",
"c": true,
"d": 12346
}
}
在使用上,我们想要的效果是:
API_RESPONSE_RETURN '{"ret":true,"errmsg":"this is errorMsg","errorcode": 0}'
API_RESPONSE_RETURN true "$data"
API_RESPONSE_RETURN false "$msg"
这个时候,我们不再依赖 exit code来判定函数的返回状态了. 所有的数据都变成一个标准的JSON输出. 这样调用方就可以很方便的获取函数的返回状态与数据了. 具体怎么实现, 下面我们接着讲. 要实现对JSON数据的操作,那就要用到一个特别强大的命令. jq
jq
基本用法
这一小节是对jq
命令的基本介绍, 如果已经熟悉的可以直接跳过,直接阅读下一章节.
安装
如果是mac环境,那安装非常简单. 使用brew命令直接安装.
brew install jq
格式化
如果你输入了一个json, 此时我想要它格式化为一个可读的状态,可以使用:
echo '{"key":"value"}' | jq .
此时可以得到一个格式化好的输出.
读取字段
比如我们有如下的json , 如果我们要取这个数据的 name
字段,怎么办呢.
{
"key": "value",
"data":{
"name": "jim",
"age": 15
}
}
我们可以使用 .data.name
这样的过滤格式把相应的字段过滤出来
jq .data.name << EOF
{
"key": "value",
"data":{
"name": "jim",
"age": 15
}
}
EOF
输出如下:
此时的输出是带有"
的,如果要输出纯文本的内部内容.可以添加 -r
参数
jq -r .data.name << EOF
{
"key": "value",
"data":{
"name": "jim",
"age": 15
}
}
EOF
此时的输出就是我们想要的内部本身了. 以上是基本的jq操作读取json
的基本用法. 我们这里的目的是要先生成JSON返回. 因此我们还要进一步的学习json的生成相关的操作方法.
基于JSON 的 模板字段填充.
我们的JSON API的返回格式如下:
{
"ret":true,
"errcode": 0,
"errmsg": "no error",
"data": {
"key":"value",
"age": 10 ,
"opResult": true
}
}
因此,我们的返回里面是有一些固定的模板字段. ret
,errcode
,errmsg
, 这几个字段是必须且固定的. 因此我们在生成的时候是希望能够直接提供这几个值.然后能够直接拼接出我们要的JSON对象. 这个就是最基本的模板填充.
此基本用法的语法可以参考访问: jq-creating-updating-json
o --null-input/-n:
Don't read any input at all! Instead, the filter is run once using null as the input. This is useful when using jq as a simple calculator or to construct JSON data from scratch.
此时我们先把json写为模板string. 比如:
jq -n --arg ret true --arg errmsg "no error" --arg errcode 0 '{"ret":$ret,"errcode":$errcode,"errmsg": $errmsg}'
# output
{
"ret": "true",
"errcode": "0",
"errmsg": "no error"
}
此时, 基本完成了我们想要的模板渲染的目标. 但是这个结果看起来有一点怪. 就是 ret
应该是一个bool值, 理应该是可以直接写为true
,而不需要写为"true"
的. 上面的errcode
也是这个问题. 里面是一个int. 理应是也是可以直接返回0的.而不用引号包起来.
上面的json格式.在java的json反序列化工具是兼容的. 换言之这个结果给java进行读取. java是可以正常识别的.
此时我们可以使用另外一个argjson参数, 这个参数可以把一个预定义的JSON对象. 此时如果value是一个json对象(如:'{}' 或者是 null ).或者是原始值.(1 , 2 , true 这些 )
○ --arg name value:
This option passes a value to the jq program as a predefined variable. If you run jq with --arg foo bar, then $foo is available in the
program and has the value "bar". Note that value will be treated as a string, so --arg foo 123 will bind $foo to "123".
Named arguments are also available to the jq program as $ARGS.named.
○ --argjson name JSON-text:
This option passes a JSON-encoded value to the jq program as a predefined variable. If you run jq with --argjson foo 123, then $foo is
available in the program and has the value 123.
我们把脚本改成如下:
jq -n --argjson ret true --arg errmsg "no error" --argjson errcode 0 '{"ret":$ret,"errcode":$errcode,"errmsg": $errmsg}'
# output
{
"ret": true,
"errcode": 0,
"errmsg": "no error"
}
此时我们已经有了基本的JSON API标准数据模板了. 对于DATA的构建,还需要我们进一步的研究.
基于JSON 的 data 字段填充
对于data字段. 我们一般认为其是一个map或者是一个字典. 这个时候就没有办法用已经存在的json模板进行填充. 我们想实现类型于java的map的接口.
Map<String,Object> obj = new HashMap<>();
obj.put("key","value");
Map<String,Object> data = new HashMap<>();
data.put("name","jim");
data.put("age": 1000);
data.put("embeddedObj": obj);
String dataJson = JSON.toJsonString(data);
这个又该怎么实现呢? 我们需要用到 jq
命令的 json生成与合并方法. jq的命令中有一个操作符号: +=
, 比如我原来有数据.
echo '{"age": 100}' | jq '. += {"key": "value"}'
# output
{
"age": 100,
"key": "value"
}
在上面的命令中, 原来有一个JSON: {"age":100}
,现在给其动态的添加了一个{"key":"value"}
, 上面的key
与value
是固定值, 我们需要使用动态参数才能实现动态填充. 由于jq
的单引号的command
无法填充shell
的变量. 同时这里也无法使用到jq
的 模板填充功能. 因此需要做一些变化.:
if [[ "${val}" =~ ^[0-9]+(.[0-9]+){0,1}$ ]] || [[ "$val" == "true" ]] || [[ "$val" =~ ^\{.*\}$ ]]; then
# 场景: bool值 , 数字 , 对象 时, 直接添加
json=$(echo "$json" | jq '. += { '"${key}"' : '"${val}"'}')
else
# value是一个string的情况下,要双引号
json=$(echo "$json" | jq '. += { '"${key}"' : '"\"${val}\""'}')
fi
上面的代码会检查一个输入的value是一个Json对象还是一个string. 如果是JSON对象,就直接把string拼接到了 value的位置. 因为我们知道一个JSON对象是以 [0-9] | true | {}
这样的数据组成的. 因此在放到一个JSON内部的时候是不需要再转义的. 而对于一个 string. 理应首先要把它用双引号包起来,以表明其是一个完整的整体. 同时, 还需要进行一次转义. 比如 这个string "hello \" world";
注: 上面的代码并没有再进一步的处理种情况. 这个需要一个quote逻辑来进行转义.
最终实现
最后我们得到了如下的一个MAP, 或者是JSON builder. 这样就方便我们直接构建JSON:
JSON_BUILDER 函数
# JSON数据构建器. 可以像构建MAP一样构建JSON对象
# 示例:
# JSON_BUILDER key1 val1 key2 '{"key":"value"}'
#
# https://unix.stackexchange.com/questions/686785/unix-shell-quoting-issues-error-in-jq-command
# https://stackoverflow.com/questions/70617932/bash-script-to-add-a-new-key-value-pair-dynamically-in-json
JSON_BUILDER(){
local json='{}';
for (( cnt = 0; cnt < $#; cnt=cnt+2 )); do
#echo "key=${*:cnt+1:1}, value=${*:cnt+2:1}"
local key="${*:cnt+1:1}"
local val="${*:cnt+2:1}"
if [[ "${val}" =~ ^[0-9]+(.[0-9]+){0,1}$ ]] || [[ "$val" == "true" ]] || [[ "$val" =~ ^\{.*\}$ ]]; then
# 场景: bool值 , 数字 , 对象 时, 直接添加
json=$(echo "$json" | jq '. += { '"\"${key}\""' : '"${val}"'}')
else
# value是一个string的情况下,要双引号
json=$(echo "$json" | jq '. += { '"\"${key}\""' : '"\"${val}\""'}')
fi
done
#cat <<< $(jq '.student1 += { "Phone'"${m}"'" : '"${i}"'}' Students.json) > Students.json
echo "${json}"
}
以下是 API_RESPONSE_RETURN
# REST_API 返回数据格式定义
# 格式示例:
# {
# "ret": true
# }
# 参数:
# $1 - ret (true|false) 必须
# $2 - errcode 可选
# $3 - errmsg 可选
# $4 - data - jsonObject json对象 可选
# 一个参数: API_RESPONSE_RETURN true
# 两个参数: API_RESPONSE_RETURN true jsonData
# 两个参数: API_RESPONSE_RETURN false msg
# 两个参数: API_RESPONSE_RETURN false code
API_RESPONSE_RETURN(){
local ret=true
local errcode=0
local errmsg=""
local data=null
case $# in
1)
ret=${1}
;;
2)
ret="$1"
# echo "================"
# echo "$2" | od -c
# echo "================"
# 两个参数:
# 第二个参数可能是 code / msg / data
# 左边的参数 $2 必须加双引号
if [[ "$2" =~ ^[-]{0,1}[0-9]+$ ]]; then
# errCode
errcode=$2
elif [[ "$2" =~ ^\{.*\}$ ]]; then
# data
data="$2"
else
errmsg="$2"
fi
;;
?)
ret=${1}
errcode="$2"
errmsg="$3"
if [[ "$4" =~ \{.*\} ]]; then
data="$4"
fi
;;
esac
if [ "$ret" = false ] && [ "$errcode" = 0 ]; then
errcode=-1
fi
# shellcheck disable=SC2155
local REST_API_JSON=$(jq -n \
--arg ret "${ret}" \
--argjson errcode "$errcode" \
--arg errmsg "$errmsg" \
--argjson data "$data" \
'
{
ret: $ret | test("true"),
errcode: $errcode ,
errmsg: $errmsg,
data: $data
}
')
echo "$REST_API_JSON"
}
测试
最后我们测试如下的
echo "========"
API_RESPONSE_RETURN true 0 'this is error msg' "$(JSON_BUILDER A '{"a":"b"}' B 2y c true d 12346)"
echo "========"
JSON_BUILDER a 1 b 2 c true "d-_+1" hello e 0.3 data '{"a":"b"}'
echo "===2222====="
API_RESPONSE_RETURN false -1 'this is error msg'
输出:
========
{
"ret": true,
"errmsg": "this is error msg",
"errcode": 0,
"data": {
"A": {
"a": "b"
},
"B": "2y",
"c": true,
"d": 12346
}
}
========
{
"a": 1,
"b": 2,
"c": true,
"d-_+1": "hello",
"e": 0.3,
"data": {
"a": "b"
}
}
===2222=====
{
"ret": false,
"errmsg": "this is error msg",
"errcode": -1,
"data": null
}