Perl numa linha, e Ofuscação

2011-03-05, por Alberto Simões ambs@cpan.org

Todos sabemos que o Perl pode ser ilegível. Mas a verdade é que não podemos culpar a sintaxe do Perl por esta ilegibilidade, mas a imensa imaginação de quem o programa. Ora vejamos um exemplo (verídico). Considerem dois ficheiros, com listas de palavras (uma palavra por linha) e que querem imprimir as palavras que constam nos dois ficheiros, e pela ordem pela qual aparecem no primeiro ficheiro.

Pediram-me ajuda para decifrar esta linha de código, que faz exactamente o pedido:

  perl -ne 'print if ($seen{$} .= @ARGV) =~ /10$/'   lista1 lista2  > comuns

Para quem ainda está a começar a usar Perl, lembro que a opção -n faz com que a linha de código apresentada seja executada para todas as linhas dos ficheiros indicados, e a opção -e indica qual a linha de código a executar.

O primeiro problema para perceber este código é a existência de parêntesis, ou a inexistência de parêntesis. Passo a explicar, os parêntesis apresentados fazem com que nós, instintivamente, pensemos que a condição da expressão if é apenas o que está dentro dos ditos parêntesis. Mas não, já que a sintaxe do if pós-comando considera que todo o resto da linha é a condição.

Estamos, pois, perante este código:

  perl -ne 'print if (($seen{$
} .= @ARGV) =~ /10$/)'   lista1 lista2  > comuns

Ou seja, na hash $seen guardamos cada linha (representada pelo $) como chave, e como valor vamos concatenando @ARGV. Este array tem todos os parâmetros passados pela linha de comando que ainda não foram processados. Então enquanto o código é aplicado ao primeiro ficheiro, @ARGV=('lista2') e, quando passa a aplicar ao segundo ficheiro, @ARGV=().

Passo seguinte, saber que @ARGV aparece num contexto em que o Perl espera um escalar (um dos operandos do operador de concatenação) e que, portanto, será convertido para um inteiro. O Perl converte arrays para inteiros retornando o número de elementos do array. Ora, ao processar o primeiro ficheiro, é usado o valor 1, ao processar o segundo ficheiro, é usado o valor 0.

Se determinada palavra ocorre no primeiro ficheiro, será guardado um 1 na hash $seen. Se a palavra ocorrer no segundo ficheiro, será concatenado a esse 1 um 0. Ou seja, o valor na hash é "10".

O passo seguinte é simples. Se o valor na hash corresponder a "10", então o valor é impresso.

Como vêem, o Perl pode ser bastante rebuscado. Mas reparem em outras duas soluções muito parecidas e que me parecem muito mais simples de perceber (para além de mais curtas):

   perl -ne 'print if ++$seen{$
} > 1'   lista1 lista2 > comuns

ou

   perl -ne 'print if $seen{$_}++'  lista1  lista2  > comuns

Nestes exemplos o único detalhe importante é saber que o incremento de uma variável funciona de forma diferente quando é colocado antes ou depois de uma variável. Se colocado antes, o valor é incrementado e só depois usado. Se colocado depois, o valor actual é usado na comparação, e só depois será incrementado.

É provável que ainda vos pareçam soluções complicadas, mas hão de convir que são bastante mais simples que a inicial.

Enhanced by Zemanta