Python, Windows: 使用shlex解析命令行

14

当你需要拆分命令行时,比如调用Popen,最佳实践似乎是:

subprocess.Popen(shlex.split(cmd), ...

但请阅读文档(RTFM)。

shlex类使编写简单语法的词法分析器变得容易,这些语法类似于Unix shell...

那么,在win32平台上应该怎么做呢?关于引号解析和POSIX模式与非POSIX模式有何区别呢?

1个回答

33

到目前为止,Python标准库中还没有有效的适用于Windows/多平台的命令行拆分函数。(2016年3月)

subprocess

简而言之,对于subprocess.Popen .call等操作,最好采用以下方式进行:

if sys.platform == 'win32':
    args = cmd
else:
    args = shlex.split(cmd)
subprocess.Popen(args, ...)
在Windows上,对于shell选项的任何值,拆分都是不必要的。在内部,Popen只是使用subprocess.list2cmdline重新组合已拆分的参数 :-)。
对于shell=True选项,在Unix上也不需要shlex.split
无论是否拆分,在Windows上启动.bat.cmd脚本(而不是.exe .com),您需要明确包含文件扩展名,除非shell=True
关于命令行拆分的说明: shlex.split(cmd, posix=0)在Windows路径中保留反斜杠,但不能正确理解引用和转义。它的posix = 0模式到底有什么用很不清楚,但99%肯定会诱使Windows /跨平台程序员…
Windows API公开了ctypes.windll.shell32.CommandLineToArgvW
解析Unicode命令行字符串并返回指向命令行参数的指针数组,以及这些参数的计数,方式类似于标准C运行时argv和argc值。
def win_CommandLineToArgvW(cmd):
    import ctypes
    nargs = ctypes.c_int()
    ctypes.windll.shell32.CommandLineToArgvW.restype = ctypes.POINTER(ctypes.c_wchar_p)
    lpargs = ctypes.windll.shell32.CommandLineToArgvW(unicode(cmd), ctypes.byref(nargs))
    args = [lpargs[i] for i in range(nargs.value)]
    if ctypes.windll.kernel32.LocalFree(lpargs):
        raise AssertionError
    return args

然而,那个函数CommandLineToArgvW是虚假的,或者说只是与强制性标准C argv & argc解析略微相似:

>>> win_CommandLineToArgvW('aaa"bbb""" ccc')
[u'aaa"bbb"""', u'ccc']
>>> win_CommandLineToArgvW('""  aaa"bbb""" ccc')
[u'', u'aaabbb" ccc']
>>> 
C:\scratch>python -c "import sys;print(sys.argv)" aaa"bbb""" ccc
['-c', 'aaabbb"', 'ccc']

C:\scratch>python -c "import sys;print(sys.argv)" ""  aaa"bbb""" ccc
['-c', '', 'aaabbb"', 'ccc']

请关注Python库中可能会添加的未来内容,详情请见http://bugs.python.org/issue1724822。(“fisheye3”服务器上的所述函数实际上并不正确。)


跨平台候选函数

在 Windows 上,有效的命令行分割是相当疯狂的。例如,请尝试\ \\ \" \\"" \\\"aaa """"……

我目前提议将以下函数作为 Python 库的候选函数,用于跨平台命令行分割。它是跨平台的;比 shlex 快大约10倍,后者使用单字符步进和流式处理;而且与管道相关的字符也受到尊重(不像 shlex)。它已经通过了 Windows 和 Linux Bash 上的一系列真实困难的 shell 测试以及 test_shlex 的遗留 POSIX 测试模式。欢迎反馈有关剩余错误的信息。


def cmdline_split(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)
    """
    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

2
请问您能否添加注释支持?您是否已经在邮件列表中提交了PEP或至少一封邮件? - gaborous
2
你好,能否给这段代码附上一个开源许可证呢?我想使用它,但由于目前没有许可证,我无法使用。谢谢。 - Strigoides
或者只是与强制标准的 C argv 和 argc 解析略有相似:这并不正确。你正在将“标准 C argv 解析”与“标准 cmd.exe argv 解析”进行比较,但在我看来,后者远非标准或明智。 - Eric
你的 win_CommandLineToArgvW 看起来是 list2cmdline 的确切反向操作。 - Eric
尽管如此,如果在Windows下使用shell=True仍可能遇到^的麻烦,因为^是转义字符。 - ted

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接