1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2026-03-12 16:25:50 +02:00

add typeset and declare command parser

This commit is contained in:
Kelly Brazil
2026-03-10 18:16:53 -07:00
parent e01287b329
commit 1d19de3005
3 changed files with 287 additions and 1 deletions

View File

@@ -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

View File

@@ -216,6 +216,7 @@ parsers: List[str] = [
'traceroute',
'traceroute-s',
'tune2fs',
'typeset',
'udevadm',
'ufw',
'ufw-appinfo',

283
jc/parsers/typeset.py Normal file
View File

@@ -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:
{
"<key1>": string,
"<key2>": 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<name>[a-zA-Z_][a-zA-Z0-9_]*)=(?P<val>[^(][^[].+)$')
SIMPLE_ARRAY_DEF_PATTERN = re.compile(r'(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)=(?P<body>\(\[\d+\]=.+\))$')
ASSOCIATIVE_ARRAY_DEF_PATTERN = re.compile(r'(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)=(?P<body>\(\[[a-zA-Z_][a-zA-Z0-9_]*\]=.+\))$')
EMPTY_ARRAY_DEF_PATTERN = re.compile(r'(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)=\(\)$')
EMPTY_VAR_DEF_PATTERN = re.compile(r'declare\s.+\s(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)$')
DECLARE_OPTS_PATTERN = re.compile(r'declare\s(?P<options>.+?)\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)