Skip to content

Instantly share code, notes, and snippets.

@tai
Last active September 21, 2025 00:35
Show Gist options
  • Select an option

  • Save tai/31b3d5d7046e05045ec78dd3555db298 to your computer and use it in GitHub Desktop.

Select an option

Save tai/31b3d5d7046e05045ec78dd3555db298 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
import sys
import os
import glob
import subprocess as sp
import re
import time
import logging
from datetime import datetime
from argparse import ArgumentParser
def usage():
p = os.path.basename(sys.argv[0])
help = f"""
{p} - Find USB device by string match
Usage: {p} [options] <regex> [<regex> ...]
Options:
-b, --bus-device: Show only USB bus/device path
-d, --device : Show only device path
-f, --fulldate : Show time in full format
-r, --recent <n>: Show recent devices within <n> seconds
Example:
// show USB serial device (with tty* name)
// OUTPUT: time bus:dev vid:pid hub-port [dev] name
# {p} tty
16:30:00 003:004 0403:6001 3-2.3 [ttyUSB0] Future Technology Devices International, Ltd FT232 Serial (UART) IC
// show USB storage under specific hub
# usb 1-8.1 sd
15:18:06 001:126 1908:0226 1-8.1.3 [sdi] GEMBIRD MicroSD Card Reader/Writer
15:18:06 001:125 1908:0226 1-8.1.2 [sdh] GEMBIRD MicroSD Card Reader/Writer
...
Note:
- Output is sorted and latter device is more recently attached.
""".strip()
print(help, file=sys.stderr)
sys.exit(0)
def usb2blk():
found = {}
for i in glob.glob("/dev/sd*"):
devname = os.path.basename(i)
syspath = os.path.realpath(f"/sys/class/block/{devname}")
cur = syspath
while cur != "/sys":
# skip partitions
if os.path.exists(f"{cur}/partition"):
break
# USB device
if os.path.exists(f"{cur}/busnum"):
busnum = int(open(f"{cur}/busnum").readline().strip())
devnum = int(open(f"{cur}/devnum").readline().strip())
busdev = f"{busnum:03d}:{devnum:03d}"
found[busdev] = devname
break
cur = os.path.dirname(cur)
return found
def usb2tty():
found = {}
for i in glob.glob("/dev/ttyUSB*") + glob.glob("/dev/ttyACM*"):
devname = os.path.basename(i)
syspath = os.path.realpath(f"/sys/class/tty/{devname}")
cur = syspath
while cur != "/sys":
# USB device
if os.path.exists(f"{cur}/busnum"):
busnum = int(open(f"{cur}/busnum").readline().strip())
devnum = int(open(f"{cur}/devnum").readline().strip())
busdev = f"{busnum:03d}:{devnum:03d}"
found[busdev] = devname
break
cur = os.path.dirname(cur)
return found
# collect device info
def usb2stat():
devstat = {}
for i in glob.glob("/sys/bus/usb/devices/*/devnum"):
devdir = os.path.dirname(i)
devnum = int(open(i).readline().strip())
busnum = int(open(os.path.join(devdir, "busnum")).readline().strip())
st = os.stat(f"/dev/bus/usb/{busnum:03d}/{devnum:03d}")
busdev = f"{busnum:03d}:{devnum:03d}"
devstat[busdev] = {
"st_ctime": st.st_ctime,
"hub_port": os.path.basename(devdir)
}
return devstat
def main(opt):
pat_list = [re.compile(i, flags=re.IGNORECASE) for i in opt.args]
dev2tty = usb2tty()
dev2blk = usb2blk()
devstat = usb2stat()
# filter 'lsusb' output
devlist = []
proc = sp.Popen("lsusb", stdout=sp.PIPE, text=True, bufsize=1)
for line in proc.stdout:
elem = line.split(None, 6)
busnum = elem[1]
devnum = elem[3][:-1]
busdev = f"{busnum}:{devnum}"
devlist.append([busdev, elem])
now = time.time()
# print in order
devlist.sort(key=lambda dev: devstat[dev[0]]["st_ctime"])
for dev in devlist:
busdev, elem = dev
vidpid = elem[5]
stat = devstat[busdev]
# show only recent device
if opt.recent > 0 and (now - stat["st_ctime"]) > opt.recent:
continue
ts = datetime.fromtimestamp(stat["st_ctime"])
if opt.fulldate:
date = ts.strftime("%Y-%m-%d/%H:%M:%S")
else:
date = ts.strftime("%H:%M:%S")
tags = ""
blk = dev2blk.get(busdev)
if blk: tags += f"[{blk}]"
tty = dev2tty.get(busdev)
if tty: tags += f"[{tty}]"
info = ""
if tags:
info = tags + " "
info += elem[6]
hub_port = devstat[busdev]["hub_port"]
# final matching string
out = f"{date} {busdev} {vidpid} {hub_port} {info}"
# show only all-matching one
if pat_list:
check_result = [pat.search(out) for pat in pat_list]
if None in check_result:
continue
# show list
if opt.device:
if blk: print(f"/dev/{blk}")
if tty: print(f"/dev/{tty}")
elif opt.bus_device:
bus, dev = busdev.split(":", 2)
print(f"/dev/bus/usb/{bus}/{dev}")
else:
print(out, end="")
if __name__ == '__main__' and '__file__' in globals():
ap = ArgumentParser()
ap.print_help = usage
ap.add_argument('-D', '--debug', nargs='?', default='INFO')
ap.add_argument('-b', '--bus-device', action='store_true')
ap.add_argument('-d', '--device', action='store_true')
ap.add_argument('-f', '--fulldate', action='store_true')
ap.add_argument('-r', '--recent', type=int, default=0)
ap.add_argument('args', nargs='*')
opt = ap.parse_args()
logging.basicConfig(level=eval('logging.' + opt.debug))
main(opt)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment