1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00
RIOT/dist/tools/usb-serial/ttys.py
Marian Buschsieweke 6192c620c8
dist/tools/usb-serial: Fix handling of None while quoting
This fixes:

    Traceback (most recent call last):
      File "/home/maribu/Repos/software/RIOT/master/dist/tools/usb-serial/ttys.py", line 259, in <module>
        print_ttys(sys.argv)
      File "/home/maribu/Repos/software/RIOT/master/dist/tools/usb-serial/ttys.py", line 255, in print_ttys
        print_results(args, ttys)
      File "/home/maribu/Repos/software/RIOT/master/dist/tools/usb-serial/ttys.py", line 189, in print_results
        if item.rfind(args.format_sep) >= 0:
           ^^^^^^^^^^
    AttributeError: 'NoneType' object has no attribute 'rfind'

Which occurs while testing whether a string requires special quoting
if an attribute is None.
2023-06-21 22:13:21 +02:00

260 lines
8.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Command line utility to list and filter TTYs
"""
import argparse
import json
import os
import re
import sys
import time
import pyudev
def unescape(string):
"""
Decodes unicode escaping in a string, e.g. "Hallo\\x20World" is decoded as
"Hallo World"
"""
res = bytes(string, "utf8").decode("unicode_escape")
res = res.encode("latin1").decode("utf8", errors="replace")
return res
def tty2dict(dev):
"""
Parse the given TTY udev interface into a dict() containing the most
relevant attributes
"""
result = {}
result["path"] = dev.get("DEVNAME")
result["ctime"] = os.stat(result["path"]).st_ctime
result["serial"] = dev.get("ID_SERIAL_SHORT")
result["driver"] = dev.get("ID_USB_DRIVER")
result["model"] = unescape(dev.get("ID_MODEL_ENC"))
result["model_db"] = dev.get("ID_MODEL_FROM_DATABASE")
result["vendor"] = unescape(dev.get("ID_VENDOR_ENC"))
result["vendor_db"] = dev.get("ID_VENDOR_FROM_DATABASE")
result["iface_num"] = str(int(dev.get("ID_USB_INTERFACE_NUM")))
return result
def filters_match(filters, tty):
"""
Check if the given TTY interface matches all given filters
"""
for key, regex in filters:
if tty[key] is None:
return False
if not regex.match(tty[key]):
return False
return True
def parse_args(args):
"""
Parse the given command line style arguments with argparse
"""
desc = "List and filter TTY interfaces that might belong to boards"
formats_combinable = {
"path",
"serial",
"vendor",
"vendor_db",
"model",
"model_db",
"driver",
"ctime",
"iface_num",
}
formats_uncombinable = {
"table",
"json",
}
supported_formats = formats_combinable.union(formats_uncombinable)
parser = argparse.ArgumentParser(description=desc)
parser.add_argument("--most-recent", action="store_true",
help="Print only the most recently connected matching "
+ "TTY")
parser.add_argument("--format", default=["table"], type=str, nargs='+',
help=f"How to format the TTYs. Supported formats: "
f"{sorted(supported_formats)}")
parser.add_argument("--format-sep", default=" ", type=str,
help="Separator between formats (default: space)")
parser.add_argument("--serial", default=None, type=str,
help="Print only devices matching this serial")
parser.add_argument("--driver", default=None, type=str,
help="Print only devices using a driver matching this "
"regex")
parser.add_argument("--model", default=None, type=str,
help="Print only devices with a model matching this "
"regex (as reported from device)")
parser.add_argument("--model-db", default=None, type=str,
help="Print only devices with a model matching this "
"regex (DB entry)")
parser.add_argument("--vendor", default=None, type=str,
help="Print only devices with a vendor matching this "
"regex (as reported from device)")
parser.add_argument("--vendor-db", default=None, type=str,
help="Print only devices with a vendor matching this "
"regex (DB entry)")
parser.add_argument("--iface-num", default=None, type=str,
help="Print only devices with a USB interface number "
"matching this regex (DB entry)")
parser.add_argument("--exclude-serial", type=str, nargs='*', default=None,
help="Ignore devices with these serial numbers. "
+ "Environment variable EXCLUDE_TTY_SERIAL can "
+ "be used alternatively.")
args = parser.parse_args()
if len(args.format) == 1:
if args.format[0] not in supported_formats:
sys.exit(f"Format \"{args.format[0]}\" not supported")
else:
for fmt in args.format:
if fmt not in formats_combinable:
if fmt in formats_uncombinable:
sys.exit(f"Format \"{fmt}\" cannot be combined with " +
"other formats")
else:
sys.exit(f"Format \"{fmt}\" not supported")
if args.exclude_serial is None:
if "EXCLUDE_TTY_SERIAL" in os.environ:
args.exclude_serial = os.environ["EXCLUDE_TTY_SERIAL"].split()
else:
args.exclude_serial = []
return args
def print_table(data, headers):
"""
Print the list of dictionaries given in data as table, where headers is
a list of keys to that dict and also servers as table headers.
"""
lengths = []
for header in headers:
lengths.append(len(header))
for item in data:
for i, header in enumerate(headers):
if len(str(item[header])) > lengths[i]:
lengths[i] = len(item[header])
sys.stdout.write(f"{headers[0]:{lengths[0]}}")
for i in range(1, len(headers)):
sys.stdout.write(f" | {headers[i]:{lengths[i]}}")
sys.stdout.write("\n" + lengths[0] * "-")
for i in range(1, len(headers)):
sys.stdout.write("-|-" + lengths[i] * "-")
for item in data:
sys.stdout.write(f"\n{str(item[headers[0]]):{lengths[0]}}")
for header, length in zip(headers[1:], lengths[1:]):
sys.stdout.write(f" | {str(item[header]):{length}}")
sys.stdout.write("\n")
sys.stdout.flush()
def print_results(args, ttys):
"""
Print the given TTY devices according to the given args
"""
if len(args.format) == 1:
if args.format[0] == "json":
print(json.dumps(ttys, indent=2))
return
if args.format[0] == "table":
for tty in ttys:
tty["ctime"] = time.strftime("%H:%M:%S",
time.localtime(tty["ctime"]))
headers = ["path", "driver", "vendor", "model", "model_db",
"serial", "ctime", "iface_num"]
print_table(ttys, headers)
return
for tty in ttys:
line = ""
for fmt in args.format:
item = tty[fmt]
if item is not None and item.rfind(args.format_sep) >= 0:
# item contains separator --> quote it
# using json.dumps to also escape quotation chars and other
# unsafe stuff
item = json.dumps(item)
line += f"{args.format_sep}{item}"
print(line[len(args.format_sep):])
def generate_filters(args):
"""
Generate filters for use in the filters_match function from the command
line arguments
"""
result = []
if args.serial is not None:
result.append(("serial", re.compile(r"^" + re.escape(args.serial)
+ r"$")))
if args.driver is not None:
result.append(("driver", re.compile(args.driver)))
if args.model is not None:
result.append(("model", re.compile(args.model)))
if args.model_db is not None:
result.append(("model_db", re.compile(args.model_db)))
if args.vendor is not None:
result.append(("vendor", re.compile(args.vendor)))
if args.vendor_db is not None:
result.append(("vendor_db", re.compile(args.vendor_db)))
if args.iface_num is not None:
result.append(("iface_num", re.compile(args.iface_num)))
return result
def print_ttys(args):
"""
Print ttys as specified by the given command line arguments
"""
args = parse_args(args)
filters = generate_filters(args)
ttys = []
for dev in pyudev.Context().list_devices(subsystem='tty', ID_BUS='usb'):
tty = tty2dict(dev)
if tty["serial"] not in args.exclude_serial and filters_match(filters, tty):
ttys.append(tty)
if args.most_recent:
if len(ttys) > 0:
most_recent = ttys[0]
for tty in ttys:
if tty["ctime"] > most_recent["ctime"]:
most_recent = tty
ttys = [most_recent]
else:
ttys = []
if len(ttys) == 0:
sys.exit(1)
print_results(args, ttys)
if __name__ == "__main__":
print_ttys(sys.argv)