Skip to content

Commit 273ad63

Browse files
authored
use grep -E, add Makefile, add -S, -A, -T options, fix ensure_deps.sh for macOS, fix command line parsing, make JSONPath.sh and test scripts robust (#16)
* change use of egrep to grep -E While we disagree with the notion that `egrep` is obsolete, tools such as gnu egrep whine when used with the warning: ``` egrep: warning: egrep is obsolescent; using ggrep -E ``` Sadly this pull request changes use of `egrep` to `grep -E` in order to stop the warnings that gnu egrep now issues. * add makefile to install Add a Makefile so that `make install` will work. Updated `README.md` to mention the `make install` alternative. Removed trailing whitespace from `README.md`. * add -S, -A, -T options We add a `-S` option to print spaces around :s'. The default (w/o -S): ```json "submit_slot":2, ``` With `-S`: ```json "submit_slot" : 2, ``` We add an `-A` option start a JSON array on line as JSON member. The default (w/o -A): ```json "authors": [ ``` With `-A`: ```json "authors":[ ``` With `-S -A`: ```json "authors" : [ ``` We add `-T` to indent with tabs. The default (w/o -T): ```json "submit_slot":2, "author_count":3, ``` With `-T`: ```json "submit_slot":2, "author_count":3, ``` We also removed a few trailing whitespace from the shell script. * fix homebrew paths for macOS in ensure_deps.sh For macOS (Darwin) we use use the correct paths for homebrew in `ensure_deps.sh` If there is an error under macOS, we also remind the user what to put in their $PATH. * fix and improve command line parsing and usage The command line parsing now uses the bash builtin `getopts`. Now, and invalid option is flagged as an error. Improper arguments to command line options are flagged as errors. The optional pattern is also obtained after parsing command line options. The usage message has been updated to include command line options that were implemented but not documented in the usage message. Updated the Invocation section of `README.md` to reflect the current command line, including command line options that were documented in the usage message, but not in `README.md`. * fix test command lines The original usage message and `README.md` invocation has the optional pattern at the end of the command line . We fix 3 tests to place the pattern at the end of the command line. Moreover, we put `--` between command line options and a pattern to prevent the pattern from being interpreted as a command line option. And while unlikely to happen, this would allow pattern that begins with a dash to be correctly parsed. Ran `all-tests.sh` to test the above. * improve test scripts We ran into some issues with the test scripts that were fixed by some shell script hardening and using some bash best practices. What follows are fixes and improvements to the test scripts. We make the tests scripts more robust: * detect when the cd fails * prevent unquoted shell variable expansion from corrupting execution We improve the use of bash: * use `env(1)` in the initial `#!` line to find `bash(1)` on `$PATH` NOTE: macOS has deprecated the distributed `bash(1)` in favor of `zsh(1)`. Not all systems have `bash(1)` as `/bin/sh`. We allow for `bash(1)` to come from home brew or mac ports, or ... We use bash constructs for slight speed gain and best practices: * increment using ((++i)), in some cases avoiding a sub-shell launch * bypass use of `cat(1)` by using `$(< "$argpfile")` * use `find(1)` to select test files instead of the slower `ls(1)` * avoid using shell glob problems by using `find(1)` to select test files * use `$(...)` instead of `\`cmd\`` to help avoid data corrupting execution Use `python3(1)` instead of just `python(1)`. NOTE: Not all systems that have `python3(1)` have `python(1)`. Some system admins have yet to select the default version of python. The macOS platform, while they have `python3(1)` to not have `python(1)`. The test scripts now pass the shellcheck script analysis tool. See [www.shellcheck.net](https://www.shellcheck.net) for details. Fixed a bug in `all-docker-tests.sh` where Centos tests were re-displayed a 2nd time as if they were Debian tests. Ran `./all-tests.sh` to verify the above under macOS and RHEL 9. TODO: Fix a number of shell script problems with `JSONPath.sh` along similar lines as above: especially where shell variables containing JSON and other test data can take on values that can corrupt the shell script execution. We wanted to make the other test scripts more robust before taking on `JSONPath.sh` so that we may use such tests. * improve JSONPath.sh We make `JSONPath.sh` much more robust and use bash constructs for slight speed gain and best practices: * prevent unquoted shell variable expansion from corrupting execution * compute numerical values using ((var=equation)) instead of let * declare local variable before assigning a value * fixed how options to grep are accumulated * fixed how main is called * fixed how an unexpected EOF with token is thrown * fixed how array elements are unset * fixed how code determines of an array contains only empty values * changed cases where `cat(1)` of a file is piped into feeding the file on stdin * use `$(...)` instead of `\`cmd\`` to help avoid data corrupting execution * replaced use of `[ ... ]` with `[[ ... ]]` * removed shell variables that were set but never unused * fixed cases where a case statement lacked a default case * fixed how the literal string `'"*'` is used * fixed cases where a scalar is followed by a literal string that starts with `[` * fixed cases where `$@` is confused with `$*` All errors, warnings and issues reported by [shellcheck](https://www.shellcheck.net) have been addressed / fix. Now `JSONPath.sh` is error, warning and issue free under [shellcheck](https://www.shellcheck.net)! Ran `./all-tests.sh` to verify the above under macOS and RHEL 9 Linux.
1 parent ce89800 commit 273ad63

File tree

10 files changed

+429
-268
lines changed

10 files changed

+429
-268
lines changed

JSONPath.sh

Lines changed: 241 additions & 180 deletions
Large diffs are not rendered by default.

Makefile

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env make
2+
#
3+
# JSONPath.sh - JSONPath implementation written in Bash
4+
5+
#############
6+
# utilities #
7+
#############
8+
9+
INSTALL= install
10+
SHELL= bash
11+
12+
######################
13+
# target information #
14+
######################
15+
16+
DESTDIR= /usr/local/bin
17+
18+
TARGETS= JSONPath.sh
19+
20+
######################################
21+
# all - default rule - must be first #
22+
######################################
23+
24+
all: ${TARGETS}
25+
26+
#################################################
27+
# .PHONY list of rules that do not create files #
28+
#################################################
29+
30+
.PHONY: all configure clean clobber install
31+
32+
###################################
33+
# standard Makefile utility rules #
34+
###################################
35+
36+
configure:
37+
38+
clean:
39+
40+
clobber: clean
41+
42+
install: all
43+
${INSTALL} -m 0555 ${TARGETS} ${DESTDIR}

README.md

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,50 @@ try wrapping the command like `./ensure_deps.sh ./JSONPath.sh`.
1212

1313
## Invocation
1414

15-
JSONPath.sh [-b] [-i] [-j] [-h] [-p] [-u] [-f FILE] [pattern]
15+
JSONPath.sh [-h] [-b] [-j] [-u] [-i] [-p] [-w] [-f FILE] [-n] [-s] [-S] [-A] [-T] [pattern]
1616

17-
pattern
18-
> the JSONPath query. Defaults to '$.\*' if not supplied.
17+
-h
18+
> Show help text.
1919
2020
-b
2121
> Brief output. Only show the values, not the path and key.
2222
23-
-f FILE
24-
> Read a FILE instead of reading from standard input.
25-
26-
-i
27-
> Case insensitive searching.
28-
2923
-j
3024
> Output in JSON format, instead of JSON.sh format.
3125
3226
-u
3327
> Strip unnecessary leading path elements.
3428
29+
-i
30+
> Case insensitive searching.
31+
3532
-p
3633
> Pass JSON.sh formatted data through to the JSON parser only. Useful after
3734
> JSON.sh data has been manipulated.
3835
39-
-h
40-
> Show help text.
36+
-w
37+
> Match whole words only (for filter script expression).
38+
39+
-f FILE
40+
> Read a FILE instead of reading from standard input.
41+
42+
-n
43+
> Do not print header.
44+
45+
-s
46+
> Normalize solidus.
47+
48+
-S
49+
> Print spaces around :'s.
50+
51+
-A
52+
> Start array on same line as JSON member.
53+
54+
-T
55+
> Indent with tabs instead of 4 character spaces.
56+
57+
pattern
58+
> the JSONPath query. Defaults to '$.\*' if not supplied.
4159
4260
## Requirements
4361

@@ -55,6 +73,10 @@ Install with npm:
5573

5674
* `sudo npm install -g jsonpath.sh`
5775

76+
Install with make:
77+
78+
* `make install`
79+
5880
Or copy the `JSONPath.sh` script to your PATH, for example:
5981

6082
``` bash
@@ -215,7 +237,7 @@ done
215237
-f test/valid/goessner.net.expanded.json \
216238
'$.store.book[?(@.price<4.20)].[title,price]'
217239

218-
# The following does not work yet (TODO)
240+
# The following does not work yet (TODO)
219241
./JSONPath.sh \
220242
-f test/valid/goessner.net.expanded.json \
221243
'$.store.book[(@.length-1)].title'
@@ -340,7 +362,7 @@ Show all authors, without showing duplicates and output in JSON format.
340362
All authors with duplicates:
341363

342364
```
343-
$ ./JSONPath.sh -f test/valid/goessner.net.expanded.json '$..author'
365+
$ ./JSONPath.sh -f test/valid/goessner.net.expanded.json '$..author'
344366
... omitted ...
345367
["store","book",9,"author"] "James S. A. Corey"
346368
["store","book",10,"author"] "James S. A. Corey"
@@ -352,7 +374,7 @@ Use standard unix tools to remove duplicates:
352374

353375
```
354376
$ ./JSONPath.sh -f test/valid/goessner.net.expanded.json '$..author' \
355-
| sort -k2 | uniq -f 1
377+
| sort -k2 | uniq -f 1
356378
... 11 lines of output ...
357379
```
358380

@@ -388,7 +410,7 @@ $ ./JSONPath.sh -f test/valid/goessner.net.expanded.json \
388410
"book":
389411
[
390412
{
391-
"author":"Douglas E. Richards"
413+
"author":"Douglas E. Richards"
392414
},
393415
{
394416
"author":"Evelyn Waugh"

all-docker-tests.sh

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,46 @@
1+
#!/usr/bin/env bash
2+
13
log=testlog.log
24

35
# Fedora
46
export IMAGE=json-path-fedora-bash
57
cp test/docker/Dockerfile-fedora test/docker/Dockerfile
6-
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee $log
8+
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee "$log"
79

8-
a=$(grep 'test(s) failed' $log)
10+
a=$(grep 'test(s) failed' "$log")
911

1012
# Ubuntu
1113
export IMAGE=json-path-ubuntu-bash
1214
cp test/docker/Dockerfile-ubuntu test/docker/Dockerfile
13-
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee $log
15+
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee "$log"
1416

15-
b=$(grep 'test(s) failed' $log)
17+
b=$(grep 'test(s) failed' "$log")
1618

1719
# Centos
1820
export IMAGE=json-path-centos-bash
1921
cp test/docker/Dockerfile-centos test/docker/Dockerfile
20-
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee $log
22+
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee "$log"
2123

22-
c=$(grep 'test(s) failed' $log)
24+
c=$(grep 'test(s) failed' "$log")
2325

2426
# Debian
2527
export IMAGE=json-path-debian-bash
2628
cp test/docker/Dockerfile-debian test/docker/Dockerfile
27-
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee $log
29+
./test/docker/wrap_in_docker.sh ./all-tests.sh | tee "$log"
2830

29-
d=$(grep 'test(s) failed' $log)
31+
d=$(grep 'test(s) failed' "$log")
3032

3133
# Cleanup
32-
rm $log
34+
rm -- "$log"
3335
rm test/docker/Dockerfile
3436

3537
# Results
3638
echo
3739
echo "Fedora tests"
38-
echo $a
40+
echo "$a"
3941
echo "Ubuntu tests"
40-
echo $b
42+
echo "$b"
4143
echo "Centos tests"
42-
echo $c
44+
echo "$c"
4345
echo "Debian tests"
44-
echo $c
45-
46+
echo "$d"

all-tests.sh

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
1-
#!/bin/sh
1+
#!/usr/bin/env bash
22

3-
cd ${0%/*}
3+
shopt -s lastpipe
4+
export CD_FAILED=
5+
cd "${0%/*}" || CD_FAILED="true"
6+
if [[ -n $CD_FAILED ]]; then
7+
echo "$0: ERROR: cannot cd ${0%/*}" 1>&2
8+
exit 1
9+
fi
410

511
#set -e
612
fail=0
713
tests=0
814
#all_tests=${__dirname:}
915
#echo PLAN ${#all_tests}
10-
for test in test/*.sh ;
16+
find test -mindepth 1 -maxdepth 1 -name '*.sh' -print | while read -r test;
1117
do
12-
tests=$((tests+1))
13-
echo TEST: $test
14-
./$test
18+
((++tests))
19+
echo TEST: "$test"
20+
./"$test"
1521
ret=$?
16-
if [ $ret -eq 0 ] ; then
17-
echo OK: ---- $test
18-
passed=$((passed+1))
22+
if [[ $ret -eq 0 ]]; then
23+
echo OK: ---- "$test"
24+
((++passed))
1925
else
20-
echo FAIL: $test $fail
21-
fail=$((fail+ret))
26+
echo FAIL: "$test" "$fail"
27+
((fail=fail+ret))
2228
fi
2329
done
2430

25-
if [ $fail -eq 0 ]; then
31+
if [[ $fail -eq 0 ]]; then
2632
echo -n 'SUCCESS '
2733
exitcode=0
2834
else
2935
echo -n 'FAILURE '
3036
exitcode=1
3137
fi
32-
echo $passed / $tests
33-
exit $exitcode
38+
echo "$passed" "/" "$tests"
39+
exit "$exitcode"

ensure_deps.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,22 @@ test_gnu_compat() {
2222
>&2 echo "Make sure you have GNU sed or compatible installed"
2323
fi
2424

25-
if [[ $error -gt 0 && is_osx ]]; then
25+
if [[ $error -gt 0 ]] && is_osx; then
2626
>&2 echo "With homebrew you may install necessary deps via \`brew install grep gnu-sed coreutils\`"
27+
>&2 echo 'Put /opt/homebrew/bin/gsed early in your PATH'
28+
>&2 echo 'Then put /opt/homebrew/bin/ggrep early in your PATH'
29+
>&2 echo 'And put /opt/homebrew/var/homebrew/linked/coreutils/libexec/gnubin early in your PATH'
30+
>&2 echo "For example: PATH=$PATH"
2731
fi
2832

29-
return $error
33+
return "$error"
3034
}
3135

3236
main() {
3337
if is_osx; then
34-
export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
35-
export PATH="/usr/local/opt/grep/libexec/gnubin:$PATH"
36-
export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"
38+
export PATH="/opt/homebrew/bin/gsed:$PATH"
39+
export PATH="/opt/homebrew/bin/ggrep:$PATH"
40+
export PATH="/opt/homebrew/var/homebrew/linked/coreutils/libexec/gnubin:$PATH"
3741
fi
3842

3943
test_gnu_compat

test/flatten-test.sh

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1-
#!/bin/sh
1+
#!/usr/bin/env bash
22

3-
cd ${0%/*}
3+
shopt -s lastpipe
4+
CD_FAILED=
5+
cd "${0%/*}" || CD_FAILED="true"
6+
if [[ -n $CD_FAILED ]]; then
7+
echo "$0: ERROR: cannot cd ${0%/*}" 1>&2
8+
exit 1
9+
fi
410
fails=0
511
i=0
6-
tests=`ls flatten/*.argp* | wc -l`
12+
tests=$(find flatten -name '*.argp*' -print | wc -l)
713
echo "1..${tests##* }"
814
# Standard flatten tests
9-
for argpfile in flatten/*.argp*
15+
find flatten -name '*.argp*' -print | while read -r argpfile;
1016
do
1117
input="${argpfile%.*}.json"
1218
expected="${argpfile%.*}_${argpfile##*.}.flattened"
13-
argp=$(cat $argpfile)
14-
i=$((i+1))
15-
if ! ../JSONPath.sh "$argp" -u < "$input" | diff -u - "$expected"
19+
argp=$(< "$argpfile")
20+
((++i))
21+
if ! ../JSONPath.sh -u -- "$argp" < "$input" | diff -u -- - "$expected"
1622
then
1723
echo "not ok $i - $argpfile"
18-
fails=$((fails+1))
24+
((++fails))
1925
else
2026
echo "ok $i - $argpfile"
2127
fi
2228
done
2329
echo "$fails test(s) failed"
24-
exit $fails
30+
exit "$fails"

test/json-encoding-test.sh

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
1-
#!/bin/sh
1+
#!/usr/bin/env bash
22

3-
cd ${0%/*}
3+
shopt -s lastpipe
4+
CD_FAILED=
5+
cd "${0%/*}" || CD_FAILED="true"
6+
if [[ -n $CD_FAILED ]]; then
7+
echo "$0: ERROR: cannot cd ${0%/*}" 1>&2
8+
exit 1
9+
fi
410
fails=0
511
i=0
6-
tests=`ls valid/*.argp* | wc -l`
12+
tests=$(find valid -name '*.argp*' -print | wc -l)
713
echo "1..${tests##* }"
814
# Json output tests
9-
for argpfile in valid/*.argp*
15+
find valid -name '*.argp*' -print | while read -r argpfile;
1016
do
1117
input="${argpfile%.*}.json"
12-
argp=$(cat $argpfile)
13-
i=$((i+1))
14-
if ! ../JSONPath.sh "$argp" -j < "$input" | python -mjson.tool >/dev/null
18+
argp=$(< "$argpfile")
19+
((++i))
20+
if ! ../JSONPath.sh -j -- "$argp" < "$input" | python3 -mjson.tool >/dev/null
1521
then
1622
echo "not ok $i - $argpfile"
17-
fails=$((fails+1))
23+
((++fails))
1824
else
1925
echo "ok $i - JSON validated for $argpfile"
2026
fi
2127
done
2228
echo "$fails test(s) failed"
23-
exit $fails
29+
exit "$fails"

0 commit comments

Comments
 (0)