如何在Python中制作一个通用方法来执行多个管道外壳命令?

时间:2018-08-08 23:20:44

标签: python shell subprocess

我有许多需要在我的python脚本中执行的shell命令。我知道我不应该像提到的here那样使用shell = true,并且如果我在所说的here的命令中有管道,我可以使用std输出和输入。

但是问题是我的shell命令非常复杂并且充满了管道,因此我想创建一个通用方法供脚本使用。

我在下面做了一个小测试,但是在打印结果后挂了(我简化了一下,只是放在这里)。有人可以让我知道吗?

  1. 为什么要挂。
  2. 如果有更好的方法可以做到这一点。

谢谢。

PS:这只是一个大型python项目的一小部分,出于商业原因,我尝试这样做。谢谢。

using

2 个答案:

答案 0 :(得分:2)

我会要求用户将命令作为单独的实体提供,而不是使用shell字符串并尝试以自己的方式解析它。这样可以避免明显的陷阱来检测|,它是命令的一部分,而不用作外壳管道。您要求他们以字符串列表或单个字符串的形式提供命令,然后shlex.split将由您要公开的接口决定。在下面的示例中,为简单起见,我选择了第一个。

一旦有了单独的命令,一个简单的for循环就足以将前一个命令的输出传递给下一个命令的输入,例如you have found yourself

def pipe_subprocesses(*commands):
    if not commands:
        return

    next_input = None
    for command in commands:
        p = subprocess.Popen(command, stdin=next_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        next_input = p.stdout

    out, err = p.communicate()
    if err:
        print(err.decode().strip())
    else:
        print(out.decode())

用法是

>>> pipe_subprocesses(['ls', '-lhtr'], ['awk', '{print $9}'], ['wc', '-l'])
25

现在,这是一种快速且肮脏的方法,可以对其进行设置,并且可以按需运行。但是此代码至少存在两个问题:

  1. 您泄漏了僵尸进程/打开的进程句柄,因为没有进程的退出代码,但收集了最后一个;并且操作系统正在为您打开资源;
  2. 您无法访问过程中途失败的信息。

为避免这种情况,您需要维护一个已打开进程的列表,并为每个进程明确wait。而且因为我不知道您的确切用例,所以我只返回失败的第一个进程(如果有)或最后一个进程(如果没有),以便您可以采取相应的行动:

def pipe_subprocesses(*commands):
    if not commands:
        return

    processes = []
    next_input = None
    for command in commands:
        if isinstance(command, str):
            command = shlex.split(command)
        p = subprocess.Popen(command, stdin=next_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        next_input = p.stdout
        processes.append(p)

    for p in processes:
        p.wait()

    for p in processes:
        if p.returncode != 0:
            return p
    return p  # return the last process in case everything went well

我还以一些shlex为例,以便您可以混合使用原始字符串和已解析的列表:

>>> pipe_subprocesses('ls -lhtr', ['awk', '{print $9}'], 'wc -l')
25

答案 1 :(得分:2)

不幸的是,在这种情况下,shell为您提供了一些便利,或者Shell完全忽略了您。一些问题:

  • 该函数应该始终wait()完成每个过程,否则您将获得称为zombie processes的内容。

  • 命令应该使用真实管道相互连接,这样就不需要立即将整个输出读入内存。这是管道的正常工作方式。

  • 每个管道的读取端都应在父进程中关闭,以便子进程可以在下一个进程关闭其输入时正确SIGPIPE。没有这个,父进程可以使管道保持打开状态,而子进程不知道退出,并且它可能永远运行。

  • 除了SIGPIPE之外,子流程中的错误应作为异常引发。留给读者练习是在最终过程中提出SIGPIPE的例外情况,因为在那里{em>不是期望SIGPIPE,但忽略它并没有害处。

请注意,subprocess.DEVNULL在Python 3.3之前不存在。我知道有些人仍然使用2.x,您将不得不手动打开/dev/null的文件,或者只是确定管道中的第一个进程要与{x1}父进程。

代码如下:

stdin

在这里我们可以看到它的实际效果。您可以看到管道正确地以import signal import subprocess def run_pipe(*cmds): """Run a pipe that chains several commands together.""" pipe = subprocess.DEVNULL procs = [] try: for cmd in cmds: proc = subprocess.Popen(cmd, stdin=pipe, stdout=subprocess.PIPE) procs.append(proc) if pipe is not subprocess.DEVNULL: pipe.close() pipe = proc.stdout stdout, _ = proc.communicate() finally: # Must call wait() on every process, otherwise you get # zombies. for proc in procs: proc.wait() # Fail if any command in the pipe failed, except due to SIGPIPE # which is expected. for proc in procs: if (proc.returncode and proc.returncode != -signal.SIGPIPE): raise subprocess.CalledProcessError( proc.returncode, proc.args) return stdout 终止(一直运行到yes),并正确地以SIGPIPE终止(总是失败)。

false