Tô no Ubuntu!

2009-04-27

Ontem eu resolvi atualizar meu Kubuntu 8.10 para 9.04 no computador de casa. Foi tudo liso. Reiniciei o computador, me loguei e estou trabalhando desde então sem qualquer empecilho.

Mas não é só nesse sentido que eu “tô no Ubuntu”. É que finalmente eu tenho um projeto meu (nesse caso, um projetinho) integrando essa distribuição:


gustavo@wise:~$ apt-cache show libsvn-look-perl
Package: libsvn-look-perl
Priority: optional
Section: universe/perl
Installed-Size: 68
Maintainer: Ubuntu MOTU Developers <ubuntu-motu@lists.ubuntu.com>
Original-Maintainer: Debian Perl Group <pkg-perl-maintainers@lists.alioth.debian.org>
Architecture: all
Version: 0.13.463-2
Depends: perl (>= 5.6.0-16), subversion
Filename: pool/universe/libs/libsvn-look-perl/libsvn-look-perl_0.13.463-2_all.deb
Size: 10250
MD5sum: 7105c8c22d06e4b1c49b78366499765f
SHA1: 4d4844e729f12cef21c7578d5b6846df9447a925
SHA256: 4c44bc636203dceb3e6beed7575553e8be0fb833644ec2716c222130bc58e733
Description: caching wrapper around the svnlook command
  The svnlook command is the workhorse of Subversion hook scripts,
  being used to gather all sorts of information about a repository, its
  revisions, and its transactions. SVN::Look provides a simple object
  oriented interface to a specific svnlook invocation, to make it
  easier to hook writers to get and use the information they
  need. Moreover, all the information gathered by calling the svnlook
  command is cached in the object, avoiding repetitious calls.
Homepage: http://search.cpan.org/dist/SVN-Look/
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Origin: Ubuntu

Tenho um projeto mais interessante que está pra entrar numa versão futura do Debian, o SVN::Hooks que deverá entrar como pacote libsvn-hooks-perl.

Tá ficando bom…


Desenferrujando

2008-08-22

Se você gosta de programar e adora Perl… nah… mesmo que você odeie Perl, pare o que está fazendo e assista já à entrevista do Damian Conway.

São meros 35 minutos em que ele fala do seu PhD em biologia computacional, do sotaque “errado” dos americanos, de linguagens de programação em geral e de Perl 6 especificamente. Eu nunca li ou ouvi uma explicação mais interessante sobre a diferença entre tipagem estática e tipagem dinâmica.

E o final da última resposta merece até uma tentativa de tradução:

…para ser um bom programador você tem que efetivamente programar. E isso é algo que não acontece. Sabe, a gente estuda computação, a gente aprende todas aquelas coisas e fica o tempo todo fazendo exercícios e provas. Daí a gente se forma e começa a freqüentar reuniões, a desenhar modelos, diagramas e todo o resto e você pára de programar. E se você é promovido, então você é literalmente promovido a perder a oportunidade de continuar programando e eu acho que isso é um problema. Se você quer ser um jogador de tênis realmente bom, você vai treinar todos os dias. Se você quer ser um grande lutador de artes marciais você vai pro tatame todo santo dia. Se você quer ser um grande programador, você vai programar todo dia—mesmo que você tenha que usar seu próprio tempo pra isso. Se você está acordado, você é um programador. Se você está acordado às 11 horas da noite, ou às 3 da manhã, você tem que usar parte desse tempo para programar porque assim que você começa a enferrujar você começa a morrer como um programador.


TIMTOWTDI

2008-08-16

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 seqüência de dígitos e terminando na extensão .jpg. Por exemplo:

 $ ls a0.jpg  b1.jpg  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 -le '/^([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 () {
        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 () {
        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 adquirir alguma experiência com elas, obviamente). Que tal nos livrarmos dessas variáveis?

    foreach () {
        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 () {
        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 () {
        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 otimizações triviais:

    foreach () {
        rename $_, sprintf("$1%03d.jpg", $2)
            if /^(.)(\d+)\.jpg$/;
    }

Ou generalizações oportunas:

    foreach () {
        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.


Desafio regex

2008-04-07

Meu amigo Rogério Zamboim me desafiou a resolver um problema com uma expressão regular. Desconfio que isso estava lhe causando insônia e ele queria transferi-la pra alguém… De qualquer modo, o desafio acabou sendo bem interessante.

O problema é validar um campo cujo conteúdo deve ser uma seqüência de dígitos, de 1 a 8, separados por barras invertidas, em ordem monotônica decrescente. Exemplificando, os seguintes valores são válidos para o campo:

8\7\6\5\4\3\2\1
8
8\1
8\7\2
1
2\1
7\5\3

Já os seguintes valores são inválidos:

8\
\7
8\6\
\2\1
6\\2
7\6\6\2
6\2\5

Confesso que de cara dei um tiro n’água e enviei-lhe a seguinte “solução”, obviamente errada:

   /^[1-8](?:\\[1-7])?(?:\\[1-6])?(?:\\[1-5])?(?:\\[1-4])?(?:\\[1-3])?(?:\\[1-2])?(?:\\1)?$/

O problema é que ela não garante que os dígitos são decrescentes. Por exemplo, a string “4\5” é aceita por ela.

Pensando melhor, ficou claro que expressões regulares “puras” não têm poder de expressão suficiente pra especificar a regra de validação de modo conciso. Isso porque elas não conseguem expressar a ordenação dos dígitos. Não se trata de um impossibilidade teórica, mas prática. Uma solução puramente regular envolveria a descrição explícita de todas as possibilidades, resultando numa expressão enorme. Algo como:

   /^(?:[1-8]|[2-8]\\1|[3-8]\\2|...|[3-8]\\2\\1|[4-8]\\3\\1|.../

Convencido de que não seria possível uma solução direta, resolvi usar uma expressão regular apenas para garantir a forma básica, i.e., uma seqüência de dígitos separados por barras invertidas, e deixar a verificação da ordem para um código posterior. O melhor que consegui foi o seguinte:

   sub validate_loop {
       my ($string) = @_;
       my $top = 9;
       for my $val (split /\\/, $string) {
           return 0 unless $val =~ /^[1-8]$/;
           return 0 unless $top > $val;
           $top = $val;
       }
       return 1;
   }

Esta função recebe uma string com o valor do campo. A variável $top contém sempre o valor máximo que o próximo dígito pode conter mais um, e começa com 9, já que o primeiro dígito pode estar entre 1 e 8. O loop quebra a string nas barras invertidas e verifica se o que há entre elas são dígitos entre 1 e 8 e se eles são menores que $top, atribuindo a $top o valor do próximo dígito.

É razoável, mas não satisfatório. Usa duas expressões regulares e faz muitos testes separados… Não estava muito bom.

Fiquei matutando uns dois dias sobre isso e pensando se dentre as várias extensões que Perl oferece além dos operadores básicos de expressões regulares não haveria algum que me permitisse construir uma solução mais sucinta. Relendo a documentação deparei-me com o operador “(?{code})“, que permite inserir código no meio de uma expressão regular. Hmmm… parecia útil, mas eu nunca havia usado algo assim.

Depois de algumas tentativas frustradas acabei bolando a seguinte solução:

   sub validate_re {
       my ($string) = @_;
       our ($dec, $last) = (1);
       return ($string =~ /^([1-8])(?{$last=$^N})(?:\\([1-8])(?{$dec=0 if $last <= $^N; $last=$^N}))*$/) && $dec;
   }

A expressão regular ficou maior por causa do código embutido dentro dela. Remova mentalmente os operadores (?{…}) e você verá que ela está simplesmente verificando se a string consiste em uma sequência de dígitos separados por barras invertidas.

No primeiro operador de código, a variável $last recebe o valor do primeiro dígito da string. (A variável implícita $^N lembra o valor da última captura por parêntesis.) No segundo operador, usamos seu valor para verificar se o próximo dígito é menor que o anterior e atribuímos o novo dígito a ela.

A variável $dec mantém o estado da verificação de ordem. Ela começa como 1 e recebe 0 caso o segundo operador de código detecte que há algum dígito fora de ordem monotônica decrescente.

Eu demorei um bom tempo pra chegar à conclusão de que essas duas variáveis tinham que ser globais (declaradas com “our”). Se elas são locais (declaradas com “my”) não funciona. A documentação não é clara a esse respeito e eu desconfio que seja um bug do próprio interpretador Perl, mas não tenho certeza.

A função acaba retornando a conjunção da avaliação da expressão regular com o valor final de $dec.

Pronto, agora eu posso dormir em paz.


Splitting hairs

2007-12-09

split hairs: to argue about whether unimportant details are correct

Sou fã de Perl porque me permite ser sucinto como em nenhuma outra linguagem.

Esta semana descobri um bug num velho script que eu uso pra processar o relatório produzido pelo comando ” bpdbjobs -report -all_columns” do NetBackup. O relatório é um arquivo texto no qual cada linha tem um número variável de campos separados por vírgulas.

Até aí, nada demais. Se eu tiver a linha na variável $line, a função “split” separa os campos nas vírgulas e os atribui ao array @fields trivialmente:

@fields = split /,/, $line;

Só que alguns campos deste relatório podem conter vírgulas, as quais são “escapadas” com barras invertidas, i.e., “\,”. O split anterior não entende essas vírgulas escapadas e vai tratá-las indevidamente como separadoras de campos.

O que queremos, então, é quebrar a linha nas vírgulas, mas apenas naquelas que não sejam precedidas de uma barra invertida. Sem problemas… hora de complicar um pouquinho a expressão regular.

@fields = split /(?<!\\),/, $line;

O “ruído” entre parêntesis é um zero-width negative lookbehind (ZWiNLoB). Ele indica que antes da vírgula não pode haver uma barra invertida. Note que o caractere com o qual o ZWiNLoB vai “casar” não fará parte do separador de linha, que continuará a ser somente a vírgula.

Muito bem, parecia tudo certo até que eu rodei novamente o script e percebi que ainda havia problemas. Algumas linhas ainda não estavam sendo quebradas corretamente. Desta vez o problema eram alguns campos que terminavam com uma barra invertida, a qual também era escapada por outra barra invertida. Por exemplo:

…,C:\\,…

Note que a vírgula não está escapada, sendo efetivamente um separador de campo.

E agora? A primeira coisa que pensei foi em usar um ZWiNLoB que case com uma barra não precedida por outra.

@fields = split /(?<![^\\]\\),/, $line;

Hmmm… mas não iria funcionar. E se o campo terminasse em duas barras?

…,C:\\\\,…

Por outro lado, se o campo termina em uma ou mais barras, como cada uma delas deve ser escapada o número total de barras antes da vírgula deve ser necessariamente par, certo? Portanto, o que eu preciso é de um ZWiNLoB que case com um número ímpar de barras. (Lembre que o ZWiNLoB é “negativo”.)

@fields = split /(?<![^\\](?:\\\\)*\\),/, $line;

Só que o Perl reclamou:

Variable length lookbehind not implemented in regex; marked by <– HERE in m/(?<![^\\](?:\\\\)*\\), <– HERE /

Os ZWiNLoBs têm uma restrição forte: eles devem especificar strings de tamanho fixo, o que significa que eu não vou conseguir usá-los pra indicar uma vírgula não precedida por um número ímpar de barras…

OK, o ZWiNLoB na expressão anterior tinha a função de garantir que eventuais barras invertidas em um campo não precedam uma vírgula escapando-a. Mas a expressão como um todo quer mesmo é casar com uma vírgula, i.e., com o separador dos campos, pois eu a estou usando como argumento da função split.

E se eu tentar uma abordagem inversa? E se ao invés de tentar encontrar os separadores eu tentasse encontrar os campos? Nesse caso, a parte de tamanho variável da expressão não estaria num ZWiNLoB.

Pensei de cara numa abordagem interativa:

push @fields, $1 while $line =~ s/(.*?(?<!\\)(?:\\\\)*),//;
push @fields, $line;

A primeira linha remove o primeiro campo de $line e o anexa ao array @fields, até que sobre apenas o último campo, que não é sucedido por uma vírgula, e que é anexado na segunda linha. Ainda há um ZWiNLoB, mas ele tem um tamanho fixo, casando com um caractere diferente de barra invertida.

Não gostei muito. Eu preferiria não ter que tratar o último campo de modo especial. Logo percebi que com um pequeno ajuste na expressão regular isso seria possível.

push @fields, $1 while $line =~ s/(.*?(?<!\\)(?:\\\\)*)(?:,|$)//;

Desse modo, os campos podem ser sucedidos por uma vírgula ou pelo final da string.

Eu estava quase satizfeito, pois a solução acabou tendo uma linha apenas, mas o split era tão mais simples…

De qualquer modo, eu ainda precisava testar. E qual não foi a minha surpresa quando o script passou a demorar dezenas de segundos a mais que a versão anterior. Por um lado ele parecia estar correto, o que era um avanço. Mas alguma coisa o havia tornado muito lento e só podia ser aquele loop.

O problema é que o relatório tem algumas linhas realmente grandes, com quase um milhão de caracteres. Como a cada iteração a linha tem um campo removido, toda essa manipulação de strings parece ser bastante pesada.

Mas eu não preciso realmente remover os campos. Basta começar a procurar um campo a partir do final do campo anterior. (Eu não estou sendo fiel à história aqui, pois não cheguei a pensar nessa solução antes de começar a escrever isso aqui. Mas, em retrospecto, acho que ela cabe logicamente entre as duas últimas.)

push @fields, $1 while $line =~ /(.*?(?<!\\)(?:\\\\)*)(?:,|$)/g;

A mudança é sutil. Troquei apanas o operador “s///” pelo “//g”. Como a expressão está sendo avaliada num contexto escalar, a cada iteração a busca começa a partir do final do trecho encontrado na iteração anterior. A vantagem é que a linha não precisa ser modificada, o que deve evitar grande parte da ineficiência anterior.

Mas, se eu quero uma lista de campos eu não preciso necessariamente encontrá-los um a um. Afinal, a mesma expressão regular anterior, quando avaliada num contexto de lista, retorna de uma vez todos os trechos encontrados dentro dos parêntesis.

Et voilá, eis minha solução final.

@fields = $line =~ /(.*?(?<!\\)(?:\\\\)*)(?:,|$)/g;

Ah, mas é claro que no script eu não usei uma variável explícita chamada $line. A linha estava realmente na variável implítica $_, o que me permitiu escrever a solução assim.

@fields = /(.*?(?<!\\)(?:\\\\)*)(?:,|$)/g;

Linda, não? E sucinta. E eficiente também. Como convém a um script Perl que se preze.


Randal Schwartz fala do FISL6.0 e do Brasil

2005-06-10

Randal Schwartz, também conhecido como $JAPH[0], comentou sua visita ao Brasil em seu diário. Ele é mais um estrangeiro que ficou impressionado com o número de pessoas que participou do FISL6.0 e com a forte presença de políticos.

Ele também comentou que vai participar do próximo CONISLI.