diff --git a/CHANGELOG b/CHANGELOG index 42c378e8..7671fea9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,13 @@ jc changelog -20260316 v1.25.7 +20260330 v1.25.7 - Add `typeset` and `declare` Bash internal command parser to convert variables simple arrays, and associative arrays along with object metadata +- Enhance `pip-show` command parser to add `-f` show files support - Enhance `rsync` and `rsync-s` parsers to add `--stats` or `--info=stats[1-3]` fields - Fix `hashsum` command parser to correctly parse the `mode` indicator - Fix `proc-pid-smaps` proc parser when unknown VmFlags are output +- Fix `ifconfig` command parser for incorrect stripping of leading zeros in some hex numbers - Fix `iptables` command parser when Target is blank and verbose output is used 20251012 v1.25.6 diff --git a/jc/parsers/ifconfig.py b/jc/parsers/ifconfig.py index 021f5026..5e3bd41c 100644 --- a/jc/parsers/ifconfig.py +++ b/jc/parsers/ifconfig.py @@ -219,7 +219,7 @@ import jc.utils class info(): """Provides parser metadata (version, author, etc.)""" - version = '2.4' + version = '2.5' description = '`ifconfig` command parser' author = 'Kelly Brazil' author_email = 'kellyjonbrazil@gmail.com' @@ -264,7 +264,7 @@ def _process(proc_data: List[JSONDictType]) -> List[JSONDictType]: try: if entry['ipv4_mask'].startswith('0x'): new_mask = entry['ipv4_mask'] - new_mask = new_mask.lstrip('0x') + new_mask = new_mask[2:] new_mask = '.'.join(str(int(i, 16)) for i in [new_mask[i:i + 2] for i in range(0, len(new_mask), 2)]) entry['ipv4_mask'] = new_mask except (ValueError, TypeError, AttributeError): @@ -289,7 +289,7 @@ def _process(proc_data: List[JSONDictType]) -> List[JSONDictType]: try: if ip_address['mask'].startswith('0x'): new_mask = ip_address['mask'] - new_mask = new_mask.lstrip('0x') + new_mask = new_mask[2:] new_mask = '.'.join(str(int(i, 16)) for i in [new_mask[i:i + 2] for i in range(0, len(new_mask), 2)]) ip_address['mask'] = new_mask except (ValueError, TypeError, AttributeError): diff --git a/jc/parsers/pip_show.py b/jc/parsers/pip_show.py index 4ce9a7a7..27932060 100644 --- a/jc/parsers/pip_show.py +++ b/jc/parsers/pip_show.py @@ -26,7 +26,10 @@ Schema: "license": string, "location": string, "requires": string, - "required_by": string + "required_by": string, + "files": [ + string + ] } ] @@ -60,13 +63,13 @@ Examples: } ] """ -from typing import List, Dict, Optional +from typing import List, Dict import jc.utils class info(): """Provides parser metadata (version, author, etc.)""" - version = '1.5' + version = '1.6' description = '`pip show` command parser' author = 'Kelly Brazil' author_email = 'kellyjonbrazil@gmail.com' @@ -120,6 +123,22 @@ def parse( last_key: str = '' last_key_data: List = [] + def flush_last_key_data() -> None: + """Append buffered continuation lines to the previous field.""" + nonlocal last_key_data + + if not last_key_data: + return + + if last_key == 'files': + package[last_key].extend(last_key_data) + else: + if not isinstance(package[last_key], str): + package[last_key] = '' + package[last_key] = package[last_key] + '\n' + '\n'.join(last_key_data) + + last_key_data = [] + # Clear any blank lines cleandata = list(filter(None, data.splitlines())) @@ -127,8 +146,7 @@ def parse( for row in cleandata: if row.startswith('---'): - if last_key_data: - package[last_key] = package[last_key] + '\n' + '\n'.join(last_key_data) + flush_last_key_data() raw_output.append(package) package = {} @@ -137,17 +155,17 @@ def parse( continue if not row.startswith(' '): - item_key = row.split(': ', maxsplit=1)[0].lower().replace('-', '_') - item_value: Optional[str] = row.split(': ', maxsplit=1)[1] + item_key, item_value = row.split(':', maxsplit=1) + item_key = item_key.lower().replace('-', '_') + item_value = item_value.lstrip() - if item_value == '': + if item_key == 'files': + item_value = [] + elif item_value == '': item_value = None if last_key_data and last_key != item_key: - if not isinstance(package[last_key], str): - package[last_key] = '' - package[last_key] = package[last_key] + '\n' + '\n'.join(last_key_data) - last_key_data = [] + flush_last_key_data() package[item_key] = item_value last_key = item_key @@ -158,8 +176,7 @@ def parse( continue if package: - if last_key_data: - package[last_key] = package[last_key] + '\n' + '\n'.join(last_key_data) + flush_last_key_data() raw_output.append(package) diff --git a/tests/test_ifconfig.py b/tests/test_ifconfig.py index 9f9953b4..785a9807 100644 --- a/tests/test_ifconfig.py +++ b/tests/test_ifconfig.py @@ -148,6 +148,21 @@ class MyTests(unittest.TestCase): """ self.assertEqual(jc.parsers.ifconfig.parse(self.osx_freebsd12_ifconfig_extra_fields4, quiet=True), self.freebsd12_ifconfig_extra_fields4_json) + def test_ifconfig_hex_mask_all_zeros(self): + """ + Test 'ifconfig' with 0x00000000 netmask (FreeBSD/macOS hex format). + Regression test: lstrip('0x') incorrectly strips leading '0' chars + from the hex digits, producing wrong mask for all-zero masks. + """ + data = ( + 'lo0: flags=8049 mtu 16384\n' + '\toptions=1203\n' + '\tinet 192.168.1.1 netmask 0x00000000\n' + ) + result = jc.parsers.ifconfig.parse(data, quiet=True) + self.assertEqual(result[0]['ipv4_mask'], '0.0.0.0') + self.assertEqual(result[0]['ipv4'][0]['mask'], '0.0.0.0') + def test_ifconfig_utun_ipv4(self): """ Test 'ifconfig' with ipv4 utun addresses (macOS) diff --git a/tests/test_pip_show.py b/tests/test_pip_show.py index f5607f2b..67c4fd01 100644 --- a/tests/test_pip_show.py +++ b/tests/test_pip_show.py @@ -89,6 +89,58 @@ class MyTests(unittest.TestCase): """ self.assertEqual(jc.parsers.pip_show.parse(self.generic_pip_show_multiline_license_first_blank, quiet=True), self.generic_pip_show_multiline_license_first_blank_json) + def test_pip_show_files_section(self): + """ + Test 'pip show -f' output with a files section + """ + data = """\ +Name: jc +Version: 1.25.4 +Summary: Converts the output of popular command-line tools and file-types to JSON. +Home-page: https://github.com/kellyjonbrazil/jc +Author: Kelly Brazil +Author-email: kelly@gmail.com +License: MIT +Location: /home/pi/.local/lib/python3.11/site-packages +Requires: Pygments, ruamel.yaml, xmltodict +Required-by: pypiwifi +Files: + ../../../bin/jc + jc-1.25.4.dist-info/RECORD +""" + expected = [{ + 'name': 'jc', + 'version': '1.25.4', + 'summary': 'Converts the output of popular command-line tools and file-types to JSON.', + 'home_page': 'https://github.com/kellyjonbrazil/jc', + 'author': 'Kelly Brazil', + 'author_email': 'kelly@gmail.com', + 'license': 'MIT', + 'location': '/home/pi/.local/lib/python3.11/site-packages', + 'requires': 'Pygments, ruamel.yaml, xmltodict', + 'required_by': 'pypiwifi', + 'files': ['../../../bin/jc', 'jc-1.25.4.dist-info/RECORD'] + }] + self.assertEqual(jc.parsers.pip_show.parse(data, quiet=True), expected) + + def test_pip_show_files_section_with_following_field(self): + """ + Test 'pip show -f' output when the files section is followed by a new field + """ + data = """\ +Name: jc +Files: + ../../../bin/jc + jc-1.25.4.dist-info/RECORD +Foo: bar +""" + expected = [{ + 'name': 'jc', + 'files': ['../../../bin/jc', 'jc-1.25.4.dist-info/RECORD'], + 'foo': 'bar' + }] + self.assertEqual(jc.parsers.pip_show.parse(data, quiet=True), expected) + if __name__ == '__main__': unittest.main()