Index: contrib/st2subprocess.py ================================================================== --- contrib/st2subprocess.py +++ contrib/st2subprocess.py @@ -1,17 +1,20 @@ # encoding: utf-8 # api: streamtuner2 # title: win32/subprocess # description: Utilizes subprocess spawning or win32 API instead of os.system -# version: 0.4 +# version: 0.5 # depends: streamtuner2 > 2.2.0, python >= 2.7 # priority: optional # config: # { name: cmd_spawn, type: select, select: "popen|shell|call|execv|spawnv|pywin32|win32api|system", value: popen, description: Spawn method } -# { name: cmd_flags, type: select, select: "none|nowait|detached|nowaito|all|sw_hide|sw_maximize|sw_minimize|sw_show", value: nowait, description: Process creation flags (win32) } +# { name: cmd_flags, type: select, select: "none|nowait|detached|nowaito|all|sw_hide|sw_maximize|sw_minimize|sw_show", value: detached, description: Process creation flags (win32) } +# { name: cmd_quote, type: bool, value: 0, description: Simple quoting override (for Windows) } +# { name: cmd_split, type: select, select: kxr|shlex|nonposix, value: kxr, description: Command string splitting variant } # type: handler # category: io +# license: MITL, CC-BY-SA-3.0 # # Overrides the action.run method with subprocess.Popen() and a bit # cmdline parsing. Which mostly makes sense on Windows to avoid some # `start` bugs, such as "http://.." arguments getting misinterpreted. # Also works on Linux, though with few advantages and some gotchas. @@ -45,10 +48,11 @@ import subprocess import os import shlex +import pipes import re from config import * from channels import FeaturePlugin import action @@ -84,11 +88,11 @@ # swap out action.run() def init2(self, parent, *k, **kw): action.run = self.run - if conf.windows: + if conf.windows or conf.cmd_split: action.quote = self.quote # override for action.quote def quote(self, ins): @@ -106,11 +110,14 @@ if re.search("streamripper|cmd\.exe", cmd): return orig_run(cmd) # split args - args = shlex.split(cmd) + if conf.cmd_split in ("kxr", "cmdline_split", "multi"): + args = self.cmdline_split(cmd, platform=(not conf.windows)) + else: + args = shlex.split(cmd, posix=(cmd_split=="shlex")) # undo win32 quoting damage if conf.windows and re.search('\^', cmd): #and action.quote != self.quote: args = [re.sub(r'\^(?=[()<>"&^])', '', s) for s in args] # flags if conf.windows and conf.cmd_flags in self.flagmap: @@ -162,6 +169,58 @@ # fallback else: return orig_run(cmd) + # shlex.split for Windows + def cmdline_split(self, s, platform='this'): + """Multi-platform variant of shlex.split() for command-line splitting. + For use with subprocess, for argv injection etc. Using fast REGEX. + + platform: 'this' = auto from current platform; + 1 = POSIX; + 0 = Windows/CMD + (other values reserved) + + author: kxr / Philip Jenvey + license: CC-BB-SA-3.0 + src: https://stackoverflow.com/questions/33560364/python-windows-parsing-command-lines-with-shlex + """ + if platform == 'this': + platform = (sys.platform != 'win32') + if platform == 1: + RE_CMD_LEX = r'''"((?:\\["\\]|[^"])*)"|'([^']*)'|(\\.)|(&&?|\|\|?|\d?\>|[<])|([^\s'"\\&|<>]+)|(\s+)|(.)''' + elif platform == 0: + RE_CMD_LEX = r'''"((?:""|\\["\\]|[^"])*)"?()|(\\\\(?=\\*")|\\")|(&&?|\|\|?|\d?>|[<])|([^\s"&|<>]+)|(\s+)|(.)''' + else: + raise AssertionError('unkown platform %r' % platform) + + args = [] + accu = None # collects pieces of one arg + for qs, qss, esc, pipe, word, white, fail in re.findall(RE_CMD_LEX, s): + if word: + pass # most frequent + elif esc: + word = esc[1] + elif white or pipe: + if accu is not None: + args.append(accu) + if pipe: + args.append(pipe) + accu = None + continue + elif fail: + raise ValueError("invalid or incomplete shell string") + elif qs: + word = qs.replace('\\"', '"').replace('\\\\', '\\') + if platform == 0: + word = word.replace('""', '"') + else: + word = qss # may be even empty; must be last + + accu = (accu or '') + word + + if accu is not None: + args.append(accu) + + return args