Execução de Comandos de Shell em Go com o Pacote os/exec

Captura de Saída Padrão e Erros

A biblioteca padrão os/exec oferece métodos diretos para capturar o retorno de processos externos. Utilize CombinedOutput() quando precisar mesclar stdout e stderr em um único fluxo, ou Output() para isolar apenas a saída padrão.

package main

import (
	"fmt"
	"log"
	"os/exec"
)

func main() {
	// Mescla stdout e stderr
	proc := exec.Command("hostname")
	resultado, erroExec := proc.CombinedOutput()
	if erroExec != nil {
		log.Fatalf("falha na execução: %v", erroExec)
	}
	fmt.Printf("Saída combinada: %s\n", resultado)

	// Retorna exclusivamente stdout
	procSistema := exec.Command("uptime")
	saidaLimpa, erroSistema := procSistema.Output()
	if erroSistema != nil {
		log.Fatal(erroSistema)
	}
	fmt.Printf("Uptime: %s", saidaLimpa)
}

Isolamento de Streams com Buffers

Para tratar stdout e stderr de forma independente, atribua instânciass de bytes.Buffer aos campos correspondentes do comando. Caso deseje espelhar a saída diretamente no terminal do usuário, basta apontar para os.Stdout e os.Stderr.

var bufSaida, bufErro bytes.Buffer
tarefa := exec.Command("df", "-h")
tarefa.Stdout = &bufSaida
tarefa.Stderr = &bufErro

if erro := tarefa.Run(); erro != nil {
	log.Printf("Erro ao executar: %v", erro)
}
fmt.Println("STDOUT:", bufSaida.String())
fmt.Println("STDERR:", bufErro.String())

// Redirecionamento direto para o console
tarefaConsole := exec.Command("df", "-h")
tarefaConsole.Stdout = os.Stdout
tarefaConsole.Stderr = os.Stderr
tarefaConsole.Run()

Execução Assíncrona e Concorrente

O método Run() é bloqueante. Para iniciar processos em segundo plano e capturar fluxos simultaneamente, utilize Start() combinado com Wait() e goroutines. Essa abordagem é ideal para comandos de longa duração ou que geram saída contínua.

var cacheOut, cacheErr bytes.Buffer
processo := exec.Command("ping", "-c", "3", "127.0.0.1")
pipeOut, _ := processo.StdoutPipe()
pipeErr, _ := processo.StderrPipe()

if erro := processo.Start(); erro != nil {
	log.Fatalf("Início falhou: %v", erro)
}

go io.Copy(io.MultiWriter(os.Stdout, &cacheOut), pipeOut)
go io.Copy(io.MultiWriter(os.Stderr, &cacheErr), pipeErr)

if erro := processo.Wait(); erro != nil {
	log.Fatalf("Processo finalizado com erro: %v", erro)
}

Injeção de Variáveis de Ambiente

Por padrão, o processo herda o ambiente do programa Go. Para sobrescrever ou adicionar variáveis específicas, modifique a slice cmd.Env. É recomendável partir de os.Environ() para manter variáveis críticas do sistema.

cmd := exec.Command("printenv", "MEU_APP_ENV")
ambienteBase := os.Environ()
ambienteCustom := append(ambienteBase, "MEU_APP_ENV=producao_v2")
cmd.Env = ambienteCustom

res, err := cmd.CombinedOutput()
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Variável capturada: %s", res)

Verificação de Disponibilidade do Binário

Entes de invocar um executável, valide sua presença no $PATH utilizando exec.LookPath. Isso evita erros de execução em tempo de runtime e permite fallbacks elegantes.

func verificarBinario(nome string) bool {
	caminho, err := exec.LookPath(nome)
	if err != nil {
		fmt.Printf("Binário '%s' não localizado no PATH.\n", nome)
		return false
	}
	fmt.Printf("Encontrado em: %s\n", caminho)
	return true
}

Encadeamento e Pipes

O exec.Command não interpreta operadores de shell como |, && ou >. Para encadear comandos, conecte manualmente o StdoutPipe de um processo ao Stdin do seguinte, ou delegue a interpretação para um shell.

// Método nativo com pipes manuais
origem := exec.Command("echo", "golang é eficiente")
destino := exec.Command("wc", "-w")

pipeLeitura, _ := origem.StdoutPipe()
destino.Stdin = pipeLeitura
destino.Stdout = os.Stdout

destino.Start()
origem.Run()
destino.Wait()

// Alternativa via interpretador shell
script := "ps aux | grep init | awk '{print $2}'"
saida, _ := exec.Command("sh", "-c", script).Output()
fmt.Println(saida)

Leitura Linha a Linha

Para processar saídas extenass sem carregar tudo na memória, utilize bufio.Scanner conectado ao pipe de saída. Essa abordagem é mais segura e idiomática que leituras baseadas em delimitadores manuais.

proc := exec.Command("seq", "1", "5")
stream, _ := proc.StdoutPipe()
proc.Start()

scanner := bufio.NewScanner(stream)
for scanner.Scan() {
	linha := strings.TrimSpace(scanner.Text())
	log.Printf("Linha processada: %s", linha)
}
proc.Wait()

Tratamento de Códigos de Saída

Quando um comando falha, o erro retornado pode ser convertido para *exec.ExitError. Versões modernas do Go expõem diretamente o código de retorno via método ExitCode(), eliminando a necessidade de conversões manuais com syscall.WaitStatus.

func executarComRetorno(nome string, args ...string) (int, string) {
	var buffer bytes.Buffer
	c := exec.Command(nome, args...)
	c.Stdout = &buffer
	c.Stderr = &buffer

	err := c.Run()
	saida := buffer.String()

	if err != nil {
		if erroSaida, ok := err.(*exec.ExitError); ok {
			return erroSaida.ExitCode(), saida
		}
		return -1, fmt.Sprintf("erro de sistema: %v", err)
	}
	return 0, saida
}

Tags: Golang os-exec Concurrency system-calls io-pipes

Publicado em 8-29 13:25