Essa semana eu e minha equipe assistimos a uma palestra muito interessante sobre Python, ministrada pelo meu colega João Bueno. Lá pelas tantas ele começou a apresentar uns slides perigosos... cada um comparando Python a uma outra linguagem de programação. Perl, bash, Java e Ruby. Esses slides são perigosos porque comparar decentemente duas linguagens é uma tarefa complexa que não se pode condensar em um só slide. Primeiro é preciso definir um conjunto de critérios objetivos para a comparação. Depois, é preciso levar em conta o contexto de uso da linguagem. Coisas como o domínio das aplicações que serão desenvolvidas, as plataformas de desenvolvimento e de implantação, a experiência dos desenvolvedores com a linguagem, o tamanho da equipe e as restrições de prazo do projeto. Depois disso tudo, é preciso resistir arduamente à tentação de "puxar a sardinha" pro lado da linguagem de nossa predileção pra não parecer que estamos apenas fazendo picuinha.
Mas tudo bem... num grupo pequeno esse tipo de discussão é tão estimulante e inofensiva quanto falar de política, futebol e bolsa de valores num happy hour.
Acho que foi no slide sobre bash que o João sugeriu um problema para o qual uma shell Unix padrão não ofereceria uma solução tão econômica e legível quanto o interpretador de comandos interativo Python. O problema era, mais ou menos, o seguinte. Suponha que haja em um diretório um conjunto de arquivos cujos nomes consistem de um prefixo alfabético, seguido de uma sequência de dígitos e terminando na extensão .jpg. Por exemplo:
$ ls
a0.jpg b1.jpg c123.jpg
O desafio é renomeá-los de modo que todos os arquivos tenham o mesmo número de dígitos. No caso acima, o resultado deveria ser:
a000.jpg,
b001.jpg e
c123.jpg.
Eu saí da palestra com o problema na cabeça e a primeira coisa que fiz foi bolar algumas soluções de uma linha e mandar pra ele por email:
# imprimindo os nomes
$ ls | perl -lpe 's/^([a-z]+)(\d+)\.jpg/sprintf "%s%03d.jpg",$1,$2/e'
a000.jpg
b001.jpg
c123.jpg
# gerando comandos para renomeá-los
$ ls | perl -lpe 's/^([a-z]+)(\d+)\.jpg/sprintf "mv %s %s%03d.jpg",$&,$1,$2/e'
mv a0.jpg a000.jpg
mv b1.jpg b001.jpg
mv c123.jpg c123.jpg
# executando os comandos na shell
$ ls | perl -lpe 's/^([a-z]+)(\d+)\.jpg/sprintf "mv %s %s%03d.jpg",$&,$1,$2/e' | sh
É assim que eu normalmente desenvolvo uma solução na shell. Ao invés de loops eu prefiro usar comandos que gerem outros comandos, como os
mv acima, de modo que eu posso verificar facilmente se estou fazendo a coisa certa. Depois de ter certeza disso, basta acrescentar um "
| sh" no final pra executar os comandos gerados.
Perl tem algumas opções muito úteis na confecção de
one liners como o anterior.
-l, -a, -n, -p e -e são as que eu utilizo mais frequentemente. Execute um "
perldoc perlrun" pra saber mais sobre elas e outras tantas opções interessantes.
Mas, pra não dizer que Perl não pode fazer as coisas sozinho eu acrescentei uma solução que não usa a shell no final.
# fazendo tudo sozinho
$ ls | perl -lne 'if (/^([a-z]+)(\d+)\.jpg/) {rename $_,sprintf "%s%03d.jpg",$1,$2}'
$ ls
a000.jpg b001.jpg c123.jpg
Mas assim como eu sou fã de Perl e o João é fã de Python, o Andreyev é fã de Bash e não deixou barato, mandando o seguinte email pro grupo:
$ ls
a0.jpg b1.jpg c123.jpg
$ for i in *.jpg; do j=${i%*.jpg}; printf "mv %s %s%03d.jpg\n" $i ${j//[0-9]/} ${j//[a-z]/}; done
mv a0.jpg a000.jpg
mv b1.jpg b001.jpg
mv c123.jpg c123.jpg
$ for i in *.jpg; do j=${i%*.jpg}; printf "mv %s %s%03d.jpg\n" $i ${j//[0-9]/} ${j//[a-z]/}; done | sh
$ ls
a000.jpg b001.jpg c123.jpg
$ echo $BASH_VERSION
3.2.25(1)-release
Ninja! Eu vou confessar que nunca tive força de vontade pra aprender esses golpes avançados de manipulação de strings em bash. Pra mim, shell é uma cola que serve pra "grudar" outros comandos. Sempre que eu preciso de algo mais complicado, como estruturas de dados ou expressões regulares, eu não penso duas vezes antes decidir por Perl.
Mas o João pagou pra ver com essa:
> $ python
> Python 2.5.2 (r252:60911, Jul 31 2008, 17:28:52)
> [GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu7)] on linux2
> Type "help", "copyright", "credits" or "license" for more information.
>>>> import os
>>>> for nome in os.listdir("."):
> ... base, numero, ext = nome[0], nome[1:nome.find(".")], nome.split(".")[-1]
> ... os.rename(nome, "%s%03d.%s" % (base, int(numero), ext))
> ...
>>>>
> # readability counts
Ah... que crítica sutil nesse último comentário... "Legibilidade conta."
É?
Mas
Succinctness is Power!
Quando eu quero resolver uma questão com um
one liner a "legibilidade" é irrelevante, porque se eu não vou salvar a solução num script, ninguém mais vai lê-la, certo? Mas vá lá... se fosse pra salvar num script eu provavelmente escreveria algo mais parecido com a sua versão em Python. Algo assim:
opendir CWD, '.';
foreach $nome (readdir CWD) {
if (($base, $numero, $ext) = ($nome =~ /^(.)(\d+)\.(.*)/)) {
rename $nome, sprintf("%s%03d.%s", $base, $numero, $ext);
}
}
closedir CWD;
Hmmm... eu nem tentei quebrar o nome com operações de strings porque eu acho a expressão regular mais direta e, nesse caso, mais legível. Pra ficar ainda mais legível eu substituiria os comandos
opendir, readdir e closedir por um
glob pattern:
foreach $nome (<*.jpg>) {
if (($base, $numero, $ext) = ($nome =~ /^(.)(\d+)\.(.*)/)) {
rename $nome, sprintf("%s%03d.%s", $base, $numero, $ext);
}
}
Melhor, né?
Mas ainda não está bom. Está muito... carregado, sei lá. Uma das grandes diferenças de Perl em relação a maioria das linguagens, e a Python em particular, é que não precisamos ser sempre explícitos. É mais ou menos como usar pronomes ou sujeito oculto. De início você não entende o idioma e fala assim:
- José é casado. José tem cinco filhos. Os filhos de José são todos solteiros.
Depois, você aprende a usar os pronomes e começa a falar de modo mais econômico.
- José é casado. Ele tem cinco filhos. Eles são todos solteiros.
Até que você fica realmente fluente no idioma e fala naturalmente assim:
- José é casado e tem cinco filhos, todos solteiros.
Ininteligível? Só pra quem está só começando a aprender o português. Normalmente conversamos com pessoas que são tão fluentes quanto nós, de modo que podemos, e devemos, ser econômicos e diretos. Evitando redundâncias nós não somos apenas mais diretos. Somos também mais inteligíveis (ou legíveis), porque não inserimos no discurso aquela série de nomes repetidos que acabam poluindo o texto, escondendo o conteúdo real da mensagem.
Bom, tudo isso pra justificar minha próxima versão, na qual eu suprimo a variável
$nome, pois em Perl o iterador de um loop é implícito:
foreach (<*.jpg>) {
if (($base, $numero, $ext) = /^(.)(\d+)\.(.*)/) {
rename $_, sprintf("%s%03d.%s", $base, $numero, $ext);
}
}
Se você não conhece Perl não vai saber que a expressão regular está sendo aplicada ao iterador implícito do
foreach. Mas se você nunca viu Perl, esse não é o seu maior problema, né? Ah, e o
$_ é o "pronome" que usamos pra nos referirmos explicitamente ao iterador dentro do loop.
Pensando bem, essas variáveis locais não estão servindo pra muita coisa além de dar nomes às partes capturadas pela expressão regular. Se fôssemos usá-las muitas vezes, vá lá. Mas pra só usarmos uma vez na próxima linha? A expressão regular já é suficientemente clara (depois de adquirir alguma experiência com elas, obviamente). Que tal nos livrarmos dessas variáveis?
foreach (<*.jpg>) {
if (/^(.)(\d+)\.(.*)/) {
rename $_, sprintf("%s%03d.%s", $1, $2, $3);
}
}
Hmmm... tá parecendo C. Em Perl é mais direto e legível interpolar as variáveis diretamente na string de formato:
foreach (<*.jpg>) {
if (/^(.)(\d+)\.(.*)/) {
rename $_, sprintf("$1%03d.$3", $2);
}
}
Hmmm... o importante é o rename... o if é acessório. Em Perl, podemos inverter o teste e a ação, mais ou menos quando escolhemos a voz ativa ou a voz passiva por razões estilísticas. Então, vamos colocar primeiro o que interessa:
foreach (<*.jpg>) {
rename $_, sprintf("$1%03d.$3", $2)
if /^(.)(\d+)\.(.*)/;
}
Legal. Economizamos um par de chaves também, viram?
Ah... direto assim fica mais fácil perceber a oportunidadade de fazer otimizações triviais:
foreach (<*.jpg>) {
rename $_, sprintf("$1%03d.jpg", $2)
if /^(.)(\d+)\.jpg$/;
}
Ou generalizações oportunas:
foreach (<*.jpg>) {
rename $_, sprintf("$1%03d.jpg", $2)
if /^([a-z]+)(\d+)\.jpg$/i;
}
Ficou bem legível pra mim. E pra vocês?
De qualquer modo, pelo menos isso prova que
There Is More Than One Way To Do It.
Adendo: Algum tempo depois de escrever isto eu descobri o comando
rename na linha de comando Linux. Com ele a solução é trivial:
rename 's/(\d+)/sprintf("%03d", $1)/e' *.jpg
Ah... o
rename é escrito em Perl. :-)