скрипт python с использованием подпроцесса, перенаправить ВСЕ выходные данные в файл

Я пишу что-то для статического анализа исходного кода на разных языках. Поскольку все должно быть с открытым исходным кодом и вызываться из командной строки, я загрузил по одному инструменту для каждого языка. Поэтому я решил написать скрипт на Python, перечисляющий все исходные файлы в папке проекта и вызывающий соответствующий инструмент.

Итак, часть моего кода выглядит так:

import os
import sys
import subprocess
from subprocess import call
from pylint.lint import Run as pylint


class Analyser:

    def __init__(self, source=os.getcwd(), logfilename=None):

        # doing initialization stuff
        self.logfilename = logfilename or 'CodeAnalysisReport.log'

        self.listFiles()
        self.analyseFiles()


    def listFiles(self):
    # lists all source files in the specified directory


    def analyseFiles(self):

        self.analysePythons()
        self.analyseCpps()
        self.analyseJss()
        self.analyseJavas()
        self.analyseCs()


if __name__ == '__main__':

    Analyser()

Давайте взглянем на часть файлов C++ (я использую Cppcheck для их анализа):

    def analyseCpps(self):

        for sourcefile in self.files['.cc'] + self.files['.cpp']:
            print '\n'*2, '*'*70, '\n', sourcefile
            call(['C:\\CodeAnalysis\\cppcheck\\cppcheck', '--enable=all', sourcefile])

Вывод консоли для одного из файлов (это просто случайно загруженный файл):

**********************************************************************
C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc
Checking C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc...
[C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc:18]: (style) The scope of the variable 'oldi' can be reduced.
[C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc:43]: (style) The scope of the variable 'lastbit' can be reduced.
[C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc:44]: (style) The scope of the variable 'two_to_power_i' can be reduced.
(information) Cppcheck cannot find all the include files (use --check-config for details)

Строки 1 и 2 взяты из моего скрипта, строки с 3 по 7 взяты из Cppcheck.

И это то, что я хочу сохранить в свой лог-файл, для всех остальных файлов тоже. Все в одном файле.

Конечно, я искал SO и нашел несколько методов. Но ни один не работает полностью.

Первая попытка:

Добавление sys.stdout = open(self.logfilename, 'w') в мой конструктор. Это заставляет строки 1 и 2 показанного выше вывода записываться в мой файл журнала. Остальное по-прежнему отображается на консоли.

Вторая попытка:

Кроме того, в analyseCpps я использую:

call(['C:\CodeAnalysis\cppcheck\cppcheck', '--enable=all', sourcefile], stdout=sys.stdout)

Это делает мой файл журнала:

Checking C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc...


********************************************************************** 
C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc

и вывод консоли:

[C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc:18]: (style) The scope of the variable 'oldi' can be reduced.
[C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc:43]: (style) The scope of the variable 'lastbit' can be reduced.
[C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc:44]: (style) The scope of the variable 'two_to_power_i' can be reduced.

Не то, что я хочу.

Третья попытка:

Использование Popen с pipe. sys.stdout возвращается к умолчанию.

В качестве предварительной работы analyseCpps сейчас стоит:

for sourcefile in self.files['.cc'] + self.files['.cpp']:

    print '\n'*2, '*'*70, '\n', sourcefile
    p = subprocess.Popen(['C:\\CodeAnalysis\\cppcheck\\cppcheck', '--enable=all', sourcefile], stdout=subprocess.PIPE)
    p.stdout.read()

p.stdout.read() показывает только последнюю строку желаемого вывода (строка 7 в поле кода 3)

Четвертая попытка:

Использование subprocess.Popen(['C:\CodeAnalysis\cppcheck\cppcheck', '--enable=all', sourcefile], stdout=open(self.logfilename, 'a+')) просто записывает одну строку Checking C:\CodeAnalysis\testproject\cpp\BiggestUnInt.cc... в мой лог-файл, остальное отображается в консоли.

Пятая попытка:

Вместо subprocess.Popen я использую os.system, поэтому моя вызывающая команда:

os.system('C:\CodeAnalysis\cppcheck\cppcheck --enable=all %s >> %s' % (sourcefile, self.logfilename))

Это приводит к тому же файлу журнала, что и моя четвертая попытка. Если я наберу ту же команду прямо в консоли Windows, результат будет таким же. Итак, я предполагаю, что это не совсем проблема с python, но все же:

Если он есть на консоли, должен быть способ поместить его в файл. Есть идеи?


Э Д И Т

Глупый я. Я все еще нуб, поэтому я забыл о stderr. Вот куда направляются решающие сообщения.

Итак, теперь у меня есть:

def analyseCpps(self):

    for sourcefile in self.files['.cc'] + self.files['.cpp']:
        p = subprocess.Popen(['C:\\CodeAnalysis\\cppcheck\\cppcheck', '--enable=all', sourcefile], stderr=subprocess.PIPE)
        with open(self.logfilename, 'a+') as logfile:
            logfile.write('%s\n%s\n' % ('*'*70, sourcefile))
            for line in p.stderr.readlines():
                logfile.write('%s\n' % line.strip())

и это работает нормально.


ДРУГОЕ ИЗМЕНЕНИЕ

согласно ответу Дидье:

с sys.stdout = open(self.logfilename, 'w', 0) в моем конструкторе:

def analyseCpps(self):

    for sourcefile in self.files['.cc'] + self.files['.cpp']:
        print '\n'*2, '*'*70, '\n', sourcefile
        p = subprocess.Popen(['C:\\CodeAnalysis\\cppcheck\\cppcheck', '--enable=all', sourcefile], stdout=sys.stdout, stderr=sys.stdout)

person seesharp    schedule 12.08.2015    source источник
comment
Спасибо за ваши быстрые комментарии и ответы, вы все в порядке.   -  person seesharp    schedule 12.08.2015
comment
связанные: Перенаправить стандартный вывод в файл в Python?   -  person jfs    schedule 13.08.2015
comment
несвязанный: не используйте for line in p.stderr.readlines():, работает только for line in p.stderr:. Также вы можете написать shutil.copyfileobj(p.stderr, logfile). Кроме того, вы можете использовать stderr=subprocess.STDOUT. Кроме того, вы можете использовать мой ответ для перенаправления stdout на уровне файлового дескриптора и объединить stderr с stdout -- удалить параметры stdout, stderr в этом случае -- они обрабатываются автоматически.   -  person jfs    schedule 13.08.2015


Ответы (3)


Есть несколько проблем:

  • вы должны перенаправить как stdout, так и stderr
  • вам следует использовать небуферизованные файлы, если вы хотите смешивать обычную печать и вывод запущенных команд.

Что-то вроде этого:

import sys, subprocess

# Note the 0 here (unbuffered file)
sys.stdout = open("mylog","w",0)

print "Hello"
print "-----"

subprocess.call(["./prog"],stdout=sys.stdout, stderr=sys.stdout)
print "-----"
subprocess.call(["./prog"],stdout=sys.stdout, stderr=sys.stdout)
print "-----"

print "End"
person Didier Spezia    schedule 12.08.2015
comment
Это на самом деле довольно круто. Я повторно изменю свою реализацию. - person seesharp; 12.08.2015
comment
@seesharp: кажется хрупким перенаправлять вывод каждого вызова подпроцесса вручную, и это не удается, если какой-то код записывает в стандартный вывод напрямую (os.write(1, b"won't catch me")). Лучшей альтернативой является перенаправление вывода в коде, запускающем скрипт Python: python your_script.py &> log (bash) или перенаправление с помощью os.dup2() (также работает в Windows). Смотрите ссылки в моих комментариях к вашему вопросу. - person jfs; 13.08.2015
comment
Я использую Python 2.7 (не упоминал, позор мне), поэтому contextlib.redirect_stdout() нет. Более того, я решил не перенаправлять каждый вывод в файл. Поскольку для запуска всего сценария требуется приличное количество времени, я использую операторы печати для отображения прогресса, а выходные данные подпроцессов направляются непосредственно в мой файл журнала. Пока это работает, поэтому я оставлю это так, но я учту ваш ответ на будущее. - person seesharp; 14.08.2015

Вам также нужно перенаправить stderr, вы можете использовать STDOUT или передать файловый объект в stderr=:

from subprocess import check_call,STDOUT
with open("log.txt","w") as f:
     for sourcefile in self.files['.cc'] + self.files['.cpp']:
        check_call(['C:\\CodeAnalysis\\cppcheck\\cppcheck', '--enable=all', sourcefile],
                   stdout=f, stderr=STDOUT)
person Padraic Cunningham    schedule 12.08.2015

Попробуйте перенаправить stdout и stderr в файл журнала:

import subprocess

def analyseCpps(self):
     with open("logfile.txt", "w") as logfile:
         for sourcefile in self.files['.cc'] + self.files['.cpp']:
             print '\n'*2, '*'*70, '\n', sourcefile
             call(['C:\\CodeAnalysis\\cppcheck\\cppcheck',
                   '--enable=all', sourcefile], stdout=logfile,
                   stderr=subprocess.STDOUT)

В этом примере имя файла жестко закодировано, но вы сможете легко изменить его (на свое self.logfilename или подобное).

person chris-sc    schedule 12.08.2015
comment
Ну, я только что понял, что сообщения попадают в stderr, и собирался сам набрать ответ :-). Так что вы правы. Я отредактирую рабочий код в ответе. - person seesharp; 12.08.2015