Python como alternativa a Bash Script

Sempre que precisava automatizar alguma tarefa no linux, apelava para scripts bash. O problema é que escrevo scripts com pouca frequência e somada à sintaxe peculiar dos comandos bash, sempre preciso consultar referências e acabo perdendo bastante tempo.

Pra piorar, algumas tarefas como processamento de strings são complicadas de fazer em bash script e então normalmente usam-se ferramentas auxiliares como sed e awk. A sintaxe delas é bem concisa e elegante, mas acho difícil dominá-las usando-as tão pouco.

Foi por isso que decidi investir um tempo pesquisando modos de substituir bash script por python.

No meu ponto de vista, há um tradeoff entre usar bash script e python. O primeiro faz comunicação direta com o sistema operacional, sendo trivial executar programas, redirecionar entrada/saída ou manipular diretórios, mas é complicado para se fazer processamentos em geral. Já python é o oposto: é bem simples de se programar mas para comunicar com o sistema operacional é necessário usar API’s.

Módulo os

Algumas tarefas de manipulação de diretórios ficam relativamente simples usando o módulo os. Comandos como mkdir, chmod, chown têm suas versões homônimas nesse módulo. Outras comuns mas com nomes ligeiramente diferentes são

* ls — os.listdir(path) — retorna uma lista com os nomes dos arquivos no diretório apontado por path.

* pwd — os.getcwd() — retorna o nome do diretório corrente.

Além dessas, temos também algumas de manipulação de caminhos no módulo os.path. As que eu mais usei até agora,

* os.path.join — Sintaxe: os.path.join(path1[, path2[, …]])

Recebe um conjunto de strings e as concatena em ordem para formar um caminho. Exemplo:

print os.path.join('/home', 'joao/', 'musica')

Vai imprimir

/home/joao/musica

Nota: como está dito no manual, se alguma das strings representar um caminho absoluto, ele ignora todas as strings anteriores. No linux o caminho absoluto é iniciado por /. Por exemplo:

print os.path.join('/home', 'joao/', '/musica')

Vai imprimir

/musica

* os.path.exists — Sintaxe: os.path.exists(path)

Esse comando retorna True se o caminho path existe e False caso contrário.

Nota: esse comando não diferencia arquivos e diretórios. Assim, se existir o diretório foo/ mas você estiver procurando um arquivo chamado foo, o comando mencionado vai retornar True mesmo que o arquivo não exista. Para obter o comportamento esperado, deve-se usar o comando os.path.isfile (ou os.path.isdir na situação oposta).

Módulo subprocess

A criação de processos dentro de scripts python ficava no módulo os, mas está depreciada desde a versão 2.6. Essa tarefa agora está implementada no módulo subprocess.

O principal componente desse módulo é a classe Popen, que recebe vários parâmetros. O primeiro deles é uma lista contendo o nome do programa a ser executado e os argumentos. Depois tem diversos outros, mas os que achei mais importantes são stdin, stderr, stdout.

Exemplo 1 — Executando um processo com argumentos

Imagine um programa chamado ./print que imprime o primeiro e o segundo parâmetro que ele receber. Em C++ pode ser algo como:

#include <iostream>
int main (int argc, char **argv){
    std::cout << argv[1] << " -- " << argv[2] << "\n"; 
}

No bash, se quisermos enviar um argumento para o programa ./print que contenha espaço, devemos pô-lo entre aspas. Por exemplo,

./print "ola mundo" "tudo bem?"

Vai imprimir: ola mundo -- tudo bem?

A vantagem de se usar uma lista de strings é que podemos especificar qual é argumento é qual. Podemos fazer assim:

import subprocess
p = subprocess.Popen(['./print', 'ola mundo',
                      'tudo bem?'])
p.wait()

Exemplo 2 — Processando stdout

No exemplo anterior, a string será impressa na saída padrão (stdout), mas o código python não terá acesso a ela. Se quisermos processar a string impressa por ./print, podemos redirecionar a saída usando pipes, como no bash.

Para fazer isso, usa-se a variável especial subprocess.PIPE. Por exemplo:

import subprocess
import sys
p = subprocess.Popen(['./print', 'oi', 'mundo'],
                     stdout=subprocess.PIPE)
saida = p.communicate()
sys.stdout.write(saida[0].upper())

Nesse caso, o método communicate() serve para fazer a comunicação com o processo. Ele retorna uma lista de dois elementos [stdout, stderr] e também pode receber como argumento a string que será redirecionada para o stdin do processo. No exemplo, só estamos redirecionando o stdout para o objeto, então saida[1] = None, enquanto que saida[0] contém a linha impressa por ./print. Estamos convertendo tudo para caixa alta, então o programa acima imprime:

OI -- MUNDO

Exemplo 3 — Usando stdin e stderr

Para completar, um exemplo onde usamos o stdin e o stdout. Seja ./read um programa que lê um inteiro da entrada padrão e imprima na saída de erro esse número multiplicado por 2, como o código C++ a seguir:

#include <iostream>
int main(){
    int n;
    std::cin >> n;
    std::cerr << 2*n << '\n';
}

Queremos executar esse programa, enviar um número na entrada padrão dele e ler da saída de erro. Podemos fazer isso com o seguinte código:

import subprocess
import sys
p = subprocess.Popen(['./read'],
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE)
sys.stdout.write(p.communicate('2')[1])

Nesse caso redirecionamos o stdin e o stderr. Conforme eu havia dito, o método communicate recebe como parâmetro os dados que serão enviados para a entrada padrão do processo. Além disso, o conteúdo da saída de erro é o segundo elemento da lista retornada.

Exemplo 4 — Redirecionamento para arquivo

Em bash script é comum redirecionarmos saída para arquivos com o operador ‘>’. Para fazer isso usando subprocess, temos que abrir o arquivo explicitamente com open e passar o objeto retornado como parâmetro para o stdout do Popen. Exemplo:

import subprocess
f = open('tmp.txt', 'w')
subprocess.call(['ls'], stdout=f)
f.close()

É comum redirecionarmos o stderr para o /dev/null para que ele não seja impresso no terminal. Se quisermos fazer isso com subprocess, basta abrir o arquivo '/dev/null' e passar o objeto para stderr.

Funções auxiliares

Adicionalmente há algumas funções para simplificar a tarefa dependendo das suas necessidades.

subprocess.call — executa um comando, espera o processo terminar e devolve o código retornado pelo processo. Seria adequado para o Exemplo 1.

subprocess.check_output — Executa um comando e retorna uma string com o conteúdo da saída padrão — só está disponível a partir da versão 2.7. Seria adequado para o Exemplo 2.

Nota: Em geral, eu adiciono permissão de execução para meus scripts bash, por exemplo script.sh, e o executo como ./script.sh. Porém, usando subprocess, tem que executá-lo usando o /bin/bash:

p = subprocess.Popen(['/bin/bash', 'script.sh'])

Conclusão

Com esses módulos mencionados, tenho conseguido me virar usando python como script. Agora só uso bash scripting mesmo quando preciso executar uma lista de comandos sem ter que fazer processamento algum.

Referências

[1] 15.1 – Miscellaneous operating system interfaces
[2] 17.1 – Subprocess management
[3] Stackoverflow — How do I check if a file exists using Python?

2 respostas a Python como alternativa a Bash Script

  1. mkdarkness diz:

    Muito boms eu artigo me ajudou bastante a optimizar aqui alguns dos meus scripts!

%d bloggers like this: