Last active
April 25, 2026 21:41
-
-
Save udance4ever/ced9d3734c3ee958c4ad6ffecf4e02ce to your computer and use it in GitHub Desktop.
script to launch roms from Terminal.app & ease integration w ES-DE (macOS) esp for steam, xbox360 & ps4; implements .squashfs support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/opt/homebrew/bin/python3 | |
| # April 8, 2026 (created: Feb 5, 2025) | |
| # | |
| # emulatorLauncher helper script used to | |
| # 1) make it easy to launch roms from CLI (Terminal.app) | |
| # 2) implement .squashfs support (compatible with Batocera Linux) | |
| # 3) ease integration with ES-DE (esp in macOS) | |
| # 4) implement rom root search path (NOTE: need to tailor below) | |
| # NOTES: | |
| #!/usr/bin/env python3 fails launched *inside* ES-DE.app | |
| # default PATH -> /usr/bin:/bin:/usr/sbin:/sbin { doesn't include homebrew } | |
| import argparse | |
| import atexit | |
| import configparser | |
| import os | |
| import re | |
| import shlex | |
| import shutil | |
| import subprocess | |
| import sys | |
| import xml.etree.ElementTree as ET | |
| from pathlib import Path | |
| # command line args | |
| # ----------------- | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--system", help='emulator system (use "squashfsmount" to mount & exit)') | |
| parser.add_argument("--tmpmount", help='force mount .squashfs in /tmp', default=False, action='store_true') | |
| parser.add_argument("--binary", type=Path, help='path to custom binary') | |
| parser.add_argument("--alt", help='select alt binary (if known)', default=False, action='store_true') | |
| parser.add_argument("--intel", help='select Intel executable in Universal binary (macOS)', default=False, action='store_true') | |
| parser.add_argument("--romsearch", help='use rom root search path', default=False, action='store_true') | |
| parser.add_argument("--exec", help="exec generated cmd (--no-exec still mounts .squashfs)", default=True, action=argparse.BooleanOptionalAction) | |
| parser.add_argument("--debug", help='debug mode', default=False, action='store_true') | |
| parser.add_argument("file", type=Path, help='rom file, squashfs archive or stub (eg. .psvita)') | |
| args = parser.parse_args() | |
| # constants | |
| # --------- | |
| _apppath = '/Applications/#emu' | |
| _ESDEdir = '/userdata/system/configs/ES-DE' | |
| _findrules = [ | |
| f'{_ESDEdir}/custom_systems/es_find_rules.xml', # custom has priority | |
| f'{_apppath}/ES-DE.app/Contents/Resources/resources/systems/macos/es_find_rules.xml' | |
| ] | |
| _configspath = '/userdata/system/configs' | |
| _devpath = f'{_configspath}/rpcs3/dev_hdd0/game' | |
| _romsdir = '/userdata/roms' | |
| _biosdir = '/userdata/bios' | |
| _savedir = '/userdata/saves' | |
| # rom root search path ($$ edit for your environment) | |
| _devdir = '/Volumes/MEDIA_DEV' # 1TB NVMe in USB-C enclosure | |
| _favdir = '/Volumes/SHARE_FAV' # 512GB micro SD | |
| _360dir = '/Volumes/EVAL' # 1TB HDD G-Drive (BadUpdate) | |
| _nasdir = '/Volumes/SHARE' # NAS (/mnt/Pool1/shares/Share) | |
| _romrootpath_default = ":".join([ | |
| f"{_romsdir}:{_romsdir}.local:{_romsdir}.eval", # local | |
| f"{_devdir}/roms", | |
| f"{_favdir}/roms", | |
| f"{_360dir}/roms:{_360dir}/roms.CONSOLE:{_360dir}/roms.emu:{_360dir}/roms.ports", | |
| f"{_nasdir}/roms", | |
| ]) | |
| _romrootpath = os.environ.get('ROMROOT_PATH') or _romrootpath_default | |
| # RetroArch (macOS) mappings | |
| # $$TODO: eval "MESS" adam, apple2e, coco, macintosh under RetroArch MAME (see fmtowns) | |
| _cores = { | |
| "3do": "opera", | |
| "amiga500": "puae", | |
| "atari2600": "stella", | |
| "atomiswave": "flycast", | |
| "dos" : "dosbox_pure", | |
| "dreamcast": "flycast", | |
| "easyrpg" : "easyrpg", | |
| "fbneo": "fbneo", | |
| "gameandwatch": "mame", # default: RetroArch core (use --alt for standalone MAME) | |
| "gamegear": "genesis_plus_gx", | |
| "gb": "gambatte", | |
| "gb2players": "tgbdual", | |
| "gbc": "gambatte", | |
| "gbc2players": "tgbdual", | |
| "gba": "mgba", | |
| "jaguar": "virtualjaguar", | |
| "lutro": "lutro", | |
| "lynx": "handy", | |
| "mame": "mame", | |
| "mastersystem": "genesis_plus_gx", | |
| "megadrive": "genesis_plus_gx", | |
| "model2": "mame", # default: RetroArch core (use --alt for standalone MAME) | |
| "msu-md": "genesis_plus_gx", | |
| "msx2": "bluemsx", | |
| "msxturbor": "bluemsx", | |
| "n64": "mupen64plus_next", | |
| "naomi" : "flycast", | |
| "naomi2" : "flycast", | |
| "naomigd" : "flycast", | |
| "nds": "desmume", | |
| "neogeo": "fbneo", | |
| "neogeocd": "neocd", | |
| "nes": "fceumm", | |
| "ngp": "mednafen_ngp", | |
| "ngpc": "mednafen_ngp", | |
| "o2em": "o2em", | |
| "palm": "mu", | |
| "pc98": "np2kai", | |
| "pcengine": "mednafen_pce", | |
| "pcenginecd": "mednafen_pce", | |
| "pcfx": "mednafen_pcfx", | |
| "prboom": "prboom", | |
| "psx": "mednafen_psx", | |
| "saturn": "mednafen_saturn", | |
| "satellaview": "snes9x", | |
| "sega32x": "picodrive", | |
| "segacd": "genesis_plus_gx", | |
| "sg1000": "genesis_plus_gx", | |
| "sgb": "mgba", | |
| "snes": "snes9x", | |
| "snes-msu1": "snes9x", | |
| "supergrafx": "mednafen_supergrafx", | |
| "superbroswar": "superbroswar", | |
| "uzebox": "uzem", | |
| "vectrex": "vecx", | |
| "virtualboy": "mednafen_vb", | |
| "wasm4": "wasm4", | |
| "wswan": "mednafen_wswan", | |
| "wswanc": "mednafen_wswan", | |
| "x68000": "px68k", | |
| "zxspectrum": "fuse", | |
| } | |
| # functions | |
| # --------- | |
| mount = None | |
| def exit_handler(): | |
| if not mount: | |
| return | |
| if not mount.exists(): | |
| return | |
| if not mount.is_mount(): | |
| return | |
| if args.system == "squashfsmount": | |
| return | |
| print("INFO: Unmounting and removing:", mount) | |
| subprocess.run(["umount", str(mount)]) | |
| mount.rmdir() | |
| if args.tmpmount: | |
| (tmpmount / emusystem).rmdir() | |
| tmpmount.rmdir() | |
| atexit.register(exit_handler) | |
| def reGroup1(pattern, string): | |
| # https://stackoverflow.com/a/8569258/9983389 | |
| m = re.search(pattern, string) | |
| return m.group(1) if m else None | |
| def ESDEFindRule(object, key, notExistsOK=False): | |
| for findrule in _findrules: | |
| for item in ET.parse(findrule).findall(object): | |
| name = item.attrib.get('name') | |
| if name != key: | |
| continue | |
| rule = item.find('rule') | |
| if rule is None: | |
| continue | |
| for entry in rule.findall('entry'): | |
| binary = (entry.text or "").strip() | |
| if rule.attrib.get('type') == 'systempath': | |
| binary = shutil.which(binary) | |
| if os.path.exists(binary) or notExistsOK: | |
| if args.debug: print(f"DEBUG> ESDEFindRule: ({object}, {key}) -> {binary}") | |
| return Path(binary) | |
| return None | |
| def emusystem_from_path(p: Path) -> str | None: | |
| p = Path(p).absolute() | |
| child = None | |
| for ancestor in (p, *p.parents): | |
| parent = ancestor.name | |
| if parent == "roms" and child: | |
| return child | |
| child = parent or child | |
| return p.parent.name or None # fallback: immediate parent (or None if root) | |
| def search_in_path(p: Path, search_path: str, base:Path=None, ignoreStubs=False, exitNotFound=False) -> Path: | |
| if args.debug: print(f"DEBUG: search_in_path: {search_path}") | |
| for path in search_path.split(':'): | |
| p_abs = Path(path) / base / p | |
| if args.debug: print(f"DEBUG: search_in_file: checking {p_abs}") | |
| if p_abs.exists() and (p_abs.stat().st_size != 0 or not ignoreStubs): | |
| print(f"Found in search path: {p_abs}") | |
| return p_abs | |
| break | |
| else: | |
| print(f"Not found in search path: {p}") | |
| if exitNotFound: exit(1) | |
| return None | |
| def squashmount(file: Path, emusystem: str, exitOnMount: bool=False): | |
| mount = Path(file.with_suffix('')) # remove .squashfs | |
| doMount = not (exitOnMount and not args.exec) # mount if *not* squashfsmount and --no-exec | |
| # handle special mount locations | |
| match emusystem: | |
| case "ps3": | |
| # redirect mount to dev_hdd0/game if "[hdd0,<serial>]" in filename | |
| serial = reGroup1(r"\[hdd0,([A-Za-z0-9_]+)\]", str(mount)) | |
| if serial: | |
| mount = Path(_devpath) / serial | |
| case "ps4": | |
| # ps4: redirect mount to <serial> (4 uppercase letters + 5 digits) | |
| serial = reGroup1(r"([A-Z]{4}[0-9]{5})", str(mount)) | |
| if not serial: | |
| print("ERROR: no serial in filename to mount title. Exiting") | |
| exit(1) | |
| mount = Path(mount).resolve().parent / serial | |
| # https://stackoverflow.com/a/2113511/9983389 | |
| if (not os.access(str(Path(mount).parent), os.W_OK)) or args.tmpmount: | |
| tmpmount = Path(f"/tmp/emulatorLauncher.{os.getpid()}") | |
| mount = tmpmount / emusystem / Path(mount).name | |
| print("INFO: mount:", mount) | |
| if doMount: | |
| if not mount.exists(): | |
| mount.mkdir(parents=True, exist_ok=True) | |
| elif mount.is_mount(): | |
| print("INFO: mount exists. unmounting:", mount) | |
| subprocess.run(["umount", str(mount)]) | |
| cmd = ['/usr/local/bin/squashfuse', file, mount] | |
| if args.debug: print("DEBUG> cmd:", cmd) | |
| try: | |
| if doMount: subprocess.run(cmd, check=True) | |
| except subprocess.CalledProcessError as e: | |
| print("ERROR: Failed to mount:", file) | |
| sys.exit(e.returncode) | |
| if doMount: | |
| print("INFO: permissions:", "".join(ch if os.access(str(mount), flag) else "-" for ch,flag in (("r",os.R_OK),("w",os.W_OK),("x",os.X_OK)))) | |
| if not os.access(str(mount), os.R_OK | os.X_OK): | |
| print(f"ERROR: Mounted file system inaccessible: {mount}") | |
| print(" You may need to recreate using `mksquashfs source FILESYSTEM -all-root`") | |
| exit(1) | |
| if exitOnMount: exit(0) | |
| singlefile = mount / mount.name | |
| # $$ wierd bug testing existence inside squashfs mounts (SMB related?) | |
| if not singlefile.exists() and args.debug: | |
| print(f'DEBUG> workaround: checking "{singlefile}" exists in squashfs 2x this session') | |
| # https://stackoverflow.com/a/2507871/9983389 | |
| if singlefile.exists() and singlefile.stat().st_size != 0: | |
| if args.debug: print("DEBUG> found non-zero singlefile:", singlefile) | |
| return mount, singlefile | |
| else: | |
| return mount, mount | |
| # setup | |
| # ----- | |
| file = args.file | |
| if args.system: | |
| emusystem, _, emu_cli = args.system.partition(":") | |
| else: | |
| emusystem, emu_cli = emusystem_from_path(file), None | |
| file = search_in_path(file.name, _romrootpath, base=emusystem, ignoreStubs=True, exitNotFound=True) if args.romsearch else file.absolute() | |
| if file.suffix == '.squashfs': | |
| if emusystem == "squashfsmount": | |
| squashmount(file, emusystem_from_path(file), exitOnMount=True) | |
| else: | |
| mount, rompath = squashmount(file, emusystem) | |
| else: | |
| rompath = file | |
| if args.debug: print(f"DEBUG: emusystem={emusystem}; emu_cli={emu_cli}; rompath={rompath}") | |
| # generate cmd by system | |
| # ---------------------- | |
| # $ alternative to big match statement: generator classes (akin to Batocera Linux configgen) | |
| mameRompath = f'{rompath.parent};{_biosdir}/mame;{_biosdir}' | |
| messRompath = f'{rompath.parent};{_biosdir}/{emusystem}' | |
| match emusystem: | |
| # NOTE: RetroArch core is used by default if mapping exists | |
| case sys if (sys in _cores) and not args.alt: | |
| appexec = ESDEFindRule('emulator', 'RETROARCH') | |
| cmdArgs = [ '-L', ESDEFindRule('core', 'RETROARCH') / f'{_cores[sys]}_libretro.dylib' ] | |
| if mount: | |
| if sys == "snes-msu1": rompath = next(rompath.glob('*.sfc')) | |
| if sys == "msu-md": rompath = next(rompath.glob('*.md')) | |
| cmdArgs += [ rompath ] | |
| case "adam" | "apple2" | "coco": | |
| appexec = ESDEFindRule('emulator', 'MAME') # standalone MAME | |
| cmdArgs = [ '-rompath', messRompath, | |
| emusystem if "apple2" not in emusystem else 'apple2e', | |
| '-ui_active', | |
| '-joystick' if "apple2" not in emusystem else '-gameio', 'joy', | |
| '-flop1' if "coco" not in emusystem else '-cart', | |
| rompath ] | |
| os.chdir(f'{_savedir}/mame') | |
| case "macintosh": | |
| appexec = ESDEFindRule('emulator', 'MAME') # standalone MAME | |
| cmdArgs = [ f'-rompath {messRompath}', | |
| 'macplus', | |
| '-ui_active', | |
| '-flop1', | |
| rompath ] | |
| os.chdir(f'{_savedir}/mame') | |
| case "apple2gs": | |
| configfile = Path(os.getenv('HOME')) / '.config.gsp' | |
| config = configparser.ConfigParser(allow_unnamed_section=True) | |
| config.read(configfile) | |
| config.set(configparser.UNNAMED_SECTION, 's7d1', str(rompath)) | |
| with configfile.open('w') as cf: config.write(cf) | |
| appexec = ESDEFindRule('emulator', 'GSPLUS') | |
| cmdArgs = [ ] | |
| case "3ds": | |
| cci_map = {"citra": False, "lime3ds": False, "azahar": True} # default is first | |
| emu = next((k for k in cci_map if emu_cli and k in emu_cli), next(iter(cci_map))) | |
| appexec = ESDEFindRule('emulator', emu.upper()) | |
| cmdArgs = [ rompath if cci_map[emu] else rompath.with_suffix(".3ds") ] | |
| case "dreamcast" | "atomiswave" | "naomi" | "naomi2" | "naomigd": | |
| appexec = ESDEFindRule('emulator', 'FLYCAST') # NOTE: --alt selects standalone Flycast | |
| cmdArgs = [ rompath ] | |
| case "daphne" | "singe": | |
| mediadir = rompath.parent if rompath.name == rompath.parent.name else (mount or rompath) | |
| basename = mediadir.name.removesuffix(".daphne") | |
| datadir = Path(_configspath) / "hypseus-singe" | |
| script = mediadir / f"{basename}.singe" | |
| singe = True if script.exists() else False | |
| commands = mediadir/ f"{basename}.commands" | |
| extraOpts = commands.read_text(encoding="utf-8") if commands.exists() else None | |
| appexec = ESDEFindRule('emulator', 'HYPSEUS-SINGE') # datadir / "bin" / "hypseus.bin" | |
| cmdArgs = [ | |
| 'singe' if singe else basename, | |
| 'vldp', | |
| '-fullscreen', | |
| '-gamepad', | |
| '-framefile', mediadir / (basename + ".txt"), | |
| '-datadir', datadir, | |
| '-homedir', datadir, | |
| ] | |
| if extraOpts: cmdArgs += shlex.split(extraOpts) | |
| if singe: cmdArgs += [ '-script', script, | |
| '-singedir', mediadir.parent ] | |
| case "flash": | |
| appexec = ESDEFindRule('emulator', 'RUFFLE') | |
| cmdArgs = [ "--fullscreen", rompath ] | |
| case "fmtowns": | |
| driver = 'fmtownshr' | |
| mameArgs = [ driver, '-rompath', messRompath, '-cdrom', f'"{rompath}"' ] | |
| appexec = ESDEFindRule('emulator', 'RETROARCH') | |
| cmdArgs = [ '-L', ESDEFindRule('core', 'RETROARCH') / 'mame_libretro.dylib', ' '.join(mameArgs) ] | |
| case "gameandwatch": | |
| appexec = ESDEFindRule('emulator', 'MAME') # standalone MAME | |
| cmdArgs = [ '-skip_gameinfo', '-artpath', f'{_biosdir}/mame/artwork', '-rompath', mameRompath, rompath.stem ] | |
| os.chdir(f'{_savedir}/mame') | |
| case "gamecube" | "wii" | "triforce": | |
| appexec = ESDEFindRule('emulator', 'DOLPHIN' if not args.alt else 'DOLPHIN-TRIFORCE') | |
| cmdArgs = [ '-b', '-e', rompath ] | |
| if args.alt: cmdArgs += [ '-u', Path(_configspath) / "Dolphin (Triforce)" ] | |
| case "hikaru": | |
| appexec = ESDEFindRule('emulator', 'WHISKYCMDRAW') | |
| # https://forum.arcadecontrols.com/index.php/topic,106954.msg1133325.html | |
| cmdArgs = [ ESDEFindRule('bottle', "EMU2"), ESDEFindRule('emulator', 'DEMUL', notExistsOK=True), | |
| f'-run={emusystem}', f'-rom={rompath.stem}' ] | |
| case "ikemen": | |
| appexec = ESDEFindRule('emulator', 'OS-SHELL') | |
| cmdArgs = [ '-c', rompath ] | |
| case "model2": # $$ controller bindings not working on macOS (NOTE: RA core is default above) | |
| appexec = ESDEFindRule('emulator', 'MAME') # standalone MAME | |
| cmdArgs = [ '-skip_gameinfo', '-rompath', mameRompath, rompath.stem ] | |
| os.chdir(f'{_savedir}/mame') | |
| case "model3": | |
| appexec = ESDEFindRule('emulator', 'SUPERMODEL') | |
| cmdArgs = [ rompath ] | |
| os.chdir(ESDEFindRule('config', 'SUPERMODEL')) | |
| case "namco2x6": | |
| appexec = ESDEFindRule('emulator', 'PLAY!') | |
| cmdArgs = [ '--fullscreen', '--arcade', rompath.stem ] | |
| case "openbor": | |
| appexec = ESDEFindRule('emulator', 'WHISKYCMDRAW') | |
| cmdArgs = [ ESDEFindRule('bottle', 'EMU2'), ESDEFindRule('emulator', 'OPENBOR', notExistsOK=True), rompath ] | |
| os.chdir(ESDEFindRule('config', 'OPENBOR')) | |
| case "ps2": | |
| appexec = ESDEFindRule('emulator', 'PCSX2') | |
| cmdArgs = [ '-fullscreen', '-nogui', rompath ] | |
| case "ps3": | |
| if rompath.suffix == '.psn': | |
| rompath = Path(_devpath) / rompath.read_text(encoding="utf-8").rstrip() | |
| appexec = ESDEFindRule('emulator', 'RPCS3' if not args.intel else 'RPCS3_INTEL') | |
| cmdArgs = [ rompath ] | |
| case "ps4": | |
| appexec = ESDEFindRule('emulator', 'SHADPS4') | |
| cmdArgs = [ '--fullscreen', 'true', rompath / "eboot.bin" ] | |
| case "psp": | |
| appexec = ESDEFindRule('emulator', 'PPSSPPGOLD' if not args.alt else 'PPSSPP') | |
| cmdArgs = [ rompath ] | |
| case "psvita": | |
| appexec = ESDEFindRule('emulator', 'VITA3K') | |
| cmdArgs = [ '-F', '-w', '-f', '-r', reGroup1(r"\[([A-Za-z0-9_]+)\]", rompath.stem) ] | |
| case "scummvm": | |
| appexec = ESDEFindRule('emulator', 'SCUMMVM') | |
| cmdArgs = [ '-f', f'--path={rompath}', next(rompath.glob("*.scummvm")).stem ] | |
| case "sdlpop": | |
| appexec = ESDEFindRule('emulator', 'SDLPOP') | |
| cmdArgs = [ 'full' ] | |
| case "solarus": | |
| appexec = ESDEFindRule('emulator', 'SOLARUS') | |
| cmdArgs = [ '-fullscreen=yes', rompath ] | |
| case "steam": | |
| appexec = ESDEFindRule('emulator', 'WHISKYSTEAMHEROIC') | |
| cmdArgs = [ open(rompath, 'r', encoding='utf-8').read() ] | |
| case "steam.macOS": | |
| appexec = ESDEFindRule('emulator', 'OPEN') # not OS-SHELL on macOS | |
| cmdArgs = [ '-W', '-a', rompath ] | |
| os.chdir(rompath.parent) # ensure rel symlinks to .app work | |
| case "switch": | |
| arg_map = {"ryubing": [rompath], "eden": ['-f', '-g', rompath]} # first is default | |
| emu = next((k for k in arg_map if emu_cli and k in emu_cli), next(iter(arg_map))) | |
| appexec = ESDEFindRule('emulator', emu.upper()) | |
| cmdArgs = arg_map[emu] | |
| case "vpinball": | |
| appexec = ESDEFindRule('emulator', 'VISUAL-PINBALL') | |
| cmdArgs = [ '-play', rompath ] | |
| case "wiiu": | |
| appexec = ESDEFindRule('emulator', 'CEMU') | |
| cmdArgs = [ "-f", '-g', rompath ] | |
| case "windows": | |
| appexec = ESDEFindRule('emulator', 'WHISKYCMDRAW') | |
| cmdArgs = [ ESDEFindRule('bottle', 'EMU2'), rompath ] | |
| # $TODO akin to apple2gs, update xemu.toml to update hdd_path on a per system/game basis | |
| case "xbox" | "chihiro": | |
| appexec = ESDEFindRule('emulator', 'XEMU') | |
| cmdArgs = [ '-dvd_path', rompath ] | |
| # NOTE: no longer uses --alt (migrated to xbox360:<emu>) | |
| case "xbox360": | |
| appexec = ESDEFindRule('emulator', (emu_cli or 'WHISKYXENIA').upper()) | |
| if rompath.suffix == '.xbox360': | |
| os.chdir(rompath.parent) # stubs are relative | |
| xex = Path(rompath.read_text(encoding="utf-8").rstrip("\r\n")) | |
| if args.romsearch: | |
| xex = search_in_path(xex, _romrootpath, base=emusystem, exitNotFound=True) | |
| elif mount: | |
| xex = rompath / 'default.xex' | |
| if not xex.exists(): # not common: some discs do not have default.xex | |
| xex = next(rompath.glob("*.xex")) | |
| else: | |
| xex = rompath | |
| cmdArgs = [ xex ] | |
| case "lindbergh": | |
| print(f"FUTURE: requires macOS port: {emusystem}") | |
| exit(1) | |
| case "openlara" | "pyxel": | |
| print(f"FUTURE: requires macOS port (RetroArch core): {emusystem}") | |
| exit(1) | |
| case "gong": | |
| print(f"ERROR: not yet supported: {emusystem}") | |
| exit(1) | |
| case _: | |
| if args.alt: | |
| print(f'ERROR: No alternative emulator known for system: {emusystem}') | |
| else: | |
| print(f'ERROR: Unknown system: {emusystem} {"(Use --system SYSTEM to be explicit)" if not args.system else ""}') | |
| exit(1) | |
| appArgs = [ args.binary or appexec ] | |
| if args.intel: appArgs = ['arch', '-x86_64', *appArgs] | |
| cmd = appArgs + cmdArgs | |
| if args.debug: print("DEBUG: cmd[]:", cmd) | |
| print("INFO: cmd:", shlex.join([str(e) for e in cmd])) # shell version | |
| # execute command | |
| # --------------- | |
| if args.exec: | |
| try: | |
| res = subprocess.run(cmd, check=True) | |
| except subprocess.CalledProcessError as e: | |
| print(f"{cmd[0]} failed to exit cleanly (status={e.returncode})") | |
| except FileNotFoundError: | |
| print(f"{cmd[0]}: command not found") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment