Skip to content

Commit

Permalink
fix: resolve merge conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
joehacksalot committed Oct 21, 2024
1 parent a75e2f4 commit defa407
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 630 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
jc changelog

20240922 v1.25.4
20241018 v1.25.4
- Add `ipconfig` command parser (`ipconfig` for Windows)
- Enhance `ping-s` streaming parser to support error replies
- Enhance `ethtool` parser to support `link_partner_advertised_link_modes`
- Enhance `ifconfig` parser to support `utun` interfaces with assigned IPv4 addresses on macOS
- Fix `bluetoothctl` parser when extra attributes like `manufacturer` and `version` exist
- Fix `df` parser to correctly output binary vs. decimal size outputs
- Fix `mount` parser for cases where there are spaces in the filesystem name
- Fix `ip-address` parser for Python 3.13 changes to IPv4 mapped IPv6 addresses
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ option.
| `--iostat` | `iostat` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iostat) |
| `--iostat-s` | `iostat` command streaming parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iostat_s) |
| `--ip-address` | IPv4 and IPv6 Address string parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/ip_address) |
| `--ipconfig` | `ipconfig` Windows command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/ipconfig) |
| `--iptables` | `iptables` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iptables) |
| `--ip-route` | `ip route` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/ip_route) |
| `--iw-scan` | `iw dev [device] scan` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iw_scan) |
Expand Down
18 changes: 15 additions & 3 deletions jc/parsers/bluetoothctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
Controller:
[
{
"manufacturer": string,
"version": string,
"name": string,
"is_default": boolean,
"is_public": boolean,
Expand Down Expand Up @@ -110,7 +112,7 @@

class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.2'
version = '1.3'
description = '`bluetoothctl` command parser'
author = 'Jake Ob'
author_email = 'iakopap at gmail.com'
Expand All @@ -127,6 +129,8 @@ class info():
Controller = TypedDict(
"Controller",
{
"manufacturer": str,
"version": str,
"name": str,
"is_default": bool,
"is_public": bool,
Expand Down Expand Up @@ -175,7 +179,9 @@ class info():
_controller_head_pattern = r"Controller (?P<address>([0-9A-F]{2}:){5}[0-9A-F]{2}) (?P<name>.+)"

_controller_line_pattern = (
r"(\s*Name:\s*(?P<name>.+)"
r"(\s*Manufacturer:\s*(?P<manufacturer>.+)"
+ r"|\s*Version:\s*(?P<version>.+)"
+ r"|\s*Name:\s*(?P<name>.+)"
+ r"|\s*Alias:\s*(?P<alias>.+)"
+ r"|\s*Class:\s*(?P<class>.+)"
+ r"|\s*Powered:\s*(?P<powered>.+)"
Expand Down Expand Up @@ -203,6 +209,8 @@ def _parse_controller(next_lines: List[str]) -> Optional[Controller]:
return None

controller: Controller = {
"manufacturer": '',
"version": '',
"name": '',
"is_default": False,
"is_public": False,
Expand Down Expand Up @@ -241,7 +249,11 @@ def _parse_controller(next_lines: List[str]) -> Optional[Controller]:

matches = result.groupdict()

if matches["name"]:
if matches["manufacturer"]:
controller["manufacturer"] = matches["manufacturer"]
elif matches["version"]:
controller["version"] = matches["version"]
elif matches["name"]:
controller["name"] = matches["name"]
elif matches["alias"]:
controller["alias"] = matches["alias"]
Expand Down
87 changes: 59 additions & 28 deletions jc/parsers/ipconfig.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
r"""jc - JSON Convert `ipconfig` command output parser
r"""jc - JSON Convert `ipconfig` Windows command output parser
Usage (cli):
$ ipconfig /all | jc --ipconfig
$ ipconfig | jc --ipconfig
$ jc ipconfig /all
Usage (module):
Expand All @@ -20,7 +20,7 @@
"ip_routing_enabled": boolean,
"wins_proxy_enabled": boolean,
"dns_suffix_search_list": [
string
string
],
"adapters": [
{
Expand Down Expand Up @@ -51,7 +51,7 @@
{
"address": string,
"status": string,
"prefix_length": int,
"prefix_length": integer,
}
],
"ipv4_addresses": [
Expand All @@ -72,25 +72,31 @@
string
],
"primary_wins_server": string,
"lease_expires": string, # [0]
"lease_obtained": string, # [0]
"lease_expires": string,
"lease_expires_epoch": integer, # [0]
"lease_expires_iso": string,
"lease_obtained": string,
"lease_obtained_epoch": integer, # [0]
"lease_obtained_iso": string,
"netbios_over_tcpip": boolean,
"media_state": string,
"extras": [
string: string
<string>: string
]
}
],
"extras": []
}
Notes:
[0] - 'lease_expires' and 'lease_obtained' are parsed to ISO8601 format date strings. if the value was unable
to be parsed by datetime, the fields will be in their raw form
[1] - 'autoconfigured' under 'ipv4_address' is only providing indication if the ipv4 address was labeled as
"Autoconfiguration IPv4 Address" vs "IPv4 Address". It does not infer any information from other fields
[2] - Windows XP uses 'IP Address' instead of 'IPv4 Address'. Both values are parsed to the 'ipv4_address'
object for consistency
[0] - The epoch calculated timestamp field is naive. (i.e. based on
the local time of the system the parser is run on)
[1] - 'autoconfigured' under 'ipv4_address' is only providing
indication if the ipv4 address was labeled as "Autoconfiguration
IPv4 Address" vs "IPv4 Address". It does not infer any
information from other fields
[2] - Windows XP uses 'IP Address' instead of 'IPv4 Address'. Both
values are parsed to the 'ipv4_address' object for consistency
Examples:
Expand Down Expand Up @@ -421,7 +427,6 @@
],
"extras": []
}
"""
from datetime import datetime
import re
Expand All @@ -431,7 +436,7 @@
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.0'
description = '`ipconfig` command parser'
description = '`ipconfig` Windows command parser'
author = 'joehacksalot'
author_email = '[email protected]'
compatible = ['windows']
Expand Down Expand Up @@ -466,6 +471,7 @@ def parse(data, raw=False, quiet=False):

return raw_output if raw else _process(raw_output)


def _process_ipv6_address(ip_address):
address_split = ip_address["address"].split('%')
try:
Expand All @@ -484,6 +490,7 @@ def _process_ipv6_address(ip_address):
"status": ip_address["status"]
}


def _process_ipv4_address(ip_address):
autoconfigured = True if ip_address.get("autoconfigured","") is not None and 'autoconfigured' in ip_address.get("autoconfigured","") else False
subnet_mask = ip_address["subnet_mask"]
Expand All @@ -494,6 +501,7 @@ def _process_ipv4_address(ip_address):
"autoconfigured": autoconfigured
}


def _process(proc_data):
"""
Final processing to conform to the schema.
Expand All @@ -507,8 +515,7 @@ def _process(proc_data):
Processed Dictionary. Structured data to conform to the schema.
"""
processed = proc_data



if "ip_routing_enabled" in processed and processed["ip_routing_enabled"] is not None:
processed["ip_routing_enabled"] = (processed["ip_routing_enabled"].lower() == "yes")

Expand All @@ -518,38 +525,47 @@ def _process(proc_data):
for adapter in processed["adapters"]:
if "dhcp_enabled" in adapter and adapter["dhcp_enabled"] is not None:
adapter["dhcp_enabled"] = (adapter["dhcp_enabled"].lower() == "yes")

if "autoconfiguration_enabled" in adapter and adapter["autoconfiguration_enabled"] is not None:
adapter["autoconfiguration_enabled"] = (adapter["autoconfiguration_enabled"].lower() == "yes")

if "netbios_over_tcpip" in adapter and adapter["netbios_over_tcpip"] is not None:
adapter["netbios_over_tcpip"] = (adapter["netbios_over_tcpip"].lower() == "enabled")
if "lease_expires" in adapter and adapter["lease_expires"] is not None and adapter["lease_expires"] != "":
try:
adapter["lease_expires"] = datetime.strptime(adapter["lease_expires"], "%A, %B %d, %Y %I:%M:%S %p").isoformat()
except:
pass # Leave date in raw format if not parseable
if "lease_obtained" in adapter and adapter["lease_obtained"] is not None and adapter["lease_obtained"] != "":
try:
adapter["lease_obtained"] = datetime.strptime(adapter["lease_obtained"], "%A, %B %d, %Y %I:%M:%S %p").isoformat()
except:
pass # Leave date in raw format if not parseable

if "lease_expires" in adapter and adapter["lease_expires"]:
ts = jc.utils.timestamp(adapter['lease_expires'], format_hint=(1720,))
adapter["lease_expires_epoch"] = ts.naive
adapter["lease_expires_iso"] = ts.iso

if "lease_obtained" in adapter and adapter["lease_obtained"]:
ts = jc.utils.timestamp(adapter['lease_obtained'], format_hint=(1720,))
adapter["lease_obtained_epoch"] = ts.naive
adapter["lease_obtained_iso"] = ts.iso

adapter["link_local_ipv6_addresses"] = [_process_ipv6_address(address) for address in adapter.get("link_local_ipv6_addresses", [])]
adapter["ipv4_addresses"] = [_process_ipv4_address(address) for address in adapter.get("ipv4_addresses", [])]

return processed


class _PushbackIterator:
def __init__(self, iterator):
self.iterator = iterator
self.pushback_stack = []

def __iter__(self):
return self

def __next__(self):
if self.pushback_stack:
return self.pushback_stack.pop()
else:
return next(self.iterator)

def pushback(self, value):
self.pushback_stack.append(value)


def _parse(data):
# Initialize the parsed output dictionary with all fields set to None or empty lists
parse_output = {
Expand Down Expand Up @@ -609,10 +625,12 @@ def _parse(data):

return parse_output


def _is_adapter_start_line(line):
# Detect adapter start lines, e.g., "Ethernet adapter Ethernet:"
return re.match(r"^[^\s].*adapter.*:", line, re.IGNORECASE)


def _initialize_adapter(adapter_name):
adapter_name_split = adapter_name.split(" adapter ", 1)
if len(adapter_name_split) > 1:
Expand Down Expand Up @@ -650,6 +668,7 @@ def _initialize_adapter(adapter_name):
"extras": [] # To store unrecognized fields
}


def _parse_line(line):
# Split the line into key and value using ':' or multiple spaces
key_value = re.split(r":", line.strip(), 1)
Expand All @@ -662,6 +681,7 @@ def _parse_line(line):
else:
return None, None


def _parse_header_line(result, key, value, line_iter):
if key in ["host_name", "primary_dns_suffix", "node_type", "ip_routing_enabled", "wins_proxy_enabled"]:
result[key] = value
Expand All @@ -674,11 +694,13 @@ def _parse_header_line(result, key, value, line_iter):
# Store unrecognized fields in extras
result["extras"].append({key: value})


def _parse_adapter_line(adapter, key, value, line_iter):
if key in ["connection_specific_dns_suffix","media_state", "description", "physical_address", "dhcp_enabled",
"autoconfiguration_enabled", "dhcpv6_iaid", "dhcpv6_client_duid", "netbios_over_tcpip", "dhcp_server",
"lease_obtained", "lease_expires", "primary_wins_server"]:
adapter[key] = value

elif key in ["ipv6_address", "temporary_ipv6_address", "link_local_ipv6_address"]:
address_dict = _parse_ipv6_address(value)
if key == "ipv6_address":
Expand All @@ -687,32 +709,39 @@ def _parse_adapter_line(adapter, key, value, line_iter):
adapter["temporary_ipv6_addresses"].append(address_dict)
elif key == "link_local_ipv6_address":
adapter["link_local_ipv6_addresses"].append(address_dict)

elif key in ["ipv4_address", "autoconfiguration_ipv4_address", "ip_address", "autoconfiguration_ip_address"]:
ipv4_address_dict = _parse_ipv4_address(value, key, line_iter)
adapter["ipv4_addresses"].append(ipv4_address_dict)

elif key == "connection_specific_dns_suffix_search_list":
if value:
adapter["connection_specific_dns_suffix_search_list"].append(value)
# Process additional connection specific dns suffix search list entries
_parse_additional_entries(adapter["connection_specific_dns_suffix_search_list"], line_iter)

elif key == "default_gateway":
if value:
adapter["default_gateways"].append(value)
# Process additional gateways
_parse_additional_entries(adapter["default_gateways"], line_iter)

elif key == "dns_servers":
if value:
adapter["dns_servers"].append(value)
# Process additional DNS servers
_parse_additional_entries(adapter["dns_servers"], line_iter)

elif key == "subnet_mask":
# Subnet Mask should be associated with the last IPv4 address
if adapter["ipv4_addresses"]:
adapter["ipv4_addresses"][-1]["subnet_mask"] = value

else:
# Store unrecognized fields in extras
adapter["extras"].append({key: value})


def _parse_ipv6_address(value):
# Handle multiple status indicators
match = re.match(r"([^\(]+)\((.*)\)", value) if value else None
Expand All @@ -727,6 +756,7 @@ def _parse_ipv6_address(value):
"status": status
}


def _parse_ipv4_address(value, key, line_iter):
# Handle autoconfigured status
match = re.match(r"([^\(]+)\((.*)\)", value) if value else None
Expand Down Expand Up @@ -758,6 +788,7 @@ def _parse_ipv4_address(value, key, line_iter):
"status": status
}


def _parse_additional_entries(entry_list, line_iter):
# Process additional lines that belong to the current entry (e.g., additional DNS servers, DNS Suffix Search List)
while True:
Expand All @@ -775,4 +806,4 @@ def _parse_additional_entries(entry_list, line_iter):
line_iter.pushback(next_line)
break
except StopIteration:
break
break
1 change: 1 addition & 0 deletions jc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ def _parse_dt(
{'id': 1700, 'format': '%m/%d/%Y, %I:%M:%S %p', 'locale': None}, # Windows english format wint non-UTC tz (found in systeminfo cli output): 3/22/2021, 1:15:51 PM (UTC-0600)
{'id': 1705, 'format': '%m/%d/%Y, %I:%M:%S %p %Z', 'locale': None}, # Windows english format with UTC tz (found in systeminfo cli output): 3/22/2021, 1:15:51 PM (UTC)
{'id': 1710, 'format': '%m/%d/%Y, %I:%M:%S %p UTC%z', 'locale': None}, # Windows english format with UTC tz (found in systeminfo cli output): 3/22/2021, 1:15:51 PM (UTC+0000)
{'id': 1720, 'format': '%A, %B %d, %Y %I:%M:%S %p', 'locale': None}, # ipconfig cli output format: Thursday, June 22, 2023 10:39:04 AM
{'id': 1750, 'format': '%Y/%m/%d-%H:%M:%S.%f', 'locale': None}, # Google Big Table format with no timezone: 1970/01/01-01:00:00.000000
{'id': 1755, 'format': '%Y/%m/%d-%H:%M:%S.%f%z', 'locale': None}, # Google Big Table format with timezone: 1970/01/01-01:00:00.000000+00:00
{'id': 1760, 'format': '%Y-%m-%d %H:%M:%S%z', 'locale': None}, # certbot format with timezone: 2023-06-12 01:35:30+00:00
Expand Down
Loading

0 comments on commit defa407

Please sign in to comment.