857 lines
24 KiB
Bash
857 lines
24 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GLOBALS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DEBUG=0
|
|
INCLEMPTY=0
|
|
NOCASE=0
|
|
WHOLEWORD=0
|
|
FILE=
|
|
NO_HEAD=0
|
|
NORMALIZE_SOLIDUS=0
|
|
BRIEF=0
|
|
PASSTHROUGH=0
|
|
JSON=0
|
|
PRINT=1
|
|
MULTIPASS=0
|
|
FLATTEN=0
|
|
STDINFILE=/var/tmp/JSONPath.$$.stdin
|
|
STDINFILE2=/var/tmp/JSONPath.$$.stdin2
|
|
PASSFILE=/var/tmp/JSONPath.$$.pass1
|
|
declare -a INDEXMATCH_QUERY
|
|
|
|
# ---------------------------------------------------------------------------
|
|
main() {
|
|
# ---------------------------------------------------------------------------
|
|
# It all starts here
|
|
|
|
parse_options "$@"
|
|
|
|
trap cleanup EXIT
|
|
|
|
if [[ $QUERY == *'?(@'* ]]; then
|
|
# This will be a multipass query
|
|
|
|
[[ -n $FILE ]] && STDINFILE=$FILE
|
|
[[ -z $FILE ]] && cat >$STDINFILE
|
|
|
|
while true; do
|
|
tokenize_path
|
|
create_filter
|
|
|
|
cat "$STDINFILE" | tokenize | parse | filter | indexmatcher >$PASSFILE
|
|
|
|
[[ $MULTIPASS -eq 1 ]] && {
|
|
# replace filter expression with index sequence
|
|
SET=$(sed -rn 's/.*,([0-9]+)[],].*/\1/p' $PASSFILE | tr '\n' ,)
|
|
SET=${SET%,}
|
|
QUERY=$(echo $QUERY | sed "s/?(@[^)]\+)/$SET/")
|
|
[[ $DEBUG -eq 1 ]] && echo "QUERY=$QUERY"
|
|
reset
|
|
continue
|
|
}
|
|
|
|
cat $PASSFILE | flatten | json | brief
|
|
|
|
break
|
|
done
|
|
|
|
else
|
|
|
|
tokenize_path
|
|
create_filter
|
|
|
|
if [[ $PASSTHROUGH -eq 1 ]]; then
|
|
JSON=1
|
|
flatten | json
|
|
elif [[ -z $FILE ]]; then
|
|
tokenize | parse | filter | indexmatcher | flatten | json | brief
|
|
else
|
|
cat "$FILE" | tokenize | parse | filter | indexmatcher | flatten | \
|
|
json | brief
|
|
fi
|
|
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
reset() {
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Reset some vars
|
|
declare -a INDEXMATCH_QUERY
|
|
PATHTOKENS=
|
|
FILTER=
|
|
OPERATOR=
|
|
RHS=
|
|
MULTIPASS=0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
cleanup() {
|
|
# ---------------------------------------------------------------------------
|
|
|
|
[[ -e "$PASSFILE" ]] && rm -f "$PASSFILE"
|
|
[[ -e "$STDINFILE2" ]] && rm -f "$STDINFILE2"
|
|
[[ -z "$FILE" && -e "$STDINFILE" ]] && rm -f "$STDINFILE"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
usage() {
|
|
# ---------------------------------------------------------------------------
|
|
|
|
echo
|
|
echo "Usage: JSONPath.sh [-b] [j] [-h] [-f FILE] [pattern]"
|
|
echo
|
|
echo "pattern - the JSONPath query. Defaults to '$.*' if not supplied."
|
|
#echo "-s - Remove escaping of the solidus symbol (straight slash)."
|
|
echo "-b - Brief. Only show values."
|
|
echo "-j - JSON ouput."
|
|
echo "-u - Strip unnecessary leading path elements."
|
|
echo "-i - Case insensitive."
|
|
echo "-p - Pass-through to the JSON parser."
|
|
echo "-w - Match whole words only (for filter script expression)."
|
|
echo "-f FILE - Read a FILE instead of stdin."
|
|
#echo "-n - No-head. Do not show nodes that have no path (lines that start with [])."
|
|
echo "-h - This help text."
|
|
echo
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
parse_options() {
|
|
# ---------------------------------------------------------------------------
|
|
|
|
set -- "$@"
|
|
local ARGN=$#
|
|
while [ "$ARGN" -ne 0 ]
|
|
do
|
|
case $1 in
|
|
-h) usage
|
|
exit 0
|
|
;;
|
|
-f) shift
|
|
FILE=$1
|
|
;;
|
|
-i) NOCASE=1
|
|
;;
|
|
-j) JSON=1
|
|
;;
|
|
-n) NO_HEAD=1
|
|
;;
|
|
-b) BRIEF=1
|
|
;;
|
|
-u) FLATTEN=1
|
|
;;
|
|
-p) PASSTHROUGH=1
|
|
;;
|
|
-w) WHOLEWORD=1
|
|
;;
|
|
-s) NORMALIZE_SOLIDUS=1
|
|
;;
|
|
?*) QUERY=$1
|
|
;;
|
|
esac
|
|
shift 1
|
|
ARGN=$((ARGN-1))
|
|
done
|
|
[[ -z $QUERY ]] && QUERY='$.*'
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
awk_egrep() {
|
|
# ---------------------------------------------------------------------------
|
|
local pattern_string=$1
|
|
|
|
gawk '{
|
|
while ($0) {
|
|
start=match($0, pattern);
|
|
token=substr($0, start, RLENGTH);
|
|
print token;
|
|
$0=substr($0, start+RLENGTH);
|
|
}
|
|
}' pattern="$pattern_string"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
tokenize() {
|
|
# ---------------------------------------------------------------------------
|
|
# json parsing
|
|
|
|
local GREP
|
|
local ESCAPE
|
|
local CHAR
|
|
|
|
if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1
|
|
then
|
|
GREP='egrep -ao --color=never'
|
|
else
|
|
GREP='egrep -ao'
|
|
fi
|
|
|
|
if echo "test string" | egrep -o "test" >/dev/null 2>&1
|
|
then
|
|
ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
|
|
CHAR='[^[:cntrl:]"\\]'
|
|
else
|
|
GREP=awk_egrep
|
|
ESCAPE='(\\\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
|
|
CHAR='[^[:cntrl:]"\\\\]'
|
|
fi
|
|
|
|
local STRING="\"$CHAR*($ESCAPE$CHAR*)*\""
|
|
local NUMBER='-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?'
|
|
local KEYWORD='null|false|true'
|
|
local SPACE='[[:space:]]+'
|
|
|
|
# Force zsh to expand $A into multiple words
|
|
local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')
|
|
if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi
|
|
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$"
|
|
if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
tokenize_path () {
|
|
# ---------------------------------------------------------------------------
|
|
local GREP
|
|
local ESCAPE
|
|
local CHAR
|
|
|
|
if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1
|
|
then
|
|
GREP='egrep -ao --color=never'
|
|
else
|
|
GREP='egrep -ao'
|
|
fi
|
|
|
|
if echo "test string" | egrep -o "test" >/dev/null 2>&1
|
|
then
|
|
CHAR='[^[:cntrl:]"\\]'
|
|
else
|
|
GREP=awk_egrep
|
|
#CHAR='[^[:cntrl:]"\\\\]'
|
|
fi
|
|
|
|
local WILDCARD='\*'
|
|
local WORD='[ A-Za-z0-9_-]*'
|
|
local INDEX="\\[$WORD(:$WORD){0,2}\\]"
|
|
local INDEXALL="\\[\\*\\]"
|
|
local STRING="[\\\"'][^[:cntrl:]\\\"']*[\\\"']"
|
|
local SET="\\[($WORD|$STRING)(,($WORD|$STRING))*\\]"
|
|
local FILTER='\?\(@[^)]+'
|
|
local DEEPSCAN="\\.\\."
|
|
local SPACE='[[:space:]]+'
|
|
|
|
# Force zsh to expand $A into multiple words
|
|
local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')
|
|
if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi
|
|
readarray -t PATHTOKENS < <( echo "$QUERY" | \
|
|
$GREP "$INDEX|$STRING|$WORD|$WILDCARD|$FILTER|$DEEPSCAN|$SET|$INDEXALL|." | \
|
|
egrep -v "^$SPACE$|^\\.$|^\[$|^\]$|^'$|^\\\$$|^\)$")
|
|
[[ $DEBUG -eq 1 ]] && {
|
|
echo "egrep -o '$INDEX|$STRING|$WORD|$WILDCARD|$FILTER|$DEEPSCAN|$SET|$INDEXALL|.'"
|
|
echo -n "TOKENISED QUERY="; echo "$QUERY" | \
|
|
$GREP "$INDEX|$STRING|$WORD|$WILDCARD|$FILTER|$DEEPSCAN|$SET|$INDEXALL|." | \
|
|
egrep -v "^$SPACE$|^\\.$|^\[$|^\]$|^'$|^\\\$$|^\)$"
|
|
}
|
|
if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
create_filter() {
|
|
# ---------------------------------------------------------------------------
|
|
# Creates the filter from the user's query.
|
|
# Filter works in a single pass through the data, unless a filter (script)
|
|
# expression is used, in which case two passes are required (MULTIPASS=1).
|
|
|
|
local len=${#PATHTOKENS[*]}
|
|
|
|
local -i i=0
|
|
local query="^\[" comma=
|
|
while [[ i -lt len ]]; do
|
|
case "${PATHTOKENS[i]}" in
|
|
'"') :
|
|
;;
|
|
'..') query+="$comma[^]]*"
|
|
comma=
|
|
;;
|
|
'[*]') query+="$comma[^,]*"
|
|
comma=","
|
|
;;
|
|
'*') query+="$comma(\"[^\"]*\"|[0-9]+[^],]*)"
|
|
comma=","
|
|
;;
|
|
'?(@'*) a=${PATHTOKENS[i]#?(@.}
|
|
elem="${a%%[<>=!]*}"
|
|
rhs="${a##*[<>=!]}"
|
|
a="${a#$elem}"
|
|
elem="${elem//./[\",.]+}" # Allows child node matching
|
|
operator="${a%$rhs}"
|
|
[[ -z $operator ]] && { operator="=="; rhs=; }
|
|
if [[ $rhs == *'"'* || $rhs == *"'"* ]]; then
|
|
case $operator in
|
|
'=='|'=') OPERATOR=
|
|
if [[ $elem == '?(@' ]]; then
|
|
# To allow search on @.property such as:
|
|
# $..book[?(@.title==".*Book 1.*")]
|
|
query+="$comma[0-9]+[],][[:space:]\"]*${rhs//\"/}"
|
|
else
|
|
# To allow search on @ (this node) such as:
|
|
# $..reviews[?(@==".*Fant.*")]
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*${rhs//\"/}"
|
|
fi
|
|
FILTER="$query"
|
|
;;
|
|
'>='|'>') OPERATOR=">"
|
|
RHS="$rhs"
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*"
|
|
FILTER="$query"
|
|
;;
|
|
'<='|'<') OPERATOR="<"
|
|
RHS="$rhs"
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*"
|
|
FILTER="$query"
|
|
;;
|
|
esac
|
|
else
|
|
case $operator in
|
|
'=='|'=') OPERATOR=
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*$rhs"
|
|
FILTER="$query"
|
|
;;
|
|
'>=') OPERATOR="-ge"
|
|
RHS="$rhs"
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*"
|
|
FILTER="$query"
|
|
;;
|
|
'>') OPERATOR="-gt"
|
|
RHS="$rhs"
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*"
|
|
FILTER="$query"
|
|
;;
|
|
'<=') OPERATOR="-le"
|
|
RHS="$rhs"
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*"
|
|
FILTER="$query"
|
|
;;
|
|
'<') OPERATOR="-lt"
|
|
RHS="$rhs"
|
|
query+="$comma[0-9]+,\"$elem\"[],][[:space:]\"]*"
|
|
FILTER="$query"
|
|
esac
|
|
fi
|
|
MULTIPASS=1
|
|
;;
|
|
"["*) if [[ ${PATHTOKENS[i]} =~ , ]]; then
|
|
a=${PATHTOKENS[i]#[}
|
|
a=${a%]}
|
|
if [[ $a =~ [[:alpha:]] ]]; then
|
|
# converts only one comma: s/("[^"]+),([^"]+")/\1`\2/g;s/"//g
|
|
#a=$(echo $a | sed 's/\([[:alpha:]]*\)/"\1"/g')
|
|
a=$(echo $a | sed -r "s/[\"']//g;s/([^,]*)/\"\1\"/g")
|
|
fi
|
|
query+="$comma(${a//,/|})"
|
|
elif [[ ${PATHTOKENS[i]} =~ : ]]; then
|
|
if ! [[ ${PATHTOKENS[i]} =~ [0-9][0-9] || ${PATHTOKENS[i]} =~ :] ]]
|
|
then
|
|
if [[ ${PATHTOKENS[i]#*:} =~ : ]]; then
|
|
INDEXMATCH_QUERY+=("${PATHTOKENS[i]}")
|
|
query+="$comma[^,]*"
|
|
else
|
|
# Index in the range of 0-9 can be handled by regex
|
|
query+="${comma}$(echo ${PATHTOKENS[i]} | \
|
|
awk '/:/ { a=substr($0,0,index($0,":")-1);
|
|
b=substr($0,index($0,":")+1,index($0,"]")-index($0,":")-1);
|
|
if(b>0) { print a ":" b-1 "]" };
|
|
if(b<=0) { print a ":]" } }' | \
|
|
sed 's/\([0-9]\):\([0-9]\)/\1-\2/;
|
|
s/\[:\([0-9]\)/[0-\1/;
|
|
s/\([0-9]\):\]/\1-9999999]/')"
|
|
fi
|
|
else
|
|
INDEXMATCH_QUERY+=("${PATHTOKENS[i]}")
|
|
query+="$comma[^,]*"
|
|
fi
|
|
else
|
|
a=${PATHTOKENS[i]#[}
|
|
a=${a%]}
|
|
if [[ $a =~ [[:alpha:]] ]]; then
|
|
a=$(echo $a | sed -r "s/[\"']//g;s/([^,]*)/\"\1\"/g")
|
|
else
|
|
[[ $i -gt 0 ]] && comma=","
|
|
fi
|
|
#idx=$(echo "${PATHTOKENS[i]}" | tr -d "[]")
|
|
query+="$comma$a"
|
|
fi
|
|
comma=","
|
|
;;
|
|
*) PATHTOKENS[i]=${PATHTOKENS[i]//\'/\"}
|
|
query+="$comma\"${PATHTOKENS[i]//\"/}\""
|
|
comma=","
|
|
;;
|
|
esac
|
|
i=i+1
|
|
done
|
|
|
|
[[ -z $FILTER ]] && FILTER="$query[],]"
|
|
[[ $DEBUG -eq 1 ]] && echo "FILTER=$FILTER"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
parse_array () {
|
|
# ---------------------------------------------------------------------------
|
|
# json parsing
|
|
|
|
local index=0
|
|
local ary=''
|
|
read -r token
|
|
case "$token" in
|
|
']')
|
|
;;
|
|
*)
|
|
while :
|
|
do
|
|
parse_value "$1" "$index"
|
|
index=$((index+1))
|
|
ary="$ary""$value"
|
|
read -r token
|
|
case "$token" in
|
|
']') break ;;
|
|
',') ary="$ary," ;;
|
|
*) throw "EXPECTED , or ] GOT ${token:-EOF}" ;;
|
|
esac
|
|
read -r token
|
|
done
|
|
;;
|
|
esac
|
|
value=
|
|
:
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
parse_object () {
|
|
# ---------------------------------------------------------------------------
|
|
# json parsing
|
|
|
|
local key
|
|
local obj=''
|
|
read -r token
|
|
case "$token" in
|
|
'}')
|
|
;;
|
|
*)
|
|
while :
|
|
do
|
|
case "$token" in
|
|
'"'*'"') key=$token ;;
|
|
*) throw "EXPECTED string GOT ${token:-EOF}" ;;
|
|
esac
|
|
read -r token
|
|
case "$token" in
|
|
':') ;;
|
|
*) throw "EXPECTED : GOT ${token:-EOF}" ;;
|
|
esac
|
|
read -r token
|
|
parse_value "$1" "$key"
|
|
obj="$obj$key:$value"
|
|
read -r token
|
|
case "$token" in
|
|
'}') break ;;
|
|
',') obj="$obj," ;;
|
|
*) throw "EXPECTED , or } GOT ${token:-EOF}" ;;
|
|
esac
|
|
read -r token
|
|
done
|
|
;;
|
|
esac
|
|
value=
|
|
:
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
parse_value () {
|
|
# ---------------------------------------------------------------------------
|
|
# json parsing
|
|
|
|
local jpath="${1:+$1,}$2" isleaf=0 isempty=0 print=0
|
|
case "$token" in
|
|
'{') parse_object "$jpath" ;;
|
|
'[') parse_array "$jpath" ;;
|
|
# At this point, the only valid single-character tokens are digits.
|
|
''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;;
|
|
*) value=$token
|
|
# if asked, replace solidus ("\/") in json strings with normalized value: "/"
|
|
[ "$NORMALIZE_SOLIDUS" -eq 1 ] && value=$(echo "$value" | sed 's#\\/#/#g')
|
|
isleaf=1
|
|
[ "$value" = '""' ] && isempty=1
|
|
;;
|
|
esac
|
|
[[ -z INCLEMPTY ]] && [ "$value" = '' ] && return
|
|
[ "$NO_HEAD" -eq 1 ] && [ -z "$jpath" ] && return
|
|
|
|
[ "$isleaf" -eq 1 ] && [ $isempty -eq 0 ] && print=1
|
|
[ "$print" -eq 1 ] && printf "[%s]\t%s\n" "$jpath" "$value"
|
|
:
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
flatten() {
|
|
# ---------------------------------------------------------------------------
|
|
# Take out
|
|
|
|
local path a prevpath pathlen
|
|
|
|
if [[ $FLATTEN -eq 1 ]]; then
|
|
cat >"$STDINFILE2"
|
|
|
|
highest=9999
|
|
|
|
while read line; do
|
|
a=${line#[};a=${a%%]*}
|
|
readarray -t path < <(grep -o "[^,]*"<<<"$a")
|
|
[[ -z $prevpath ]] && {
|
|
prevpath=("${path[@]}")
|
|
highest=$((${#path[*]}-1))
|
|
continue
|
|
}
|
|
|
|
pathlen=$((${#path[*]}-1))
|
|
|
|
for i in `seq 0 $pathlen`; do
|
|
[[ ${path[i]} != ${prevpath[i]} ]] && {
|
|
high=$i
|
|
break
|
|
}
|
|
done
|
|
|
|
[[ $high -lt $highest ]] && highest=$high
|
|
|
|
prevpath=("${path[@]}")
|
|
done <"$STDINFILE2"
|
|
|
|
if [[ $highest -gt 0 ]]; then
|
|
sed -r 's/\[(([0-9]+|"[^"]+")[],]){'$((highest))'}(.*)/[\3/' \
|
|
"$STDINFILE2"
|
|
else
|
|
cat "$STDINFILE2"
|
|
fi
|
|
else
|
|
cat
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
indexmatcher() {
|
|
# ---------------------------------------------------------------------------
|
|
# For double digit or greater indexes match each line individually
|
|
# Single digit indexes are handled more efficiently by regex
|
|
|
|
local a b
|
|
|
|
[[ $DEBUG -eq 1 ]] && {
|
|
for i in `seq 0 $((${#INDEXMATCH_QUERY[*]}-1))`; do
|
|
echo "INDEXMATCH_QUERY[$i]=${INDEXMATCH_QUERY[i]}"
|
|
done
|
|
}
|
|
|
|
matched=1
|
|
|
|
step=
|
|
if [[ ${#INDEXMATCH_QUERY[*]} -gt 0 ]]; then
|
|
while read -r line; do
|
|
for i in `seq 0 $((${#INDEXMATCH_QUERY[*]}-1))`; do
|
|
[[ ${INDEXMATCH_QUERY[i]#*:} =~ : ]] && {
|
|
step=${INDEXMATCH_QUERY[i]##*:}
|
|
step=${step%]}
|
|
INDEXMATCH_QUERY[i]="${INDEXMATCH_QUERY[i]%:*}]"
|
|
}
|
|
q=${INDEXMATCH_QUERY[i]:1:-1} # <- strip '[' and ']'
|
|
a=${q%:*} # <- number before ':'
|
|
b=${q#*:} # <- number after ':'
|
|
[[ -z $b ]] && b=99999999999
|
|
readarray -t num < <( (grep -Eo ',[0-9]+[],]' | tr -d ,])<<<$line )
|
|
if [[ ${num[i]} -ge $a && ${num[i]} -lt $b && matched -eq 1 ]]; then
|
|
matched=1
|
|
[[ $i -eq $((${#INDEXMATCH_QUERY[*]}-1)) ]] && {
|
|
if [[ $step -gt 1 ]]; then
|
|
[[ $(((num[i]-a)%step)) -eq 0 ]] && {
|
|
[[ $DEBUG -eq 1 ]] && echo -n "($a,$b,${num[i]}) "
|
|
echo "$line"
|
|
}
|
|
else
|
|
[[ $DEBUG -eq 1 ]] && echo -n "($a,$b,${num[i]}) "
|
|
echo "$line"
|
|
fi
|
|
}
|
|
else
|
|
matched=0
|
|
continue
|
|
fi
|
|
done
|
|
matched=1
|
|
done
|
|
else
|
|
cat -
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
brief() {
|
|
# ---------------------------------------------------------------------------
|
|
# Only show the value
|
|
|
|
if [[ $BRIEF -eq 1 ]]; then
|
|
sed 's/^[^\t]*\t//;s/^"//;s/"$//;'
|
|
else
|
|
cat
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
json() {
|
|
# ---------------------------------------------------------------------------
|
|
# Turn output into JSON
|
|
|
|
local a tab=$(echo -e "\t")
|
|
local UP=1 DOWN=2 SAME=3
|
|
local prevpathlen=-1 prevpath=() path a
|
|
declare -a closers
|
|
|
|
if [[ $JSON -eq 0 ]]; then
|
|
cat -
|
|
else
|
|
while read -r line; do
|
|
a=${line#[};a=${a%%]*}
|
|
readarray -t path < <(grep -o "[^,]*"<<<"$a")
|
|
value=${line#*$tab}
|
|
|
|
# Not including the object itself (last item)
|
|
pathlen=$((${#path[*]}-1))
|
|
|
|
# General direction
|
|
|
|
direction=$SAME
|
|
[[ $pathlen -gt $prevpathlen ]] && direction=$DOWN
|
|
[[ $pathlen -lt $prevpathlen ]] && direction=$UP
|
|
|
|
# Handle jumps UP the tree (close previous paths)
|
|
|
|
[[ $prevpathlen != -1 ]] && {
|
|
for i in `seq 0 $((pathlen-1))`; do
|
|
[[ ${prevpath[i]} == ${path[i]} ]] && continue
|
|
[[ ${path[i]} != '"'* ]] && {
|
|
a=(${!arrays[*]})
|
|
[[ -n $a ]] && {
|
|
for k in `seq $((i+1)) ${a[-1]}`; do
|
|
arrays[k]=
|
|
done
|
|
}
|
|
a=(${!comma[*]})
|
|
[[ -n $a ]] && {
|
|
for k in `seq $((i+1)) ${a[-1]}`; do
|
|
comma[k]=
|
|
done
|
|
}
|
|
for j in `seq $((prevpathlen)) -1 $((i+2))`
|
|
do
|
|
arrays[j]=
|
|
[[ -n ${closers[j]} ]] && {
|
|
let indent=j*4
|
|
printf "\n%0${indent}s${closers[j]}" ""
|
|
unset closers[j]
|
|
comma[j]=
|
|
}
|
|
done
|
|
direction=$DOWN
|
|
break
|
|
}
|
|
direction=$DOWN
|
|
for j in `seq $((prevpathlen)) -1 $((i+1))`
|
|
do
|
|
arrays[j]=
|
|
[[ -n ${closers[j]} ]] && {
|
|
let indent=j*4
|
|
printf "\n%0${indent}s${closers[j]}" ""
|
|
unset closers[j]
|
|
comma[j]=
|
|
}
|
|
done
|
|
a=(${!arrays[*]})
|
|
[[ -n $a ]] && {
|
|
for k in `seq $i ${a[-1]}`; do
|
|
arrays[k]=
|
|
done
|
|
}
|
|
break
|
|
done
|
|
}
|
|
|
|
[[ $direction -eq $UP ]] && {
|
|
[[ $prevpathlen != -1 ]] && comma[prevpathlen]=
|
|
for i in `seq $((prevpathlen+1)) -1 $((pathlen+1))`
|
|
do
|
|
arrays[i]=
|
|
[[ -n ${closers[i]} ]] && {
|
|
let indent=i*4
|
|
printf "\n%0${indent}s${closers[i]}" ""
|
|
unset closers[i]
|
|
comma[i]=
|
|
}
|
|
done
|
|
a=(${!arrays[*]})
|
|
[[ -n $a ]] && {
|
|
for k in `seq $i ${a[-1]}`; do
|
|
arrays[k]=
|
|
done
|
|
}
|
|
}
|
|
|
|
# Opening braces (the path leading up to the key)
|
|
|
|
broken=
|
|
for i in `seq 0 $((pathlen-1))`; do
|
|
[[ -z $broken && ${prevpath[i]} == ${path[i]} ]] && continue
|
|
[[ -z $broken ]] && {
|
|
broken=$i
|
|
[[ $prevpathlen -ne -1 ]] && broken=$((i+1))
|
|
}
|
|
if [[ ${path[i]} == '"'* ]]; then
|
|
# Object
|
|
[[ $i -ge $broken ]] && {
|
|
let indent=i*4
|
|
printf "${comma[i]}%0${indent}s{\n" ""
|
|
closers[i]='}'
|
|
comma[i]=
|
|
}
|
|
let indent=(i+1)*4
|
|
printf "${comma[i]}%0${indent}s${path[i]}:\n" ""
|
|
comma[i]=",\n"
|
|
else
|
|
# Array
|
|
if [[ ${arrays[i]} != 1 ]]; then
|
|
let indent=i*4
|
|
printf "%0${indent}s" ""
|
|
echo "["
|
|
closers[i]=']'
|
|
arrays[i]=1
|
|
comma[i]=
|
|
else
|
|
let indent=(i+1)*4
|
|
printf "\n%0${indent}s${closers[i-1]}" ""
|
|
direction=$DOWN
|
|
comma[i+1]=",\n"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# keys & values
|
|
|
|
if [[ ${path[-1]} == '"'* ]]; then
|
|
# Object
|
|
[[ $direction -eq $DOWN ]] && {
|
|
let indent=pathlen*4
|
|
printf "${comma[pathlen]}%0${indent}s{\n" ""
|
|
closers[pathlen]='}'
|
|
comma[pathlen]=
|
|
}
|
|
let indent=(pathlen+1)*4
|
|
printf "${comma[pathlen]}%0${indent}s" ""
|
|
echo -n "${path[-1]}:$value"
|
|
comma[pathlen]=",\n"
|
|
else
|
|
# Array
|
|
[[ ${arrays[i]} != 1 ]] && {
|
|
let indent=(pathlen-0)*4
|
|
printf "%0${indent}s[\n" ""
|
|
closers[pathlen]=']'
|
|
comma[pathlen]=
|
|
arrays[i]=1
|
|
}
|
|
let indent=(pathlen+1)*4
|
|
printf "${comma[pathlen]}%0${indent}s" ""
|
|
echo -n "$value"
|
|
comma[pathlen]=",\n"
|
|
fi
|
|
|
|
prevpath=("${path[@]}")
|
|
prevpathlen=$pathlen
|
|
done
|
|
|
|
# closing braces
|
|
|
|
for i in `seq $((pathlen)) -1 0`
|
|
do
|
|
let indent=i*4
|
|
printf "\n%0${indent}s${closers[i]}" ""
|
|
done
|
|
echo
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
filter() {
|
|
# ---------------------------------------------------------------------------
|
|
# Apply the query filter
|
|
|
|
local a tab=$(echo -e "\t") v
|
|
|
|
[[ $NOCASE -eq 1 ]] && opts+="-i"
|
|
[[ $WHOLEWORD -eq 1 ]] && opts+=" -w"
|
|
if [[ -z $OPERATOR ]]; then
|
|
egrep $opts "$FILTER"
|
|
else
|
|
egrep $opts "$FILTER" | \
|
|
while read line; do
|
|
v=${line#*$tab}
|
|
case $OPERATOR in
|
|
'-ge') if awk '{exit !($1>=$2)}'<<<"$v $RHS";then echo "$line"; fi
|
|
;;
|
|
'-gt') if awk '{exit !($1>$2) }'<<<"$v $RHS";then echo "$line"; fi
|
|
;;
|
|
'-le') if awk '{exit !($1<=$2) }'<<<"$v $RHS";then echo "$line"; fi
|
|
;;
|
|
'-lt') if awk '{exit !($1<$2) }'<<<"$v $RHS";then echo "$line"; fi
|
|
;;
|
|
'>') v=${v#\"};v=${v%\"}
|
|
RHS=${RHS#\"};RHS=${RHS%\"}
|
|
[[ "$v" > "$RHS" ]] && echo "$line"
|
|
;;
|
|
'<') v=${v#\"};v=${v%\"}
|
|
RHS=${RHS#\"};RHS=${RHS%\"}
|
|
[[ "$v" < "$RHS" ]] && echo "$line"
|
|
;;
|
|
esac
|
|
done #< <(egrep $opts "$FILTER")
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
parse () {
|
|
# ---------------------------------------------------------------------------
|
|
# Parses json
|
|
|
|
read -r token
|
|
parse_value
|
|
read -r token
|
|
case "$token" in
|
|
'') ;;
|
|
*) throw "EXPECTED EOF GOT $token"
|
|
exit 1;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
throw() {
|
|
# ---------------------------------------------------------------------------
|
|
echo "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
if ([ "$0" = "$BASH_SOURCE" ] || ! [ -n "$BASH_SOURCE" ]);
|
|
then
|
|
main "$@"
|
|
fi
|
|
|
|
# vi: expandtab sw=2 ts=2 |