Skip to content
Merged
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
30 changes: 21 additions & 9 deletions bin/jsonpointer
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ parser = argparse.ArgumentParser(
# Accept pointer as option, as file or as first positional argument
ptr_group = parser.add_mutually_exclusive_group()

ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r', encoding='utf-8'),
ptr_group.add_argument('-f', '--pointer-file', type=str,
help='File containing a JSON pointer expression')

ptr_group.add_argument('-p', '--pointer', type=str,
Expand All @@ -37,12 +37,26 @@ def main():
try:
ok = resolve_files()
except KeyboardInterrupt:
sys.exit(1)
# Conventional status for termination by SIGINT (128 + 2)
sys.exit(130)

if not ok:
sys.exit(1)


def open_input(name):
""" Open a UTF-8 input file, or return stdin for "-"

Exits with a usage error if the file cannot be opened. """
if name == '-':
return sys.stdin

try:
return open(name, encoding='utf-8')
except OSError as e:
parser.error("can't open '%s': %s" % (name, e))


def parse_pointer(args):
""" Return the parsed pointer and the list of files to resolve it on

Expand All @@ -59,13 +73,14 @@ def parse_pointer(args):
else:
# Strip only the trailing line ending; other whitespace may be
# part of a member name.
pointer_file = open_input(args.pointer_file)
try:
ptr = args.pointer_file.read().rstrip("\r\n")
ptr = pointer_file.read().rstrip("\r\n")
except UnicodeDecodeError as e:
parser.error('could not decode pointer file: %s' % e)
finally:
if args.pointer_file is not sys.stdin:
args.pointer_file.close()
if pointer_file is not sys.stdin:
pointer_file.close()

elif args.POINTER is not None:
ptr = args.POINTER
Expand Down Expand Up @@ -94,10 +109,7 @@ def resolve_files():
ok = True

for name in files:
try:
f = argparse.FileType('r', encoding='utf-8')(name)
except argparse.ArgumentTypeError as e:
parser.error(str(e))
f = open_input(name)

try:
doc = json.load(f)
Expand Down
4 changes: 2 additions & 2 deletions doc/commandline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ JSON input files and pointer files are read as UTF-8.

The exit status is 0 if the pointer could be resolved on all files, 1 if it
could not be resolved on at least one file (the remaining files are still
processed) and 2 on usage errors, such as an invalid pointer or a missing
file.
processed), 2 on usage errors, such as an invalid pointer or a missing
file, and 130 if interrupted with Ctrl-C.

By default, invalid JSON or a text decoding error stops processing. With
``--continue-on-error``, these errors are reported on standard error with
Expand Down
15 changes: 15 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,15 @@ def test_keeps_stdin_open(self):
self.assertTrue(cli['resolve_files']())
self.assertFalse(stdin.closed)

def test_keyboard_interrupt_exit_status(self):
cli = runpy.run_path(self.SCRIPT)
args = [self.SCRIPT, '/a', os.path.join(self.dir, 'a.json')]
with mock.patch.object(sys, 'argv', args), \
mock.patch('json.load', side_effect=KeyboardInterrupt):
with self.assertRaises(SystemExit) as caught:
cli['main']()
self.assertEqual(caught.exception.code, 130)

def test_utf8_json_file_without_utf8_mode(self):
self._write('unicode.json', '{"value": "🚀"}')
proc = self._run('/value', 'unicode.json', utf8=False)
Expand Down Expand Up @@ -971,6 +980,12 @@ def test_continue_on_error_does_not_skip_missing_files(self):
self.assertEqual(proc.stdout, self.A_OUT)
self.assertIn('missing.json', proc.stderr)

def test_missing_pointer_file(self):
proc = self._run('-f', 'missing.txt', 'a.json')
self.assertEqual(proc.returncode, 2)
self.assertEqual(proc.stdout, '')
self.assertIn("can't open 'missing.txt'", proc.stderr)

def test_pointer_file_requires_value(self):
proc = self._run('-f', '-p', '/a', 'a.json')
self.assertEqual(proc.returncode, 2)
Expand Down