Capturando Saída de Comandos do Console com Python

A execução de comandos externos a partir de um script Python é uma necessidade comum em diversas aplicações, como automação de tarefas, integração com ferramentas de linha de comando ou monitoramento de sistemas. Mais importante do que apenas executar o comando, é a capacidade de capturar e processar a sua saída, seja ela de forma completa após a finalização ou em tempo real enquanto o comando ainda está em execução.

Python oferece módulos poderosos para interagir com o shell do sistema operacional e processos externos. Este artigo explora as principais abordagens utilizando os módulos subprocess e os para gerneciar a saída de comandos.

  1. Utilizando o Módulo subprocess

O módulo subprocess é a maneira recomendada e mais flexível de interagir com processos externos em Python. Ele permite a criação de novos processos, a conexão aos seus canais de entrada/saída/erro e a obtenção de seus códigos de retorno.

Captura de Saída Completa (Não-Interativa)

Para comandos onde a saída é necessária apenas após sua conclusão, o método subprocess.run() é ideal. Ele executa o comando, aguarda sua finalização e retorna um objeto CompletedProcess contendo a saída, erros e código de retorno.

import subprocess
import locale
import platform

def executar_e_obter_saida(lista_comando: list) -> subprocess.CompletedProcess:
    """
    Executa um comando e captura sua saída completa após a finalização.
    """
    # Determina a codificação preferencial do sistema para decodificação
    encoding_sistema = locale.getpreferredencoding(do_setlocale=True)

    try:
        # subprocess.run é o método recomendado para executar comandos
        # e aguardar sua conclusão.
        # capture_output=True redireciona stdout e stderr para atributos do resultado.
        # text=True decodifica automaticamente a saída e erro usando a codificação especificada.
        resultado = subprocess.run(
            lista_comando,
            capture_output=True,
            text=True,
            encoding=encoding_sistema,
            check=False  # Define se deve levantar CalledProcessError para códigos de retorno != 0
        )
        return resultado
    except FileNotFoundError:
        print(f"Erro: O comando '{lista_comando[0]}' não foi encontrado no sistema.", file=sys.stderr)
        # Retorna um objeto CompletedProcess com erro para manter a consistência
        return subprocess.CompletedProcess(lista_comando, 1, stdout="", stderr=f"Comando '{lista_comando[0]}' não encontrado.")
    except Exception as e:
        print(f"Erro inesperado ao executar o comando: {e}", file=sys.stderr)
        return subprocess.CompletedProcess(lista_comando, 1, stdout="", stderr=str(e))

if __name__ == "__main__":
    # Exemplo de comando 'ping' adaptado para diferentes sistemas operacionais
    if platform.system() == "Windows":
        comando_teste_sucesso = ["ping", "google.com", "-n", "4"]
        comando_teste_falha = ["ping", "siteinexistente.xyz", "-n", "1"]
    else: # Linux, macOS
        comando_teste_sucesso = ["ping", "-c", "4", "google.com"]
        comando_teste_falha = ["ping", "-c", "1", "siteinexistente.xyz"]

    print("--- Execução de comando com sucesso ---")
    saida_sucesso = executar_e_obter_saida(comando_teste_sucesso)
    print("Saída Padrão:\n", saida_sucesso.stdout)
    print("Saída de Erro:\n", saida_sucesso.stderr)
    print(f"Código de Retorno: {saida_sucesso.returncode}\n")

    print("--- Execução de comando com falha (domínio inválido) ---")
    saida_falha = executar_e_obter_saida(comando_teste_falha)
    print("Saída Padrão:\n", saida_falha.stdout)
    print("Saída de Erro:\n", saida_falha.stderr)
    print(f"Código de Retorno: {saida_falha.returncode}\n")

    print("--- Execução de comando inexistente ---")
    saida_comando_nao_encontrado = executar_e_obter_saida(["comando_que_nao_existe_123"])
    print("Saída Padrão:\n", saida_comando_nao_encontrado.stdout)
    print("Saída de Erro:\n", saida_comando_nao_encontrado.stderr)
    print(f"Código de Retorno: {saida_comando_nao_encontrado.returncode}\n")

Saída de Exemplo (Sucesso):

--- Execução de comando com sucesso ---
Saída Padrão:
 Pinging google.com [142.250.78.142] with 32 bytes of data:
 Reply from 142.250.78.142: bytes=32 time=12ms TTL=119
 Reply from 142.250.78.142: bytes=32 time=13ms TTL=119
 Reply from 142.250.78.142: bytes=32 time=13ms TTL=119
 Reply from 142.250.78.142: bytes=32 time=13ms TTL=119

 Ping statistics for 142.250.78.142:
     Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
 Approximate round trip times in milli-seconds:
     Minimum = 12ms, Maximum = 13ms, Average = 12ms

Saída de Erro:

Código de Retorno: 0

Saída de Exemplo (Falha - Domínio Inválido):

--- Execução de comando com falha (domínio inválido) ---
Saída Padrão:
 Ping request could not find host siteinexistente.xyz. Please check the name and try again.

Saída de Erro:

Código de Retorno: 1

Captura de Saída em Tempo Real (Interativa)

Quando é necessário processar a saída de um comando à medida que ela é gerada, como para mostrar o prgoresso ou para interagir com o processo, subprocess.Popen() é a escolha apropriada. Com Popen, você cria um objeto de processo que pode ser lido linha por linha ou caractere por caractere.

import subprocess
import locale
import sys
import platform

def executar_e_ler_em_tempo_real(comando_args: list) -> int:
    """
    Executa um comando externo e exibe sua saída em tempo real.
    Retorna o código de saída do processo.
    """
    encoding_sistema = locale.getpreferredencoding(do_setlocale=True)
    processo = None
    try:
        print(f"Iniciando a execução em tempo real: {' '.join(comando_args)}\n")

        # Popen permite interação com o processo filho.
        # stdout=subprocess.PIPE e stderr=subprocess.PIPE redirecionam as saídas.
        # bufsize=1 garante que as saídas são armazenadas em buffer linha por linha.
        processo = subprocess.Popen(
            comando_args,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=False, # Manter como bytes para decodificar linha por linha
            bufsize=1
        )

        # Usar um loop para ler a saída padrão linha por linha
        # A leitura em tempo real é mais complexa e pode exigir threads para stderr e stdout
        # em paralelo, mas para exemplos simples, ler stdout primeiro é comum.
        for linha_bytes in processo.stdout:
            try:
                linha_decodificada = linha_bytes.decode(encoding_sistema, errors='replace')
                sys.stdout.write(linha_decodificada)
                sys.stdout.flush() # Garante que a saída é impressa imediatamente
            except UnicodeDecodeError:
                sys.stderr.write(f"Erro de decodificação no stdout: {linha_bytes}\n")
                sys.stderr.flush()

        # Após esvaziar o stdout, ler o stderr restante
        erros_bytes = processo.stderr.read()
        if erros_bytes:
            try:
                erros_decodificados = erros_bytes.decode(encoding_sistema, errors='replace')
                sys.stderr.write(f"\n--- Erros detectados durante a execução ---\n{erros_decodificados}")
                sys.stderr.flush()
            except UnicodeDecodeError:
                sys.stderr.write(f"\nErro de decodificação no stderr: {erros_bytes}\n")
                sys.stderr.flush()

        # Aguarda o processo terminar e retorna seu código de saída
        codigo_retorno = processo.wait()
        print(f"\nExecução finalizada. Código de retorno: {codigo_retorno}")
        return codigo_retorno

    except FileNotFoundError:
        print(f"Erro: Comando '{comando_args[0]}' não encontrado.", file=sys.stderr)
        return -1
    except Exception as e:
        print(f"Ocorreu um erro: {e}", file=sys.stderr)
        if processo:
            processo.kill() # Tenta matar o processo se algo der errado
        return -1

if __name__ == "__main__":
    # Exemplo de comando 'ping' adaptado para diferentes sistemas operacionais
    if platform.system() == "Windows":
        comando_sucesso = ["ping", "google.com", "-n", "4"]
        comando_falha = ["ping", "site_que_nao_existe.com", "-n", "1"]
    else: # Linux, macOS
        comando_sucesso = ["ping", "-c", "4", "google.com"]
        comando_falha = ["ping", "-c", "1", "site_que_nao_existe.com"]

    print("\n--- Teste de sucesso (ping google.com) ---")
    retorno_sucesso = executar_e_ler_em_tempo_real(comando_sucesso)

    print("\n--- Teste de falha (domínio inexistente) ---")
    retorno_falha_dominio = executar_e_ler_em_tempo_real(comando_falha)

    print("\n--- Teste de falha (comando inexistente) ---")
    retorno_falha_comando = executar_e_ler_em_tempo_real(["comando_falso_abc_xyz", "parametro"])

    print(f"\nResumo dos códigos de retorno: Sucesso={retorno_sucesso}, FalhaDomínio={retorno_falha_dominio}, FalhaComando={retorno_falha_comando}")

Saída de Exemplo (Sucesso em Tempo Real):

--- Teste de sucesso (ping google.com) ---
Iniciando a execução em tempo real: ping google.com -n 4

Pinging google.com [142.250.78.142] with 32 bytes of data:
Reply from 142.250.78.142: bytes=32 time=13ms TTL=119
Reply from 142.250.78.142: bytes=32 time=13ms TTL=119
Reply from 142.250.78.142: bytes=32 time=12ms TTL=119
Reply from 142.250.78.142: bytes=32 time=12ms TTL=119

Ping statistics for 142.250.78.142:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 12ms, Maximum = 13ms, Average = 12ms

Execução finalizada. Código de retorno: 0

  1. Uma Abordagem Mais Simples com os.popen

O módulo os oferece a função os.popen(), uma alternativa mais antiga e de nível mais alto para executar comandos e obter sua saída. Embora seja mais simples de usar para casos básicos, subprocess é geralmente preferível por sua flexibilidade e segurança, especialmente ao lidar com argumentos de comando complexos ou interação com processos.

import os
import locale
import platform

def obter_saida_os_popen(comando_string: str) -> str:
    """
    Executa um comando usando os.popen e retorna sua saída completa.
    """
    # Determina a codificação preferencial do sistema
    encoding_sistema = locale.getpreferredencoding(do_setlocale=True)
    try:
        # os.popen retorna um objeto semelhante a arquivo.
        # É importante usar 'with' para garantir que o fluxo seja fechado.
        # A codificação pode ser especificada a partir do Python 3.6.
        with os.popen(comando_string, 'r', encoding=encoding_sistema, errors='replace') as fluxo_saida:
            saida_completa = fluxo_saida.read()
        return saida_completa
    except Exception as e:
        return f"Erro ao executar o comando com os.popen: {e}"

if __name__ == "__main__":
    # Exemplo de comando 'ping' adaptado para diferentes sistemas operacionais
    if platform.system() == "Windows":
        comando_sucesso_str = "ping google.com -n 4"
        comando_falha_str = "ping site_sempre_falha.com -n 1"
    else: # Linux, macOS
        comando_sucesso_str = "ping -c 4 google.com"
        comando_falha_str = "ping -c 1 site_sempre_falha.com"

    print("--- Saída de os.popen (sucesso) ---")
    resultado_sucesso = obter_saida_os_popen(comando_sucesso_str)
    print(resultado_sucesso)

    print("\n--- Saída de os.popen (falha - domínio inválido) ---")
    resultado_falha = obter_saida_os_popen(comando_falha_str)
    print(resultado_falha)

Saída de Exemplo (Sucesso):

--- Saída de os.popen (sucesso) ---
Pinging google.com [142.250.78.142] with 32 bytes of data:
Reply from 142.250.78.142: bytes=32 time=12ms TTL=119
Reply from 142.250.78.142: bytes=32 time=13ms TTL=119
Reply from 142.250.78.142: bytes=32 time=13ms TTL=119
Reply from 142.250.78.142: bytes=32 time=13ms TTL=119

Ping statistics for 142.250.78.142:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 12ms, Maximum = 13ms, Average = 12ms

Saída de Exemplo (Falha):

--- Saída de os.popen (falha - domínio inválido) ---
Ping request could not find host site_sempre_falha.com. Please check the name and try again.

Tags: Python subprocess os ConsoleOutput CommandExecution

Publicado em 7-4 21:54