From 1d19de30055ff3255d4a8e3033f6ae54f67d6a7a Mon Sep 17 00:00:00 2001 From: Kelly Brazil Date: Tue, 10 Mar 2026 18:16:53 -0700 Subject: [PATCH] add typeset and declare command parser --- CHANGELOG | 4 +- jc/lib.py | 1 + jc/parsers/typeset.py | 283 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 jc/parsers/typeset.py diff --git a/CHANGELOG b/CHANGELOG index aba657d9..064eb6aa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ jc changelog -20260309 v1.25.7 +20260310 v1.25.7 +- Add `typeset` and `declare` Bash internal command parser to convert variables + simple arrays, and associative arrays along with object metadata - Enhance `rsync` and `rsync-s` parsers to add `--stats` or `--info=stats[1-3]` fields - Fix `proc-pid-smaps` proc parser when unknown VmFlags are output - Fix `iptables` command parser when Target is blank and verbose output is used diff --git a/jc/lib.py b/jc/lib.py index 7d1ea9d3..f29f3b3c 100644 --- a/jc/lib.py +++ b/jc/lib.py @@ -216,6 +216,7 @@ parsers: List[str] = [ 'traceroute', 'traceroute-s', 'tune2fs', + 'typeset', 'udevadm', 'ufw', 'ufw-appinfo', diff --git a/jc/parsers/typeset.py b/jc/parsers/typeset.py new file mode 100644 index 00000000..2a836c7d --- /dev/null +++ b/jc/parsers/typeset.py @@ -0,0 +1,283 @@ +r"""jc - JSON Convert `typeset` and `declare` Bash internal command output parser + +Convert `typeset` and `declare` bash internal commands with no options or the +following: `-a`, `-A`, `-i`, `-l`, `-p`, `-r`, `-u`, and `-x` + +Note: function parsing is not supported (e.g. `-f` or `-F`) + +Usage (cli): + + $ typeset | jc --typeset + +Usage (module): + + import jc + result = jc.parse('typeset', typeset_command_output) + +Schema: + + [ + { + "name": string, + "value": string/array/object/null, # [0] + "int_value": integer/array/object/null, # [1] + "type": string, # [2] + "readonly": boolean/null, + "integer": boolean/null, + "lowercase": boolean/null, + "uppercase": boolean/null, + "exported": boolean/null + } + ] + + Key/value pairs other than `name`, `value`, and `type` will only be non-null + when the information is available from the `typeset` or `declare` output. + + If declare options are not given to `jc` within the `typeset` output, then + it will assume all arrays are simple `array` type. + + [0] Based on type. `variable` type is always string value when set, null if + not set. `array` type value is an array of strings. `associative` type + value is an object of key/value pairs where values are strings. + Objects have the schema of: + + { + "": string, + "": string + } + + [1] If the variable is set as `integer` then same as above except values are + integers. This value is set to null if the `integer` flag is not set. + + [2] Possible values: `variable`, `array`, or `associative` + +Examples: + + $ typeset | jc --typeset -p + [] + + $ typeset | jc --typeset -p -r + [] +""" +import shlex +import re +from typing import List, Dict +from jc.jc_types import JSONDictType +import jc.utils + + +class info(): + """Provides parser metadata (version, author, etc.)""" + version = '1.0' + description = '`typeset` and `declare` command parser' + author = 'Kelly Brazil' + author_email = 'kellyjonbrazil@gmail.com' + compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd'] + tags = ['command'] + + +__version__ = info.version + +VAR_DEF_PATTERN = re.compile(r'(?P[a-zA-Z_][a-zA-Z0-9_]*)=(?P[^(][^[].+)$') +SIMPLE_ARRAY_DEF_PATTERN = re.compile(r'(?P[a-zA-Z_][a-zA-Z0-9_]*)=(?P\(\[\d+\]=.+\))$') +ASSOCIATIVE_ARRAY_DEF_PATTERN = re.compile(r'(?P[a-zA-Z_][a-zA-Z0-9_]*)=(?P\(\[[a-zA-Z_][a-zA-Z0-9_]*\]=.+\))$') +EMPTY_ARRAY_DEF_PATTERN = re.compile(r'(?P[a-zA-Z_][a-zA-Z0-9_]*)=\(\)$') +EMPTY_VAR_DEF_PATTERN = re.compile(r'declare\s.+\s(?P[a-zA-Z_][a-zA-Z0-9_]*)$') +DECLARE_OPTS_PATTERN = re.compile(r'declare\s(?P.+?)\s[a-zA-Z_][a-zA-Z0-9_]*') + + +def _process(proc_data: List[JSONDictType]) -> List[JSONDictType]: + """ + Final processing to conform to the schema. + + Parameters: + + proc_data: (List of Dictionaries) raw structured data to process + + Returns: + + List of Dictionaries. Structured to conform to the schema. + """ + for item in proc_data: + if item['type'] == 'variable' and item['integer']: + item['int_value'] = jc.utils.convert_to_int(item['value']) + + elif item['type'] == 'array' and item['integer'] \ + and isinstance(item['value'], list): + + new_num_list = [] + for number in item['value']: + new_num_list.append(jc.utils.convert_to_int(number)) + + item['int_value'] = new_num_list + + elif (item['type'] == 'array' and item['integer'] \ + and isinstance(item['value'], dict)) \ + or (item['type'] == 'associative' and item['integer']): + + new_num_dict: Dict[str, int] = {} + for key, val in item['value'].items(): + new_num_dict.update({key: jc.utils.convert_to_int(val)}) + + item['int_value'] = new_num_dict + + return proc_data + + +def _get_simple_array_vals(body: str) -> List[str]: + body = _remove_bookends(body) + body_split = shlex.split(body) + values = [] + for item in body_split: + _, val = item.split('=', maxsplit=1) + values.append(_remove_quotes(val)) + return values + + +def _get_associative_array_vals(body: str) -> Dict[str, str]: + body = _remove_bookends(body) + body_split = shlex.split(body) + values: Dict = {} + for item in body_split: + key, val = item.split('=', maxsplit=1) + key = _remove_bookends(key, '[', ']') + key_val = {key: val} + values.update(key_val) + return values + + +def _get_declare_options(line: str, type_hint: str = 'variable') -> Dict: + opts = { + 'type': type_hint, + 'readonly': None, + 'integer': None, + 'lowercase': None, + 'uppercase': None, + 'exported': None + } + + opts_map = { + 'r': 'readonly', + 'i': 'integer', + 'l': 'lowercase', + 'u': 'uppercase', + 'x': 'exported' + } + + declare_opts_match = re.match(DECLARE_OPTS_PATTERN, line) + if declare_opts_match: + for opt in declare_opts_match['options']: + if opt == '-': + continue + if opt in opts_map: + opts[opts_map[opt]] = True + continue + if 'a' in declare_opts_match['options']: + opts['type'] = 'array' + elif 'A' in declare_opts_match['options']: + opts['type'] = 'associative' + + # flip all remaining Nones to False + for option in opts.items(): + key, val = option + if val is None: + opts[key] = False + return opts + + +def _remove_bookends(data: str, start_char: str = '(', end_char: str = ')') -> str: + if data.startswith(start_char) and data.endswith(end_char): + return data[1:-1] + return data + + +def _remove_quotes(data: str, remove_char: str ='"') -> str: + if data.startswith(remove_char) and data.endswith(remove_char): + return data[1:-1] + return data + +def parse( + data: str, + raw: bool = False, + quiet: bool = False +) -> List[JSONDictType]: + """ + Main text parsing function + + Parameters: + + data: (string) text data to parse + raw: (boolean) unprocessed output if True + quiet: (boolean) suppress warning messages if True + + Returns: + + List of Dictionaries. Raw or processed structured data. + """ + jc.utils.compatibility(__name__, info.compatible, quiet) + jc.utils.input_type_check(data) + + raw_output: List[Dict] = [] + + if jc.utils.has_data(data): + + for line in filter(None, data.splitlines()): + + item = { + "name": '', + "value": '', + "int_value": None, + "type": None, + "readonly": None, + "integer": None, + "lowercase": None, + "uppercase": None, + "exported": None + } + + # regular variable + var_def_match = re.search(VAR_DEF_PATTERN, line) + if var_def_match: + item['name'] = var_def_match['name'] + item['value'] = _remove_quotes(var_def_match['val']) + item.update(_get_declare_options(line, 'variable')) + raw_output.append(item) + continue + + # empty variable + empty_var_def_match = re.search(EMPTY_VAR_DEF_PATTERN, line) + if empty_var_def_match: + item['name'] = empty_var_def_match['name'] + item['value'] = None + item.update(_get_declare_options(line, 'variable')) + raw_output.append(item) + continue + + # simple array + simple_arr_def_match = re.search(SIMPLE_ARRAY_DEF_PATTERN, line) + if simple_arr_def_match: + item['name'] = simple_arr_def_match['name'] + item['value'] = _get_simple_array_vals(simple_arr_def_match['body']) + item.update(_get_declare_options(line, 'array')) + raw_output.append(item) + continue + + # associative array + associative_arr_def_match = re.search(ASSOCIATIVE_ARRAY_DEF_PATTERN, line) + if associative_arr_def_match: + item['name'] = associative_arr_def_match['name'] + item['value'] = _get_associative_array_vals(associative_arr_def_match['body']) + item.update(_get_declare_options(line, 'associative')) + raw_output.append(item) + continue + + # empty array + empty_arr_def_match = re.search(EMPTY_ARRAY_DEF_PATTERN, line) + if empty_arr_def_match: + item['name'] = empty_arr_def_match['name'] + item['value'] = [] + item.update(_get_declare_options(line, 'array')) + raw_output.append(item) + continue + + return raw_output if raw else _process(raw_output)