需要一种更好的方式从Python中执行控制台命令并记录结果。

4

我有一个Python脚本,需要执行几个命令行工具。stdout输出有时会用于进一步处理。在所有情况下,我都想记录结果并在检测到错误时引发异常。为此,我使用以下函数:

def execute(cmd, logsink):
    logsink.log("executing: %s\n" % cmd)
    popen_obj = subprocess.Popen(\
          cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (stdout, stderr) = popen_obj.communicate()
    returncode = popen_obj.returncode
    if (returncode <> 0):
       logsink.log("   RETURN CODE: %s\n" % str(returncode))
    if (len(stdout.strip()) > 0):
       logsink.log("   STDOUT:\n%s\n" % stdout)
    if (len(stderr.strip()) > 0):
       logsink.log("   STDERR:\n%s\n" % stderr)
    if (returncode <> 0):
       raise Exception, "execute failed with error output:\n%s" % stderr
    return stdout

"logsink"可以是任何具有日志方法的Python对象。我通常使用它将日志数据转发到特定文件、回显到控制台、同时执行两者或其他操作...
这个方法非常有效,但存在三个问题,我需要比communicate()提供更细粒度的控制:
1.在控制台上,stdout和stderr输出可能会交错,但上述函数会将它们分开记录。这可能会使日志的解释变得复杂。如何记录stdout和stderr行,按照它们输出的顺序交错记录? 2.上述函数只会在命令完成后记录命令输出。当命令陷入无限循环或因其他原因需要很长时间时,这会使问题的诊断变得复杂。如何在命令仍在执行时实时获取日志? 3.如果日志很大,很难解释哪个命令生成了哪个输出。是否有一种方法可以在每行前加上某些内容(例如cmd字符串的第一个单词后跟:)?

日志包有什么问题?为什么不使用它? - S.Lott
@S.Lott:execute函数只需要有一个输出的对象即可。使用logging.Logger对象会使事情变得复杂,因为execute函数需要知道在哪个级别记录日志(debug、info、warning等)。这些问题超出了execute函数的职责范围。 - Wim Coenen
4个回答

5

如果您只想将输出保存到文件以供以后评估,则可以重定向到文件。

您已经通过stdout=/stderr=方法定义了要执行的进程的标准输出/标准错误。

在您的示例代码中,您只是将输出重定向到脚本当前的out/err分配。

subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

sys.stdout和sys.stderr只是类似文件的对象。正如sys.stdout文档中所提到的,“只要具有接受字符串参数的write()方法,任何对象都可以接受。”

f = open('cmd_fileoutput.txt', 'w')
subprocess.Popen(cmd, shell=True, stdout=f, stderr=f)

所以你只需要给它一个具有write方法的类,就可以重新定向输出。

如果你想要同时在控制台和文件中输出,可以创建一个管理输出的类。

常规重定向:

# Redirecting stdout and stderr to a file
f = open('log.txt', 'w')
sys.stdout = f
sys.stderr = f

创建一个重定向类:
# redirecting to both
class OutputManager:
    def __init__(self, filename, console):
        self.f = open(filename, 'w')
        self.con = console

    def write(self, data):
        self.con.write(data)
        self.f.write(data)

new_stdout = OutputManager("log.txt", sys.stdout)

交错取决于缓冲区,因此您可能会得到您期望的输出,也可能不会。 (您可以尝试关闭或减少使用的缓冲区,但我暂时不记得如何操作)


os.write(self.f.fileno(), data) 如果你需要确保无缓存。 - Ali Afshar
+1 我之前没有意识到可以通过提供自己的文件对象实现来利用Python的鸭子类型。我会尝试一下,如果成功了就会发布更新后的“execute”函数。 - Wim Coenen
wconenen - 是的,这是Python的一个很棒的特性,你可以轻松地通过模拟替换接口。 - monkut
1
我刚刚尝试了一下,得到了“AttributeError:OutputManager实例没有'fileno'属性”的错误。文件对象文档表示,除非它是真正的文件,否则不应该实现这个属性。因此,似乎subprocess模块对除真正的文件以外的任何内容都不满意。 - Wim Coenen
我假设你正在使用 Ali A. 提到的 f.fileno() 缓冲区零方法。如果你不编写该方法,它将不可用,只有在你要写入实际文件时才使用该方法。 - monkut
@monkut:不,我是将您的OutputManager类作为subprocess.Popen中的stdout参数使用。 我自己不调用fileno()函数。 - Wim Coenen

2

+1 表示在不重新发明轮子的情况下完成了大部分工作。我猜可以通过替换 .before 函数输出中的换行符来完成前缀添加。 - Wim Coenen
我刚刚尝试了一下,结果发现这个只有UNIX系统可用。当它尝试加载Windows系统上不可用的标准“resource”模块时,会抛出一个错误。 - Wim Coenen
真遗憾,我只在UNIX系统上使用Python,所以我没有考虑可移植性。 - SvenAron

1

另外一个选项:

def run_test(test_cmd):
    with tempfile.TemporaryFile() as cmd_out:
        proc = subprocess.Popen(test_cmd, stdout=cmd_out, stderr=cmd_out)
        proc.wait()
        cmd_out.seek(0)
        output = "".join(cmd_out.readlines())
    return (proc.returncode, output)

这将按需要交替输出stdoutstderr,并将其写入一个真实文件中,使您方便地打开。


1

这绝不是一个完整或详尽的答案,但也许你应该研究一下 Fabric 模块。

http://docs.fabfile.org/0.9.1/

使并行执行 shell 命令和错误处理变得非常容易。

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