subprocess.Popen的args参数最大长度是多少?

21
我正在使用subprocess模块中的Popen函数来执行命令行工具:
subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)

我正在使用的工具会接收一个文件列表进行处理。在某些情况下,这个文件列表可能会非常长。有没有办法找到参数 args 的最大长度?当传递大量文件给工具时,我遇到了以下错误:

Traceback (most recent call last):
  File "dump_output_sopuids.py", line 68, in <module>
    uid_map = create_sopuid_to_path_dict_dcmdump(dicom_files)
  File "dump_output_sopuids.py", line 41, in create_sopuid_to_path_dict_dcmdump
    dcmdump_output = subprocess.Popen(cmd,stdout=subprocess.PIPE).communicate(0)[0]
  File "c:\python26\lib\subprocess.py", line 621, in __init__
    errread, errwrite)
  File "c:\python26\lib\subprocess.py", line 830, in _execute_child
    startupinfo)
WindowsError: [Error 206] The filename or extension is too long

有没有一种通用的方法来找到这个最大长度?我在msdn上找到了以下文章:命令提示符(Cmd.exe)命令行字符串限制,但我不想硬编码该值。我宁愿在运行时获取该值以将命令分解为多个调用。

我正在使用Python 2.6在Windows XP 64上。

编辑:添加代码示例

paths = ['file1.dat','file2.dat',...,'fileX.dat']
cmd = ['process_file.exe','+p'] + paths
cmd_output = subprocess.Popen(cmd,stdout=subprocess.PIPE).communicate(0)[0]

问题出现的原因是paths列表中每个实际条目通常是非常长的文件路径,而且有几千个这样的条目。
我不介意将命令拆分成多个对process_file.exe的调用。 我正在寻找一种通用的方法来获取参数的最大长度,以便我知道每次运行发送多少路径。

你能提供一组args参数的示例值吗? - gurney alex
我来晚了,但是我想补充一下,我因为添加了很多条目后,我的PATH环境变量变得太长而遇到了相同的错误。 - RedX
2个回答

14
如果您传递的是shell=False,那么Cmd.exe就不会起作用。
在Windows上,subprocess将使用Win32 API的CreateProcess函数创建新进程。该函数的文档指出,第二个参数(由subprocess.list2cmdline构建)的最大长度为32768个字符,包括Unicode终止空字符。如果lpApplicationName为NULL,则lpCommandLine的模块名部分限制为MAX_PATH个字符。
考虑到您的示例,建议为executable(args [0])提供一个值,并将args用于第一个参数。如果我的CreateProcess文档和subprocess模块源代码的阅读是正确的,这应该可以解决您的问题。
[编辑:在获得Windows机器并测试后删除了args [1:]部分]

我不确定我是否理解了你关于使用args[1:]作为第一个参数的建议。我已经在我的问题中更新了一个代码示例。感谢你提供CreateProcess的链接和提示,给你点赞。 - Jesse Vogt
正如我引用的文档中提到的那样,32768限制在CreateProcess基元中是硬编码的(即16位有符号整数的上限为2 ** 15)。由于list2cmd在构建命令行时会添加引号和空格,因此在sum([len(a) for a in args])达到2 ** 15之前,您将会遇到该限制。难道没有一种使用通配符将参数传递给可执行文件的方法吗?(通配符通常由Windows下的可执行文件处理) - gurney alex
由于这个问题没有太多的活动,我将接受你的答案,因为你能找到一个好的限制。我本来希望能找到一种更普遍地从操作系统中获取限制的方法,但这也可以。感谢你的帮助! - Jesse Vogt
1
额,Windows的限制是硬编码到2 ** 15,这在64位版本的操作系统上也可能是如此。在posix系统上没有限制,除了你的RAM:Popen使用execvp或execvpe,它们使用一个空终止的char *数组作为参数,没有大小约束。 - gurney alex
@gurneyalex "POSIX下没有限制"这句话并不正确;请看我的回答。 - tripleee
显示剩余2条评论

5

对于类Unix平台,内核常量ARG_MAX由POSIX定义。它要求至少为4096字节,尽管在现代系统上,可能是1兆字节或更多。

在许多系统上,getconf ARG_MAX将在shell提示符下显示其值。

shell实用程序xargs方便地允许您分解长命令行。例如,如果

python myscript.py *

如果文件列表扩展到超过 ARG_MAX 字节长度,会在大目录中导致失败,你可以通过以下方法解决:

printf '%s\0' * |
xargs -0 python myscript.py

选项-0是GNU的一个扩展,但这确实是传递包含换行符、引号等特殊字符文件名列表的唯一完全安全且明确的方法。也许还可以探索:

find . -maxdepth 1 -type f -exec python myscript.py {} +

这些方法绕过限制的方式是,如果参数列表太长,它们会将其分成若干份,并在每次可以放入命令行的参数数上运行 myscript.py。这取决于 myscript.py 的操作,可能正好达到您想要的目的,也可能出现灾难性错误。(例如,如果它对传递的文件中的数字求和,那么您将会得到多个处理过该集合参数的结果。)
相反地,如果要向 subprocess.Popen() 和相关函数传递一长串参数,类似以下代码可实现:
p = subprocess.Popen(['xargs', '-0', 'command'],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE,
    stderr=subprocess.PIPE)
out, err = p.communicate('\0'.join(long_long_argument_list))

在大多数情况下,您应该避免使用原始的Popen()函数,并让像run()check_call()这样的包装函数来完成大部分工作:

r = subprocess.run(['xargs', '-0', 'command'],
    input='\0'.join(long_long_argument_list),
    universal_newlines=True)
out = r.stdout

subprocess.run()支持在3.7及以上版本中使用text=True作为universal_newlines=True的新名称。Python版本低于3.5的旧版本没有run,因此需要退回到旧的遗留函数check_outputcheck_call或者(很少)call

如果你想在Python中重新实现xargs,可以尝试类似以下的代码。

import os

def arg_max_args(args):
    """
    Split up the list in `args` into a list of lists
    where each list contains fewer than ARG_MAX bytes
    (including room for a terminating null byte for each
    entry)
    """
    arg_max = os.sysconf("SC_ARG_MAX")
    result = []
    sublist = []
    count = 0
    for arg in args:
        argl = len(arg) + 1
        if count + argl > arg_max:
            result.append(sublist)
            sublist = [arg]
            count = argl
        else:
            sublist.append(arg)
            count += argl
    if sublist:
        result.append(sublist)
    return result

就像真正的xargs一样,你需要在此函数返回的每个子列表上运行单独的子进程。

一个合适的实现应该在任何一个参数大于ARG_MAX时引发错误,但这只是一个快速的演示。


也许可以参考 https://dev59.com/cG855IYBdhLWcg3wlVca#51950538,了解在 U*x 平台上使用 subprocess 时的一些问题和扩展。 - tripleee
xargs 如何能够使用所有这些参数调用 command,而 Python 却不能?这是否意味着限制在 Python 中而不是系统本身? - Pedro A
xargs 将命令行分成较小的块。当然,您可以在 Python 中重新实现相同的逻辑;但是既然已经有一个可以做到这一点的工具,为什么不使用它呢? - tripleee
非常感谢,我现在明白了!因此,如果需要,它会创建多个子进程。但这对我的特定用例没有帮助,因为我只需要一个进程,因为我正在传递许多标志到命令 - 这显然是无法分割的。 - Pedro A
是的,这正是我想的。很有道理。只是确认一下:每种子进程生成都使用“exec边界”,对吗?在我的情况下,我无法更改工具(它是带有许多“-e”标志的“docker run”)。我将不得不找到另一种方法。非常感谢您的帮助!! - Pedro A
显示剩余5条评论

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