#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Quoting-safe ADB helper. Why this exists: if you build device commands inside a host shell, the host expands `$`, `|`, `>`, and quotes *before* adb sees them. Symptoms are host-side errors that look like device failures -- "Could not find a part of the path", "no closing quote", "Missing type name after '['", unexpanded variables. Windows PowerShell is the worst offender (it also eats `$var:`, `[^"...`, `$(...)`). Calling adb from Python with an argument list avoids all of it. Configuration (first match wins): 1. CLI flags: --serial / --adb 2. env: ADB_SERIAL / ADB_PATH 3. defaults below Usage ----- python devsh.py sh "pm list packages | grep myapp" # as the shell user python devsh.py su "pm grant android.permission.X" python devsh.py sh "dumpsys window | grep mCurrentFocus" python devsh.py pull /data/local/tmp/x.apk ./x.apk python devsh.py push ./x.apk /data/local/tmp/x.apk python devsh.py dev # list devices `su` wraps the command in `su -c "..."`; internal double quotes are escaped. """ import argparse import os import subprocess import sys DEFAULT_ADB = 'adb' DEFAULT_SERIAL = None def resolve(): ap = argparse.ArgumentParser(add_help=False) ap.add_argument('--adb') ap.add_argument('--serial') known, _ = ap.parse_known_args() adb = known.adb or os.environ.get('ADB_PATH') or DEFAULT_ADB serial = known.serial or os.environ.get('ADB_SERIAL') or DEFAULT_SERIAL return adb, serial def run(adb, serial, args, timeout=300): cmd = [adb] if serial: cmd += ['-s', serial] cmd += args r = subprocess.run(cmd, capture_output=True, text=True, errors='replace', timeout=timeout) out = r.stdout or '' if (r.stderr or '').strip(): out += '\n[stderr]\n' + r.stderr return r.returncode, out def main(): if len(sys.argv) < 2: print(__doc__) return 2 adb, serial = resolve() argv = [a for a in sys.argv[1:] if not a.startswith('--adb=') and not a.startswith('--serial=')] # strip flag/value pairs cleaned = [] skip = False for a in argv: if skip: skip = False continue if a in ('--adb', '--serial'): skip = True continue cleaned.append(a) argv = cleaned if len(argv) < 2 and argv and argv[0] not in ('dev',): print(__doc__) return 2 mode = argv[0] if mode == 'dev': rc, out = run(adb, None, ['devices', '-l']) elif mode in ('sh', 'shell'): rc, out = run(adb, serial, ['shell', argv[1]]) elif mode == 'su': cmd = argv[1].replace('"', '\\"') rc, out = run(adb, serial, ['shell', 'su -c "%s"' % cmd]) elif mode == 'suf': # read a device text file cmd = argv[1].replace('"', '\\"') size = argv[2] if len(argv) > 2 else '6000' rc, out = run(adb, serial, ['shell', 'su -c "head -c %s %s"' % (size, cmd)]) elif mode == 'pull': rc, out = run(adb, serial, ['pull', argv[1], argv[2]]) elif mode == 'push': rc, out = run(adb, serial, ['push', argv[1], argv[2]]) else: print(__doc__) return 2 sys.stdout.write(out) return 0 if rc == 0 else 1 if __name__ == '__main__': sys.exit(main())