Skip to content

Commit 3771021

Browse files
committed
Add jpp which is an extended superset of the jp command
$ jpp --help usage: jpp [-h] [-a] [-c] [-e EXPR_FILE] [-f FILENAME] [-s] [-u] [--ast] [expression] jpp is an extended superset of the jp CLI for JMESPath positional arguments: expression optional arguments: -h, --help show this help message and exit -a, --accumulate Accumulate all output objects into a single recursively merged output object. -c, --compact Produce compact JSON output that omits nonessential whitespace. -e EXPR_FILE, --expr-file EXPR_FILE Read JMESPath expression from the specified file. -f FILENAME, --filename FILENAME The filename containing the input data. If a filename is not given then data is read from stdin. -s, --slurp Read one or more input JSON objects into an array and apply the JMESPath expression to the resulting array. -u, --unquoted If the final result is a string, it will be printed without quotes. --ast Only print the AST of the parsed expression. Do not rely on this output, only useful for debugging purposes. There's also a golang implementation in jmespath/jp#30.
2 parents 1c46efc + 72e273e commit 3771021

File tree

3 files changed

+246
-1
lines changed

3 files changed

+246
-1
lines changed

bin/jpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
from jmespath.jpp import jpp_main
5+
6+
if __name__ == "__main__":
7+
sys.exit(jpp_main(argv=sys.argv))

jmespath/jpp.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import argparse
2+
import json
3+
import os
4+
import pprint
5+
import sys
6+
import itertools
7+
8+
import jmespath
9+
10+
# This 2 space indent matches https://github.com/jmespath/jp behavior.
11+
JP_COMPAT_DUMP_KWARGS = (
12+
("indent", 2),
13+
("ensure_ascii", False),
14+
)
15+
16+
17+
def decode_json_stream(stream):
18+
"""
19+
Decode a text JSON input stream and generate objects until EOF.
20+
"""
21+
eof = False
22+
line_buffer = []
23+
while line_buffer or not eof:
24+
progress = False
25+
if not eof:
26+
line = stream.readline()
27+
if line:
28+
progress = True
29+
line_buffer.append(line)
30+
else:
31+
eof = True
32+
33+
if line_buffer:
34+
chunk = "".join(line_buffer)
35+
del line_buffer[:]
36+
37+
try:
38+
yield json.loads(chunk)
39+
progress = True
40+
except json.JSONDecodeError as e:
41+
if e.pos > 0:
42+
try:
43+
yield json.loads(chunk[: e.pos])
44+
progress = True
45+
except json.JSONDecodeError:
46+
# Raise if there's no progress, since a given
47+
# chunk should be growning if it is not yet
48+
# decodable.
49+
if not progress:
50+
raise
51+
line_buffer.append(chunk)
52+
else:
53+
line_buffer.append(chunk[e.pos :])
54+
else:
55+
raise
56+
57+
58+
def jpp_main(argv=None):
59+
argv = sys.argv if argv is None else argv
60+
parser = argparse.ArgumentParser(
61+
prog=os.path.basename(argv[0]),
62+
)
63+
parser.add_argument("expression", nargs="?", default=None)
64+
parser.add_argument(
65+
"-a",
66+
"--accumulate",
67+
action="store_true",
68+
dest="accumulate",
69+
default=False,
70+
help=(
71+
"Accumulate all output objects into a single recursively merged output object."
72+
),
73+
)
74+
parser.add_argument(
75+
"-c",
76+
"--compact",
77+
action="store_true",
78+
dest="compact",
79+
default=False,
80+
help=("Produce compact JSON output that omits nonessential whitespace."),
81+
)
82+
parser.add_argument(
83+
"-e",
84+
"--expr-file",
85+
dest="expr_file",
86+
default=None,
87+
help=("Read JMESPath expression from the specified file."),
88+
)
89+
parser.add_argument(
90+
"-f",
91+
"--filename",
92+
dest="filename",
93+
default=None,
94+
help=(
95+
"The filename containing the input data. "
96+
"If a filename is not given then data is "
97+
"read from stdin."
98+
),
99+
)
100+
parser.add_argument(
101+
"-s",
102+
"--slurp",
103+
action="store_true",
104+
dest="slurp",
105+
default=False,
106+
help=(
107+
"Read one or more input JSON objects into an array and apply the JMESPath expression to the resulting array."
108+
),
109+
)
110+
parser.add_argument(
111+
"-u",
112+
"--unquoted",
113+
action="store_false",
114+
dest="quoted",
115+
default=True,
116+
help=("If the final result is a string, it will be printed without quotes."),
117+
)
118+
parser.add_argument(
119+
"--ast",
120+
action="store_true",
121+
help=(
122+
"Only print the AST of the parsed expression. Do not rely on this output, only useful for debugging purposes."
123+
),
124+
)
125+
parser.usage = "{}\n {}".format(
126+
parser.format_usage().partition("usage: ")[-1],
127+
"jpp is an extended superset of the jp CLI for JMESPath",
128+
)
129+
130+
args = parser.parse_args(argv[1:])
131+
expression = args.expression
132+
if expression == "help":
133+
parser.print_help()
134+
return 1
135+
136+
if expression and args.expr_file:
137+
parser.error("Only use one of the expression or --expr-file arguments.")
138+
139+
dump_kwargs = dict(JP_COMPAT_DUMP_KWARGS)
140+
if args.compact:
141+
dump_kwargs.pop("indent", None)
142+
dump_kwargs["separators"] = (",", ":")
143+
144+
if args.expr_file:
145+
with open(args.expr_file, "rt") as f:
146+
expression = f.read()
147+
148+
if args.ast:
149+
# Only print the AST
150+
expression = jmespath.compile(args.expression)
151+
sys.stdout.write(pprint.pformat(expression.parsed))
152+
sys.stdout.write("\n")
153+
return 0
154+
155+
if args.filename:
156+
f = open(args.filename, "rt")
157+
else:
158+
f = sys.stdin
159+
160+
accumulator = None
161+
eof = False
162+
163+
with f:
164+
stream_iter = decode_json_stream(f)
165+
while True:
166+
while True:
167+
if args.slurp:
168+
data = list(stream_iter)
169+
if not data:
170+
eof = True
171+
break
172+
else:
173+
try:
174+
data = next(stream_iter)
175+
except StopIteration:
176+
eof = True
177+
break
178+
179+
result = jmespath.search(expression, data)
180+
181+
if args.accumulate:
182+
if accumulator is None:
183+
accumulator = result
184+
else:
185+
accumulator = merge(accumulator, result)
186+
else:
187+
break
188+
189+
if args.accumulate:
190+
result = accumulator
191+
elif eof:
192+
break
193+
194+
if args.quoted or not isinstance(result, str):
195+
result = json.dumps(result, **dump_kwargs)
196+
197+
sys.stdout.write(result)
198+
sys.stdout.write("\n")
199+
if eof or args.accumulate or args.slurp:
200+
break
201+
return 0
202+
203+
204+
def merge(base, head):
205+
"""
206+
Recursively merge head onto base.
207+
"""
208+
if isinstance(head, dict):
209+
if not isinstance(base, dict):
210+
return head
211+
212+
result = {}
213+
for k in itertools.chain(head, base):
214+
try:
215+
result[k] = merge(base[k], head[k])
216+
except KeyError:
217+
try:
218+
result[k] = head[k]
219+
except KeyError:
220+
result[k] = base[k]
221+
222+
elif isinstance(head, list):
223+
result = []
224+
if isinstance(base, list):
225+
result.extend(base)
226+
for node in head:
227+
if node not in result:
228+
result.append(node)
229+
else:
230+
result.extend(head)
231+
else:
232+
result = head
233+
234+
return result
235+
236+
237+
if __name__ == "__main__":
238+
sys.exit(jpp_main(argv=sys.argv))

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
author='James Saryerwinnie',
2525
author_email='[email protected]',
2626
url='https://github.com/jmespath/jmespath.py',
27-
scripts=['bin/jp.py'],
27+
scripts=['bin/jp.py', 'bin/jpp'],
2828
packages=find_packages(exclude=['tests']),
2929
license='MIT',
3030
python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*',

0 commit comments

Comments
 (0)