From cb9979ac9429dfda18f0606e11e64c751a3fad24 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Sun, 2 Oct 2022 16:58:20 -0700 Subject: [PATCH 01/23] refactor cli --- jc/cli.old.py | 767 +++++++++++++++++++++++++++++++++++++++ jc/cli.py | 986 +++++++++++++++++++++++++------------------------- 2 files changed, 1266 insertions(+), 487 deletions(-) create mode 100644 jc/cli.old.py diff --git a/jc/cli.old.py b/jc/cli.old.py new file mode 100644 index 00000000..fb2ecbb2 --- /dev/null +++ b/jc/cli.old.py @@ -0,0 +1,767 @@ +"""jc - JSON Convert +JC cli module +""" + +import io +import sys +import os +from datetime import datetime, timezone +import textwrap +import signal +import shlex +import subprocess +from .lib import (__version__, parser_info, all_parser_info, parsers, + _get_parser, _parser_is_streaming, parser_mod_list, + standard_parser_mod_list, plugin_parser_mod_list, + streaming_parser_mod_list) +from . import utils +from .cli_data import long_options_map, new_pygments_colors, old_pygments_colors +from .shell_completions import bash_completion, zsh_completion +from . import tracebackplus +from .exceptions import LibraryNotInstalled, ParseError + +# make pygments import optional +try: + import pygments + from pygments import highlight + from pygments.style import Style + from pygments.token import (Name, Number, String, Keyword) + from pygments.lexers.data import JsonLexer, YamlLexer + from pygments.formatters import Terminal256Formatter + PYGMENTS_INSTALLED = True +except Exception: + PYGMENTS_INSTALLED = False + +JC_ERROR_EXIT = 100 + + +class info(): + version = __version__ + description = 'JSON Convert' + author = 'Kelly Brazil' + author_email = 'kellyjonbrazil@gmail.com' + website = 'https://github.com/kellyjonbrazil/jc' + copyright = '© 2019-2022 Kelly Brazil' + license = 'MIT License' + + +# We only support 2.3.0+, pygments changed color names in 2.4.0. +# startswith is sufficient and avoids potential exceptions from split and int. +if PYGMENTS_INSTALLED: + if pygments.__version__.startswith('2.3.'): + PYGMENT_COLOR = old_pygments_colors + else: + PYGMENT_COLOR = new_pygments_colors + + +def set_env_colors(env_colors=None): + """ + Return a dictionary to be used in Pygments custom style class. + + Grab custom colors from JC_COLORS environment variable. JC_COLORS env + variable takes 4 comma separated string values and should be in the + format of: + + JC_COLORS=,,, + + Where colors are: black, red, green, yellow, blue, magenta, cyan, gray, + brightblack, brightred, brightgreen, brightyellow, + brightblue, brightmagenta, brightcyan, white, default + + Default colors: + + JC_COLORS=blue,brightblack,magenta,green + or + JC_COLORS=default,default,default,default + """ + input_error = False + + if env_colors: + color_list = env_colors.split(',') + else: + color_list = ['default', 'default', 'default', 'default'] + + if len(color_list) != 4: + input_error = True + + for color in color_list: + if color != 'default' and color not in PYGMENT_COLOR: + input_error = True + + # if there is an issue with the env variable, just set all colors to default and move on + if input_error: + utils.warning_message(['Could not parse JC_COLORS environment variable']) + color_list = ['default', 'default', 'default', 'default'] + + # Try the color set in the JC_COLORS env variable first. If it is set to default, then fall back to default colors + return { + Name.Tag: f'bold {PYGMENT_COLOR[color_list[0]]}' if color_list[0] != 'default' else f"bold {PYGMENT_COLOR['blue']}", # key names + Keyword: PYGMENT_COLOR[color_list[1]] if color_list[1] != 'default' else PYGMENT_COLOR['brightblack'], # true, false, null + Number: PYGMENT_COLOR[color_list[2]] if color_list[2] != 'default' else PYGMENT_COLOR['magenta'], # numbers + String: PYGMENT_COLOR[color_list[3]] if color_list[3] != 'default' else PYGMENT_COLOR['green'] # strings + } + + +def piped_output(force_color): + """ + Return False if `STDOUT` is a TTY. True if output is being piped to + another program and foce_color is True. This allows forcing of ANSI + color codes even when using pipes. + """ + return not sys.stdout.isatty() and not force_color + + +def ctrlc(signum, frame): + """Exit with error on SIGINT""" + sys.exit(JC_ERROR_EXIT) + + +def parser_shortname(parser_arg): + """Return short name of the parser with dashes and no -- prefix""" + return parser_arg[2:] + + +def parsers_text(indent=0, pad=0, show_hidden=False): + """Return the argument and description information from each parser""" + ptext = '' + padding_char = ' ' + for p in all_parser_info(show_hidden=show_hidden): + parser_arg = p.get('argument', 'UNKNOWN') + padding = pad - len(parser_arg) + parser_desc = p.get('description', 'No description available.') + indent_text = padding_char * indent + padding_text = padding_char * padding + ptext += indent_text + parser_arg + padding_text + parser_desc + '\n' + + return ptext + + +def options_text(indent=0, pad=0): + """Return the argument and description information from each option""" + otext = '' + padding_char = ' ' + for option in long_options_map: + o_short = '-' + long_options_map[option][0] + o_desc = long_options_map[option][1] + o_combined = o_short + ', ' + option + padding = pad - len(o_combined) + indent_text = padding_char * indent + padding_text = padding_char * padding + otext += indent_text + o_combined + padding_text + o_desc + '\n' + + return otext + + +def about_jc(): + """Return jc info and the contents of each parser.info as a dictionary""" + return { + 'name': 'jc', + 'version': info.version, + 'description': info.description, + 'author': info.author, + 'author_email': info.author_email, + 'website': info.website, + 'copyright': info.copyright, + 'license': info.license, + 'python_version': '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))), + 'python_path': sys.executable, + 'parser_count': len(parser_mod_list()), + 'standard_parser_count': len(standard_parser_mod_list()), + 'streaming_parser_count': len(streaming_parser_mod_list()), + 'plugin_parser_count': len(plugin_parser_mod_list()), + 'parsers': all_parser_info(show_hidden=True) + } + + +def helptext(show_hidden=False): + """Return the help text with the list of parsers""" + parsers_string = parsers_text(indent=4, pad=20, show_hidden=show_hidden) + options_string = options_text(indent=4, pad=20) + + helptext_string = f'''\ +jc converts the output of many commands, file-types, and strings to JSON or YAML + +Usage: + + Standard syntax: + + COMMAND | jc [OPTIONS] PARSER + + cat FILE | jc [OPTIONS] PARSER + + echo STRING | jc [OPTIONS] PARSER + + Magic syntax: + + jc [OPTIONS] COMMAND + + jc [OPTIONS] /proc/ + +Parsers: +{parsers_string} +Options: +{options_string} +Examples: + Standard Syntax: + $ dig www.google.com | jc --pretty --dig + $ cat /proc/meminfo | jc --pretty --proc + + Magic Syntax: + $ jc --pretty dig www.google.com + $ jc --pretty /proc/meminfo + + Parser Documentation: + $ jc --help --dig + + Show Hidden Parsers: + $ jc -hh +''' + + return helptext_string + + +def help_doc(options, show_hidden=False): + """ + Returns the parser documentation if a parser is found in the arguments, + otherwise the general help text is returned. + """ + for arg in options: + parser_name = parser_shortname(arg) + + if parser_name in parsers: + p_info = parser_info(arg, documentation=True) + compatible = ', '.join(p_info.get('compatible', ['unknown'])) + documentation = p_info.get('documentation', 'No documentation available.') + version = p_info.get('version', 'unknown') + author = p_info.get('author', 'unknown') + author_email = p_info.get('author_email', 'unknown') + doc_text = \ + f'{documentation}\n'\ + f'Compatibility: {compatible}\n\n'\ + f'Version {version} by {author} ({author_email})\n' + + utils._safe_pager(doc_text) + return + + utils._safe_print(helptext(show_hidden=show_hidden)) + return + + +def versiontext(): + """Return the version text""" + py_ver = '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))) + versiontext_string = f'''\ + jc version: {info.version} + python interpreter version: {py_ver} + python path: {sys.executable} + + {info.website} + {info.copyright} + ''' + return textwrap.dedent(versiontext_string) + + +def yaml_out(data, pretty=False, env_colors=None, mono=False, piped_out=False, ascii_only=False): + """ + Return a YAML formatted string. String may include color codes. If the + YAML library is not installed, output will fall back to JSON with a + warning message to STDERR""" + # make ruamel.yaml import optional + try: + from ruamel.yaml import YAML, representer + YAML_INSTALLED = True + except Exception: + YAML_INSTALLED = False + + if YAML_INSTALLED: + y_string_buf = io.BytesIO() + + # monkey patch to disable plugins since we don't use them and in + # ruamel.yaml versions prior to 0.17.0 the use of __file__ in the + # plugin code is incompatible with the pyoxidizer packager + YAML.official_plug_ins = lambda a: [] + + # monkey patch to disable aliases + representer.RoundTripRepresenter.ignore_aliases = lambda x, y: True + + yaml = YAML() + yaml.default_flow_style = False + yaml.explicit_start = True + yaml.allow_unicode = not ascii_only + yaml.encoding = 'utf-8' + yaml.dump(data, y_string_buf) + y_string = y_string_buf.getvalue().decode('utf-8')[:-1] + + if not mono and not piped_out: + # set colors + class JcStyle(Style): + styles = set_env_colors(env_colors) + + return str(highlight(y_string, YamlLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) + + return y_string + + utils.warning_message(['YAML Library not installed. Reverting to JSON output.']) + return json_out(data, pretty=pretty, env_colors=env_colors, mono=mono, piped_out=piped_out, ascii_only=ascii_only) + + +def json_out(data, pretty=False, env_colors=None, mono=False, piped_out=False, ascii_only=False): + """ + Return a JSON formatted string. String may include color codes or be + pretty printed. + """ + import json + + separators = (',', ':') + indent = None + + if pretty: + separators = None + indent = 2 + + j_string = json.dumps(data, indent=indent, separators=separators, ensure_ascii=ascii_only) + + if not mono and not piped_out: + # set colors + class JcStyle(Style): + styles = set_env_colors(env_colors) + + return str(highlight(j_string, JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) + + return j_string + + +def safe_print_out(list_or_dict, pretty=None, env_colors=None, mono=None, + piped_out=None, flush=None, yaml=None): + """Safely prints JSON or YAML output in both UTF-8 and ASCII systems""" + if yaml: + try: + print(yaml_out(list_or_dict, + pretty=pretty, + env_colors=env_colors, + mono=mono, + piped_out=piped_out), + flush=flush) + except UnicodeEncodeError: + print(yaml_out(list_or_dict, + pretty=pretty, + env_colors=env_colors, + mono=mono, + piped_out=piped_out, + ascii_only=True), + flush=flush) + + else: + try: + print(json_out(list_or_dict, + pretty=pretty, + env_colors=env_colors, + mono=mono, + piped_out=piped_out), + flush=flush) + except UnicodeEncodeError: + print(json_out(list_or_dict, + pretty=pretty, + env_colors=env_colors, + mono=mono, + piped_out=piped_out, + ascii_only=True), + flush=flush) + + +def magic_parser(args): + """ + Parse command arguments for magic syntax: jc -p ls -al + + Return a tuple: + valid_command (bool) is this a valid cmd? (exists in magic dict) + run_command (list) list of the user's cmd to run. None if no cmd. + jc_parser (str) parser to use for this user's cmd. + jc_options (list) list of jc options + """ + # bail immediately if there are no args or a parser is defined + if len(args) <= 1 or (args[1].startswith('--') and args[1] not in long_options_map): + return False, None, None, [] + + args_given = args[1:] + options = [] + + # find the options + for arg in list(args_given): + # long option found - populate option list + if arg in long_options_map: + options.extend(long_options_map[arg][0]) + args_given.pop(0) + continue + + # parser found - use standard syntax + if arg.startswith('--'): + return False, None, None, [] + + # option found - populate option list + if arg.startswith('-'): + options.extend(args_given.pop(0)[1:]) + + # command found if iterator didn't already stop - stop iterating + else: + break + + # 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, None, [] + + # all options popped and no command found - for case like 'jc -x' + if len(args_given) == 0: + return False, None, None, [] + + # create a dictionary of magic_commands to their respective parsers. + magic_dict = {} + for entry in all_parser_info(): + magic_dict.update({mc: entry['argument'] for mc in entry.get('magic_commands', [])}) + + # find the command and parser + one_word_command = args_given[0] + two_word_command = ' '.join(args_given[0:2]) + + # 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)) + + return ( + bool(found_parser), # was a suitable parser found? + args_given, # run_command + found_parser, # the parser selected + options # jc options to preserve + ) + + +def open_text_file(path_string): + with open(path_string, 'r') as f: + return f.read() + + +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, + close_fds=False, # Allows inheriting file descriptors; + universal_newlines=True, # useful for process substitution + encoding='UTF-8') + stdout, stderr = proc.communicate() + + return ( + stdout or '\n', + stderr, + proc.returncode + ) + + +def combined_exit_code(program_exit=0, jc_exit=0): + exit_code = program_exit + jc_exit + exit_code = min(exit_code, 255) + return exit_code + + +def add_metadata_to(list_or_dict, + runtime=None, + run_command=None, + magic_exit_code=None, + parser_name=None): + """ + This function mutates a list or dict in place. If the _jc_meta field + does not already exist, it will be created with the metadata fields. If + the _jc_meta field already exists, the metadata fields will be added to + the existing object. + + In the case of an empty list (no data), a dictionary with a _jc_meta + object will be added to the list. This way you always get metadata, + even if there are no results. + """ + run_timestamp = runtime.timestamp() + + meta_obj = { + 'parser': parser_name, + 'timestamp': run_timestamp + } + + if run_command: + meta_obj['magic_command'] = run_command + meta_obj['magic_command_exit'] = magic_exit_code + + if isinstance(list_or_dict, dict): + if '_jc_meta' not in list_or_dict: + list_or_dict['_jc_meta'] = {} + + list_or_dict['_jc_meta'].update(meta_obj) + + elif isinstance(list_or_dict, list): + if not list_or_dict: + list_or_dict.append({}) + + for item in list_or_dict: + if '_jc_meta' not in item: + item['_jc_meta'] = {} + + item['_jc_meta'].update(meta_obj) + + else: + utils.error_message(['Parser returned an unsupported object type.']) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + +def main(): + # break on ctrl-c keyboard interrupt + signal.signal(signal.SIGINT, ctrlc) + + # break on pipe error. need try/except for windows compatibility + try: + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + except AttributeError: + pass + + # enable colors for Windows cmd.exe terminal + if sys.platform.startswith('win32'): + os.system('') + + # parse magic syntax first: e.g. jc -p ls -al + magic_options = [] + valid_command, run_command, magic_found_parser, magic_options = magic_parser(sys.argv) + + # set colors + jc_colors = os.getenv('JC_COLORS') + + # set options + options = [] + options.extend(magic_options) + + # find options if magic_parser did not find a command + if not valid_command: + for opt in sys.argv: + if opt in long_options_map: + options.extend(long_options_map[opt][0]) + + if opt.startswith('-') and not opt.startswith('--'): + options.extend(opt[1:]) + + about = 'a' in options + debug = 'd' in options + verbose_debug = options.count('d') > 1 + force_color = 'C' in options + mono = ('m' in options or bool(os.getenv('NO_COLOR'))) and not force_color + help_me = 'h' in options + verbose_help = options.count('h') > 1 + pretty = 'p' in options + quiet = 'q' in options + ignore_exceptions = options.count('q') > 1 + raw = 'r' in options + meta_out = 'M' in options + unbuffer = 'u' in options + version_info = 'v' in options + yaml_out = 'y' in options + bash_comp = 'B' in options + zsh_comp = 'Z' in options + + if verbose_debug: + tracebackplus.enable(context=11) + + if not PYGMENTS_INSTALLED: + mono = True + + if about: + safe_print_out(about_jc(), + pretty=pretty, + env_colors=jc_colors, + mono=mono, + piped_out=piped_output(force_color), + yaml=yaml_out) + sys.exit(0) + + if help_me: + help_doc(sys.argv, show_hidden=verbose_help) + sys.exit(0) + + if version_info: + utils._safe_print(versiontext()) + sys.exit(0) + + if bash_comp: + utils._safe_print(bash_completion()) + sys.exit(0) + + if zsh_comp: + utils._safe_print(zsh_completion()) + sys.exit(0) + + # if magic syntax used, try to run the command and error if it's not found, etc. + magic_stdout, magic_stderr, magic_exit_code = None, None, 0 + run_command_str = '' + if run_command: + try: + run_command_str = shlex.join(run_command) # python 3.8+ + except AttributeError: + run_command_str = ' '.join(run_command) # older python versions + + if run_command_str.startswith('/proc'): + try: + magic_found_parser = 'proc' + magic_stdout = open_text_file(run_command_str) + + except OSError as e: + if debug: + raise + + error_msg = os.strerror(e.errno) + utils.error_message([ + f'"{run_command_str}" file could not be opened: {error_msg}.' + ]) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + except Exception: + if debug: + raise + + utils.error_message([ + f'"{run_command_str}" file could not be opened. For details use the -d or -dd option.' + ]) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + elif valid_command: + try: + magic_stdout, magic_stderr, magic_exit_code = run_user_command(run_command) + if magic_stderr: + utils._safe_print(magic_stderr[:-1], file=sys.stderr) + + except OSError as e: + if debug: + raise + + error_msg = os.strerror(e.errno) + utils.error_message([ + f'"{run_command_str}" command could not be run: {error_msg}.' + ]) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + except Exception: + if debug: + raise + + utils.error_message([ + f'"{run_command_str}" command could not be run. For details use the -d or -dd option.' + ]) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + elif run_command is not None: + utils.error_message([f'"{run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + # find the correct parser + if magic_found_parser: + parser = _get_parser(magic_found_parser) + parser_name = parser_shortname(magic_found_parser) + + else: + found = False + for arg in sys.argv: + parser_name = parser_shortname(arg) + + if parser_name in parsers: + parser = _get_parser(arg) + found = True + break + + if not found: + utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + if sys.stdin.isatty() and magic_stdout is None: + utils.error_message(['Missing piped data. Use "jc -h" for help.']) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + # parse and print to stdout + try: + # differentiate between regular and streaming parsers + + # streaming (only supports UTF-8 string data for now) + if _parser_is_streaming(parser): + result = parser.parse(sys.stdin, + raw=raw, + quiet=quiet, + ignore_exceptions=ignore_exceptions) + + for line in result: + if meta_out: + run_dt_utc = datetime.now(timezone.utc) + add_metadata_to(line, run_dt_utc, run_command, magic_exit_code, parser_name) + + safe_print_out(line, + pretty=pretty, + env_colors=jc_colors, + mono=mono, + piped_out=piped_output(force_color), + flush=unbuffer, + yaml=yaml_out) + + sys.exit(combined_exit_code(magic_exit_code, 0)) + + # regular (supports binary and UTF-8 string data) + else: + data = magic_stdout or sys.stdin.buffer.read() + + # convert to UTF-8, if possible. Otherwise, leave as bytes + try: + if isinstance(data, bytes): + data = data.decode('utf-8') + except UnicodeDecodeError: + pass + + result = parser.parse(data, + raw=raw, + quiet=quiet) + + if meta_out: + run_dt_utc = datetime.now(timezone.utc) + add_metadata_to(result, run_dt_utc, run_command, magic_exit_code, parser_name) + + safe_print_out(result, + pretty=pretty, + env_colors=jc_colors, + mono=mono, + piped_out=piped_output(force_color), + flush=unbuffer, + yaml=yaml_out) + + sys.exit(combined_exit_code(magic_exit_code, 0)) + + except (ParseError, LibraryNotInstalled) as e: + if debug: + raise + + utils.error_message([ + f'Parser issue with {parser_name}:', f'{e.__class__.__name__}: {e}', + 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', + f'For details use the -d or -dd option. Use "jc -h --{parser_name}" for help.' + ]) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + except Exception: + if debug: + raise + + streaming_msg = '' + if _parser_is_streaming(parser): + streaming_msg = 'Use the -qq option to ignore streaming parser errors.' + + utils.error_message([ + f'{parser_name} parser could not parse the input data.', + f'{streaming_msg}', + 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', + f'For details use the -d or -dd option. Use "jc -h --{parser_name}" for help.' + ]) + sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + + +if __name__ == '__main__': + main() diff --git a/jc/cli.py b/jc/cli.py index fb2ecbb2..05759bc1 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -10,6 +10,7 @@ import textwrap import signal import shlex import subprocess +from typing import List, Dict from .lib import (__version__, parser_info, all_parser_info, parsers, _get_parser, _parser_is_streaming, parser_mod_list, standard_parser_mod_list, plugin_parser_mod_list, @@ -32,8 +33,6 @@ try: except Exception: PYGMENTS_INSTALLED = False -JC_ERROR_EXIT = 100 - class info(): version = __version__ @@ -54,131 +53,183 @@ if PYGMENTS_INSTALLED: PYGMENT_COLOR = new_pygments_colors -def set_env_colors(env_colors=None): - """ - Return a dictionary to be used in Pygments custom style class. +class JcCli(): - Grab custom colors from JC_COLORS environment variable. JC_COLORS env - variable takes 4 comma separated string values and should be in the - format of: + def __init__(self) -> None: + self.data_in = None + self.data_out = None + self.current_out_line = None + self.options: List[str] = [] + self.args: List[str] = [] + self.parser_arg = None + self.parser_module = None + self.parser_name = None + self.indent = 0 + self.pad = 0 + self.env_colors: Dict = {} + self.documentation = False + self.show_hidden = False + self.piped_out = False + self.ascii_only = False + self.flush = False + self.json_separators = (',', ':') + self.json_indent = None + self.path_string = None + self.run_command_str = '' + self.command = None + self.program_exit = 0 + self.jc_exit = 0 + self.JC_ERROR_EXIT = 100 + self.exit_code = 0 + self.runtime: datetime = datetime.now() + self.run_timestamp = None - JC_COLORS=,,, + # cli options + self.about = False + self.debug = False + self.verbose_debug = False + self.force_color = False + self.mono = False + self.help_me = False + self.verbose_help = False + self.pretty = False + self.quiet = False + self.ignore_exceptions = False + self.raw = False + self.meta_out = False + self.unbuffer = False + self.version_info = False + self.yaml_output = False + self.bash_comp = False + self.zsh_comp = False - Where colors are: black, red, green, yellow, blue, magenta, cyan, gray, - brightblack, brightred, brightgreen, brightyellow, - brightblue, brightmagenta, brightcyan, white, default + # Magic options + self.valid_command = False + self.run_command = None + self.magic_found_parser = None + self.magic_options: List[str] = [] + self.magic_stdout = None, + self.magic_stderr = None, + self.magic_returncode = None - Default colors: - JC_COLORS=blue,brightblack,magenta,green - or - JC_COLORS=default,default,default,default - """ - input_error = False + def set_env_colors(self): + """ + Return a dictionary to be used in Pygments custom style class. - if env_colors: - color_list = env_colors.split(',') - else: - color_list = ['default', 'default', 'default', 'default'] + Grab custom colors from JC_COLORS environment variable. JC_COLORS env + variable takes 4 comma separated string values and should be in the + format of: - if len(color_list) != 4: - input_error = True + JC_COLORS=,,, - for color in color_list: - if color != 'default' and color not in PYGMENT_COLOR: + Where colors are: black, red, green, yellow, blue, magenta, cyan, gray, + brightblack, brightred, brightgreen, brightyellow, + brightblue, brightmagenta, brightcyan, white, default + + Default colors: + + JC_COLORS=blue,brightblack,magenta,green + or + JC_COLORS=default,default,default,default + """ + input_error = False + + if self.env_colors: + color_list = self.env_colors.split(',') + else: + color_list = ['default', 'default', 'default', 'default'] + + if len(color_list) != 4: input_error = True - # if there is an issue with the env variable, just set all colors to default and move on - if input_error: - utils.warning_message(['Could not parse JC_COLORS environment variable']) - color_list = ['default', 'default', 'default', 'default'] + for color in color_list: + if color != 'default' and color not in PYGMENT_COLOR: + input_error = True - # Try the color set in the JC_COLORS env variable first. If it is set to default, then fall back to default colors - return { - Name.Tag: f'bold {PYGMENT_COLOR[color_list[0]]}' if color_list[0] != 'default' else f"bold {PYGMENT_COLOR['blue']}", # key names - Keyword: PYGMENT_COLOR[color_list[1]] if color_list[1] != 'default' else PYGMENT_COLOR['brightblack'], # true, false, null - Number: PYGMENT_COLOR[color_list[2]] if color_list[2] != 'default' else PYGMENT_COLOR['magenta'], # numbers - String: PYGMENT_COLOR[color_list[3]] if color_list[3] != 'default' else PYGMENT_COLOR['green'] # strings - } + # if there is an issue with the env variable, just set all colors to default and move on + if input_error: + utils.warning_message(['Could not parse JC_COLORS environment variable']) + color_list = ['default', 'default', 'default', 'default'] + # Try the color set in the JC_COLORS env variable first. If it is set to default, then fall back to default colors + self.set_env_colors = { + Name.Tag: f'bold {PYGMENT_COLOR[color_list[0]]}' if color_list[0] != 'default' else f"bold {PYGMENT_COLOR['blue']}", # key names + Keyword: PYGMENT_COLOR[color_list[1]] if color_list[1] != 'default' else PYGMENT_COLOR['brightblack'], # true, false, null + Number: PYGMENT_COLOR[color_list[2]] if color_list[2] != 'default' else PYGMENT_COLOR['magenta'], # numbers + String: PYGMENT_COLOR[color_list[3]] if color_list[3] != 'default' else PYGMENT_COLOR['green'] # strings + } -def piped_output(force_color): - """ - Return False if `STDOUT` is a TTY. True if output is being piped to - another program and foce_color is True. This allows forcing of ANSI - color codes even when using pipes. - """ - return not sys.stdout.isatty() and not force_color + def piped_output(self): + """ + Return False if `STDOUT` is a TTY. True if output is being piped to + another program and foce_color is True. This allows forcing of ANSI + color codes even when using pipes. + """ + return not sys.stdout.isatty() and not self.force_color + def parser_shortname(self): + """Return short name of the parser with dashes and no -- prefix""" + return self.parser_arg[2:] -def ctrlc(signum, frame): - """Exit with error on SIGINT""" - sys.exit(JC_ERROR_EXIT) + def parsers_text(self): + """Return the argument and description information from each parser""" + ptext = '' + padding_char = ' ' + for p in all_parser_info(show_hidden=self.show_hidden): + parser_arg = p.get('argument', 'UNKNOWN') + padding = self.pad - len(parser_arg) + parser_desc = p.get('description', 'No description available.') + indent_text = padding_char * self.indent + padding_text = padding_char * padding + ptext += indent_text + parser_arg + padding_text + parser_desc + '\n' + return ptext -def parser_shortname(parser_arg): - """Return short name of the parser with dashes and no -- prefix""" - return parser_arg[2:] + def options_text(self): + """Return the argument and description information from each option""" + otext = '' + padding_char = ' ' + for option in long_options_map: + o_short = '-' + long_options_map[option][0] + o_desc = long_options_map[option][1] + o_combined = o_short + ', ' + option + padding = self.pad - len(o_combined) + indent_text = padding_char * self.indent + padding_text = padding_char * padding + otext += indent_text + o_combined + padding_text + o_desc + '\n' + return otext -def parsers_text(indent=0, pad=0, show_hidden=False): - """Return the argument and description information from each parser""" - ptext = '' - padding_char = ' ' - for p in all_parser_info(show_hidden=show_hidden): - parser_arg = p.get('argument', 'UNKNOWN') - padding = pad - len(parser_arg) - parser_desc = p.get('description', 'No description available.') - indent_text = padding_char * indent - padding_text = padding_char * padding - ptext += indent_text + parser_arg + padding_text + parser_desc + '\n' + @staticmethod + def about_jc(): + """Return jc info and the contents of each parser.info as a dictionary""" + return { + 'name': 'jc', + 'version': info.version, + 'description': info.description, + 'author': info.author, + 'author_email': info.author_email, + 'website': info.website, + 'copyright': info.copyright, + 'license': info.license, + 'python_version': '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))), + 'python_path': sys.executable, + 'parser_count': len(parser_mod_list()), + 'standard_parser_count': len(standard_parser_mod_list()), + 'streaming_parser_count': len(streaming_parser_mod_list()), + 'plugin_parser_count': len(plugin_parser_mod_list()), + 'parsers': all_parser_info(show_hidden=True) + } - return ptext + def helptext(self): + """Return the help text with the list of parsers""" + self.indent = 4 + self.pad = 20 + parsers_string = self.parsers_text() + options_string = self.options_text() - -def options_text(indent=0, pad=0): - """Return the argument and description information from each option""" - otext = '' - padding_char = ' ' - for option in long_options_map: - o_short = '-' + long_options_map[option][0] - o_desc = long_options_map[option][1] - o_combined = o_short + ', ' + option - padding = pad - len(o_combined) - indent_text = padding_char * indent - padding_text = padding_char * padding - otext += indent_text + o_combined + padding_text + o_desc + '\n' - - return otext - - -def about_jc(): - """Return jc info and the contents of each parser.info as a dictionary""" - return { - 'name': 'jc', - 'version': info.version, - 'description': info.description, - 'author': info.author, - 'author_email': info.author_email, - 'website': info.website, - 'copyright': info.copyright, - 'license': info.license, - 'python_version': '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))), - 'python_path': sys.executable, - 'parser_count': len(parser_mod_list()), - 'standard_parser_count': len(standard_parser_mod_list()), - 'streaming_parser_count': len(streaming_parser_mod_list()), - 'plugin_parser_count': len(plugin_parser_mod_list()), - 'parsers': all_parser_info(show_hidden=True) - } - - -def helptext(show_hidden=False): - """Return the help text with the list of parsers""" - parsers_string = parsers_text(indent=4, pad=20, show_hidden=show_hidden) - options_string = options_text(indent=4, pad=20) - - helptext_string = f'''\ + helptext_string = f'''\ jc converts the output of many commands, file-types, and strings to JSON or YAML Usage: @@ -217,304 +268,272 @@ Examples: $ jc -hh ''' - return helptext_string + return helptext_string + + def help_doc(self): + """ + Returns the parser documentation if a parser is found in the arguments, + otherwise the general help text is returned. + """ + for arg in self.options: + self.parser_arg = arg + parser_name = self.parser_shortname() + + if parser_name in parsers: + self.documentation = True + p_info = parser_info() + compatible = ', '.join(p_info.get('compatible', ['unknown'])) + docs = p_info.get('documentation', 'No documentation available.') + version = p_info.get('version', 'unknown') + author = p_info.get('author', 'unknown') + author_email = p_info.get('author_email', 'unknown') + doc_text = \ + f'{docs}\n'\ + f'Compatibility: {compatible}\n\n'\ + f'Version {version} by {author} ({author_email})\n' + + utils._safe_pager(doc_text) + return + + utils._safe_print(self.helptext()) + return + + @staticmethod + def versiontext(): + """Return the version text""" + py_ver = '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))) + versiontext_string = f'''\ + jc version: {info.version} + python interpreter version: {py_ver} + python path: {sys.executable} + + {info.website} + {info.copyright} + ''' + return textwrap.dedent(versiontext_string) -def help_doc(options, show_hidden=False): - """ - Returns the parser documentation if a parser is found in the arguments, - otherwise the general help text is returned. - """ - for arg in options: - parser_name = parser_shortname(arg) + def yaml_out(self): + """ + Return a YAML formatted string. String may include color codes. If the + YAML library is not installed, output will fall back to JSON with a + warning message to STDERR""" + # make ruamel.yaml import optional + try: + from ruamel.yaml import YAML, representer + YAML_INSTALLED = True + except Exception: + YAML_INSTALLED = False - if parser_name in parsers: - p_info = parser_info(arg, documentation=True) - compatible = ', '.join(p_info.get('compatible', ['unknown'])) - documentation = p_info.get('documentation', 'No documentation available.') - version = p_info.get('version', 'unknown') - author = p_info.get('author', 'unknown') - author_email = p_info.get('author_email', 'unknown') - doc_text = \ - f'{documentation}\n'\ - f'Compatibility: {compatible}\n\n'\ - f'Version {version} by {author} ({author_email})\n' + if YAML_INSTALLED: + y_string_buf = io.BytesIO() - utils._safe_pager(doc_text) - return + # monkey patch to disable plugins since we don't use them and in + # ruamel.yaml versions prior to 0.17.0 the use of __file__ in the + # plugin code is incompatible with the pyoxidizer packager + YAML.official_plug_ins = lambda a: [] - utils._safe_print(helptext(show_hidden=show_hidden)) - return + # monkey patch to disable aliases + representer.RoundTripRepresenter.ignore_aliases = lambda x, y: True + yaml = YAML() + yaml.default_flow_style = False + yaml.explicit_start = True + yaml.allow_unicode = not self.ascii_only + yaml.encoding = 'utf-8' + yaml.dump(self.data_out, y_string_buf) + y_string = y_string_buf.getvalue().decode('utf-8')[:-1] -def versiontext(): - """Return the version text""" - py_ver = '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))) - versiontext_string = f'''\ - jc version: {info.version} - python interpreter version: {py_ver} - python path: {sys.executable} + if not self.mono and not self.piped_out: + # set colors + class JcStyle(Style): + styles = self.set_env_colors() - {info.website} - {info.copyright} - ''' - return textwrap.dedent(versiontext_string) + return str(highlight(y_string, YamlLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) + return y_string -def yaml_out(data, pretty=False, env_colors=None, mono=False, piped_out=False, ascii_only=False): - """ - Return a YAML formatted string. String may include color codes. If the - YAML library is not installed, output will fall back to JSON with a - warning message to STDERR""" - # make ruamel.yaml import optional - try: - from ruamel.yaml import YAML, representer - YAML_INSTALLED = True - except Exception: - YAML_INSTALLED = False + utils.warning_message(['YAML Library not installed. Reverting to JSON output.']) + return self.json_out() - if YAML_INSTALLED: - y_string_buf = io.BytesIO() + def json_out(self): + """ + Return a JSON formatted string. String may include color codes or be + pretty printed. + """ + import json - # monkey patch to disable plugins since we don't use them and in - # ruamel.yaml versions prior to 0.17.0 the use of __file__ in the - # plugin code is incompatible with the pyoxidizer packager - YAML.official_plug_ins = lambda a: [] + if self.pretty: + self.json_indent = 2 + self.json_separators = None - # monkey patch to disable aliases - representer.RoundTripRepresenter.ignore_aliases = lambda x, y: True + j_string = json.dumps(self.data_out, + indent=self.json_indent, + separators=self.json_separators, + ensure_ascii=self.ascii_only) - yaml = YAML() - yaml.default_flow_style = False - yaml.explicit_start = True - yaml.allow_unicode = not ascii_only - yaml.encoding = 'utf-8' - yaml.dump(data, y_string_buf) - y_string = y_string_buf.getvalue().decode('utf-8')[:-1] - - if not mono and not piped_out: + if not self.mono and not self.piped_out: # set colors class JcStyle(Style): - styles = set_env_colors(env_colors) + styles = self.set_env_colors() - return str(highlight(y_string, YamlLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) + return str(highlight(j_string, JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) - return y_string + return j_string - utils.warning_message(['YAML Library not installed. Reverting to JSON output.']) - return json_out(data, pretty=pretty, env_colors=env_colors, mono=mono, piped_out=piped_out, ascii_only=ascii_only) + def safe_print_out(self): + """Safely prints JSON or YAML output in both UTF-8 and ASCII systems""" + if self.yaml_output: + try: + print(self.yaml_out(), flush=self.flush) + except UnicodeEncodeError: + self.ascii_only = True + print(self.yaml_out(), flush=self.flush) + else: + try: + print(self.json_out(), flush=self.flush) + except UnicodeEncodeError: + self.ascii_only = True + print(self.json_out(), flush=self.flush) -def json_out(data, pretty=False, env_colors=None, mono=False, piped_out=False, ascii_only=False): - """ - Return a JSON formatted string. String may include color codes or be - pretty printed. - """ - import json + def magic_parser(self): + """ + Parse command arguments for magic syntax: jc -p ls -al - separators = (',', ':') - indent = None + Return a tuple: + valid_command (bool) is this a valid cmd? (exists in magic dict) + run_command (list) list of the user's cmd to run. None if no cmd. + jc_parser (str) parser to use for this user's cmd. + jc_options (list) list of jc options + """ + # bail immediately if there are no args or a parser is defined + if len(self.args) <= 1 or (self.args[1].startswith('--') and self.args[1] not in long_options_map): + pass - if pretty: - separators = None - indent = 2 + args_given = self.args[1:] - j_string = json.dumps(data, indent=indent, separators=separators, ensure_ascii=ascii_only) + # find the options + for arg in list(args_given): + # long option found - populate option list + if arg in long_options_map: + self.magic_options.extend(long_options_map[arg][0]) + args_given.pop(0) + continue - if not mono and not piped_out: - # set colors - class JcStyle(Style): - styles = set_env_colors(env_colors) + # parser found - use standard syntax + if arg.startswith('--'): + return False, None, None, [] - return str(highlight(j_string, JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) + # option found - populate option list + if arg.startswith('-'): + self.magic_options.extend(args_given.pop(0)[1:]) - return j_string + # command found if iterator didn't already stop - stop iterating + else: + break - -def safe_print_out(list_or_dict, pretty=None, env_colors=None, mono=None, - piped_out=None, flush=None, yaml=None): - """Safely prints JSON or YAML output in both UTF-8 and ASCII systems""" - if yaml: - try: - print(yaml_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out), - flush=flush) - except UnicodeEncodeError: - print(yaml_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out, - ascii_only=True), - flush=flush) - - else: - try: - print(json_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out), - flush=flush) - except UnicodeEncodeError: - print(json_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out, - ascii_only=True), - flush=flush) - - -def magic_parser(args): - """ - Parse command arguments for magic syntax: jc -p ls -al - - Return a tuple: - valid_command (bool) is this a valid cmd? (exists in magic dict) - run_command (list) list of the user's cmd to run. None if no cmd. - jc_parser (str) parser to use for this user's cmd. - jc_options (list) list of jc options - """ - # bail immediately if there are no args or a parser is defined - if len(args) <= 1 or (args[1].startswith('--') and args[1] not in long_options_map): - return False, None, None, [] - - args_given = args[1:] - options = [] - - # find the options - for arg in list(args_given): - # long option found - populate option list - if arg in long_options_map: - options.extend(long_options_map[arg][0]) - args_given.pop(0) - continue - - # parser found - use standard syntax - if arg.startswith('--'): + # if -h, -a, or -v found in options, then bail out + if 'h' in self.magic_options or 'a' in self.magic_options or 'v' in self.magic_options: return False, None, None, [] - # option found - populate option list - if arg.startswith('-'): - options.extend(args_given.pop(0)[1:]) + # all options popped and no command found - for case like 'jc -x' + if len(args_given) == 0: + return False, None, None, [] + + # create a dictionary of magic_commands to their respective parsers. + magic_dict = {} + for entry in all_parser_info(): + magic_dict.update({mc: entry['argument'] for mc in entry.get('magic_commands', [])}) + + # find the command and parser + one_word_command = args_given[0] + two_word_command = ' '.join(args_given[0:2]) + + # try to get a parser for two_word_command, otherwise get one for one_word_command + self.magic_found_parser = magic_dict.get(two_word_command, magic_dict.get(one_word_command)) + + # set the instance variables + self.valid_command = bool(self.magic_found_parser) + self.run_command = args_given + + def open_text_file(self): + with open(self.path_string, 'r') as f: + return f.read() + + def run_user_command(self): + """ + Use subprocess to run the user's command. Returns the STDOUT, STDERR, + and the Exit Code as a tuple. + """ + proc = subprocess.Popen(self.command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False, # Allows inheriting file descriptors; + universal_newlines=True, # useful for process substitution + encoding='UTF-8') + self.magic_stdout, self.magic_stderr = proc.communicate() + + self.magic_stdout = self.magic_stdout or '\n' + self.magic_returncode = proc.returncode + + def combined_exit_code(self): + self.exit_code = self.program_exit + self.jc_exit + self.exit_code = min(self.exit_code, 255) + + def add_metadata_to_output(self): + """ + This function mutates a list or dict in place. If the _jc_meta field + does not already exist, it will be created with the metadata fields. If + the _jc_meta field already exists, the metadata fields will be added to + the existing object. + + In the case of an empty list (no data), a dictionary with a _jc_meta + object will be added to the list. This way you always get metadata, + even if there are no results. + """ + self.run_timestamp = self.runtime.timestamp() + + meta_obj = { + 'parser': self.parser_name, + 'timestamp': self.run_timestamp + } + + if self.run_command: + meta_obj['magic_command'] = self.run_command + meta_obj['magic_command_exit'] = self.magic_exit_code + + if isinstance(self.data_out, dict): + if '_jc_meta' not in self.data_out: + self.data_out['_jc_meta'] = {} + + self.data_out['_jc_meta'].update(meta_obj) + + elif isinstance(self.data_out, list): + if not self.data_out: + self.data_out.append({}) + + for item in self.data_out: + if '_jc_meta' not in item: + item['_jc_meta'] = {} + + item['_jc_meta'].update(meta_obj) - # command found if iterator didn't already stop - stop iterating else: - break + utils.error_message(['Parser returned an unsupported object type.']) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) - # 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, None, [] - - # all options popped and no command found - for case like 'jc -x' - if len(args_given) == 0: - return False, None, None, [] - - # create a dictionary of magic_commands to their respective parsers. - magic_dict = {} - for entry in all_parser_info(): - magic_dict.update({mc: entry['argument'] for mc in entry.get('magic_commands', [])}) - - # find the command and parser - one_word_command = args_given[0] - two_word_command = ' '.join(args_given[0:2]) - - # 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)) - - return ( - bool(found_parser), # was a suitable parser found? - args_given, # run_command - found_parser, # the parser selected - options # jc options to preserve - ) - - -def open_text_file(path_string): - with open(path_string, 'r') as f: - return f.read() - - -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, - close_fds=False, # Allows inheriting file descriptors; - universal_newlines=True, # useful for process substitution - encoding='UTF-8') - stdout, stderr = proc.communicate() - - return ( - stdout or '\n', - stderr, - proc.returncode - ) - - -def combined_exit_code(program_exit=0, jc_exit=0): - exit_code = program_exit + jc_exit - exit_code = min(exit_code, 255) - return exit_code - - -def add_metadata_to(list_or_dict, - runtime=None, - run_command=None, - magic_exit_code=None, - parser_name=None): - """ - This function mutates a list or dict in place. If the _jc_meta field - does not already exist, it will be created with the metadata fields. If - the _jc_meta field already exists, the metadata fields will be added to - the existing object. - - In the case of an empty list (no data), a dictionary with a _jc_meta - object will be added to the list. This way you always get metadata, - even if there are no results. - """ - run_timestamp = runtime.timestamp() - - meta_obj = { - 'parser': parser_name, - 'timestamp': run_timestamp - } - - if run_command: - meta_obj['magic_command'] = run_command - meta_obj['magic_command_exit'] = magic_exit_code - - if isinstance(list_or_dict, dict): - if '_jc_meta' not in list_or_dict: - list_or_dict['_jc_meta'] = {} - - list_or_dict['_jc_meta'].update(meta_obj) - - elif isinstance(list_or_dict, list): - if not list_or_dict: - list_or_dict.append({}) - - for item in list_or_dict: - if '_jc_meta' not in item: - item['_jc_meta'] = {} - - item['_jc_meta'].update(meta_obj) - - else: - utils.error_message(['Parser returned an unsupported object type.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + def ctrlc(self, signum, frame): + """Exit with error on SIGINT""" + sys.exit(self.JC_ERROR_EXIT) def main(): + cli = JcCli() + # break on ctrl-c keyboard interrupt - signal.signal(signal.SIGINT, ctrlc) + signal.signal(signal.SIGINT, cli.ctrlc) # break on pipe error. need try/except for windows compatibility try: @@ -527,240 +546,233 @@ def main(): os.system('') # parse magic syntax first: e.g. jc -p ls -al - magic_options = [] - valid_command, run_command, magic_found_parser, magic_options = magic_parser(sys.argv) + cli.args = sys.argv + cli.magic_parser() # set colors - jc_colors = os.getenv('JC_COLORS') + cli.env_colors = os.getenv('JC_COLORS') # set options - options = [] - options.extend(magic_options) + cli.options.extend(cli.magic_options) # find options if magic_parser did not find a command - if not valid_command: - for opt in sys.argv: + if not cli.valid_command: + for opt in cli.args: if opt in long_options_map: - options.extend(long_options_map[opt][0]) + cli.options.extend(long_options_map[opt][0]) if opt.startswith('-') and not opt.startswith('--'): - options.extend(opt[1:]) + cli.options.extend(opt[1:]) - about = 'a' in options - debug = 'd' in options - verbose_debug = options.count('d') > 1 - force_color = 'C' in options - mono = ('m' in options or bool(os.getenv('NO_COLOR'))) and not force_color - help_me = 'h' in options - verbose_help = options.count('h') > 1 - pretty = 'p' in options - quiet = 'q' in options - ignore_exceptions = options.count('q') > 1 - raw = 'r' in options - meta_out = 'M' in options - unbuffer = 'u' in options - version_info = 'v' in options - yaml_out = 'y' in options - bash_comp = 'B' in options - zsh_comp = 'Z' in options + cli.about = 'a' in cli.options + cli.debug = 'd' in cli.options + cli.verbose_debug = cli.options.count('d') > 1 + cli.force_color = 'C' in cli.options + cli.mono = ('m' in cli.options or bool(os.getenv('NO_COLOR'))) and not cli.force_color + cli.help_me = 'h' in cli.options + cli.verbose_help = cli.options.count('h') > 1 + cli.pretty = 'p' in cli.options + cli.quiet = 'q' in cli.options + cli.ignore_exceptions = cli.options.count('q') > 1 + cli.raw = 'r' in cli.options + cli.meta_out = 'M' in cli.options + cli.unbuffer = 'u' in cli.options + cli.version_info = 'v' in cli.options + cli.yaml_output = 'y' in cli.options + cli.bash_comp = 'B' in cli.options + cli.zsh_comp = 'Z' in cli.options - if verbose_debug: + if cli.verbose_debug: tracebackplus.enable(context=11) if not PYGMENTS_INSTALLED: - mono = True + cli.mono = True - if about: - safe_print_out(about_jc(), - pretty=pretty, - env_colors=jc_colors, - mono=mono, - piped_out=piped_output(force_color), - yaml=yaml_out) + if cli.about: + cli.data_out = cli.about_jc() + cli.safe_print_out() sys.exit(0) - if help_me: - help_doc(sys.argv, show_hidden=verbose_help) + if cli.help_me: + cli.help_doc() sys.exit(0) - if version_info: - utils._safe_print(versiontext()) + if cli.version_info: + utils._safe_print(cli.versiontext()) sys.exit(0) - if bash_comp: + if cli.bash_comp: utils._safe_print(bash_completion()) sys.exit(0) - if zsh_comp: + if cli.zsh_comp: utils._safe_print(zsh_completion()) sys.exit(0) # if magic syntax used, try to run the command and error if it's not found, etc. - magic_stdout, magic_stderr, magic_exit_code = None, None, 0 - run_command_str = '' - if run_command: + if cli.run_command: try: - run_command_str = shlex.join(run_command) # python 3.8+ + cli.run_command_str = shlex.join(cli.run_command) # python 3.8+ except AttributeError: - run_command_str = ' '.join(run_command) # older python versions + cli.run_command_str = ' '.join(cli.run_command) # older python versions - if run_command_str.startswith('/proc'): + if cli.run_command_str.startswith('/proc'): try: - magic_found_parser = 'proc' - magic_stdout = open_text_file(run_command_str) + cli.magic_found_parser = 'proc' + cli.stdout = cli.open_text_file() except OSError as e: - if debug: + if cli.debug: raise error_msg = os.strerror(e.errno) utils.error_message([ - f'"{run_command_str}" file could not be opened: {error_msg}.' + f'"{cli.run_command_str}" file could not be opened: {error_msg}.' ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) except Exception: - if debug: + if cli.debug: raise utils.error_message([ - f'"{run_command_str}" file could not be opened. For details use the -d or -dd option.' + f'"{cli.run_command_str}" file could not be opened. For details use the -d or -dd option.' ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) - elif valid_command: + elif cli.valid_command: try: - magic_stdout, magic_stderr, magic_exit_code = run_user_command(run_command) - if magic_stderr: - utils._safe_print(magic_stderr[:-1], file=sys.stderr) + cli.run_user_command() + if cli.magic_stderr: + utils._safe_print(cli.magic_stderr[:-1], file=sys.stderr) except OSError as e: - if debug: + if cli.debug: raise error_msg = os.strerror(e.errno) utils.error_message([ - f'"{run_command_str}" command could not be run: {error_msg}.' + f'"{cli.run_command_str}" command could not be run: {error_msg}.' ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) except Exception: - if debug: + if cli.debug: raise utils.error_message([ - f'"{run_command_str}" command could not be run. For details use the -d or -dd option.' + f'"{cli.run_command_str}" command could not be run. For details use the -d or -dd option.' ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) - elif run_command is not None: - utils.error_message([f'"{run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + elif cli.run_command is not None: + utils.error_message([f'"{cli.run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) + sys.exit(cli.combined_exit_code()) # find the correct parser - if magic_found_parser: - parser = _get_parser(magic_found_parser) - parser_name = parser_shortname(magic_found_parser) + if cli.magic_found_parser: + cli.parser_module = _get_parser(cli.magic_found_parser) + cli.parser_name = cli.parser_shortname() else: found = False - for arg in sys.argv: - parser_name = parser_shortname(arg) + for arg in cli.args: + cli.parser_arg = arg + cli.parser_name = cli.parser_shortname() - if parser_name in parsers: - parser = _get_parser(arg) + if cli.parser_name in parsers: + cli.parser_module = _get_parser(cli.parser_arg) found = True break if not found: utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) - if sys.stdin.isatty() and magic_stdout is None: + if sys.stdin.isatty() and cli.stdout is None: utils.error_message(['Missing piped data. Use "jc -h" for help.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) # parse and print to stdout try: # differentiate between regular and streaming parsers # streaming (only supports UTF-8 string data for now) - if _parser_is_streaming(parser): - result = parser.parse(sys.stdin, - raw=raw, - quiet=quiet, - ignore_exceptions=ignore_exceptions) + if _parser_is_streaming(cli.parser_module): + cli.data_in = sys.stdin + result = cli.parser_module.parse(cli.data_in, + raw=cli.raw, + quiet=cli.quiet, + ignore_exceptions=cli.ignore_exceptions) for line in result: - if meta_out: - run_dt_utc = datetime.now(timezone.utc) - add_metadata_to(line, run_dt_utc, run_command, magic_exit_code, parser_name) + cli.data_out = line + if cli.meta_out: + cli.run_timestamp = datetime.now(timezone.utc) + cli.add_metadata_to_output() - safe_print_out(line, - pretty=pretty, - env_colors=jc_colors, - mono=mono, - piped_out=piped_output(force_color), - flush=unbuffer, - yaml=yaml_out) + cli.safe_print_out() - sys.exit(combined_exit_code(magic_exit_code, 0)) + sys.exit(cli.combined_exit_code()) # regular (supports binary and UTF-8 string data) else: - data = magic_stdout or sys.stdin.buffer.read() + print(cli.stdout) + cli.data_in = cli.stdout or sys.stdin.buffer.read() # convert to UTF-8, if possible. Otherwise, leave as bytes try: - if isinstance(data, bytes): - data = data.decode('utf-8') + if isinstance(cli.data_in, bytes): + cli.data_in = cli.data_in.decode('utf-8') except UnicodeDecodeError: pass - result = parser.parse(data, - raw=raw, - quiet=quiet) + cli.data_out = cli.parser_module.parse(cli.data_in, + raw=cli.raw, + quiet=cli.quiet) - if meta_out: - run_dt_utc = datetime.now(timezone.utc) - add_metadata_to(result, run_dt_utc, run_command, magic_exit_code, parser_name) + if cli.meta_out: + cli.run_timestamp = datetime.now(timezone.utc) + cli.add_metadata_to_output() - safe_print_out(result, - pretty=pretty, - env_colors=jc_colors, - mono=mono, - piped_out=piped_output(force_color), - flush=unbuffer, - yaml=yaml_out) + cli.safe_print_out() - sys.exit(combined_exit_code(magic_exit_code, 0)) + sys.exit(cli.combined_exit_code()) except (ParseError, LibraryNotInstalled) as e: - if debug: + if cli.debug: raise utils.error_message([ - f'Parser issue with {parser_name}:', f'{e.__class__.__name__}: {e}', + f'Parser issue with {cli.parser_name}:', f'{e.__class__.__name__}: {e}', 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', - f'For details use the -d or -dd option. Use "jc -h --{parser_name}" for help.' + f'For details use the -d or -dd option. Use "jc -h --{cli.parser_name}" for help.' ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) except Exception: - if debug: + if cli.debug: raise streaming_msg = '' - if _parser_is_streaming(parser): + if _parser_is_streaming(cli.parser_module): streaming_msg = 'Use the -qq option to ignore streaming parser errors.' utils.error_message([ - f'{parser_name} parser could not parse the input data.', + f'{cli.parser_name} parser could not parse the input data.', f'{streaming_msg}', 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', - f'For details use the -d or -dd option. Use "jc -h --{parser_name}" for help.' + f'For details use the -d or -dd option. Use "jc -h --{cli.parser_name}" for help.' ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) + cli.jc_error = cli.JC_ERROR_EXIT + sys.exit(cli.combined_exit_code()) if __name__ == '__main__': From 557afc95bd496647a7128556152f696576715445 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Sun, 2 Oct 2022 18:20:14 -0700 Subject: [PATCH 02/23] initial working --- jc/cli.py | 536 +++++++++++++++++++++++++++--------------------------- 1 file changed, 269 insertions(+), 267 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 05759bc1..94502843 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -61,12 +61,12 @@ class JcCli(): self.current_out_line = None self.options: List[str] = [] self.args: List[str] = [] - self.parser_arg = None self.parser_module = None self.parser_name = None self.indent = 0 self.pad = 0 - self.env_colors: Dict = {} + self.env_colors = None + self.custom_colors: Dict = {} self.documentation = False self.show_hidden = False self.piped_out = False @@ -75,9 +75,6 @@ class JcCli(): self.json_separators = (',', ':') self.json_indent = None self.path_string = None - self.run_command_str = '' - self.command = None - self.program_exit = 0 self.jc_exit = 0 self.JC_ERROR_EXIT = 100 self.exit_code = 0 @@ -106,14 +103,15 @@ class JcCli(): # Magic options self.valid_command = False self.run_command = None + self.run_command_str = '' self.magic_found_parser = None self.magic_options: List[str] = [] - self.magic_stdout = None, - self.magic_stderr = None, + self.magic_stdout = None + self.magic_stderr = None self.magic_returncode = None - def set_env_colors(self): + def set_custom_colors(self): """ Return a dictionary to be used in Pygments custom style class. @@ -153,24 +151,26 @@ class JcCli(): color_list = ['default', 'default', 'default', 'default'] # Try the color set in the JC_COLORS env variable first. If it is set to default, then fall back to default colors - self.set_env_colors = { + self.custom_colors = { Name.Tag: f'bold {PYGMENT_COLOR[color_list[0]]}' if color_list[0] != 'default' else f"bold {PYGMENT_COLOR['blue']}", # key names Keyword: PYGMENT_COLOR[color_list[1]] if color_list[1] != 'default' else PYGMENT_COLOR['brightblack'], # true, false, null Number: PYGMENT_COLOR[color_list[2]] if color_list[2] != 'default' else PYGMENT_COLOR['magenta'], # numbers String: PYGMENT_COLOR[color_list[3]] if color_list[3] != 'default' else PYGMENT_COLOR['green'] # strings } - def piped_output(self): + def set_piped_out(self): """ Return False if `STDOUT` is a TTY. True if output is being piped to another program and foce_color is True. This allows forcing of ANSI color codes even when using pipes. """ - return not sys.stdout.isatty() and not self.force_color + if not sys.stdout.isatty() and not self.force_color: + self.piped_out = True - def parser_shortname(self): + @staticmethod + def parser_shortname(parser_arg): """Return short name of the parser with dashes and no -- prefix""" - return self.parser_arg[2:] + return parser_arg[2:] def parsers_text(self): """Return the argument and description information from each parser""" @@ -276,8 +276,7 @@ Examples: otherwise the general help text is returned. """ for arg in self.options: - self.parser_arg = arg - parser_name = self.parser_shortname() + parser_name = self.parser_shortname(arg) if parser_name in parsers: self.documentation = True @@ -347,7 +346,7 @@ Examples: if not self.mono and not self.piped_out: # set colors class JcStyle(Style): - styles = self.set_env_colors() + styles = self.custom_colors return str(highlight(y_string, YamlLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) @@ -375,7 +374,7 @@ Examples: if not self.mono and not self.piped_out: # set colors class JcStyle(Style): - styles = self.set_env_colors() + styles = self.custom_colors return str(highlight(j_string, JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) @@ -409,7 +408,7 @@ Examples: """ # bail immediately if there are no args or a parser is defined if len(self.args) <= 1 or (self.args[1].startswith('--') and self.args[1] not in long_options_map): - pass + return args_given = self.args[1:] @@ -423,7 +422,7 @@ Examples: # parser found - use standard syntax if arg.startswith('--'): - return False, None, None, [] + return # option found - populate option list if arg.startswith('-'): @@ -435,11 +434,11 @@ Examples: # if -h, -a, or -v found in options, then bail out if 'h' in self.magic_options or 'a' in self.magic_options or 'v' in self.magic_options: - return False, None, None, [] + return # all options popped and no command found - for case like 'jc -x' if len(args_given) == 0: - return False, None, None, [] + return # create a dictionary of magic_commands to their respective parsers. magic_dict = {} @@ -466,7 +465,7 @@ Examples: Use subprocess to run the user's command. Returns the STDOUT, STDERR, and the Exit Code as a tuple. """ - proc = subprocess.Popen(self.command, + proc = subprocess.Popen(self.run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False, # Allows inheriting file descriptors; @@ -478,7 +477,7 @@ Examples: self.magic_returncode = proc.returncode def combined_exit_code(self): - self.exit_code = self.program_exit + self.jc_exit + self.exit_code = self.magic_returncode + self.jc_exit self.exit_code = min(self.exit_code, 255) def add_metadata_to_output(self): @@ -501,7 +500,7 @@ Examples: if self.run_command: meta_obj['magic_command'] = self.run_command - meta_obj['magic_command_exit'] = self.magic_exit_code + meta_obj['magic_command_exit'] = self.magic_returncode if isinstance(self.data_out, dict): if '_jc_meta' not in self.data_out: @@ -528,251 +527,254 @@ Examples: """Exit with error on SIGINT""" sys.exit(self.JC_ERROR_EXIT) + def run(self): + + # break on ctrl-c keyboard interrupt + signal.signal(signal.SIGINT, self.ctrlc) + + # break on pipe error. need try/except for windows compatibility + try: + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + except AttributeError: + pass + + # enable colors for Windows cmd.exe terminal + if sys.platform.startswith('win32'): + os.system('') + + # parse magic syntax first: e.g. jc -p ls -al + self.args = sys.argv + self.magic_parser() + + # set colors + self.env_colors = os.getenv('JC_COLORS') + + # set options + self.options.extend(self.magic_options) + + # find options if magic_parser did not find a command + if not self.valid_command: + for opt in self.args: + if opt in long_options_map: + self.options.extend(long_options_map[opt][0]) + + if opt.startswith('-') and not opt.startswith('--'): + self.options.extend(opt[1:]) + + self.about = 'a' in self.options + self.debug = 'd' in self.options + self.verbose_debug = self.options.count('d') > 1 + self.force_color = 'C' in self.options + self.mono = ('m' in self.options or bool(os.getenv('NO_COLOR'))) and not self.force_color + self.help_me = 'h' in self.options + self.verbose_help = self.options.count('h') > 1 + self.pretty = 'p' in self.options + self.quiet = 'q' in self.options + self.ignore_exceptions = self.options.count('q') > 1 + self.raw = 'r' in self.options + self.meta_out = 'M' in self.options + self.unbuffer = 'u' in self.options + self.version_info = 'v' in self.options + self.yaml_output = 'y' in self.options + self.bash_comp = 'B' in self.options + self.zsh_comp = 'Z' in self.options + + self.set_piped_out() + self.set_custom_colors() + + if self.verbose_debug: + tracebackplus.enable(context=11) + + if not PYGMENTS_INSTALLED: + self.mono = True + + if self.about: + self.data_out = self.about_jc() + self.safe_print_out() + sys.exit(0) + + if self.help_me: + self.help_doc() + sys.exit(0) + + if self.version_info: + utils._safe_print(self.versiontext()) + sys.exit(0) + + if self.bash_comp: + utils._safe_print(bash_completion()) + sys.exit(0) + + if self.zsh_comp: + utils._safe_print(zsh_completion()) + sys.exit(0) + + # if magic syntax used, try to run the command and error if it's not found, etc. + if self.run_command: + try: + self.run_command_str = shlex.join(self.run_command) # python 3.8+ + except AttributeError: + self.run_command_str = ' '.join(self.run_command) # older python versions + + if self.run_command_str.startswith('/proc'): + try: + self.magic_found_parser = 'proc' + self.magic_stdout = self.open_text_file() + + except OSError as e: + if self.debug: + raise + + error_msg = os.strerror(e.errno) + utils.error_message([ + f'"{self.run_command_str}" file could not be opened: {error_msg}.' + ]) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + except Exception: + if self.debug: + raise + + utils.error_message([ + f'"{self.run_command_str}" file could not be opened. For details use the -d or -dd option.' + ]) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + elif self.valid_command: + try: + self.run_user_command() + if self.magic_stderr: + utils._safe_print(self.magic_stderr[:-1], file=sys.stderr) + + except OSError as e: + if self.debug: + raise + + error_msg = os.strerror(e.errno) + utils.error_message([ + f'"{self.run_command_str}" command could not be run: {error_msg}.' + ]) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + except Exception: + if self.debug: + raise + + utils.error_message([ + f'"{self.run_command_str}" command could not be run. For details use the -d or -dd option.' + ]) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + elif self.run_command is not None: + utils.error_message([f'"{self.run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) + sys.exit(self.combined_exit_code()) + + # find the correct parser + if self.magic_found_parser: + self.parser_module = _get_parser(self.magic_found_parser) + self.parser_name = self.parser_shortname(self.magic_found_parser) + + else: + found = False + for arg in self.args: + self.parser_name = self.parser_shortname(arg) + + if self.parser_name in parsers: + self.parser_module = _get_parser(arg) + found = True + break + + if not found: + utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + if sys.stdin.isatty() and self.magic_stdout is None: + utils.error_message(['Missing piped data. Use "jc -h" for help.']) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + # parse and print to stdout + try: + # differentiate between regular and streaming parsers + + # streaming (only supports UTF-8 string data for now) + if _parser_is_streaming(self.parser_module): + self.data_in = sys.stdin + result = self.parser_module.parse(self.data_in, + raw=self.raw, + quiet=self.quiet, + ignore_exceptions=self.ignore_exceptions) + + for line in result: + self.data_out = line + if self.meta_out: + self.run_timestamp = datetime.now(timezone.utc) + self.add_metadata_to_output() + + self.safe_print_out() + + sys.exit(self.combined_exit_code()) + + # regular (supports binary and UTF-8 string data) + else: + self.data_in = self.magic_stdout or sys.stdin.buffer.read() + + # convert to UTF-8, if possible. Otherwise, leave as bytes + try: + if isinstance(self.data_in, bytes): + self.data_in = self.data_in.decode('utf-8') + except UnicodeDecodeError: + pass + + self.data_out = self.parser_module.parse(self.data_in, + raw=self.raw, + quiet=self.quiet) + + if self.meta_out: + self.run_timestamp = datetime.now(timezone.utc) + self.add_metadata_to_output() + + self.safe_print_out() + + sys.exit(self.combined_exit_code()) + + except (ParseError, LibraryNotInstalled) as e: + if self.debug: + raise + + utils.error_message([ + f'Parser issue with {self.parser_name}:', f'{e.__class__.__name__}: {e}', + 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', + f'For details use the -d or -dd option. Use "jc -h --{self.parser_name}" for help.' + ]) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + except Exception: + if self.debug: + raise + + streaming_msg = '' + if _parser_is_streaming(self.parser_module): + streaming_msg = 'Use the -qq option to ignore streaming parser errors.' + + utils.error_message([ + f'{self.parser_name} parser could not parse the input data.', + f'{streaming_msg}', + 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', + f'For details use the -d or -dd option. Use "jc -h --{self.parser_name}" for help.' + ]) + self.jc_error = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + def main(): - cli = JcCli() - - # break on ctrl-c keyboard interrupt - signal.signal(signal.SIGINT, cli.ctrlc) - - # break on pipe error. need try/except for windows compatibility - try: - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - except AttributeError: - pass - - # enable colors for Windows cmd.exe terminal - if sys.platform.startswith('win32'): - os.system('') - - # parse magic syntax first: e.g. jc -p ls -al - cli.args = sys.argv - cli.magic_parser() - - # set colors - cli.env_colors = os.getenv('JC_COLORS') - - # set options - cli.options.extend(cli.magic_options) - - # find options if magic_parser did not find a command - if not cli.valid_command: - for opt in cli.args: - if opt in long_options_map: - cli.options.extend(long_options_map[opt][0]) - - if opt.startswith('-') and not opt.startswith('--'): - cli.options.extend(opt[1:]) - - cli.about = 'a' in cli.options - cli.debug = 'd' in cli.options - cli.verbose_debug = cli.options.count('d') > 1 - cli.force_color = 'C' in cli.options - cli.mono = ('m' in cli.options or bool(os.getenv('NO_COLOR'))) and not cli.force_color - cli.help_me = 'h' in cli.options - cli.verbose_help = cli.options.count('h') > 1 - cli.pretty = 'p' in cli.options - cli.quiet = 'q' in cli.options - cli.ignore_exceptions = cli.options.count('q') > 1 - cli.raw = 'r' in cli.options - cli.meta_out = 'M' in cli.options - cli.unbuffer = 'u' in cli.options - cli.version_info = 'v' in cli.options - cli.yaml_output = 'y' in cli.options - cli.bash_comp = 'B' in cli.options - cli.zsh_comp = 'Z' in cli.options - - if cli.verbose_debug: - tracebackplus.enable(context=11) - - if not PYGMENTS_INSTALLED: - cli.mono = True - - if cli.about: - cli.data_out = cli.about_jc() - cli.safe_print_out() - sys.exit(0) - - if cli.help_me: - cli.help_doc() - sys.exit(0) - - if cli.version_info: - utils._safe_print(cli.versiontext()) - sys.exit(0) - - if cli.bash_comp: - utils._safe_print(bash_completion()) - sys.exit(0) - - if cli.zsh_comp: - utils._safe_print(zsh_completion()) - sys.exit(0) - - # if magic syntax used, try to run the command and error if it's not found, etc. - if cli.run_command: - try: - cli.run_command_str = shlex.join(cli.run_command) # python 3.8+ - except AttributeError: - cli.run_command_str = ' '.join(cli.run_command) # older python versions - - if cli.run_command_str.startswith('/proc'): - try: - cli.magic_found_parser = 'proc' - cli.stdout = cli.open_text_file() - - except OSError as e: - if cli.debug: - raise - - error_msg = os.strerror(e.errno) - utils.error_message([ - f'"{cli.run_command_str}" file could not be opened: {error_msg}.' - ]) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) - - except Exception: - if cli.debug: - raise - - utils.error_message([ - f'"{cli.run_command_str}" file could not be opened. For details use the -d or -dd option.' - ]) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) - - elif cli.valid_command: - try: - cli.run_user_command() - if cli.magic_stderr: - utils._safe_print(cli.magic_stderr[:-1], file=sys.stderr) - - except OSError as e: - if cli.debug: - raise - - error_msg = os.strerror(e.errno) - utils.error_message([ - f'"{cli.run_command_str}" command could not be run: {error_msg}.' - ]) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) - - except Exception: - if cli.debug: - raise - - utils.error_message([ - f'"{cli.run_command_str}" command could not be run. For details use the -d or -dd option.' - ]) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) - - elif cli.run_command is not None: - utils.error_message([f'"{cli.run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) - sys.exit(cli.combined_exit_code()) - - # find the correct parser - if cli.magic_found_parser: - cli.parser_module = _get_parser(cli.magic_found_parser) - cli.parser_name = cli.parser_shortname() - - else: - found = False - for arg in cli.args: - cli.parser_arg = arg - cli.parser_name = cli.parser_shortname() - - if cli.parser_name in parsers: - cli.parser_module = _get_parser(cli.parser_arg) - found = True - break - - if not found: - utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) - - if sys.stdin.isatty() and cli.stdout is None: - utils.error_message(['Missing piped data. Use "jc -h" for help.']) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) - - # parse and print to stdout - try: - # differentiate between regular and streaming parsers - - # streaming (only supports UTF-8 string data for now) - if _parser_is_streaming(cli.parser_module): - cli.data_in = sys.stdin - result = cli.parser_module.parse(cli.data_in, - raw=cli.raw, - quiet=cli.quiet, - ignore_exceptions=cli.ignore_exceptions) - - for line in result: - cli.data_out = line - if cli.meta_out: - cli.run_timestamp = datetime.now(timezone.utc) - cli.add_metadata_to_output() - - cli.safe_print_out() - - sys.exit(cli.combined_exit_code()) - - # regular (supports binary and UTF-8 string data) - else: - print(cli.stdout) - cli.data_in = cli.stdout or sys.stdin.buffer.read() - - # convert to UTF-8, if possible. Otherwise, leave as bytes - try: - if isinstance(cli.data_in, bytes): - cli.data_in = cli.data_in.decode('utf-8') - except UnicodeDecodeError: - pass - - cli.data_out = cli.parser_module.parse(cli.data_in, - raw=cli.raw, - quiet=cli.quiet) - - if cli.meta_out: - cli.run_timestamp = datetime.now(timezone.utc) - cli.add_metadata_to_output() - - cli.safe_print_out() - - sys.exit(cli.combined_exit_code()) - - except (ParseError, LibraryNotInstalled) as e: - if cli.debug: - raise - - utils.error_message([ - f'Parser issue with {cli.parser_name}:', f'{e.__class__.__name__}: {e}', - 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', - f'For details use the -d or -dd option. Use "jc -h --{cli.parser_name}" for help.' - ]) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) - - except Exception: - if cli.debug: - raise - - streaming_msg = '' - if _parser_is_streaming(cli.parser_module): - streaming_msg = 'Use the -qq option to ignore streaming parser errors.' - - utils.error_message([ - f'{cli.parser_name} parser could not parse the input data.', - f'{streaming_msg}', - 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', - f'For details use the -d or -dd option. Use "jc -h --{cli.parser_name}" for help.' - ]) - cli.jc_error = cli.JC_ERROR_EXIT - sys.exit(cli.combined_exit_code()) + JcCli().run() if __name__ == '__main__': From d341e91290cf28838c6e46e48bb86af3d72f8b9c Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Sun, 2 Oct 2022 18:29:02 -0700 Subject: [PATCH 03/23] cleanup --- jc/cli.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 94502843..510618d1 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -58,7 +58,6 @@ class JcCli(): def __init__(self) -> None: self.data_in = None self.data_out = None - self.current_out_line = None self.options: List[str] = [] self.args: List[str] = [] self.parser_module = None @@ -108,7 +107,7 @@ class JcCli(): self.magic_options: List[str] = [] self.magic_stdout = None self.magic_stderr = None - self.magic_returncode = None + self.magic_returncode = 0 def set_custom_colors(self): @@ -520,7 +519,7 @@ Examples: else: utils.error_message(['Parser returned an unsupported object type.']) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) def ctrlc(self, signum, frame): @@ -629,7 +628,7 @@ Examples: utils.error_message([ f'"{self.run_command_str}" file could not be opened: {error_msg}.' ]) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) except Exception: @@ -639,7 +638,7 @@ Examples: utils.error_message([ f'"{self.run_command_str}" file could not be opened. For details use the -d or -dd option.' ]) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) elif self.valid_command: @@ -656,7 +655,7 @@ Examples: utils.error_message([ f'"{self.run_command_str}" command could not be run: {error_msg}.' ]) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) except Exception: @@ -666,7 +665,7 @@ Examples: utils.error_message([ f'"{self.run_command_str}" command could not be run. For details use the -d or -dd option.' ]) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) elif self.run_command is not None: @@ -690,12 +689,12 @@ Examples: if not found: utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) if sys.stdin.isatty() and self.magic_stdout is None: utils.error_message(['Missing piped data. Use "jc -h" for help.']) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) # parse and print to stdout @@ -752,7 +751,7 @@ Examples: 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', f'For details use the -d or -dd option. Use "jc -h --{self.parser_name}" for help.' ]) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) except Exception: @@ -769,7 +768,7 @@ Examples: 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', f'For details use the -d or -dd option. Use "jc -h --{self.parser_name}" for help.' ]) - self.jc_error = self.JC_ERROR_EXIT + self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) From e36a8c627b6434f8805c6507446c6c96a37b11f3 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Sun, 2 Oct 2022 18:55:54 -0700 Subject: [PATCH 04/23] fix meta timestamp --- jc/cli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 510618d1..4948f293 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -77,7 +77,7 @@ class JcCli(): self.jc_exit = 0 self.JC_ERROR_EXIT = 100 self.exit_code = 0 - self.runtime: datetime = datetime.now() + # self.runtime = None self.run_timestamp = None # cli options @@ -490,11 +490,9 @@ Examples: object will be added to the list. This way you always get metadata, even if there are no results. """ - self.run_timestamp = self.runtime.timestamp() - meta_obj = { 'parser': self.parser_name, - 'timestamp': self.run_timestamp + 'timestamp': self.run_timestamp.timestamp() } if self.run_command: From 094b059aea0cd378a5403dd0282dffd8bcac009f Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Sun, 2 Oct 2022 19:30:50 -0700 Subject: [PATCH 05/23] fix magic options --- jc/cli.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 4948f293..8812840d 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -66,7 +66,6 @@ class JcCli(): self.pad = 0 self.env_colors = None self.custom_colors: Dict = {} - self.documentation = False self.show_hidden = False self.piped_out = False self.ascii_only = False @@ -77,7 +76,6 @@ class JcCli(): self.jc_exit = 0 self.JC_ERROR_EXIT = 100 self.exit_code = 0 - # self.runtime = None self.run_timestamp = None # cli options @@ -87,7 +85,6 @@ class JcCli(): self.force_color = False self.mono = False self.help_me = False - self.verbose_help = False self.pretty = False self.quiet = False self.ignore_exceptions = False @@ -274,12 +271,11 @@ Examples: Returns the parser documentation if a parser is found in the arguments, otherwise the general help text is returned. """ - for arg in self.options: + for arg in self.args: parser_name = self.parser_shortname(arg) if parser_name in parsers: - self.documentation = True - p_info = parser_info() + p_info = parser_info(parser_name, documentation=True) compatible = ', '.join(p_info.get('compatible', ['unknown'])) docs = p_info.get('documentation', 'No documentation available.') version = p_info.get('version', 'unknown') @@ -426,6 +422,7 @@ Examples: # option found - populate option list if arg.startswith('-'): self.magic_options.extend(args_given.pop(0)[1:]) + continue # command found if iterator didn't already stop - stop iterating else: @@ -525,7 +522,6 @@ Examples: sys.exit(self.JC_ERROR_EXIT) def run(self): - # break on ctrl-c keyboard interrupt signal.signal(signal.SIGINT, self.ctrlc) @@ -546,8 +542,9 @@ Examples: # set colors self.env_colors = os.getenv('JC_COLORS') - # set options - self.options.extend(self.magic_options) + # set magic options if magic syntax was found + if self.magic_found_parser: + self.options.extend(self.magic_options) # find options if magic_parser did not find a command if not self.valid_command: @@ -564,7 +561,7 @@ Examples: self.force_color = 'C' in self.options self.mono = ('m' in self.options or bool(os.getenv('NO_COLOR'))) and not self.force_color self.help_me = 'h' in self.options - self.verbose_help = self.options.count('h') > 1 + self.show_hidden = self.options.count('h') > 1 # verbose help self.pretty = 'p' in self.options self.quiet = 'q' in self.options self.ignore_exceptions = self.options.count('q') > 1 From 83d388613fe1b90ea86af1a228af0189d46cf95e Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 08:58:09 -0700 Subject: [PATCH 06/23] cleanup --- jc/cli.py | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 8812840d..c366c029 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -96,12 +96,11 @@ class JcCli(): self.bash_comp = False self.zsh_comp = False - # Magic options - self.valid_command = False - self.run_command = None - self.run_command_str = '' + # magic variables self.magic_found_parser = None self.magic_options: List[str] = [] + self.magic_run_command = None + self.magic_run_command_str = '' self.magic_stdout = None self.magic_stderr = None self.magic_returncode = 0 @@ -442,16 +441,13 @@ Examples: magic_dict.update({mc: entry['argument'] for mc in entry.get('magic_commands', [])}) # find the command and parser + self.magic_run_command = args_given one_word_command = args_given[0] two_word_command = ' '.join(args_given[0:2]) # try to get a parser for two_word_command, otherwise get one for one_word_command self.magic_found_parser = magic_dict.get(two_word_command, magic_dict.get(one_word_command)) - # set the instance variables - self.valid_command = bool(self.magic_found_parser) - self.run_command = args_given - def open_text_file(self): with open(self.path_string, 'r') as f: return f.read() @@ -461,7 +457,7 @@ Examples: Use subprocess to run the user's command. Returns the STDOUT, STDERR, and the Exit Code as a tuple. """ - proc = subprocess.Popen(self.run_command, + proc = subprocess.Popen(self.magic_run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False, # Allows inheriting file descriptors; @@ -492,8 +488,8 @@ Examples: 'timestamp': self.run_timestamp.timestamp() } - if self.run_command: - meta_obj['magic_command'] = self.run_command + if self.magic_run_command: + meta_obj['magic_command'] = self.magic_run_command meta_obj['magic_command_exit'] = self.magic_returncode if isinstance(self.data_out, dict): @@ -539,15 +535,12 @@ Examples: self.args = sys.argv self.magic_parser() - # set colors - self.env_colors = os.getenv('JC_COLORS') - # set magic options if magic syntax was found if self.magic_found_parser: self.options.extend(self.magic_options) # find options if magic_parser did not find a command - if not self.valid_command: + if not self.magic_found_parser: for opt in self.args: if opt in long_options_map: self.options.extend(long_options_map[opt][0]) @@ -555,6 +548,8 @@ Examples: if opt.startswith('-') and not opt.startswith('--'): self.options.extend(opt[1:]) + self.env_colors = os.getenv('JC_COLORS') + self.about = 'a' in self.options self.debug = 'd' in self.options self.verbose_debug = self.options.count('d') > 1 @@ -604,13 +599,13 @@ Examples: sys.exit(0) # if magic syntax used, try to run the command and error if it's not found, etc. - if self.run_command: + if self.magic_run_command: try: - self.run_command_str = shlex.join(self.run_command) # python 3.8+ + self.magic_run_command_str = shlex.join(self.magic_run_command) # python 3.8+ except AttributeError: - self.run_command_str = ' '.join(self.run_command) # older python versions + self.magic_run_command_str = ' '.join(self.magic_run_command) # older python versions - if self.run_command_str.startswith('/proc'): + if self.magic_run_command_str.startswith('/proc'): try: self.magic_found_parser = 'proc' self.magic_stdout = self.open_text_file() @@ -621,7 +616,7 @@ Examples: error_msg = os.strerror(e.errno) utils.error_message([ - f'"{self.run_command_str}" file could not be opened: {error_msg}.' + f'"{self.magic_run_command_str}" file could not be opened: {error_msg}.' ]) self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) @@ -631,12 +626,12 @@ Examples: raise utils.error_message([ - f'"{self.run_command_str}" file could not be opened. For details use the -d or -dd option.' + f'"{self.magic_run_command_str}" file could not be opened. For details use the -d or -dd option.' ]) self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) - elif self.valid_command: + elif self.magic_found_parser: try: self.run_user_command() if self.magic_stderr: @@ -648,7 +643,7 @@ Examples: error_msg = os.strerror(e.errno) utils.error_message([ - f'"{self.run_command_str}" command could not be run: {error_msg}.' + f'"{self.magic_run_command_str}" command could not be run: {error_msg}.' ]) self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) @@ -658,13 +653,13 @@ Examples: raise utils.error_message([ - f'"{self.run_command_str}" command could not be run. For details use the -d or -dd option.' + f'"{self.magic_run_command_str}" command could not be run. For details use the -d or -dd option.' ]) self.jc_exit = self.JC_ERROR_EXIT sys.exit(self.combined_exit_code()) - elif self.run_command is not None: - utils.error_message([f'"{self.run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) + elif self.magic_run_command is not None: + utils.error_message([f'"{self.magic_run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) sys.exit(self.combined_exit_code()) # find the correct parser From c420547ff8387b8232f5c4731640aaff247f41eb Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 11:30:29 -0700 Subject: [PATCH 07/23] simplify monochrome settings --- jc/cli.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index c366c029..5675fb3b 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -67,7 +67,6 @@ class JcCli(): self.env_colors = None self.custom_colors: Dict = {} self.show_hidden = False - self.piped_out = False self.ascii_only = False self.flush = False self.json_separators = (',', ':') @@ -96,7 +95,7 @@ class JcCli(): self.bash_comp = False self.zsh_comp = False - # magic variables + # magic attributes self.magic_found_parser = None self.magic_options: List[str] = [] self.magic_run_command = None @@ -108,7 +107,7 @@ class JcCli(): def set_custom_colors(self): """ - Return a dictionary to be used in Pygments custom style class. + Sets the custom_colors dictionary to be used in Pygments custom style class. Grab custom colors from JC_COLORS environment variable. JC_COLORS env variable takes 4 comma separated string values and should be in the @@ -153,14 +152,23 @@ class JcCli(): String: PYGMENT_COLOR[color_list[3]] if color_list[3] != 'default' else PYGMENT_COLOR['green'] # strings } - def set_piped_out(self): + def set_mono(self): """ - Return False if `STDOUT` is a TTY. True if output is being piped to + Sets mono attribute based on CLI options. + + Then set to False if `STDOUT` is a TTY. True if output is being piped to another program and foce_color is True. This allows forcing of ANSI color codes even when using pipes. + + Also set mono to True if Pygments is not installed. """ + self.mono = ('m' in self.options or bool(os.getenv('NO_COLOR'))) and not self.force_color + if not sys.stdout.isatty() and not self.force_color: - self.piped_out = True + self.mono = True + + if not PYGMENTS_INSTALLED: + self.mono = True @staticmethod def parser_shortname(parser_arg): @@ -337,7 +345,7 @@ Examples: yaml.dump(self.data_out, y_string_buf) y_string = y_string_buf.getvalue().decode('utf-8')[:-1] - if not self.mono and not self.piped_out: + if not self.mono: # set colors class JcStyle(Style): styles = self.custom_colors @@ -365,7 +373,7 @@ Examples: separators=self.json_separators, ensure_ascii=self.ascii_only) - if not self.mono and not self.piped_out: + if not self.mono: # set colors class JcStyle(Style): styles = self.custom_colors @@ -392,13 +400,8 @@ Examples: def magic_parser(self): """ - Parse command arguments for magic syntax: jc -p ls -al - - Return a tuple: - valid_command (bool) is this a valid cmd? (exists in magic dict) - run_command (list) list of the user's cmd to run. None if no cmd. - jc_parser (str) parser to use for this user's cmd. - jc_options (list) list of jc options + Parse command arguments for magic syntax: `jc -p ls -al` and set the + magic attributes. """ # bail immediately if there are no args or a parser is defined if len(self.args) <= 1 or (self.args[1].startswith('--') and self.args[1] not in long_options_map): @@ -554,7 +557,6 @@ Examples: self.debug = 'd' in self.options self.verbose_debug = self.options.count('d') > 1 self.force_color = 'C' in self.options - self.mono = ('m' in self.options or bool(os.getenv('NO_COLOR'))) and not self.force_color self.help_me = 'h' in self.options self.show_hidden = self.options.count('h') > 1 # verbose help self.pretty = 'p' in self.options @@ -568,15 +570,12 @@ Examples: self.bash_comp = 'B' in self.options self.zsh_comp = 'Z' in self.options - self.set_piped_out() + self.set_mono() self.set_custom_colors() if self.verbose_debug: tracebackplus.enable(context=11) - if not PYGMENTS_INSTALLED: - self.mono = True - if self.about: self.data_out = self.about_jc() self.safe_print_out() From cacda0f3cc7b4839233ee8391eee11a496c2b440 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 12:49:13 -0700 Subject: [PATCH 08/23] fix cli tests --- tests/test_jc_cli.py | 158 +++++++++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 66 deletions(-) diff --git a/tests/test_jc_cli.py b/tests/test_jc_cli.py index 48a642b3..7bac6eac 100644 --- a/tests/test_jc_cli.py +++ b/tests/test_jc_cli.py @@ -2,38 +2,42 @@ import unittest from datetime import datetime, timezone import pygments from pygments.token import (Name, Number, String, Keyword) -import jc.cli +from jc.cli import JcCli class MyTests(unittest.TestCase): def test_cli_magic_parser(self): commands = { - 'jc -p systemctl list-sockets': (True, ['systemctl', 'list-sockets'], '--systemctl-ls', ['p']), - 'jc -p systemctl list-unit-files': (True, ['systemctl', 'list-unit-files'], '--systemctl-luf', ['p']), - 'jc -p pip list': (True, ['pip', 'list'], '--pip-list', ['p']), - 'jc -p pip3 list': (True, ['pip3', 'list'], '--pip-list', ['p']), - 'jc -p pip show jc': (True, ['pip', 'show', 'jc'], '--pip-show', ['p']), - 'jc -p pip3 show jc': (True, ['pip3', 'show', 'jc'], '--pip-show', ['p']), - 'jc -prd last': (True, ['last'], '--last', ['p', 'r', 'd']), - 'jc -prdd lastb': (True, ['lastb'], '--last', ['p', 'r', 'd', 'd']), - 'jc -p airport -I': (True, ['airport', '-I'], '--airport', ['p']), - 'jc -p -r airport -I': (True, ['airport', '-I'], '--airport', ['p', 'r']), - 'jc -prd airport -I': (True, ['airport', '-I'], '--airport', ['p', 'r', 'd']), - 'jc -p nonexistent command': (False, ['nonexistent', 'command'], None, ['p']), - 'jc -ap': (False, None, None, []), - 'jc -a arp -a': (False, None, None, []), - 'jc -v': (False, None, None, []), - 'jc -h': (False, None, None, []), - 'jc -h --arp': (False, None, None, []), - 'jc -h arp': (False, None, None, []), - 'jc -h arp -a': (False, None, None, []), - 'jc --pretty dig': (True, ['dig'], '--dig', ['p']), - 'jc --pretty --monochrome --quiet --raw dig': (True, ['dig'], '--dig', ['p', 'm', 'q', 'r']), - 'jc --about --yaml-out': (False, None, None, []) + 'jc -p systemctl list-sockets': ('--systemctl-ls', ['p'], ['systemctl', 'list-sockets']), + 'jc -p systemctl list-unit-files': ('--systemctl-luf', ['p'], ['systemctl', 'list-unit-files']), + 'jc -p pip list': ('--pip-list', ['p'], ['pip', 'list']), + 'jc -p pip3 list': ('--pip-list', ['p'], ['pip3', 'list']), + 'jc -p pip show jc': ('--pip-show', ['p'], ['pip', 'show', 'jc']), + 'jc -p pip3 show jc': ('--pip-show', ['p'], ['pip3', 'show', 'jc']), + 'jc -prd last': ('--last', ['p', 'r', 'd'], ['last']), + 'jc -prdd lastb': ('--last', ['p', 'r', 'd', 'd'], ['lastb']), + 'jc -p airport -I': ('--airport', ['p'], ['airport', '-I']), + 'jc -p -r airport -I': ('--airport', ['p', 'r'], ['airport', '-I']), + 'jc -prd airport -I': ('--airport', ['p', 'r', 'd'], ['airport', '-I']), + 'jc -p nonexistent command': (None, ['p'], ['nonexistent', 'command']), + 'jc -ap': (None, ['a', 'p'], None), + 'jc -a arp -a': (None, ['a'], None), + 'jc -v': (None, ['v'], None), + 'jc -h': (None, ['h'], None), + 'jc -h --arp': (None, ['h'], None), + 'jc -h arp': (None, ['h'], None), + 'jc -h arp -a': (None, ['h'], None), + 'jc --pretty dig': ('--dig', ['p'], ['dig']), + 'jc --pretty --monochrome --quiet --raw dig': ('--dig', ['p', 'm', 'q', 'r'], ['dig']), + 'jc --about --yaml-out': (None, ['a', 'y'], None) } - for command, expected_command in commands.items(): - self.assertEqual(jc.cli.magic_parser(command.split(' ')), expected_command) + for command, expected in commands.items(): + cli = JcCli() + cli.args = command.split() + cli.magic_parser() + resulting_attributes = (cli.magic_found_parser, cli.magic_options, cli.magic_run_command) + self.assertEqual(expected, resulting_attributes) def test_cli_set_env_colors(self): if pygments.__version__.startswith('2.3.'): @@ -128,7 +132,10 @@ class MyTests(unittest.TestCase): } for jc_colors, expected_colors in env.items(): - self.assertEqual(jc.cli.set_env_colors(jc_colors), expected_colors) + cli = JcCli() + cli.env_colors = jc_colors + cli.set_custom_colors() + self.assertEqual(cli.custom_colors, expected_colors) def test_cli_json_out(self): test_input = [ @@ -157,7 +164,10 @@ class MyTests(unittest.TestCase): ] for test_dict, expected_json in zip(test_input, expected_output): - self.assertEqual(jc.cli.json_out(test_dict), expected_json) + cli = JcCli() + cli.set_custom_colors() + cli.data_out = test_dict + self.assertEqual(cli.json_out(), expected_json) def test_cli_json_out_mono(self): test_input = [ @@ -177,7 +187,11 @@ class MyTests(unittest.TestCase): ] for test_dict, expected_json in zip(test_input, expected_output): - self.assertEqual(jc.cli.json_out(test_dict, mono=True), expected_json) + cli = JcCli() + cli.set_custom_colors() + cli.mono = True + cli.data_out = test_dict + self.assertEqual(cli.json_out(), expected_json) def test_cli_json_out_pretty(self): test_input = [ @@ -197,7 +211,11 @@ class MyTests(unittest.TestCase): ] for test_dict, expected_json in zip(test_input, expected_output): - self.assertEqual(jc.cli.json_out(test_dict, pretty=True), expected_json) + cli = JcCli() + cli.pretty = True + cli.set_custom_colors() + cli.data_out = test_dict + self.assertEqual(cli.json_out(), expected_json) def test_cli_yaml_out(self): test_input = [ @@ -226,7 +244,10 @@ class MyTests(unittest.TestCase): ] for test_dict, expected_json in zip(test_input, expected_output): - self.assertEqual(jc.cli.yaml_out(test_dict), expected_json) + cli = JcCli() + cli.set_custom_colors() + cli.data_out = test_dict + self.assertEqual(cli.yaml_out(), expected_json) def test_cli_yaml_out_mono(self): test_input = [ @@ -248,56 +269,61 @@ class MyTests(unittest.TestCase): ] for test_dict, expected_json in zip(test_input, expected_output): - self.assertEqual(jc.cli.yaml_out(test_dict, mono=True), expected_json) + cli = JcCli() + cli.set_custom_colors() + cli.mono = True + cli.data_out = test_dict + self.assertEqual(cli.yaml_out(), expected_json) def test_cli_about_jc(self): - self.assertEqual(jc.cli.about_jc()['name'], 'jc') - self.assertGreaterEqual(jc.cli.about_jc()['parser_count'], 55) - self.assertEqual(jc.cli.about_jc()['parser_count'], len(jc.cli.about_jc()['parsers'])) + cli = JcCli() + self.assertEqual(cli.about_jc()['name'], 'jc') + self.assertGreaterEqual(cli.about_jc()['parser_count'], 55) + self.assertEqual(cli.about_jc()['parser_count'], len(cli.about_jc()['parsers'])) def test_add_meta_to_simple_dict(self): - list_or_dict = {'a': 1, 'b': 2} - runtime = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) - magic_exit_code = 2 - run_command = ['ping', '-c3', '192.168.1.123'] - parser_name = 'ping' + cli = JcCli() + cli.data_out = {'a': 1, 'b': 2} + cli.run_timestamp = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) + cli.magic_returncode = 2 + cli.magic_run_command = ['ping', '-c3', '192.168.1.123'] + cli.parser_name = 'ping' expected = {'a': 1, 'b': 2, '_jc_meta': {'parser': 'ping', 'magic_command': ['ping', '-c3', '192.168.1.123'], 'magic_command_exit': 2, 'timestamp': 1659659829.273349}} - jc.cli.add_metadata_to(list_or_dict, runtime, run_command, magic_exit_code, parser_name) - - self.assertEqual(list_or_dict, expected) + cli.add_metadata_to_output() + self.assertEqual(cli.data_out, expected) def test_add_meta_to_simple_list(self): - list_or_dict = [{'a': 1, 'b': 2},{'a': 3, 'b': 4}] - runtime = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) - magic_exit_code = 2 - run_command = ['ping', '-c3', '192.168.1.123'] - parser_name = 'ping' + cli = JcCli() + cli.data_out = [{'a': 1, 'b': 2},{'a': 3, 'b': 4}] + cli.run_timestamp = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) + cli.magic_returncode = 2 + cli.magic_run_command = ['ping', '-c3', '192.168.1.123'] + cli.parser_name = 'ping' expected = [{'a': 1, 'b': 2, '_jc_meta': {'parser': 'ping', 'magic_command': ['ping', '-c3', '192.168.1.123'], 'magic_command_exit': 2, 'timestamp': 1659659829.273349}}, {'a': 3, 'b': 4, '_jc_meta': {'parser': 'ping', 'magic_command': ['ping', '-c3', '192.168.1.123'], 'magic_command_exit': 2, 'timestamp': 1659659829.273349}}] - jc.cli.add_metadata_to(list_or_dict, runtime, run_command, magic_exit_code, parser_name) - - self.assertEqual(list_or_dict, expected) + cli.add_metadata_to_output() + self.assertEqual(cli.data_out, expected) def test_add_meta_to_dict_existing_meta(self): - list_or_dict = {'a': 1, 'b': 2, '_jc_meta': {'foo': 'bar'}} - runtime = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) - magic_exit_code = 2 - run_command = ['ping', '-c3', '192.168.1.123'] - parser_name = 'ping' + cli = JcCli() + cli.magic_run_command = ['ping', '-c3', '192.168.1.123'] + cli.magic_returncode = 2 + cli.data_out = {'a': 1, 'b': 2, '_jc_meta': {'foo': 'bar'}} + cli.run_timestamp = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) + cli.parser_name = 'ping' expected = {'a': 1, 'b': 2, '_jc_meta': {'foo': 'bar', 'parser': 'ping', 'magic_command': ['ping', '-c3', '192.168.1.123'], 'magic_command_exit': 2, 'timestamp': 1659659829.273349}} - jc.cli.add_metadata_to(list_or_dict, runtime, run_command, magic_exit_code, parser_name) - - self.assertEqual(list_or_dict, expected) + cli.add_metadata_to_output() + self.assertEqual(cli.data_out, expected) def test_add_meta_to_list_existing_meta(self): - list_or_dict = [{'a': 1, 'b': 2, '_jc_meta': {'foo': 'bar'}},{'a': 3, 'b': 4, '_jc_meta': {'foo': 'bar'}}] - runtime = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) - magic_exit_code = 2 - run_command = ['ping', '-c3', '192.168.1.123'] - parser_name = 'ping' + cli = JcCli() + cli.data_out = [{'a': 1, 'b': 2, '_jc_meta': {'foo': 'bar'}},{'a': 3, 'b': 4, '_jc_meta': {'foo': 'bar'}}] + cli.run_timestamp = datetime(2022, 8, 5, 0, 37, 9, 273349, tzinfo=timezone.utc) + cli.magic_returncode = 2 + cli.magic_run_command = ['ping', '-c3', '192.168.1.123'] + cli.parser_name = 'ping' expected = [{'a': 1, 'b': 2, '_jc_meta': {'foo': 'bar', 'parser': 'ping', 'magic_command': ['ping', '-c3', '192.168.1.123'], 'magic_command_exit': 2, 'timestamp': 1659659829.273349}}, {'a': 3, 'b': 4, '_jc_meta': {'foo': 'bar', 'parser': 'ping', 'magic_command': ['ping', '-c3', '192.168.1.123'], 'magic_command_exit': 2, 'timestamp': 1659659829.273349}}] - jc.cli.add_metadata_to(list_or_dict, runtime, run_command, magic_exit_code, parser_name) - - self.assertEqual(list_or_dict, expected) + cli.add_metadata_to_output() + self.assertEqual(cli.data_out, expected) if __name__ == '__main__': unittest.main() \ No newline at end of file From 2792d05c7f58d79ca8ae092a3bc2756171de000c Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 13:03:42 -0700 Subject: [PATCH 09/23] fix magic parser and tests --- jc/cli.py | 8 ++++---- tests/test_jc_cli.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 5675fb3b..85346f35 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -430,8 +430,9 @@ Examples: else: break - # if -h, -a, or -v found in options, then bail out + # if -h, -a, or -v found in options, then clear options and bail out if 'h' in self.magic_options or 'a' in self.magic_options or 'v' in self.magic_options: + self.magic_options = [] return # all options popped and no command found - for case like 'jc -x' @@ -538,9 +539,8 @@ Examples: self.args = sys.argv self.magic_parser() - # set magic options if magic syntax was found - if self.magic_found_parser: - self.options.extend(self.magic_options) + # add magic options to regular options + self.options.extend(self.magic_options) # find options if magic_parser did not find a command if not self.magic_found_parser: diff --git a/tests/test_jc_cli.py b/tests/test_jc_cli.py index 7bac6eac..96c106f8 100644 --- a/tests/test_jc_cli.py +++ b/tests/test_jc_cli.py @@ -20,16 +20,16 @@ class MyTests(unittest.TestCase): 'jc -p -r airport -I': ('--airport', ['p', 'r'], ['airport', '-I']), 'jc -prd airport -I': ('--airport', ['p', 'r', 'd'], ['airport', '-I']), 'jc -p nonexistent command': (None, ['p'], ['nonexistent', 'command']), - 'jc -ap': (None, ['a', 'p'], None), - 'jc -a arp -a': (None, ['a'], None), - 'jc -v': (None, ['v'], None), - 'jc -h': (None, ['h'], None), + 'jc -ap': (None, [], None), + 'jc -a arp -a': (None, [], None), + 'jc -v': (None, [], None), + 'jc -h': (None, [], None), 'jc -h --arp': (None, ['h'], None), - 'jc -h arp': (None, ['h'], None), - 'jc -h arp -a': (None, ['h'], None), + 'jc -h arp': (None, [], None), + 'jc -h arp -a': (None, [], None), 'jc --pretty dig': ('--dig', ['p'], ['dig']), 'jc --pretty --monochrome --quiet --raw dig': ('--dig', ['p', 'm', 'q', 'r'], ['dig']), - 'jc --about --yaml-out': (None, ['a', 'y'], None) + 'jc --about --yaml-out': (None, [], None) } for command, expected in commands.items(): From 49ba6ed0f2480dc34d2a5445582e84d039ba8267 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 13:13:10 -0700 Subject: [PATCH 10/23] formatting --- jc/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 85346f35..7c08ba9b 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -694,9 +694,9 @@ Examples: if _parser_is_streaming(self.parser_module): self.data_in = sys.stdin result = self.parser_module.parse(self.data_in, - raw=self.raw, - quiet=self.quiet, - ignore_exceptions=self.ignore_exceptions) + raw=self.raw, + quiet=self.quiet, + ignore_exceptions=self.ignore_exceptions) for line in result: self.data_out = line From d173b2f237b1f1cf0cc2266603088c5e182a157b Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 15:04:18 -0700 Subject: [PATCH 11/23] fix magic parser for cases where standard parser is found --- jc/cli.py | 1 + tests/test_jc_cli.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jc/cli.py b/jc/cli.py index 7c08ba9b..f3cd7eba 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -419,6 +419,7 @@ Examples: # parser found - use standard syntax if arg.startswith('--'): + self.magic_options = [] return # option found - populate option list diff --git a/tests/test_jc_cli.py b/tests/test_jc_cli.py index 96c106f8..1678a514 100644 --- a/tests/test_jc_cli.py +++ b/tests/test_jc_cli.py @@ -24,7 +24,7 @@ class MyTests(unittest.TestCase): 'jc -a arp -a': (None, [], None), 'jc -v': (None, [], None), 'jc -h': (None, [], None), - 'jc -h --arp': (None, ['h'], None), + 'jc -h --arp': (None, [], None), 'jc -h arp': (None, [], None), 'jc -h arp -a': (None, [], None), 'jc --pretty dig': ('--dig', ['p'], ['dig']), From e7a8cc3b8b8b105372f9824894e9d1be2e2056c8 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 17:38:55 -0700 Subject: [PATCH 12/23] fix unbuffer --- jc/cli.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index f3cd7eba..fd16a07c 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -68,7 +68,6 @@ class JcCli(): self.custom_colors: Dict = {} self.show_hidden = False self.ascii_only = False - self.flush = False self.json_separators = (',', ':') self.json_indent = None self.path_string = None @@ -386,17 +385,17 @@ Examples: """Safely prints JSON or YAML output in both UTF-8 and ASCII systems""" if self.yaml_output: try: - print(self.yaml_out(), flush=self.flush) + print(self.yaml_out(), flush=self.unbuffer) except UnicodeEncodeError: self.ascii_only = True - print(self.yaml_out(), flush=self.flush) + print(self.yaml_out(), flush=self.unbuffer) else: try: - print(self.json_out(), flush=self.flush) + print(self.json_out(), flush=self.unbuffer) except UnicodeEncodeError: self.ascii_only = True - print(self.json_out(), flush=self.flush) + print(self.json_out(), flush=self.unbuffer) def magic_parser(self): """ From 7650d831e37665942b87d23c245ec0e8cac561e3 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Mon, 3 Oct 2022 18:03:06 -0700 Subject: [PATCH 13/23] add slots --- jc/cli.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index fd16a07c..3fa8f1d8 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -54,6 +54,15 @@ if PYGMENTS_INSTALLED: class JcCli(): + __slots__ = [ + 'data_in', 'data_out', 'options', 'args', 'parser_module', 'parser_name', + 'indent', 'pad', 'env_colors', 'custom_colors', 'show_hidden', 'ascii_only', 'json_separators', + 'json_indent', 'path_string', 'jc_exit', 'JC_ERROR_EXIT', 'exit_code', 'run_timestamp', + 'about', 'debug', 'verbose_debug', 'force_color', 'mono', 'help_me', 'pretty', 'quiet', + 'ignore_exceptions', 'raw', 'meta_out', 'unbuffer', 'version_info', 'yaml_output', + 'bash_comp', 'zsh_comp', 'magic_found_parser', 'magic_options', 'magic_run_command', + 'magic_run_command_str', 'magic_stdout', 'magic_stderr', 'magic_returncode' + ] def __init__(self) -> None: self.data_in = None @@ -103,7 +112,6 @@ class JcCli(): self.magic_stderr = None self.magic_returncode = 0 - def set_custom_colors(self): """ Sets the custom_colors dictionary to be used in Pygments custom style class. @@ -312,7 +320,6 @@ Examples: ''' return textwrap.dedent(versiontext_string) - def yaml_out(self): """ Return a YAML formatted string. String may include color codes. If the From c881653d55db6af430a4597a9c8faaefd6490ab7 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 09:26:40 -0700 Subject: [PATCH 14/23] simplify the run() method --- jc/cli.py | 333 +++++++++++++++++++++++-------------------------- jc/cli_data.py | 39 ++++++ 2 files changed, 197 insertions(+), 175 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 3fa8f1d8..acd40e28 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -11,12 +11,15 @@ import signal import shlex import subprocess from typing import List, Dict -from .lib import (__version__, parser_info, all_parser_info, parsers, - _get_parser, _parser_is_streaming, parser_mod_list, - standard_parser_mod_list, plugin_parser_mod_list, - streaming_parser_mod_list) +from .lib import ( + __version__, parser_info, all_parser_info, parsers, _get_parser, _parser_is_streaming, + parser_mod_list, standard_parser_mod_list, plugin_parser_mod_list, streaming_parser_mod_list +) from . import utils -from .cli_data import long_options_map, new_pygments_colors, old_pygments_colors +from .cli_data import ( + long_options_map, new_pygments_colors, old_pygments_colors, helptext_preamble_string, + helptext_end_string +) from .shell_completions import bash_completion, zsh_completion from . import tracebackplus from .exceptions import LibraryNotInstalled, ParseError @@ -54,15 +57,15 @@ if PYGMENTS_INSTALLED: class JcCli(): - __slots__ = [ - 'data_in', 'data_out', 'options', 'args', 'parser_module', 'parser_name', - 'indent', 'pad', 'env_colors', 'custom_colors', 'show_hidden', 'ascii_only', 'json_separators', + __slots__ = ( + 'data_in', 'data_out', 'options', 'args', 'parser_module', 'parser_name', 'indent', 'pad', + 'env_colors', 'custom_colors', 'show_hidden', 'ascii_only', 'json_separators', 'json_indent', 'path_string', 'jc_exit', 'JC_ERROR_EXIT', 'exit_code', 'run_timestamp', 'about', 'debug', 'verbose_debug', 'force_color', 'mono', 'help_me', 'pretty', 'quiet', 'ignore_exceptions', 'raw', 'meta_out', 'unbuffer', 'version_info', 'yaml_output', 'bash_comp', 'zsh_comp', 'magic_found_parser', 'magic_options', 'magic_run_command', 'magic_run_command_str', 'magic_stdout', 'magic_stderr', 'magic_returncode' - ] + ) def __init__(self) -> None: self.data_in = None @@ -238,46 +241,7 @@ class JcCli(): self.pad = 20 parsers_string = self.parsers_text() options_string = self.options_text() - - helptext_string = f'''\ -jc converts the output of many commands, file-types, and strings to JSON or YAML - -Usage: - - Standard syntax: - - COMMAND | jc [OPTIONS] PARSER - - cat FILE | jc [OPTIONS] PARSER - - echo STRING | jc [OPTIONS] PARSER - - Magic syntax: - - jc [OPTIONS] COMMAND - - jc [OPTIONS] /proc/ - -Parsers: -{parsers_string} -Options: -{options_string} -Examples: - Standard Syntax: - $ dig www.google.com | jc --pretty --dig - $ cat /proc/meminfo | jc --pretty --proc - - Magic Syntax: - $ jc --pretty dig www.google.com - $ jc --pretty /proc/meminfo - - Parser Documentation: - $ jc --help --dig - - Show Hidden Parsers: - $ jc -hh -''' - + helptext_string = f'{helptext_preamble_string}{parsers_string}\nOptions:\n{options_string}\n{helptext_end_string}' return helptext_string def help_doc(self): @@ -479,6 +443,145 @@ Examples: self.magic_stdout = self.magic_stdout or '\n' self.magic_returncode = proc.returncode + def do_magic(self): + """ + Try to run the command and error if it's not found, executable, etc. + + Supports running magic commands or opening /proc files to set the + output to magic_stdout. + """ + if self.magic_run_command: + try: + self.magic_run_command_str = shlex.join(self.magic_run_command) # python 3.8+ + except AttributeError: + self.magic_run_command_str = ' '.join(self.magic_run_command) # older python versions + + if self.magic_run_command_str.startswith('/proc'): + try: + self.magic_found_parser = 'proc' + self.magic_stdout = self.open_text_file() + + except OSError as e: + if self.debug: + raise + + error_msg = os.strerror(e.errno) + utils.error_message([ + f'"{self.magic_run_command_str}" file could not be opened: {error_msg}.' + ]) + self.jc_exit = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + except Exception: + if self.debug: + raise + + utils.error_message([ + f'"{self.magic_run_command_str}" file could not be opened. For details use the -d or -dd option.' + ]) + self.jc_exit = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + elif self.magic_found_parser: + try: + self.run_user_command() + if self.magic_stderr: + utils._safe_print(self.magic_stderr[:-1], file=sys.stderr) + + except OSError as e: + if self.debug: + raise + + error_msg = os.strerror(e.errno) + utils.error_message([ + f'"{self.magic_run_command_str}" command could not be run: {error_msg}.' + ]) + self.jc_exit = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + except Exception: + if self.debug: + raise + + utils.error_message([ + f'"{self.magic_run_command_str}" command could not be run. For details use the -d or -dd option.' + ]) + self.jc_exit = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + elif self.magic_run_command is not None: + utils.error_message([f'"{self.magic_run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) + sys.exit(self.combined_exit_code()) + + def find_parser(self): + if self.magic_found_parser: + self.parser_module = _get_parser(self.magic_found_parser) + self.parser_name = self.parser_shortname(self.magic_found_parser) + + else: + found = False + for arg in self.args: + self.parser_name = self.parser_shortname(arg) + + if self.parser_name in parsers: + self.parser_module = _get_parser(arg) + found = True + break + + if not found: + utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) + self.jc_exit = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + if sys.stdin.isatty() and self.magic_stdout is None: + utils.error_message(['Missing piped data. Use "jc -h" for help.']) + self.jc_exit = self.JC_ERROR_EXIT + sys.exit(self.combined_exit_code()) + + def streaming_parse_and_print(self): + """only supports UTF-8 string data for now""" + self.data_in = sys.stdin + result = self.parser_module.parse( + self.data_in, + raw=self.raw, + quiet=self.quiet, + ignore_exceptions=self.ignore_exceptions + ) + + for line in result: + self.data_out = line + if self.meta_out: + self.run_timestamp = datetime.now(timezone.utc) + self.add_metadata_to_output() + + self.safe_print_out() + + sys.exit(self.combined_exit_code()) + + def standard_parse_and_print(self): + """supports binary and UTF-8 string data""" + self.data_in = self.magic_stdout or sys.stdin.buffer.read() + + # convert to UTF-8, if possible. Otherwise, leave as bytes + try: + if isinstance(self.data_in, bytes): + self.data_in = self.data_in.decode('utf-8') + except UnicodeDecodeError: + pass + + self.data_out = self.parser_module.parse( + self.data_in, + raw=self.raw, + quiet=self.quiet + ) + + if self.meta_out: + self.run_timestamp = datetime.now(timezone.utc) + self.add_metadata_to_output() + + self.safe_print_out() + sys.exit(self.combined_exit_code()) + def combined_exit_code(self): self.exit_code = self.magic_returncode + self.jc_exit self.exit_code = min(self.exit_code, 255) @@ -525,8 +628,8 @@ Examples: sys.exit(self.combined_exit_code()) def ctrlc(self, signum, frame): - """Exit with error on SIGINT""" - sys.exit(self.JC_ERROR_EXIT) + """Exit with error on SIGINT""" + sys.exit(self.JC_ERROR_EXIT) def run(self): # break on ctrl-c keyboard interrupt @@ -605,138 +708,18 @@ Examples: sys.exit(0) # if magic syntax used, try to run the command and error if it's not found, etc. - if self.magic_run_command: - try: - self.magic_run_command_str = shlex.join(self.magic_run_command) # python 3.8+ - except AttributeError: - self.magic_run_command_str = ' '.join(self.magic_run_command) # older python versions - - if self.magic_run_command_str.startswith('/proc'): - try: - self.magic_found_parser = 'proc' - self.magic_stdout = self.open_text_file() - - except OSError as e: - if self.debug: - raise - - error_msg = os.strerror(e.errno) - utils.error_message([ - f'"{self.magic_run_command_str}" file could not be opened: {error_msg}.' - ]) - self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) - - except Exception: - if self.debug: - raise - - utils.error_message([ - f'"{self.magic_run_command_str}" file could not be opened. For details use the -d or -dd option.' - ]) - self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) - - elif self.magic_found_parser: - try: - self.run_user_command() - if self.magic_stderr: - utils._safe_print(self.magic_stderr[:-1], file=sys.stderr) - - except OSError as e: - if self.debug: - raise - - error_msg = os.strerror(e.errno) - utils.error_message([ - f'"{self.magic_run_command_str}" command could not be run: {error_msg}.' - ]) - self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) - - except Exception: - if self.debug: - raise - - utils.error_message([ - f'"{self.magic_run_command_str}" command could not be run. For details use the -d or -dd option.' - ]) - self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) - - elif self.magic_run_command is not None: - utils.error_message([f'"{self.magic_run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) - sys.exit(self.combined_exit_code()) + self.do_magic() # find the correct parser - if self.magic_found_parser: - self.parser_module = _get_parser(self.magic_found_parser) - self.parser_name = self.parser_shortname(self.magic_found_parser) - - else: - found = False - for arg in self.args: - self.parser_name = self.parser_shortname(arg) - - if self.parser_name in parsers: - self.parser_module = _get_parser(arg) - found = True - break - - if not found: - utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) - self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) - - if sys.stdin.isatty() and self.magic_stdout is None: - utils.error_message(['Missing piped data. Use "jc -h" for help.']) - self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.find_parser() # parse and print to stdout try: - # differentiate between regular and streaming parsers - - # streaming (only supports UTF-8 string data for now) if _parser_is_streaming(self.parser_module): - self.data_in = sys.stdin - result = self.parser_module.parse(self.data_in, - raw=self.raw, - quiet=self.quiet, - ignore_exceptions=self.ignore_exceptions) + self.streaming_parse_and_print() - for line in result: - self.data_out = line - if self.meta_out: - self.run_timestamp = datetime.now(timezone.utc) - self.add_metadata_to_output() - - self.safe_print_out() - - sys.exit(self.combined_exit_code()) - - # regular (supports binary and UTF-8 string data) else: - self.data_in = self.magic_stdout or sys.stdin.buffer.read() - - # convert to UTF-8, if possible. Otherwise, leave as bytes - try: - if isinstance(self.data_in, bytes): - self.data_in = self.data_in.decode('utf-8') - except UnicodeDecodeError: - pass - - self.data_out = self.parser_module.parse(self.data_in, - raw=self.raw, - quiet=self.quiet) - - if self.meta_out: - self.run_timestamp = datetime.now(timezone.utc) - self.add_metadata_to_output() - - self.safe_print_out() - - sys.exit(self.combined_exit_code()) + self.standard_parse_and_print() except (ParseError, LibraryNotInstalled) as e: if self.debug: diff --git a/jc/cli_data.py b/jc/cli_data.py index 7147e897..42651433 100644 --- a/jc/cli_data.py +++ b/jc/cli_data.py @@ -55,3 +55,42 @@ old_pygments_colors = { 'brightcyan': '#ansiturquoise', 'white': '#ansiwhite', } + +helptext_preamble_string = f'''\ +jc converts the output of many commands, file-types, and strings to JSON or YAML + +Usage: + + Standard syntax: + + COMMAND | jc [OPTIONS] PARSER + + cat FILE | jc [OPTIONS] PARSER + + echo STRING | jc [OPTIONS] PARSER + + Magic syntax: + + jc [OPTIONS] COMMAND + + jc [OPTIONS] /proc/ + +Parsers: +''' + +helptext_end_string = '''\ +Examples: + Standard Syntax: + $ dig www.google.com | jc --pretty --dig + $ cat /proc/meminfo | jc --pretty --proc + + Magic Syntax: + $ jc --pretty dig www.google.com + $ jc --pretty /proc/meminfo + + Parser Documentation: + $ jc --help --dig + + Show Hidden Parsers: + $ jc -hh +''' \ No newline at end of file From 0306b6b73bb1563541aa647da0f40d82bffd7ccc Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 09:38:46 -0700 Subject: [PATCH 15/23] simplify file open method --- jc/cli.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index acd40e28..5918f9d5 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -60,11 +60,11 @@ class JcCli(): __slots__ = ( 'data_in', 'data_out', 'options', 'args', 'parser_module', 'parser_name', 'indent', 'pad', 'env_colors', 'custom_colors', 'show_hidden', 'ascii_only', 'json_separators', - 'json_indent', 'path_string', 'jc_exit', 'JC_ERROR_EXIT', 'exit_code', 'run_timestamp', - 'about', 'debug', 'verbose_debug', 'force_color', 'mono', 'help_me', 'pretty', 'quiet', - 'ignore_exceptions', 'raw', 'meta_out', 'unbuffer', 'version_info', 'yaml_output', - 'bash_comp', 'zsh_comp', 'magic_found_parser', 'magic_options', 'magic_run_command', - 'magic_run_command_str', 'magic_stdout', 'magic_stderr', 'magic_returncode' + 'json_indent', 'jc_exit', 'JC_ERROR_EXIT', 'exit_code', 'run_timestamp', 'about', 'debug', + 'verbose_debug', 'force_color', 'mono', 'help_me', 'pretty', 'quiet', 'ignore_exceptions', + 'raw', 'meta_out', 'unbuffer', 'version_info', 'yaml_output', 'bash_comp', 'zsh_comp', + 'magic_found_parser', 'magic_options', 'magic_run_command', 'magic_run_command_str', + 'magic_stdout', 'magic_stderr', 'magic_returncode' ) def __init__(self) -> None: @@ -82,7 +82,6 @@ class JcCli(): self.ascii_only = False self.json_separators = (',', ':') self.json_indent = None - self.path_string = None self.jc_exit = 0 self.JC_ERROR_EXIT = 100 self.exit_code = 0 @@ -246,8 +245,8 @@ class JcCli(): def help_doc(self): """ - Returns the parser documentation if a parser is found in the arguments, - otherwise the general help text is returned. + Pages the parser documentation if a parser is found in the arguments, + otherwise the general help text is printed. """ for arg in self.args: parser_name = self.parser_shortname(arg) @@ -423,8 +422,9 @@ class JcCli(): # try to get a parser for two_word_command, otherwise get one for one_word_command self.magic_found_parser = magic_dict.get(two_word_command, magic_dict.get(one_word_command)) - def open_text_file(self): - with open(self.path_string, 'r') as f: + @staticmethod + def open_text_file(path_string): + with open(path_string, 'r') as f: return f.read() def run_user_command(self): @@ -459,7 +459,7 @@ class JcCli(): if self.magic_run_command_str.startswith('/proc'): try: self.magic_found_parser = 'proc' - self.magic_stdout = self.open_text_file() + self.magic_stdout = self.open_text_file(self.magic_run_command_str) except OSError as e: if self.debug: From 46fdc457fc63a7d814802ff0396ef4378893d63b Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 10:10:59 -0700 Subject: [PATCH 16/23] fix exit codes --- jc/cli.py | 70 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 5918f9d5..b3949f2e 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -60,7 +60,7 @@ class JcCli(): __slots__ = ( 'data_in', 'data_out', 'options', 'args', 'parser_module', 'parser_name', 'indent', 'pad', 'env_colors', 'custom_colors', 'show_hidden', 'ascii_only', 'json_separators', - 'json_indent', 'jc_exit', 'JC_ERROR_EXIT', 'exit_code', 'run_timestamp', 'about', 'debug', + 'json_indent', 'jc_exit', 'JC_ERROR_EXIT', 'run_timestamp', 'about', 'debug', 'verbose_debug', 'force_color', 'mono', 'help_me', 'pretty', 'quiet', 'ignore_exceptions', 'raw', 'meta_out', 'unbuffer', 'version_info', 'yaml_output', 'bash_comp', 'zsh_comp', 'magic_found_parser', 'magic_options', 'magic_run_command', 'magic_run_command_str', @@ -84,7 +84,6 @@ class JcCli(): self.json_indent = None self.jc_exit = 0 self.JC_ERROR_EXIT = 100 - self.exit_code = 0 self.run_timestamp = None # cli options @@ -337,10 +336,12 @@ class JcCli(): self.json_indent = 2 self.json_separators = None - j_string = json.dumps(self.data_out, - indent=self.json_indent, - separators=self.json_separators, - ensure_ascii=self.ascii_only) + j_string = json.dumps( + self.data_out, + indent=self.json_indent, + separators=self.json_separators, + ensure_ascii=self.ascii_only + ) if not self.mono: # set colors @@ -432,14 +433,16 @@ class JcCli(): Use subprocess to run the user's command. Returns the STDOUT, STDERR, and the Exit Code as a tuple. """ - proc = subprocess.Popen(self.magic_run_command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=False, # Allows inheriting file descriptors; - universal_newlines=True, # useful for process substitution - encoding='UTF-8') - self.magic_stdout, self.magic_stderr = proc.communicate() + proc = subprocess.Popen( + self.magic_run_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False, # Allows inheriting file descriptors + universal_newlines=True, # which is useful for process substitution + encoding='UTF-8' + ) + self.magic_stdout, self.magic_stderr = proc.communicate() self.magic_stdout = self.magic_stdout or '\n' self.magic_returncode = proc.returncode @@ -452,9 +455,11 @@ class JcCli(): """ if self.magic_run_command: try: - self.magic_run_command_str = shlex.join(self.magic_run_command) # python 3.8+ + # python 3.8+ + self.magic_run_command_str = shlex.join(self.magic_run_command) except AttributeError: - self.magic_run_command_str = ' '.join(self.magic_run_command) # older python versions + # older python versions + self.magic_run_command_str = ' '.join(self.magic_run_command) if self.magic_run_command_str.startswith('/proc'): try: @@ -470,7 +475,7 @@ class JcCli(): f'"{self.magic_run_command_str}" file could not be opened: {error_msg}.' ]) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() except Exception: if self.debug: @@ -480,7 +485,7 @@ class JcCli(): f'"{self.magic_run_command_str}" file could not be opened. For details use the -d or -dd option.' ]) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() elif self.magic_found_parser: try: @@ -497,7 +502,7 @@ class JcCli(): f'"{self.magic_run_command_str}" command could not be run: {error_msg}.' ]) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() except Exception: if self.debug: @@ -507,11 +512,12 @@ class JcCli(): f'"{self.magic_run_command_str}" command could not be run. For details use the -d or -dd option.' ]) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() elif self.magic_run_command is not None: utils.error_message([f'"{self.magic_run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) - sys.exit(self.combined_exit_code()) + self.jc_exit = self.JC_ERROR_EXIT + self.compute_exit_code_and_quit() def find_parser(self): if self.magic_found_parser: @@ -531,12 +537,12 @@ class JcCli(): if not found: utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() if sys.stdin.isatty() and self.magic_stdout is None: utils.error_message(['Missing piped data. Use "jc -h" for help.']) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() def streaming_parse_and_print(self): """only supports UTF-8 string data for now""" @@ -556,8 +562,6 @@ class JcCli(): self.safe_print_out() - sys.exit(self.combined_exit_code()) - def standard_parse_and_print(self): """supports binary and UTF-8 string data""" self.data_in = self.magic_stdout or sys.stdin.buffer.read() @@ -580,15 +584,15 @@ class JcCli(): self.add_metadata_to_output() self.safe_print_out() - sys.exit(self.combined_exit_code()) - def combined_exit_code(self): - self.exit_code = self.magic_returncode + self.jc_exit - self.exit_code = min(self.exit_code, 255) + def compute_exit_code_and_quit(self): + exit_code = self.magic_returncode + self.jc_exit + exit_code = min(exit_code, 255) + sys.exit(exit_code) def add_metadata_to_output(self): """ - This function mutates a list or dict in place. If the _jc_meta field + This function mutates data_out in place. If the _jc_meta field does not already exist, it will be created with the metadata fields. If the _jc_meta field already exists, the metadata fields will be added to the existing object. @@ -625,7 +629,7 @@ class JcCli(): else: utils.error_message(['Parser returned an unsupported object type.']) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() def ctrlc(self, signum, frame): """Exit with error on SIGINT""" @@ -717,9 +721,11 @@ class JcCli(): try: if _parser_is_streaming(self.parser_module): self.streaming_parse_and_print() + self.compute_exit_code_and_quit() else: self.standard_parse_and_print() + self.compute_exit_code_and_quit() except (ParseError, LibraryNotInstalled) as e: if self.debug: @@ -731,7 +737,7 @@ class JcCli(): f'For details use the -d or -dd option. Use "jc -h --{self.parser_name}" for help.' ]) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() except Exception: if self.debug: @@ -748,7 +754,7 @@ class JcCli(): f'For details use the -d or -dd option. Use "jc -h --{self.parser_name}" for help.' ]) self.jc_exit = self.JC_ERROR_EXIT - sys.exit(self.combined_exit_code()) + self.compute_exit_code_and_quit() def main(): From 677e04ab7dfc8d82c778b20bbcc9d17309aa4a65 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 11:15:37 -0700 Subject: [PATCH 17/23] cleanup --- jc/cli.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index b3949f2e..c09de268 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -314,7 +314,6 @@ class JcCli(): y_string = y_string_buf.getvalue().decode('utf-8')[:-1] if not self.mono: - # set colors class JcStyle(Style): styles = self.custom_colors @@ -344,7 +343,6 @@ class JcCli(): ) if not self.mono: - # set colors class JcStyle(Style): styles = self.custom_colors @@ -408,6 +406,7 @@ class JcCli(): # all options popped and no command found - for case like 'jc -x' if len(args_given) == 0: + self.magic_options = [] return # create a dictionary of magic_commands to their respective parsers. @@ -632,8 +631,8 @@ class JcCli(): self.compute_exit_code_and_quit() def ctrlc(self, signum, frame): - """Exit with error on SIGINT""" - sys.exit(self.JC_ERROR_EXIT) + """Exit on SIGINT""" + self.compute_exit_code_and_quit() def run(self): # break on ctrl-c keyboard interrupt @@ -665,8 +664,6 @@ class JcCli(): if opt.startswith('-') and not opt.startswith('--'): self.options.extend(opt[1:]) - self.env_colors = os.getenv('JC_COLORS') - self.about = 'a' in self.options self.debug = 'd' in self.options self.verbose_debug = self.options.count('d') > 1 @@ -685,6 +682,7 @@ class JcCli(): self.zsh_comp = 'Z' in self.options self.set_mono() + self.env_colors = os.getenv('JC_COLORS') self.set_custom_colors() if self.verbose_debug: @@ -693,28 +691,28 @@ class JcCli(): if self.about: self.data_out = self.about_jc() self.safe_print_out() - sys.exit(0) + self.compute_exit_code_and_quit() if self.help_me: self.help_doc() - sys.exit(0) + self.compute_exit_code_and_quit() if self.version_info: utils._safe_print(self.versiontext()) - sys.exit(0) + self.compute_exit_code_and_quit() if self.bash_comp: utils._safe_print(bash_completion()) - sys.exit(0) + self.compute_exit_code_and_quit() if self.zsh_comp: utils._safe_print(zsh_completion()) - sys.exit(0) + self.compute_exit_code_and_quit() - # if magic syntax used, try to run the command and error if it's not found, etc. + # if magic syntax used, try to run the command and set the magic attributes self.do_magic() - # find the correct parser + # find the correct parser from magic_parser or user-supplied self.find_parser() # parse and print to stdout From 6be92498bc9a375b4205201a55c223c087cee47b Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 11:32:18 -0700 Subject: [PATCH 18/23] remove self.env_colors attribute --- jc/cli.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index c09de268..0151f4ea 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -59,12 +59,12 @@ if PYGMENTS_INSTALLED: class JcCli(): __slots__ = ( 'data_in', 'data_out', 'options', 'args', 'parser_module', 'parser_name', 'indent', 'pad', - 'env_colors', 'custom_colors', 'show_hidden', 'ascii_only', 'json_separators', - 'json_indent', 'jc_exit', 'JC_ERROR_EXIT', 'run_timestamp', 'about', 'debug', - 'verbose_debug', 'force_color', 'mono', 'help_me', 'pretty', 'quiet', 'ignore_exceptions', - 'raw', 'meta_out', 'unbuffer', 'version_info', 'yaml_output', 'bash_comp', 'zsh_comp', - 'magic_found_parser', 'magic_options', 'magic_run_command', 'magic_run_command_str', - 'magic_stdout', 'magic_stderr', 'magic_returncode' + 'custom_colors', 'show_hidden', 'ascii_only', 'json_separators', 'json_indent', 'jc_exit', + 'JC_ERROR_EXIT', 'run_timestamp', 'about', 'debug', 'verbose_debug', 'force_color', 'mono', + 'help_me', 'pretty', 'quiet', 'ignore_exceptions', 'raw', 'meta_out', 'unbuffer', + 'version_info', 'yaml_output', 'bash_comp', 'zsh_comp', 'magic_found_parser', + 'magic_options', 'magic_run_command', 'magic_run_command_str', 'magic_stdout', + 'magic_stderr', 'magic_returncode' ) def __init__(self) -> None: @@ -76,7 +76,6 @@ class JcCli(): self.parser_name = None self.indent = 0 self.pad = 0 - self.env_colors = None self.custom_colors: Dict = {} self.show_hidden = False self.ascii_only = False @@ -134,9 +133,10 @@ class JcCli(): JC_COLORS=default,default,default,default """ input_error = False + env_colors = os.getenv('JC_COLORS') - if self.env_colors: - color_list = self.env_colors.split(',') + if env_colors: + color_list = env_colors.split(',') else: color_list = ['default', 'default', 'default', 'default'] @@ -682,7 +682,6 @@ class JcCli(): self.zsh_comp = 'Z' in self.options self.set_mono() - self.env_colors = os.getenv('JC_COLORS') self.set_custom_colors() if self.verbose_debug: From f652ccd4b136a011d9d3894c2e2aa563749275c9 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 11:39:50 -0700 Subject: [PATCH 19/23] fix tests for env_colors --- tests/test_jc_cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_jc_cli.py b/tests/test_jc_cli.py index 1678a514..5e7d0c52 100644 --- a/tests/test_jc_cli.py +++ b/tests/test_jc_cli.py @@ -1,3 +1,4 @@ +import os import unittest from datetime import datetime, timezone import pygments @@ -133,7 +134,7 @@ class MyTests(unittest.TestCase): for jc_colors, expected_colors in env.items(): cli = JcCli() - cli.env_colors = jc_colors + os.environ["JC_COLORS"] = jc_colors cli.set_custom_colors() self.assertEqual(cli.custom_colors, expected_colors) @@ -165,6 +166,7 @@ class MyTests(unittest.TestCase): for test_dict, expected_json in zip(test_input, expected_output): cli = JcCli() + os.environ["JC_COLORS"] = "default,default,default,default" cli.set_custom_colors() cli.data_out = test_dict self.assertEqual(cli.json_out(), expected_json) @@ -245,6 +247,7 @@ class MyTests(unittest.TestCase): for test_dict, expected_json in zip(test_input, expected_output): cli = JcCli() + os.environ["JC_COLORS"] = "default,default,default,default" cli.set_custom_colors() cli.data_out = test_dict self.assertEqual(cli.yaml_out(), expected_json) From d7684d39a84a2140a01f38a2e76f58d73bd5e5a5 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 11:48:36 -0700 Subject: [PATCH 20/23] set magic_run_command_str in magic_parser --- jc/cli.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 0151f4ea..ceea1f23 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -414,12 +414,20 @@ class JcCli(): for entry in all_parser_info(): magic_dict.update({mc: entry['argument'] for mc in entry.get('magic_commands', [])}) - # find the command and parser + # set the command list and string self.magic_run_command = args_given - one_word_command = args_given[0] - two_word_command = ' '.join(args_given[0:2]) + + if self.magic_run_command: + try: + # python 3.8+ + self.magic_run_command_str = shlex.join(self.magic_run_command) + except AttributeError: + # older python versions + self.magic_run_command_str = ' '.join(self.magic_run_command) # try to get a parser for two_word_command, otherwise get one for one_word_command + one_word_command = self.magic_run_command[0] + two_word_command = ' '.join(self.magic_run_command[0:2]) self.magic_found_parser = magic_dict.get(two_word_command, magic_dict.get(one_word_command)) @staticmethod @@ -452,14 +460,6 @@ class JcCli(): Supports running magic commands or opening /proc files to set the output to magic_stdout. """ - if self.magic_run_command: - try: - # python 3.8+ - self.magic_run_command_str = shlex.join(self.magic_run_command) - except AttributeError: - # older python versions - self.magic_run_command_str = ' '.join(self.magic_run_command) - if self.magic_run_command_str.startswith('/proc'): try: self.magic_found_parser = 'proc' From 028f55910afbd9a997eca70d5e9124d12e43d113 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 12:14:32 -0700 Subject: [PATCH 21/23] fix for rare instances when the output is a list of lists (yaml | yaml -> json) --- jc/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index ceea1f23..9f9c5523 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -620,10 +620,11 @@ class JcCli(): self.data_out.append({}) for item in self.data_out: - if '_jc_meta' not in item: - item['_jc_meta'] = {} + if isinstance(item, dict): + if '_jc_meta' not in item: + item['_jc_meta'] = {} - item['_jc_meta'].update(meta_obj) + item['_jc_meta'].update(meta_obj) else: utils.error_message(['Parser returned an unsupported object type.']) From cf6c13e6050fd3e356a33e2ac77aa21719c27b91 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 14:07:02 -0700 Subject: [PATCH 22/23] remove old cli module --- jc/cli.old.py | 767 -------------------------------------------------- 1 file changed, 767 deletions(-) delete mode 100644 jc/cli.old.py diff --git a/jc/cli.old.py b/jc/cli.old.py deleted file mode 100644 index fb2ecbb2..00000000 --- a/jc/cli.old.py +++ /dev/null @@ -1,767 +0,0 @@ -"""jc - JSON Convert -JC cli module -""" - -import io -import sys -import os -from datetime import datetime, timezone -import textwrap -import signal -import shlex -import subprocess -from .lib import (__version__, parser_info, all_parser_info, parsers, - _get_parser, _parser_is_streaming, parser_mod_list, - standard_parser_mod_list, plugin_parser_mod_list, - streaming_parser_mod_list) -from . import utils -from .cli_data import long_options_map, new_pygments_colors, old_pygments_colors -from .shell_completions import bash_completion, zsh_completion -from . import tracebackplus -from .exceptions import LibraryNotInstalled, ParseError - -# make pygments import optional -try: - import pygments - from pygments import highlight - from pygments.style import Style - from pygments.token import (Name, Number, String, Keyword) - from pygments.lexers.data import JsonLexer, YamlLexer - from pygments.formatters import Terminal256Formatter - PYGMENTS_INSTALLED = True -except Exception: - PYGMENTS_INSTALLED = False - -JC_ERROR_EXIT = 100 - - -class info(): - version = __version__ - description = 'JSON Convert' - author = 'Kelly Brazil' - author_email = 'kellyjonbrazil@gmail.com' - website = 'https://github.com/kellyjonbrazil/jc' - copyright = '© 2019-2022 Kelly Brazil' - license = 'MIT License' - - -# We only support 2.3.0+, pygments changed color names in 2.4.0. -# startswith is sufficient and avoids potential exceptions from split and int. -if PYGMENTS_INSTALLED: - if pygments.__version__.startswith('2.3.'): - PYGMENT_COLOR = old_pygments_colors - else: - PYGMENT_COLOR = new_pygments_colors - - -def set_env_colors(env_colors=None): - """ - Return a dictionary to be used in Pygments custom style class. - - Grab custom colors from JC_COLORS environment variable. JC_COLORS env - variable takes 4 comma separated string values and should be in the - format of: - - JC_COLORS=,,, - - Where colors are: black, red, green, yellow, blue, magenta, cyan, gray, - brightblack, brightred, brightgreen, brightyellow, - brightblue, brightmagenta, brightcyan, white, default - - Default colors: - - JC_COLORS=blue,brightblack,magenta,green - or - JC_COLORS=default,default,default,default - """ - input_error = False - - if env_colors: - color_list = env_colors.split(',') - else: - color_list = ['default', 'default', 'default', 'default'] - - if len(color_list) != 4: - input_error = True - - for color in color_list: - if color != 'default' and color not in PYGMENT_COLOR: - input_error = True - - # if there is an issue with the env variable, just set all colors to default and move on - if input_error: - utils.warning_message(['Could not parse JC_COLORS environment variable']) - color_list = ['default', 'default', 'default', 'default'] - - # Try the color set in the JC_COLORS env variable first. If it is set to default, then fall back to default colors - return { - Name.Tag: f'bold {PYGMENT_COLOR[color_list[0]]}' if color_list[0] != 'default' else f"bold {PYGMENT_COLOR['blue']}", # key names - Keyword: PYGMENT_COLOR[color_list[1]] if color_list[1] != 'default' else PYGMENT_COLOR['brightblack'], # true, false, null - Number: PYGMENT_COLOR[color_list[2]] if color_list[2] != 'default' else PYGMENT_COLOR['magenta'], # numbers - String: PYGMENT_COLOR[color_list[3]] if color_list[3] != 'default' else PYGMENT_COLOR['green'] # strings - } - - -def piped_output(force_color): - """ - Return False if `STDOUT` is a TTY. True if output is being piped to - another program and foce_color is True. This allows forcing of ANSI - color codes even when using pipes. - """ - return not sys.stdout.isatty() and not force_color - - -def ctrlc(signum, frame): - """Exit with error on SIGINT""" - sys.exit(JC_ERROR_EXIT) - - -def parser_shortname(parser_arg): - """Return short name of the parser with dashes and no -- prefix""" - return parser_arg[2:] - - -def parsers_text(indent=0, pad=0, show_hidden=False): - """Return the argument and description information from each parser""" - ptext = '' - padding_char = ' ' - for p in all_parser_info(show_hidden=show_hidden): - parser_arg = p.get('argument', 'UNKNOWN') - padding = pad - len(parser_arg) - parser_desc = p.get('description', 'No description available.') - indent_text = padding_char * indent - padding_text = padding_char * padding - ptext += indent_text + parser_arg + padding_text + parser_desc + '\n' - - return ptext - - -def options_text(indent=0, pad=0): - """Return the argument and description information from each option""" - otext = '' - padding_char = ' ' - for option in long_options_map: - o_short = '-' + long_options_map[option][0] - o_desc = long_options_map[option][1] - o_combined = o_short + ', ' + option - padding = pad - len(o_combined) - indent_text = padding_char * indent - padding_text = padding_char * padding - otext += indent_text + o_combined + padding_text + o_desc + '\n' - - return otext - - -def about_jc(): - """Return jc info and the contents of each parser.info as a dictionary""" - return { - 'name': 'jc', - 'version': info.version, - 'description': info.description, - 'author': info.author, - 'author_email': info.author_email, - 'website': info.website, - 'copyright': info.copyright, - 'license': info.license, - 'python_version': '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))), - 'python_path': sys.executable, - 'parser_count': len(parser_mod_list()), - 'standard_parser_count': len(standard_parser_mod_list()), - 'streaming_parser_count': len(streaming_parser_mod_list()), - 'plugin_parser_count': len(plugin_parser_mod_list()), - 'parsers': all_parser_info(show_hidden=True) - } - - -def helptext(show_hidden=False): - """Return the help text with the list of parsers""" - parsers_string = parsers_text(indent=4, pad=20, show_hidden=show_hidden) - options_string = options_text(indent=4, pad=20) - - helptext_string = f'''\ -jc converts the output of many commands, file-types, and strings to JSON or YAML - -Usage: - - Standard syntax: - - COMMAND | jc [OPTIONS] PARSER - - cat FILE | jc [OPTIONS] PARSER - - echo STRING | jc [OPTIONS] PARSER - - Magic syntax: - - jc [OPTIONS] COMMAND - - jc [OPTIONS] /proc/ - -Parsers: -{parsers_string} -Options: -{options_string} -Examples: - Standard Syntax: - $ dig www.google.com | jc --pretty --dig - $ cat /proc/meminfo | jc --pretty --proc - - Magic Syntax: - $ jc --pretty dig www.google.com - $ jc --pretty /proc/meminfo - - Parser Documentation: - $ jc --help --dig - - Show Hidden Parsers: - $ jc -hh -''' - - return helptext_string - - -def help_doc(options, show_hidden=False): - """ - Returns the parser documentation if a parser is found in the arguments, - otherwise the general help text is returned. - """ - for arg in options: - parser_name = parser_shortname(arg) - - if parser_name in parsers: - p_info = parser_info(arg, documentation=True) - compatible = ', '.join(p_info.get('compatible', ['unknown'])) - documentation = p_info.get('documentation', 'No documentation available.') - version = p_info.get('version', 'unknown') - author = p_info.get('author', 'unknown') - author_email = p_info.get('author_email', 'unknown') - doc_text = \ - f'{documentation}\n'\ - f'Compatibility: {compatible}\n\n'\ - f'Version {version} by {author} ({author_email})\n' - - utils._safe_pager(doc_text) - return - - utils._safe_print(helptext(show_hidden=show_hidden)) - return - - -def versiontext(): - """Return the version text""" - py_ver = '.'.join((str(sys.version_info.major), str(sys.version_info.minor), str(sys.version_info.micro))) - versiontext_string = f'''\ - jc version: {info.version} - python interpreter version: {py_ver} - python path: {sys.executable} - - {info.website} - {info.copyright} - ''' - return textwrap.dedent(versiontext_string) - - -def yaml_out(data, pretty=False, env_colors=None, mono=False, piped_out=False, ascii_only=False): - """ - Return a YAML formatted string. String may include color codes. If the - YAML library is not installed, output will fall back to JSON with a - warning message to STDERR""" - # make ruamel.yaml import optional - try: - from ruamel.yaml import YAML, representer - YAML_INSTALLED = True - except Exception: - YAML_INSTALLED = False - - if YAML_INSTALLED: - y_string_buf = io.BytesIO() - - # monkey patch to disable plugins since we don't use them and in - # ruamel.yaml versions prior to 0.17.0 the use of __file__ in the - # plugin code is incompatible with the pyoxidizer packager - YAML.official_plug_ins = lambda a: [] - - # monkey patch to disable aliases - representer.RoundTripRepresenter.ignore_aliases = lambda x, y: True - - yaml = YAML() - yaml.default_flow_style = False - yaml.explicit_start = True - yaml.allow_unicode = not ascii_only - yaml.encoding = 'utf-8' - yaml.dump(data, y_string_buf) - y_string = y_string_buf.getvalue().decode('utf-8')[:-1] - - if not mono and not piped_out: - # set colors - class JcStyle(Style): - styles = set_env_colors(env_colors) - - return str(highlight(y_string, YamlLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) - - return y_string - - utils.warning_message(['YAML Library not installed. Reverting to JSON output.']) - return json_out(data, pretty=pretty, env_colors=env_colors, mono=mono, piped_out=piped_out, ascii_only=ascii_only) - - -def json_out(data, pretty=False, env_colors=None, mono=False, piped_out=False, ascii_only=False): - """ - Return a JSON formatted string. String may include color codes or be - pretty printed. - """ - import json - - separators = (',', ':') - indent = None - - if pretty: - separators = None - indent = 2 - - j_string = json.dumps(data, indent=indent, separators=separators, ensure_ascii=ascii_only) - - if not mono and not piped_out: - # set colors - class JcStyle(Style): - styles = set_env_colors(env_colors) - - return str(highlight(j_string, JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1]) - - return j_string - - -def safe_print_out(list_or_dict, pretty=None, env_colors=None, mono=None, - piped_out=None, flush=None, yaml=None): - """Safely prints JSON or YAML output in both UTF-8 and ASCII systems""" - if yaml: - try: - print(yaml_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out), - flush=flush) - except UnicodeEncodeError: - print(yaml_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out, - ascii_only=True), - flush=flush) - - else: - try: - print(json_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out), - flush=flush) - except UnicodeEncodeError: - print(json_out(list_or_dict, - pretty=pretty, - env_colors=env_colors, - mono=mono, - piped_out=piped_out, - ascii_only=True), - flush=flush) - - -def magic_parser(args): - """ - Parse command arguments for magic syntax: jc -p ls -al - - Return a tuple: - valid_command (bool) is this a valid cmd? (exists in magic dict) - run_command (list) list of the user's cmd to run. None if no cmd. - jc_parser (str) parser to use for this user's cmd. - jc_options (list) list of jc options - """ - # bail immediately if there are no args or a parser is defined - if len(args) <= 1 or (args[1].startswith('--') and args[1] not in long_options_map): - return False, None, None, [] - - args_given = args[1:] - options = [] - - # find the options - for arg in list(args_given): - # long option found - populate option list - if arg in long_options_map: - options.extend(long_options_map[arg][0]) - args_given.pop(0) - continue - - # parser found - use standard syntax - if arg.startswith('--'): - return False, None, None, [] - - # option found - populate option list - if arg.startswith('-'): - options.extend(args_given.pop(0)[1:]) - - # command found if iterator didn't already stop - stop iterating - else: - break - - # 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, None, [] - - # all options popped and no command found - for case like 'jc -x' - if len(args_given) == 0: - return False, None, None, [] - - # create a dictionary of magic_commands to their respective parsers. - magic_dict = {} - for entry in all_parser_info(): - magic_dict.update({mc: entry['argument'] for mc in entry.get('magic_commands', [])}) - - # find the command and parser - one_word_command = args_given[0] - two_word_command = ' '.join(args_given[0:2]) - - # 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)) - - return ( - bool(found_parser), # was a suitable parser found? - args_given, # run_command - found_parser, # the parser selected - options # jc options to preserve - ) - - -def open_text_file(path_string): - with open(path_string, 'r') as f: - return f.read() - - -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, - close_fds=False, # Allows inheriting file descriptors; - universal_newlines=True, # useful for process substitution - encoding='UTF-8') - stdout, stderr = proc.communicate() - - return ( - stdout or '\n', - stderr, - proc.returncode - ) - - -def combined_exit_code(program_exit=0, jc_exit=0): - exit_code = program_exit + jc_exit - exit_code = min(exit_code, 255) - return exit_code - - -def add_metadata_to(list_or_dict, - runtime=None, - run_command=None, - magic_exit_code=None, - parser_name=None): - """ - This function mutates a list or dict in place. If the _jc_meta field - does not already exist, it will be created with the metadata fields. If - the _jc_meta field already exists, the metadata fields will be added to - the existing object. - - In the case of an empty list (no data), a dictionary with a _jc_meta - object will be added to the list. This way you always get metadata, - even if there are no results. - """ - run_timestamp = runtime.timestamp() - - meta_obj = { - 'parser': parser_name, - 'timestamp': run_timestamp - } - - if run_command: - meta_obj['magic_command'] = run_command - meta_obj['magic_command_exit'] = magic_exit_code - - if isinstance(list_or_dict, dict): - if '_jc_meta' not in list_or_dict: - list_or_dict['_jc_meta'] = {} - - list_or_dict['_jc_meta'].update(meta_obj) - - elif isinstance(list_or_dict, list): - if not list_or_dict: - list_or_dict.append({}) - - for item in list_or_dict: - if '_jc_meta' not in item: - item['_jc_meta'] = {} - - item['_jc_meta'].update(meta_obj) - - else: - utils.error_message(['Parser returned an unsupported object type.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - -def main(): - # break on ctrl-c keyboard interrupt - signal.signal(signal.SIGINT, ctrlc) - - # break on pipe error. need try/except for windows compatibility - try: - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - except AttributeError: - pass - - # enable colors for Windows cmd.exe terminal - if sys.platform.startswith('win32'): - os.system('') - - # parse magic syntax first: e.g. jc -p ls -al - magic_options = [] - valid_command, run_command, magic_found_parser, magic_options = magic_parser(sys.argv) - - # set colors - jc_colors = os.getenv('JC_COLORS') - - # set options - options = [] - options.extend(magic_options) - - # find options if magic_parser did not find a command - if not valid_command: - for opt in sys.argv: - if opt in long_options_map: - options.extend(long_options_map[opt][0]) - - if opt.startswith('-') and not opt.startswith('--'): - options.extend(opt[1:]) - - about = 'a' in options - debug = 'd' in options - verbose_debug = options.count('d') > 1 - force_color = 'C' in options - mono = ('m' in options or bool(os.getenv('NO_COLOR'))) and not force_color - help_me = 'h' in options - verbose_help = options.count('h') > 1 - pretty = 'p' in options - quiet = 'q' in options - ignore_exceptions = options.count('q') > 1 - raw = 'r' in options - meta_out = 'M' in options - unbuffer = 'u' in options - version_info = 'v' in options - yaml_out = 'y' in options - bash_comp = 'B' in options - zsh_comp = 'Z' in options - - if verbose_debug: - tracebackplus.enable(context=11) - - if not PYGMENTS_INSTALLED: - mono = True - - if about: - safe_print_out(about_jc(), - pretty=pretty, - env_colors=jc_colors, - mono=mono, - piped_out=piped_output(force_color), - yaml=yaml_out) - sys.exit(0) - - if help_me: - help_doc(sys.argv, show_hidden=verbose_help) - sys.exit(0) - - if version_info: - utils._safe_print(versiontext()) - sys.exit(0) - - if bash_comp: - utils._safe_print(bash_completion()) - sys.exit(0) - - if zsh_comp: - utils._safe_print(zsh_completion()) - sys.exit(0) - - # if magic syntax used, try to run the command and error if it's not found, etc. - magic_stdout, magic_stderr, magic_exit_code = None, None, 0 - run_command_str = '' - if run_command: - try: - run_command_str = shlex.join(run_command) # python 3.8+ - except AttributeError: - run_command_str = ' '.join(run_command) # older python versions - - if run_command_str.startswith('/proc'): - try: - magic_found_parser = 'proc' - magic_stdout = open_text_file(run_command_str) - - except OSError as e: - if debug: - raise - - error_msg = os.strerror(e.errno) - utils.error_message([ - f'"{run_command_str}" file could not be opened: {error_msg}.' - ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - except Exception: - if debug: - raise - - utils.error_message([ - f'"{run_command_str}" file could not be opened. For details use the -d or -dd option.' - ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - elif valid_command: - try: - magic_stdout, magic_stderr, magic_exit_code = run_user_command(run_command) - if magic_stderr: - utils._safe_print(magic_stderr[:-1], file=sys.stderr) - - except OSError as e: - if debug: - raise - - error_msg = os.strerror(e.errno) - utils.error_message([ - f'"{run_command_str}" command could not be run: {error_msg}.' - ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - except Exception: - if debug: - raise - - utils.error_message([ - f'"{run_command_str}" command could not be run. For details use the -d or -dd option.' - ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - elif run_command is not None: - utils.error_message([f'"{run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - # find the correct parser - if magic_found_parser: - parser = _get_parser(magic_found_parser) - parser_name = parser_shortname(magic_found_parser) - - else: - found = False - for arg in sys.argv: - parser_name = parser_shortname(arg) - - if parser_name in parsers: - parser = _get_parser(arg) - found = True - break - - if not found: - utils.error_message(['Missing or incorrect arguments. Use "jc -h" for help.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - if sys.stdin.isatty() and magic_stdout is None: - utils.error_message(['Missing piped data. Use "jc -h" for help.']) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - # parse and print to stdout - try: - # differentiate between regular and streaming parsers - - # streaming (only supports UTF-8 string data for now) - if _parser_is_streaming(parser): - result = parser.parse(sys.stdin, - raw=raw, - quiet=quiet, - ignore_exceptions=ignore_exceptions) - - for line in result: - if meta_out: - run_dt_utc = datetime.now(timezone.utc) - add_metadata_to(line, run_dt_utc, run_command, magic_exit_code, parser_name) - - safe_print_out(line, - pretty=pretty, - env_colors=jc_colors, - mono=mono, - piped_out=piped_output(force_color), - flush=unbuffer, - yaml=yaml_out) - - sys.exit(combined_exit_code(magic_exit_code, 0)) - - # regular (supports binary and UTF-8 string data) - else: - data = magic_stdout or sys.stdin.buffer.read() - - # convert to UTF-8, if possible. Otherwise, leave as bytes - try: - if isinstance(data, bytes): - data = data.decode('utf-8') - except UnicodeDecodeError: - pass - - result = parser.parse(data, - raw=raw, - quiet=quiet) - - if meta_out: - run_dt_utc = datetime.now(timezone.utc) - add_metadata_to(result, run_dt_utc, run_command, magic_exit_code, parser_name) - - safe_print_out(result, - pretty=pretty, - env_colors=jc_colors, - mono=mono, - piped_out=piped_output(force_color), - flush=unbuffer, - yaml=yaml_out) - - sys.exit(combined_exit_code(magic_exit_code, 0)) - - except (ParseError, LibraryNotInstalled) as e: - if debug: - raise - - utils.error_message([ - f'Parser issue with {parser_name}:', f'{e.__class__.__name__}: {e}', - 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', - f'For details use the -d or -dd option. Use "jc -h --{parser_name}" for help.' - ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - except Exception: - if debug: - raise - - streaming_msg = '' - if _parser_is_streaming(parser): - streaming_msg = 'Use the -qq option to ignore streaming parser errors.' - - utils.error_message([ - f'{parser_name} parser could not parse the input data.', - f'{streaming_msg}', - 'If this is the correct parser, try setting the locale to C (LC_ALL=C).', - f'For details use the -d or -dd option. Use "jc -h --{parser_name}" for help.' - ]) - sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT)) - - -if __name__ == '__main__': - main() From 6f1ef09d2a24de2a0a33e174cf36830952c98c52 Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 4 Oct 2022 14:13:48 -0700 Subject: [PATCH 23/23] formatting --- jc/cli.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/jc/cli.py b/jc/cli.py index 9f9c5523..f343a297 100644 --- a/jc/cli.py +++ b/jc/cli.py @@ -123,14 +123,12 @@ class JcCli(): JC_COLORS=,,, Where colors are: black, red, green, yellow, blue, magenta, cyan, gray, - brightblack, brightred, brightgreen, brightyellow, - brightblue, brightmagenta, brightcyan, white, default + brightblack, brightred, brightgreen, brightyellow, brightblue, brightmagenta, + brightcyan, white, default Default colors: - - JC_COLORS=blue,brightblack,magenta,green - or - JC_COLORS=default,default,default,default + JC_COLORS=blue,brightblack,magenta,green + JC_COLORS=default,default,default,default """ input_error = False env_colors = os.getenv('JC_COLORS')