close
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions JSON.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ throw () {
BRIEF=0
LEAFONLY=0
PRUNE=0
FORCE_NO_HEAD=0
NO_HEAD=0
NORMALIZE_SOLIDUS=0
FORMAT=default
FORMAT_STRING="[%s]\t%s\n"

usage() {
echo
Expand All @@ -18,16 +21,47 @@ usage() {
echo "-p - Prune empty. Exclude fields with empty values."
echo "-l - Leaf only. Only show leaf nodes, which stops data duplication."
echo "-b - Brief. Combines 'Leaf only' and 'Prune empty' options."
echo "-f - Output format (array (implies -n), default, key-only (implies -n), key-value (implies -n), value-only or short forms). See README."
echo "-n - No-head. Do not show nodes that have no path (lines that start with [])."
echo "-s - Remove escaping of the solidus symbol (stright slash)."
echo "-h - This help text."
echo
}

parse_format_option() {
NO_HEAD=$FORCE_NO_HEAD
case $1 in
a|array)
FORMAT_STRING="[%s]=%s\n"
NO_HEAD=1
FORMAT=array
;;
d|default)
FORMAT_STRING="[%s]\t%s\n"
FORMAT=default
;;
key|key-only)
FORMAT_STRING= # Set empty as a flag to parse_value()
FORMAT=key-only
NO_HEAD=1
;;
kv|key-value)
FORMAT_STRING="%s\t%s\n"
FORMAT=key-value
NO_HEAD=1
;;
value|value-only)
FORMAT_STRING= # Set empty as a flag to parse_value()
FORMAT=value-only
;;
# It's important to throw an error here if we were passed an empty argument
*) throw "Invalid format '$1' specified. Valid options are array, default, key-only, key-value or value-only."
esac
}

parse_options() {
set -- "$@"
local ARGN=$#
while [ "$ARGN" -ne 0 ]
while [ $# -ne 0 ]
do
case $1 in
-h) usage
Expand All @@ -37,11 +71,15 @@ parse_options() {
LEAFONLY=1
PRUNE=1
;;
-f) parse_format_option "$2"
shift 1
;;
-l) LEAFONLY=1
;;
-p) PRUNE=1
;;
-n) NO_HEAD=1
-n) FORCE_NO_HEAD=1
NO_HEAD=1
;;
-s) NORMALIZE_SOLIDUS=1
;;
Expand All @@ -51,7 +89,6 @@ parse_options() {
;;
esac
shift 1
ARGN=$((ARGN-1))
done
}

Expand Down Expand Up @@ -181,7 +218,25 @@ parse_value () {
[ "$LEAFONLY" -eq 0 ] && [ "$PRUNE" -eq 1 ] && [ "$isempty" -eq 0 ] && print=1
[ "$LEAFONLY" -eq 1 ] && [ "$isleaf" -eq 1 ] && \
[ $PRUNE -eq 1 ] && [ $isempty -eq 0 ] && print=1
[ "$print" -eq 1 ] && printf "[%s]\t%s\n" "$jpath" "$value"

if [ "$print" -eq 1 ]; then
if [ -n "$FORMAT_STRING" ]; then
printf "$FORMAT_STRING" "$jpath" "$value"
else
# Have to handle key-only and value-only special because they don't use both variables
case $FORMAT in
key-only)
echo "$jpath"
;;
value-only)
echo "$value"
;;
*)
throw "Unknown format option '$FORMAT'. HINT: Use parse_options()."
;;
esac
fi
fi
:
}

Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ curl registry.npmjs.org/express | ./JSON.sh | egrep '\["versions","[^"]*"\]'
-b
> Brief output. Combines 'Leaf only' and 'Prune empty' options.

-f format
> Output format. See [below](#format-options).

-l
> Leaf only. Only show leaf nodes, which stops data duplication.

Expand All @@ -47,6 +50,45 @@ curl registry.npmjs.org/express | ./JSON.sh | egrep '\["versions","[^"]*"\]'
-h
> Show help text.

## Format options
By default, parsed values are output in the form

``` bash
[path]<tab>value
```

where ```path``` is the path for the value, ```<tab>``` is a literal tab, and value is the JSON value. That's nice and human-readable, but not always the best for parsing. These additional output formats are available. There are also short-forms available.

### default (short: d)
This is the default output format that you get if ```-f``` isn't specified.

### array (short: a)
This produces output suitable for loading directly into a bash associative array. It implies -n.

```bash
[path]=value
```

### key-only (short: key)
Output just paths, one per line, without []'s. For example, run against package.json you get:

``` bash
...
"bin","JSON.sh"
"bin"
...
```

### key-value (short: kv)
Suitable for processing with the read built-in.

``` bash
path<tab>value
```

### value-only (short: value)
Similar to key mode, except you get values one-per-line instead.

## Cool Links

* [step-/JSON.awk](https://github.com/step-/JSON.awk) JSON.sh ported to awk
Expand Down
8 changes: 5 additions & 3 deletions all-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ do
passed=$((passed+1))
else
echo FAIL: $test $fail
failed="$failed $test"
fail=$((fail+ret))
fi
done

if [ $fail -eq 0 ]; then
echo -n 'SUCCESS '
echo "SUCCESS $passed / $tests"
else
echo -n 'FAILURE '
echo "FAILURE $passed / $tests"
echo "Failed tests: $failed"
exit 1
fi
echo $passed / $tests
115 changes: 115 additions & 0 deletions example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env bash

#trap 'echo "$BASH_SOURCE: line $LINENO: returned $?" >&2' ERR
#set -o errexit -o errtrace -o pipefail

cd ${0%/*}
. JSON.sh

declare -A Aexec Afunc Akv
spaces='{ "nested key": {"key with spaces": "value with spaces"}, "tab\tkey":"tab\tvalue", "unnested key": "unnested value","\"qk": "qv\"", "bad": "=\\\"" }'
spacetoken=`echo $spaces | tokenize`

echo "You can save tokens in a variable"
tokens=$(tokenize < package.json)
echo
echo "But be careful with quoting"
echo -n ' Raw tokens: ' ; echo $tokens | wc
echo -n ' Quoted tokens: ' ; echo "$tokens" | wc
echo -n ' Raw parsed tokens: ' ; echo $tokens | parse | wc
echo -n 'Quoted parsed tokens: ' ; echo "$tokens" | parse | wc
echo
echo "You can change output options on the fly"
parse_options -f key-only
keys=$( echo "$tokens" | parse )
echo 'Keys are:' $keys
parse_options -f value-only
echo 'Values are:' $( echo "$tokens" | parse )
echo
echo
echo "Grab a single key-value"
parse_options -f default
echo "$tokens" | parse | egrep '^\["repository","url"]'
echo "Or with key-value output"
parse_options -f kv
echo "$tokens" | parse | egrep '^"repository","url"'
echo "Which you can feed to cut because it's tab delimited and JSON doesn't allow tabs"
echo "$tokens" | parse | egrep '^"repository","url"' | cut -f2
echo
echo
echo "Key-value mode is really meant for use with read, and is probably the safest way to use JSON.sh"
lines=0
echo "$tokens" | parse | {
IFS=$'\t'
while read -r key value; do
((lines++))
[ "$key" == "author" ] && echo Be careful of quoting $key # Won't match!
[ "$key" == '"author"' ] && echo Be careful of quoting $value # Note that this can output out of order!
[ "$key" == '"repository","url"' ] || continue
echo "line $lines: $value"
printf 'Or with printf: value=%s\n' "$value"
done
echo $lines total lines
}
echo
echo "With spaces in key ($spaces)"
echo Without IFS IS BAD
echo "$spacetoken" | parse | {
while read -r key value; do
if echo "$key" | grep -q nest; then
echo -n 'BAD!: '
else
echo -n 'good: '
fi
echo "'$key' = '$value'"
done
}
echo With IFS is good
saveifs=$IFS
IFS=$'\t'
while read -r key value; do
echo "'$key' = '$value'"
done < <( echo "$spacetoken" | parse )
IFS=$saveifs
echo
echo "You can also use associative arrays, but it's dangerous. If someone figures out how to bypass quoting their arbitrary value will get eval'ed!"
eval Aexec=(`./JSON.sh -f array < package.json`)
parse_options -f array
eval Afunc=(`cat package.json | tokenize | parse`)
echo
[ "${!Aexec[*]}" == "${!Afunc[*]}" ] && echo "Keys match" || echo "Keys don't match!!"
[ "${Aexec[*]}" == "${Afunc[*]}" ] && echo "Values match" || echo "Values don't match!!"
for key in "${!Afunc[@]}"; do
printf '%20s = %s\n' "$key" "${Afunc[$key]}"
done
echo
echo "Need to be careful with array quoting too"
out=`echo "$spacetoken" | parse`
echo $out
eval Afunc=($out)
echo OK
for key in "${!Afunc[@]}"; do printf '%40s = %s\n' "$key" "${Afunc[$key]}"; done
echo bad
for key in ${!Afunc[@]}; do printf '%40s = %s\n' "$key" "${Afunc[$key]}"; done
echo bad
for key in "${!Afunc[*]}"; do printf '%40s = %s\n' "$key" "${Afunc[$key]}"; done
echo bad
for key in ${!Afunc[*]}; do printf '%40s = %s\n' "$key" "${Afunc[$key]}"; done
echo
echo "You can define an associative array in a read loop, and get similar results without the danger of eval."
parse_options -f kv
saveifs=$IFS
IFS=$'\t'
while read -r key value; do
Akv[$key]=$value
done < <( echo "$spacetoken" | parse )
IFS=$saveifs
parse_options -f array
eval Afunc=(`echo "$spacetoken" | parse`)

echo Akv:
for key in "${!Akv[@]}"; do printf '%40s = %s\n' "$key" "${Akv[$key]}"; done
echo Afunc:
for key in "${!Afunc[@]}"; do printf '%40s = %s\n' "$key" "${Afunc[$key]}"; done

# vi: expandtab ts=2 sw=2
Loading