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…

Anúncios

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.


Diff recursivo com filtro

2007-10-27

Às vezes nos deparamos com problemas aparentemente simples mas que nos dão trabalho pra resolver. Não estou reclamando… esses são, normalmente, os problemas mais divertidos e instrutivos.

Ontem um colega mandou o seguinte problema numa lista de discussão:

Srs,

Estou fazendo um diff recursivo, comparando dois diretórios de arquivos de códigos, eu gostaria que neste diff não fosse considerado as linhas com comentários.

Ex:

//linha de comentário

/*
* comentário
*/

Alguém conhece uma maneira de fazer isso?

Ele quer comparar código fonte Java desconsiderando as diferenças dentro dos comentários.

Eu sabia que o comando GNU diff, presente em todas as distribuições Linux, tem algumas opções para desprezar espaços em branco. Dei uma olhada no manpage pra ver se por acaso haveria alguma opção pra desprezar comentários… não custava nada, né?

-E –ignore-tab-expansion
Ignore changes due to tab expansion.

-b –ignore-space-change
Ignore changes in the amount of white space.

-w –ignore-all-space
Ignore all white space.

-B –ignore-blank-lines
Ignore changes whose lines are all blank.

–strip-trailing-cr
Strip trailing carriage return on input.

É um bocado de opção pra ignorar espaço em branco, não é? Tantas opções e, ainda assim, nenhuma me servia. Aliás, isso me fez pensar se essa proliferação de opções específicas não poderia ser evitada com a implementação de uma opção de filtro mais genérica. Quero dizer, o diff poderia suportar uma opção pra qual pudéssemos passar uma expressão regular especificando strings que deveriam ser ignoradas. Ou, talvez, outra opção passando um comando que o diff pudesse chamar para filtrar os arquivos que fosse comparar.

Como não achei nenhuma solução direta no diff, passei a pensar em como remover os comentários do código Java. Se fosse apenas o comentário estilo C++ (//…) eu usaria um sed, mas como há também o comentário tipo C (/*…*/) não pensei duas vezes antes de usar Perl. O script mais conciso e elegante que obtive eu chamei de stripcomments.pl:

#!/usr/bin/perl
$/ = undef;    # slurp mode
$_ = <>;       # slurp file
s:/*.*?*/::sg; # strip C comments
s://.*::g;     # strip C++ comments
print;

Ele lê todo o arquivo em memória e remove os comentários com expressões regulares simples. Com o tamanho da memória dos desktops atuais eu não creio que ler o arquivo todo seja problemático. Qualquer solução mais comedida no uso de memória seria muito mais complexa.

Pois bem, a idéia é passar pro diff o resultado da aplicação deste filtro a cada um dos seus dois argumentos. A melhor maneira que eu conheço pra fazer isso é usando a sintaxe de substituição de processos da bash. O comando seria o seguinte:

diff <(stripcomments.pl file1) <(stripcomments.pl file2)

Hmmm… há duas coisas no resultado deste diff de que eu não gosto. A primeira é que eu prefiro o resultado no formato unified, que eu obtenho com a opção -u. A segunda é que como o diff recebe o resultado do filtro ele não sabe o nome dos arquivos originais.

Descobri ontem que eu posso instruí-lo sobre o nome dos argumentos através da opção -L. O comando final ficou assim:

diff -u -L file1 <(stripcomments.pl file1) -L file2 <(stripcomments.pl file2)

OK, eu já tenho o filtro e já sei como fazer o diff usá-lo. Mas há um problema: meu colega quer um diff recursivo. Logo, eu não posso passar os nomes dos arquivos diretamente.

Estudei um pouco mais e descobri outra opção interessante do diff: a -q, ou –brief. (Só de curiosidade: o comando “man diff | grep ‘^ *-‘ | wc -l” mostra que o diff suporta 44 opções diferentes!) Com esta opção o diff não mostra as diferenças em si, apenas o nome dos arquivos diferentes.

Minha idéia final foi a seguinte. Primeiro eu executo um “diff -qr” nos dois diretórios pra descobrir quais arquivos são diferentes. Do resultado disso eu coleciono os pares de arquivos diferentes e comparo cada par com o comando diff acima, utilizando o filtro. O script final eu chamei de diff-java-stripped.pl:

01  #!/usr/bin/perl
02  BEGIN {
03      $ENV{PATH} = '/bin:/usr/bin';
04      $ENV{LANG} = 'C';
05  }
06  use strict;
07  use warnings;
08  my $usage = "$0 DIR1 DIR2\n";

09  my $dir1 = shift or die $usage;
10  my $dir2 = shift or die $usage;

11  my @pairs;   # hold pairs of different files

12  open DIFF, "diff -rq $dir1 $dir2 |"
13      or die "Can't exec diff -rq: $!";
14  while (readline(*DIFF)) {
15      if (/^Files (.*) and (.*) differ$/) {
16          push @pairs, [$1, $2];
17      } else {
18          print;
19      }
20  }
21  close DIFF or die "closing DIFF";

22  open BASH, "| bash --norc"
23      or die "Can't exec bash --norc: $!";
24  for my $pair (@pairs) {
25      my ($l, $r) = @$pair;
26      print BASH "diff -u -L '$l' <(stripcomments.pl '$l') -L '$r' <(stripcomments.pl '$r')\n";
27  }
28  close BASH or die "closing BASH";

O primeiro diff é chamado na linha 12 e é processado no loop da linha 14. A saída do comando mostra três tipos de linhas, dependendo do tipo de diferença encontrada:

Only in dir1: file1
Only in dir2: file2
Files dir1/file3 and dir2/file3 differ

Quando um arquivo só existe debaixo de um dos dois diretórios o comando mostra a linha “Only in dir: file”. Nesse caso, só estou interessado em mostrar este fato ao usuário.

Quando há dois arquivos diferentes com o mesmo nome dos dois lados, então eu guardo os caminhos de ambos em @pairs para compará-los depois.

O loop da linha 24 obtém cada par encontrado e constrói uma linha de comando para compará-los depois de devidamente filtrados. Cada linha de comando destas é submetida para execução por uma bash invocada a propósito na linha 22.

A definição da variável de ambiente LANG na linha 4 é para garantir que as mensagens do comando diff sejam em inglês independentemente do locale em que o comando esteja sendo executado.

Tá aí. Acho que ficou legal. E vocês?

(Obs: Foi difícil embutir os scripts acima neste post porque o editor do Blogger teimava eu não aceitar alguns caracteres e eu eliminar alguns newlines. Tive até que usar uma sintaxe alternativa na linha 14 porque não consegui fazer a sintaxe normal ser aceita.)