Bash One Liners #6

Para quem utiliza um sistema de controle de versões, sempre há aquele momento em que você criou alguns arquivos (às vezes vários), e o repositório ainda não os têm sob controle, fazendo com que você precise adicioná-los um a um, caso não utilize uma IDE. Este é o caso, pelo menos, com o Subversion e com o CVS (que são os que conheço melhor). No Subversion, por exemplo, para verificar quais arquivos da cópia local ainda não foram adicionados ao repositório é só fazer:

svn status | grep '^?'

Esse é o início do one liner deste artigo. Digamos que a saída fosse a seguinte:

?      b.txt
?      c.txt
?      a.txt

O objetivo aqui é, com um one liner, adicionar todos esses arquivos ao Subversion, ou seja, aplicar o comando svn add sobre cada um deles. Naturalmente, o próximo passo é recortar essa saída, descartando a primeira coluna inteira, e nos deixando com a segunda, que é a lista dos arquivos ainda não adicionados. Poderíamos utilizar o cut para realizar o corte, mas a opção -d do comando aceita um único caracter (e neste caso o delimitador seria 1 ou mais espaços em branco). Logo, precisamos de um programa mais robusto: awk:

svn status | grep '^?' | awk '{ print $2 }'

Como o delimitador padrão do awk é 1 ou mais espaços, a tarefa tornou-se muitos simples. Agora nossa saída se transformou em:

b.txt
c.txt
a.txt

Em seguida utilizamos o xargs, que irá inserir a string “svn add” no início de cada uma dessas linhas e passá-las ao Bash como um conjunto de comandos montar um comando único na forma svn add linha1 linha2 ... linhaN.

svn status | grep '^?' | awk '{ print $2 }' | xargs svn add

E o resultado do próximo svn status:

A         b.txt
A         c.txt
A         a.txt

Há uma limitação para esse one liner: se um dos arquivos possuísse espaços em branco no nome, então o comando awk removeria todos os caracteres que aparecem após o primeiro espaço em branco. Não sei como alterar o programa awk para contornar esse problema e e ao mesmo tempo mantê-lo como um programa de um único comando, portanto vou marcar esse bug como won’t fix (se alguém souber, por favor, deixe um comentário que atualizarei o post).


Bash One Liners #4

Em: 01/07/2008 Tags: tail, w3m, awk, Bash Comentários (0) Referencie do seu blog (Trackback)

Este é trivial: com w3m e tail podemos descobrir nosso número IP. O one liner é o seguinte:

w3m -dump http://www.ip-adress.com/ | tail -1

O ip-address.com, como o nome indica, é um site que indica qual o número IP que você está utilizando. É a partir do conteúdo da página inicial desse site que vamos chegar ao nosso one liner.

Com a opção -dump do w3m indicamos que queremos que a página seja impressa na saída padrão, sem tags HTML, de uma forma que uma pessoa ache agradável de ler. Essa saída é sempre a mesma, com exceção da última linha, que indica (ta-dá!) seu IP:

Counter

my ip address IP-a[S:d:S]dress.com - What is my IP address?
An IP address (Internet Protocol Address) is a logical address of a
network adapter. The IP address is unique and identifies computers on a
network. An IP address can be private, for use on a LAN, or public, for
use on the Internet or other WAN.
Too many bots are spidering our site. Therefore this small frontpage
version. IP address Tracer / Locator

My IP address:
200.32.10.11

Como o dado que queremos está exatamente (e sempre) na última linha da saída, não ficou muito difícil escolher qual programa vai extrai-la: o tail. Com a opção -1 pegamos exatamente a última linha, finalizando nosso one liner.

Como tenho usado esse site constantemente achei útil criar um atalho myip para o meu usuário. Basta adicionar em ~/.bashrc a linha:

alias myip='w3m -dump http://www.ip-adress.com/ | tail -1'

Depois recarregue a configuração:

source ~/.bashrc

Atualização (2/7/2008): Hoje o one liner já deixou de funcionar, pois a estrutura do site se modificou. Agora a linha que imprime o IP não é mais a última, mas sim uma no meio do documento. Talvez o w3m simplesmente não tenha carregado a página completamente ontem, quando escrevi o artigo, e por isso havia sido tão fácil. Agora seria necessário utilizar o awk para chegar até a linha que diz “My IP Adress:”, em seguida pular para a próxima linha, e então imprimi-la e sair do programa. O novo one liner é, portanto, menos simples, mas ainda trivial:

w3m -dump http://www.ip-adress.com/ | awk '/My IP address/ { getline; print $1; exit }'

Não garanto que esse one liner vai continuar funcionando, afinal eu não mantenho esse site. Ao menos a modificação rendeu mais um one liner…


Bash One Liners #3

Em: 26/06/2008 Tags: awk, Bash Comentários (0) Referencie do seu blog (Trackback)

Em Bash One Liners #2 apresentei uma combinação de programas Linux para construirmos um ranking dos meses com mais artigos publicados em um blog. Com um pouco mais de esforço podemos calcular o somatório e a média de artigos publicados durante a vida do blog. É o que vamos ver agora.

Vamos começar com o seguinte arquivo (o ranking do meu blog antes de postar este artigo):

14 Fevereiro 2008
12 Novembro 2007
9 Janeiro 2008
8 Setembro 2007
7 Junho 2008
6 Dezembro 2007
5 Outubro 2007
5 Maio 2008
3 Março 2008
2 Julho 2007
2 Agosto 2007
1 Abril 2008

Supondo que esse conteúdo esteja no arquivo `ranking’, o seguinte one liner nos dá o total de artigos publicados:

awk '{ sum += $1 } END { print sum }' ranking

E para termos a média:

awk '{ sum += $1 } END { print sum / NR }' ranking

Nem precisamos de pipes desta vez. No primeiro script utilizamos a variável sum para armazenar o somatório da primeira coluna de dados do arquivo. O awk não se incomoda com variáveis não inicializadas, e é isso que nos permite fazer a totalização com um único comando. A cláusula END é especial, pois é executada apenas quando todas as linhas do arquivo já foram avaliadas. Para programas como esse que geram relatórios, essa é a parte ideal para imprimir o rodapé ou algum tipo de totalização. É exatamente isso que fazemos.

O segundo script é idêntico ao primeiro, com a exceção de que imprime a divisão do somatório pela quantidade de linhas analisadas. Na verdade NR, quando utilizado fora da cláusula END, guarda sempre o número da linha corrente, mas como estamos dentro de END ela guarda o número da última linha lida, que é exatamente o total de linhas analisadas.


Bash One Liners #2

Em: 26/06/2008 Tags: sed, sort, awk, Bash Comentários (1) Referencie do seu blog (Trackback)

Este segundo episódio da série apresenta um one liner que imprime o ranking de meses com mais artigos publicados em um blog (e uma explicação detalhada de como o one liner foi construído, para que o leitor conheça mais as ferramentas que qualquer distribuição Linux nos dá de graça e muitas vezes nem sabemos que existem).

Este script funciona apenas para blogs que tenham uma listagem de artigos mais publicados com a seguinte estrutura geral (esse padrão pode se repetir várias vezes):

mês ano_com_quatro_dígitos (quantidade_de_artigos)

Para este artigo utilizo como exemplo o blog da Thânia Clair, pois se eu utilizar o meu, após publicado o artigo as expressões regulares utilizadas passam a casar com as listagens deste artigo, e todos os exemplos passam a apresentar resultados diferentes dos que eu obtive antes da publicação.

Eis o one liner:

w3m -dump http://www.thaniaclair.com | awk '/[0-9][0-9][0-9][0-9] \([0-9]+\)$/ { print $4, $2, $3 }' | sed 's/[\(|\)]//g' | sort -gr

Eis o resultado:

23 Janeiro 2008
18 Dezembro 2007
16 Fevereiro 2008
13 Novembro 2007
6 Abril 2008
5 Março 2008
2 Maio 2008
1 Junho 2008

Não parece muito simples de entender, né? Não se preocupe, vou apresentar aqui, passo a passo, como cheguei a essa solução. Considero esse um exemplo significativo de one liner, pois demonstra a filosofia que tenho seguido para criar minhas soluções: escolher a ferramenta mais adequada para a tarefa. Note que utilizei 4 programas para chegar a esse resultado, em vez de uma única linguagem de programação, ao contrário do que nos ensinam em cursos Brasil afora.

Estes 4 programas têm propósitos bem específicos, e por isso atendem tão bem ao domínio do meu problema:

  • w3m: navegador web modo texto, com suporte a screen scraping
  • awk: linguagem criada para processamento de dados estruturados
  • sed: linguagem criada para transformação de texto
  • sort: programa criado para ordenar linhas de um arquivo

Como sugestão, proponho ao leitor realizar a mesma tarefa com uma linguagem de programação de propósito geral, como Perl, PHP, Python ou Ruby, e depois me contar em quantas linhas resolveu o mesmo problema (ouso chutar que Ruby e Perl gastariam menos linhas). Só pra não perder a deixa: tente em Java também!

Obviamente, não tenho nada contra nenhuma dessas linguagens, estou apenas reforçando a minha fisolofia da melhor ferramenta para o trabalho.

Agora vamos à solução.

Passo 1 - Imprimir o conteúdo do site

Para imprimir o conteúdo de um site na saída padrão a opção mais simples que eu conheço é utilizar ou o lynx ou o w3m com a opção -dump habilitada. O conteúdo é impresso em um formato estruturado, mas todas as tags são removidas. O seguinte comando:

w3m -dump http://www.thaniaclair.com

Imprimiria um documento que terminaria assim (estou suprimindo caracteres especiais que só fazem sentido quando lidos em um terminal):

  ruby (6)
  tirinhas (21)
  web (1)

Arquivos

  Junho 2008 (1)
  Maio 2008 (2)
  Abril 2008 (6)
  Março 2008 (5)
  Fevereiro 2008 (16)
  Janeiro 2008 (23)
  Dezembro 2007 (18)
  Novembro 2007 (13)

Nosso próximo passo é filtrar esse documento, para trabalhar apenas com as linhas que nos interessam. Nesse ponto entra o awk.

Passo 2 - Selecionar as linhas que interessam com awk

As linhas que nos interessam são todas aquelas que exibem a quantidade de artigos publicados durante cada mês de vida do blog. Essas linhas seguem o mesmo padrão: o nome de um mês, seguido de um espaço em branco, seguido de um ano, seguido de um espaço em branco, seguido pelo número de artigos entre parênteses.

A função do awk, nesse momento, é varrer todo o documento impresso pelo w3m, e desse documento excluir todas as linhas que não se enquadrem nesse padrão. Nossa declaração (na forma de uma expressão regular) é a seguinte: imprima qualquer linha que termine com 4 números (o ano), seguido por um espaço em branco, seguido por um ou mais números (a quantidade de artigos) cercados por parênteses, caso tudo isso esteja no fim da linha.

awk '/[0-9][0-9][0-9][0-9] \([0-9]+\)$/ { print $0 }'

O awk, então, imprimiria:

Junho 2008 (1)
Maio 2008 (2)
Abril 2008 (6)
Março 2008 (5)
Fevereiro 2008 (16)
Janeiro 2008 (23)
Dezembro 2007 (18)
Novembro 2007 (13)

Ótimo! Apenas as linhas que nos interessam. Mas como vamos ordenar esses dados? Como sabemos de antemão que o programa que irá ordená-los será o sort (e que o sort ordena as linhas lendo-as da esquerda para a direita), temos que posicionar a quantidade de artigos do mês no início da linha, e não no final. Nosso texto precisa, portanto, de transformação. Mas essa transformação é trivial para o awk. Vamos ver porquê.

No momento em que o awk encontra uma linha que atende ao padrão que especificamos ele define um conjunto de variáveis iniciadas por $. $0 corresponde à própria linha; para definir as demais variáveis $ o awk utiliza (por padrão) o espaço em branco como caracter divisor de dados, e atribui cada dado a uma variável, da esquerda para direita, de 1 até N (N = quantidade de dados). No nosso exemplo, nossas linhas geram as seguintes variáveis:

  • $1: um caracter especial gerado pelo w3m que eu decidi ignorar
  • $2: o mês
  • $3: o ano
  • $4: a quantidade de artigos publicados, cercados por parênteses

Nosso programa awk anterior, portanto, deve ser modificado da seguinte forma:

awk '/[0-9][0-9][0-9][0-9] \([0-9]+\)$/ { print $4, $2, $3 }'

Nossa saída, agora, seria:

(1) Junho 2008
(2) Maio 2008
(6) Abril 2008
(5) Março 2008
(16) Fevereiro 2008
(23) Janeiro 2008
(18) Dezembro 2007
(13) Novembro 2007

Ótimo! Agora nossos dados estão quase prontos para serem passados ao sort. O problema são os parênteses cercando a quantidade de artigos. Se passarmos os dados dessa forma para o sort ele não realizará a ordenação corretamente. Precisamos, portanto, realizar uma simples substituição em todas as linhas: eliminar os 2 parênteses. Poderíamos tentar fazer isso diretamente no awk, mas certamente o código-fonte perderia legibilidade, e talvez fosse obrigado a deixar de ser um one liner (e tirar todo o propósito deste artigo!). A saída, portanto, é escolher a melhor ferramenta para o trabalho: sed.

Passo 3: Eliminar caracteres desnecessários

Este simples comando sed irá substituir os parênteses de abertura e fechamento por uma string vazia em todas as linhas do texto que chegar como entrada:

sed 's/[\(|\)]//g'

Nossa entrada, que era assim (apenas as 2 primeiras linhas):

(1) Junho 2008
(2) Maio 2008

Sairá do sed assim:

1 Junho 2008
2 Maio 2008

Com essa saída o sort é capaz de fazer a tarefa dele: ordenar as linhas.

Passo 4: Ordenar as linhas

Por padrão o sort ordena as linhas da mesma forma que um dicionário ordena seus verbetes. Assim, por exemplo, 10 apareceria antes de 9. Para instruir o sort a ordenar as linhas de entrada como se elas fossem dados numéricos, utilizamos a opção -g, e para inverter a ordenação (colocando o maior número no início e o menor no fim), utilizamos a opção -r. Nosso último filtro, portanto, é o seguinte:

sort -gr

E quando aplicado à saída do programa sed:

1 Junho 2008
2 Maio 2008
6 Abril 2008
5 Março 2008
16 Fevereiro 2008
23 Janeiro 2008
18 Dezembro 2007
13 Novembro 2007

Nos devolve:

23 Janeiro 2008
18 Dezembro 2007
16 Fevereiro 2008
13 Novembro 2007
6 Abril 2008
5 Março 2008
2 Maio 2008
1 Junho 2008

Problema resolvido!

Comentários adicionais

É importante ressaltar que se, ao fim do passo 1, a quantidade de artigos publicados no mês estivesse aparecendo na primeira coluna das linhas, não seria necessário utilizar o awk no passo 2, pois para filtrar linhas de um arquivo basta utilizar o programa grep.

Também seria possível, após o passo 4, fazer com que a quantidade de artigos publicados no mês voltasse a ser a terceira coluna de cada linha se adicionássemos mais uma chamada ao awk:

awk '{ print $2, $3, $1 }'

E se quiséssemos restaurar os parênteses, então o programa acima se tornaria:

awk '{ print $2, $3, "("$1")" }'

Agora você já deve entender o que esses dois programas fazem.


Bash One Liners #1

Em: 17/06/2008 Tags: sort, awk, Bash Comentários (1) Referencie do seu blog (Trackback)

Recentemente passei a trabalhar em um projeto que faz muito uso de Bash (ou Shell Script, o termo mais popular no Brasil). Passei, desde então, a prestar mais atenção em construções corriqueiras como um history | grep, descobrindo formas de resolver com diferentes programas baseados em linha de comando tarefas pequenas (ou não) do dia-a-dia. Esses programas (sort, grep, sed, awk, entre outros) tornam-se muito mais úteis, claro, quando podem ser combinados através de um pipeline. Quando conseguimos combinar programas Linux em 1 só comando compacto, acabamos de criar um programa one liner (de uma só linha).

É possível que esses programas funcionem em qualquer shell que você escolher (csh, ksh, entre outros), mas como há algumas diferenças na sintaxe dessas implementações, intitulei a série “Bash One Liners”, pois é apenas no Bash que as soluções serão testadas. Para saber que Shell você utiliza digite, num terminal:

echo $SHELL

Sem muita pretensão, irei publicar aqui no blog soluções one liners que criei (ou copiei) para resolver problemas no trabalho. Todos os artigos terão a mesma estrutura, que basicamente descreve o problema inicial, os programas utilizados na solução, e como a solução pode ser construída através de pipelines. Vamos ao primeiro!

Problema: tenho um arquivo com uma lista de nomes que eu gostaria de ordenar, colocando um asterisco à frente de cada linha, para formar uma “bulleted list”.

Solução inclui: sort e awk.

Exemplo: temos um arquivo types.txt com o seguinte conteúdo:

decimal
integer
float
datetime
date
timestamp
time
text
string
binary
boolean

Nossa transformação é feita em 2 passos. No primeiro ocorre a ordenação:

$ sort types.txt

Que devolve o texto na saída padrão como:

binary
boolean
date
datetime
decimal
float
integer
string
text
time
timestamp

Com um pipe podemos direcionar essa saída para a entrada do Awk, que irá imprimir cada linha precedida por um asterisco e um espaço em branco:

$ sort types.txt | awk '{print "* " $0}'
* binary
* boolean
* date
* datetime
* decimal
* float
* integer
* string
* text
* time
* timestamp

Problema resolvido!