Skip to content

Commit defa407

Browse files
committed
fix: resolve merge conflicts
1 parent a75e2f4 commit defa407

File tree

10 files changed

+89
-630
lines changed

10 files changed

+89
-630
lines changed

CHANGELOG

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
jc changelog
22

3-
20240922 v1.25.4
3+
20241018 v1.25.4
4+
- Add `ipconfig` command parser (`ipconfig` for Windows)
45
- Enhance `ping-s` streaming parser to support error replies
56
- Enhance `ethtool` parser to support `link_partner_advertised_link_modes`
67
- Enhance `ifconfig` parser to support `utun` interfaces with assigned IPv4 addresses on macOS
8+
- Fix `bluetoothctl` parser when extra attributes like `manufacturer` and `version` exist
79
- Fix `df` parser to correctly output binary vs. decimal size outputs
810
- Fix `mount` parser for cases where there are spaces in the filesystem name
911
- Fix `ip-address` parser for Python 3.13 changes to IPv4 mapped IPv6 addresses

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ option.
218218
| `--iostat` | `iostat` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iostat) |
219219
| `--iostat-s` | `iostat` command streaming parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iostat_s) |
220220
| `--ip-address` | IPv4 and IPv6 Address string parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/ip_address) |
221+
| `--ipconfig` | `ipconfig` Windows command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/ipconfig) |
221222
| `--iptables` | `iptables` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iptables) |
222223
| `--ip-route` | `ip route` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/ip_route) |
223224
| `--iw-scan` | `iw dev [device] scan` command parser | [details](https://kellyjonbrazil.github.io/jc/docs/parsers/iw_scan) |

jc/parsers/bluetoothctl.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
Controller:
2929
[
3030
{
31+
"manufacturer": string,
32+
"version": string,
3133
"name": string,
3234
"is_default": boolean,
3335
"is_public": boolean,
@@ -110,7 +112,7 @@
110112

111113
class info():
112114
"""Provides parser metadata (version, author, etc.)"""
113-
version = '1.2'
115+
version = '1.3'
114116
description = '`bluetoothctl` command parser'
115117
author = 'Jake Ob'
116118
author_email = 'iakopap at gmail.com'
@@ -127,6 +129,8 @@ class info():
127129
Controller = TypedDict(
128130
"Controller",
129131
{
132+
"manufacturer": str,
133+
"version": str,
130134
"name": str,
131135
"is_default": bool,
132136
"is_public": bool,
@@ -175,7 +179,9 @@ class info():
175179
_controller_head_pattern = r"Controller (?P<address>([0-9A-F]{2}:){5}[0-9A-F]{2}) (?P<name>.+)"
176180

177181
_controller_line_pattern = (
178-
r"(\s*Name:\s*(?P<name>.+)"
182+
r"(\s*Manufacturer:\s*(?P<manufacturer>.+)"
183+
+ r"|\s*Version:\s*(?P<version>.+)"
184+
+ r"|\s*Name:\s*(?P<name>.+)"
179185
+ r"|\s*Alias:\s*(?P<alias>.+)"
180186
+ r"|\s*Class:\s*(?P<class>.+)"
181187
+ r"|\s*Powered:\s*(?P<powered>.+)"
@@ -203,6 +209,8 @@ def _parse_controller(next_lines: List[str]) -> Optional[Controller]:
203209
return None
204210

205211
controller: Controller = {
212+
"manufacturer": '',
213+
"version": '',
206214
"name": '',
207215
"is_default": False,
208216
"is_public": False,
@@ -241,7 +249,11 @@ def _parse_controller(next_lines: List[str]) -> Optional[Controller]:
241249

242250
matches = result.groupdict()
243251

244-
if matches["name"]:
252+
if matches["manufacturer"]:
253+
controller["manufacturer"] = matches["manufacturer"]
254+
elif matches["version"]:
255+
controller["version"] = matches["version"]
256+
elif matches["name"]:
245257
controller["name"] = matches["name"]
246258
elif matches["alias"]:
247259
controller["alias"] = matches["alias"]

jc/parsers/ipconfig.py

+59-28
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
r"""jc - JSON Convert `ipconfig` command output parser
2-
1+
r"""jc - JSON Convert `ipconfig` Windows command output parser
32
43
Usage (cli):
54
65
$ ipconfig /all | jc --ipconfig
76
$ ipconfig | jc --ipconfig
7+
$ jc ipconfig /all
88
99
Usage (module):
1010
@@ -20,7 +20,7 @@
2020
"ip_routing_enabled": boolean,
2121
"wins_proxy_enabled": boolean,
2222
"dns_suffix_search_list": [
23-
string
23+
string
2424
],
2525
"adapters": [
2626
{
@@ -51,7 +51,7 @@
5151
{
5252
"address": string,
5353
"status": string,
54-
"prefix_length": int,
54+
"prefix_length": integer,
5555
}
5656
],
5757
"ipv4_addresses": [
@@ -72,25 +72,31 @@
7272
string
7373
],
7474
"primary_wins_server": string,
75-
"lease_expires": string, # [0]
76-
"lease_obtained": string, # [0]
75+
"lease_expires": string,
76+
"lease_expires_epoch": integer, # [0]
77+
"lease_expires_iso": string,
78+
"lease_obtained": string,
79+
"lease_obtained_epoch": integer, # [0]
80+
"lease_obtained_iso": string,
7781
"netbios_over_tcpip": boolean,
7882
"media_state": string,
7983
"extras": [
80-
string: string
84+
<string>: string
8185
]
8286
}
8387
],
8488
"extras": []
8589
}
8690
8791
Notes:
88-
[0] - 'lease_expires' and 'lease_obtained' are parsed to ISO8601 format date strings. if the value was unable
89-
to be parsed by datetime, the fields will be in their raw form
90-
[1] - 'autoconfigured' under 'ipv4_address' is only providing indication if the ipv4 address was labeled as
91-
"Autoconfiguration IPv4 Address" vs "IPv4 Address". It does not infer any information from other fields
92-
[2] - Windows XP uses 'IP Address' instead of 'IPv4 Address'. Both values are parsed to the 'ipv4_address'
93-
object for consistency
92+
[0] - The epoch calculated timestamp field is naive. (i.e. based on
93+
the local time of the system the parser is run on)
94+
[1] - 'autoconfigured' under 'ipv4_address' is only providing
95+
indication if the ipv4 address was labeled as "Autoconfiguration
96+
IPv4 Address" vs "IPv4 Address". It does not infer any
97+
information from other fields
98+
[2] - Windows XP uses 'IP Address' instead of 'IPv4 Address'. Both
99+
values are parsed to the 'ipv4_address' object for consistency
94100
95101
Examples:
96102
@@ -421,7 +427,6 @@
421427
],
422428
"extras": []
423429
}
424-
425430
"""
426431
from datetime import datetime
427432
import re
@@ -431,7 +436,7 @@
431436
class info():
432437
"""Provides parser metadata (version, author, etc.)"""
433438
version = '1.0'
434-
description = '`ipconfig` command parser'
439+
description = '`ipconfig` Windows command parser'
435440
author = 'joehacksalot'
436441
author_email = '[email protected]'
437442
compatible = ['windows']
@@ -466,6 +471,7 @@ def parse(data, raw=False, quiet=False):
466471

467472
return raw_output if raw else _process(raw_output)
468473

474+
469475
def _process_ipv6_address(ip_address):
470476
address_split = ip_address["address"].split('%')
471477
try:
@@ -484,6 +490,7 @@ def _process_ipv6_address(ip_address):
484490
"status": ip_address["status"]
485491
}
486492

493+
487494
def _process_ipv4_address(ip_address):
488495
autoconfigured = True if ip_address.get("autoconfigured","") is not None and 'autoconfigured' in ip_address.get("autoconfigured","") else False
489496
subnet_mask = ip_address["subnet_mask"]
@@ -494,6 +501,7 @@ def _process_ipv4_address(ip_address):
494501
"autoconfigured": autoconfigured
495502
}
496503

504+
497505
def _process(proc_data):
498506
"""
499507
Final processing to conform to the schema.
@@ -507,8 +515,7 @@ def _process(proc_data):
507515
Processed Dictionary. Structured data to conform to the schema.
508516
"""
509517
processed = proc_data
510-
511-
518+
512519
if "ip_routing_enabled" in processed and processed["ip_routing_enabled"] is not None:
513520
processed["ip_routing_enabled"] = (processed["ip_routing_enabled"].lower() == "yes")
514521

@@ -518,38 +525,47 @@ def _process(proc_data):
518525
for adapter in processed["adapters"]:
519526
if "dhcp_enabled" in adapter and adapter["dhcp_enabled"] is not None:
520527
adapter["dhcp_enabled"] = (adapter["dhcp_enabled"].lower() == "yes")
528+
521529
if "autoconfiguration_enabled" in adapter and adapter["autoconfiguration_enabled"] is not None:
522530
adapter["autoconfiguration_enabled"] = (adapter["autoconfiguration_enabled"].lower() == "yes")
531+
523532
if "netbios_over_tcpip" in adapter and adapter["netbios_over_tcpip"] is not None:
524533
adapter["netbios_over_tcpip"] = (adapter["netbios_over_tcpip"].lower() == "enabled")
525-
if "lease_expires" in adapter and adapter["lease_expires"] is not None and adapter["lease_expires"] != "":
526-
try:
527-
adapter["lease_expires"] = datetime.strptime(adapter["lease_expires"], "%A, %B %d, %Y %I:%M:%S %p").isoformat()
528-
except:
529-
pass # Leave date in raw format if not parseable
530-
if "lease_obtained" in adapter and adapter["lease_obtained"] is not None and adapter["lease_obtained"] != "":
531-
try:
532-
adapter["lease_obtained"] = datetime.strptime(adapter["lease_obtained"], "%A, %B %d, %Y %I:%M:%S %p").isoformat()
533-
except:
534-
pass # Leave date in raw format if not parseable
534+
535+
if "lease_expires" in adapter and adapter["lease_expires"]:
536+
ts = jc.utils.timestamp(adapter['lease_expires'], format_hint=(1720,))
537+
adapter["lease_expires_epoch"] = ts.naive
538+
adapter["lease_expires_iso"] = ts.iso
539+
540+
if "lease_obtained" in adapter and adapter["lease_obtained"]:
541+
ts = jc.utils.timestamp(adapter['lease_obtained'], format_hint=(1720,))
542+
adapter["lease_obtained_epoch"] = ts.naive
543+
adapter["lease_obtained_iso"] = ts.iso
544+
535545
adapter["link_local_ipv6_addresses"] = [_process_ipv6_address(address) for address in adapter.get("link_local_ipv6_addresses", [])]
536546
adapter["ipv4_addresses"] = [_process_ipv4_address(address) for address in adapter.get("ipv4_addresses", [])]
547+
537548
return processed
538549

550+
539551
class _PushbackIterator:
540552
def __init__(self, iterator):
541553
self.iterator = iterator
542554
self.pushback_stack = []
555+
543556
def __iter__(self):
544557
return self
558+
545559
def __next__(self):
546560
if self.pushback_stack:
547561
return self.pushback_stack.pop()
548562
else:
549563
return next(self.iterator)
564+
550565
def pushback(self, value):
551566
self.pushback_stack.append(value)
552567

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

610626
return parse_output
611627

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

633+
616634
def _initialize_adapter(adapter_name):
617635
adapter_name_split = adapter_name.split(" adapter ", 1)
618636
if len(adapter_name_split) > 1:
@@ -650,6 +668,7 @@ def _initialize_adapter(adapter_name):
650668
"extras": [] # To store unrecognized fields
651669
}
652670

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

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

697+
677698
def _parse_adapter_line(adapter, key, value, line_iter):
678699
if key in ["connection_specific_dns_suffix","media_state", "description", "physical_address", "dhcp_enabled",
679700
"autoconfiguration_enabled", "dhcpv6_iaid", "dhcpv6_client_duid", "netbios_over_tcpip", "dhcp_server",
680701
"lease_obtained", "lease_expires", "primary_wins_server"]:
681702
adapter[key] = value
703+
682704
elif key in ["ipv6_address", "temporary_ipv6_address", "link_local_ipv6_address"]:
683705
address_dict = _parse_ipv6_address(value)
684706
if key == "ipv6_address":
@@ -687,32 +709,39 @@ def _parse_adapter_line(adapter, key, value, line_iter):
687709
adapter["temporary_ipv6_addresses"].append(address_dict)
688710
elif key == "link_local_ipv6_address":
689711
adapter["link_local_ipv6_addresses"].append(address_dict)
712+
690713
elif key in ["ipv4_address", "autoconfiguration_ipv4_address", "ip_address", "autoconfiguration_ip_address"]:
691714
ipv4_address_dict = _parse_ipv4_address(value, key, line_iter)
692715
adapter["ipv4_addresses"].append(ipv4_address_dict)
716+
693717
elif key == "connection_specific_dns_suffix_search_list":
694718
if value:
695719
adapter["connection_specific_dns_suffix_search_list"].append(value)
696720
# Process additional connection specific dns suffix search list entries
697721
_parse_additional_entries(adapter["connection_specific_dns_suffix_search_list"], line_iter)
722+
698723
elif key == "default_gateway":
699724
if value:
700725
adapter["default_gateways"].append(value)
701726
# Process additional gateways
702727
_parse_additional_entries(adapter["default_gateways"], line_iter)
728+
703729
elif key == "dns_servers":
704730
if value:
705731
adapter["dns_servers"].append(value)
706732
# Process additional DNS servers
707733
_parse_additional_entries(adapter["dns_servers"], line_iter)
734+
708735
elif key == "subnet_mask":
709736
# Subnet Mask should be associated with the last IPv4 address
710737
if adapter["ipv4_addresses"]:
711738
adapter["ipv4_addresses"][-1]["subnet_mask"] = value
739+
712740
else:
713741
# Store unrecognized fields in extras
714742
adapter["extras"].append({key: value})
715743

744+
716745
def _parse_ipv6_address(value):
717746
# Handle multiple status indicators
718747
match = re.match(r"([^\(]+)\((.*)\)", value) if value else None
@@ -727,6 +756,7 @@ def _parse_ipv6_address(value):
727756
"status": status
728757
}
729758

759+
730760
def _parse_ipv4_address(value, key, line_iter):
731761
# Handle autoconfigured status
732762
match = re.match(r"([^\(]+)\((.*)\)", value) if value else None
@@ -758,6 +788,7 @@ def _parse_ipv4_address(value, key, line_iter):
758788
"status": status
759789
}
760790

791+
761792
def _parse_additional_entries(entry_list, line_iter):
762793
# Process additional lines that belong to the current entry (e.g., additional DNS servers, DNS Suffix Search List)
763794
while True:
@@ -775,4 +806,4 @@ def _parse_additional_entries(entry_list, line_iter):
775806
line_iter.pushback(next_line)
776807
break
777808
except StopIteration:
778-
break
809+
break

jc/utils.py

+1
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ def _parse_dt(
696696
{'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)
697697
{'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)
698698
{'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)
699+
{'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
699700
{'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
700701
{'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
701702
{'id': 1760, 'format': '%Y-%m-%d %H:%M:%S%z', 'locale': None}, # certbot format with timezone: 2023-06-12 01:35:30+00:00

0 commit comments

Comments
 (0)