diff --git a/jc/cli.py b/jc/cli.py index f8989e13..8c95fa0d 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -10,6 +10,7 @@ import shlex import importlib import textwrap import signal +import subprocess import json import jc import jc.appdirs as appdirs @@ -395,15 +396,21 @@ def json_out(data, pretty=False, env_colors=None, mono=False, piped_out=False): return json.dumps(data, separators=(',', ':'), ensure_ascii=False) -def generate_magic_command(args): +def magic_parser(args): """ - Return a tuple with a boolean and a command, where the boolean signifies that - the command is valid, and the command is either a command string or None. + Return a tuple: + valid_command (bool) is this a valid command? (exists in magic dict) + run_command (list) list of the user's command to run. None if no command. + jc_parser (str) parser to use for this user's command. + jc_options (list) list of jc options + + Side-effect: + This function will reset sys.argv to an empty list """ # Parse with magic syntax: jc -p ls -al if len(args) <= 1 or args[1].startswith('--'): - return False, None + return False, None, None, [] # correctly parse escape characters and spaces with shlex args_given = ' '.join(map(shlex.quote, args[1:])).split() @@ -413,7 +420,7 @@ def generate_magic_command(args): for arg in list(args_given): # parser found - use standard syntax if arg.startswith('--'): - return False, None + return False, None, None, [] # option found - populate option list elif arg.startswith('-'): @@ -423,13 +430,16 @@ def generate_magic_command(args): else: break + # reset sys.argv since we are now done with it + sys.argv = [] + # if -h, -a, or -v found in options, then bail out if 'h' in options or 'a' in options or 'v' in options: - return False, None + return False, None, None, [] # all options popped and no command found - for case like 'jc -a' if len(args_given) == 0: - return False, None + return False, None, None, [] magic_dict = {} parser_info = about_jc()['parsers'] @@ -446,26 +456,25 @@ def generate_magic_command(args): # try to get a parser for two_word_command, otherwise get one for one_word_command found_parser = magic_dict.get(two_word_command, magic_dict.get(one_word_command)) - # construct a new command line using the standard syntax: COMMAND | jc --PARSER -OPTIONS - run_command = ' '.join(args_given) - if found_parser: - cmd_options = ('-' + ''.join(options)) if options else '' - return True, ' '.join([run_command, '|', 'jc', found_parser, cmd_options]) - else: - return False, run_command + run_command = args_given + return ( + True if found_parser else False, + run_command, + found_parser, + options + ) -def magic(): - """Runs the command generated by generate_magic_command() to support magic syntax""" - valid_command, run_command = generate_magic_command(sys.argv) - if valid_command: - os.system(run_command) - sys.exit(0) - elif run_command is None: - return - else: - jc.utils.error_message(f'parser not found for "{run_command}". Use "jc -h" for help.') - sys.exit(1) +def run_user_command(command): + """Use subprocess to run the user's command. Returns the STDOUT, STDERR, and the Exit Code as a tuple.""" + proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + stdout, stderr = proc.communicate() + + return ( + stdout or '\n', + stderr, + proc.returncode + ) def main(): @@ -481,13 +490,27 @@ def main(): pass # try magic syntax first: e.g. jc -p ls -al - magic() + magic_stdout, magic_stderr, magic_exit_code, magic_options = None, None, None, [] + valid_command, run_command, magic_found_parser, magic_options = magic_parser(sys.argv) + if valid_command: + magic_stdout, magic_stderr, magic_exit_code = run_user_command(run_command) + if magic_stderr: + print(magic_stderr, file=sys.stderr) + elif run_command is None: + pass + else: + run_command_str = ' '.join(run_command) + jc.utils.error_message(f'parser not found for "{run_command_str}". Use "jc -h" for help.') + sys.exit(1) + # set colors jc_colors = os.getenv('JC_COLORS') + # set options options = [] + options.extend(magic_options) - # options + # note that sys.argv will be an empty list after magic_parser finds the magic syntax is used for opt in sys.argv: if opt.startswith('-') and not opt.startswith('--'): options.extend(opt[1:]) @@ -521,40 +544,48 @@ def main(): import jc.tracebackplus jc.tracebackplus.enable(context=11) - if sys.stdin.isatty(): + if sys.stdin.isatty() and magic_stdout is None: jc.utils.error_message('Missing piped data. Use "jc -h" for help.') - sys.exit(1) + sys.exit(magic_exit_code + 1 if magic_exit_code else 1) - data = sys.stdin.read() + data = magic_stdout or sys.stdin.read() - found = False + # find the correct parser + if magic_found_parser: + parser = parser_module(magic_found_parser) - for arg in sys.argv: - parser_name = parser_shortname(arg) + else: + found = False + for arg in sys.argv: + parser_name = parser_shortname(arg) - if parser_name in parsers: - # load parser module just in time so we don't need to load all modules - parser = parser_module(arg) - try: - result = parser.parse(data, raw=raw, quiet=quiet) + if parser_name in parsers: + # load parser module just in time so we don't need to load all modules + parser = parser_module(arg) found = True break - except Exception: - if debug: - raise - else: - import jc.utils - jc.utils.error_message( - f'{parser_name} parser could not parse the input data. Did you use the correct parser?\n' - ' For details use the -d or -dd option. Use "jc -h" for help.') - sys.exit(1) + if not found: + jc.utils.error_message('Missing or incorrect arguments. Use "jc -h" for help.') + sys.exit(1) - if not found: - jc.utils.error_message('Missing or incorrect arguments. Use "jc -h" for help.') - sys.exit(1) + # parse the data + try: + result = parser.parse(data, raw=raw, quiet=quiet) + except Exception: + if debug: + raise + else: + import jc.utils + jc.utils.error_message( + f'{parser_name} parser could not parse the input data. Did you use the correct parser?\n' + ' For details use the -d or -dd option. Use "jc -h" for help.') + sys.exit(magic_exit_code or 1) + + # output the json print(json_out(result, pretty=pretty, env_colors=jc_colors, mono=mono, piped_out=piped_output())) + sys.exit(magic_exit_code or 0) if __name__ == '__main__':