Utilitários Unix, Parte 2

Ferramentas simples, problemas complexos

Peter Seebach ( unixcomponents@seebs.plethora.net)
Escritor freelance
Junho de 2001

Vamos explorar como combinar os componentes do ambiente de programação Unix para resolver uma variedade de tarefas.

Conteúdo

Os pipes Unix são efetivamente uma arquitetura de componentes, como discutimos na Parte 1 desta série. Nesta parte, iremos reforçar o este argumento. Mas, antes de qualquer coisa, iremos ver como o Unix suporta um objetivo de projeto que está por trás da arquitetura de componentes, antes da arquitetura orientada a objetos e da programação estruturada. É o princípio da decomposição, que permite que se construa resultados a partir de resultados parciais.

Resultados Parciais são melhores que nada

Às vezes, o objetivo é simplesmente produzir um resultado imediato. Você não está muito preocupado com performance (dentro de alguns limites) e não precisa se preocupar com casos especiais pro que você conhece bem os dados de entrada. Nestes casos, o foco está na simplicidade e conveniência do sistema. O Unix é muito bom principalmente em resolver um problema em poucos passos, levando mais alguns passos a seguir para formatar os resultados.

As ferramentas Unxi são feitas com a idéia que está tudo bem se a primeira ferramenta em uma pipeline resolva somente 90% do problema. De fato, já está bem se ela resolve 10% do problema, desde que ela ajude a dividir o problema em pedaços menores.

Digamos então que você queira saber o quão comum uma palavra é em um arquivo. Ao invés de usar a "ferramenta que conta ocorrências de cada palavra em um arquivo" (que raramente será usada), eu divido o problema em vários passos:

Assim, por exemplo, eu poderia usar a seguinte pipeline:

	tr -cs a-zA-Z '\n' | grep '^foo$' | wc -l

O que isto daí faz? O tr troca caracteres. Neste caso, ele troca todos os caracterse que não estão em a-zA-Z (o -c significa "complemento") em novas-linhas, "espremendo" (-s) múltiplas cópias de um dado caracter juntas. Desta forma, eu obtenho cada palavra em uma linha. A seguir, eu uso o grep para encontrar linhas que não tenham nada a não ser a palavra que eu estou procurando ("foo"), e então conto as linhas.

Depois disto, você pode ficar pensando em como irá aprender todos estes comandos estranhos. Você irá aprender da mesma forma que aprende a usar outros sistemas de ferramentas, pela exposição gradual e estudo, e uso extensivo da documentação. As páginas man do Unix, que todos os novatos odeiam, são excelentes referências se você já sabe (em boa parte) o que você quer e está tentando lembrar como escrever. Não é necessariamente mais difícil de aprender que qualquer outra coisa, apenas parece ser difícil se não é a primeira coisa que você aprendeu.

As páginas man são especialmente úteis com os comandos apropos ou man -k, que permitem que se pesquise os resumos das páginas man. Desta forma, você pode entrar o comando "apropos sort" se está procurando por um comando que ordene ("sort") os dados. Você pode pensar neles como uma interface de um repositório com capacidades básicas de pesquisa.

Procurar apenas por linhas que contenham "foo" não resolve meu problema, eu não iria contar "foo bar foo" na mesma linha como duas instâncias. Contar as linhas também seria inútil. Entretante, uma vez que eu divida os dados em registros individuais que contém os dados que eu estou procurando, as coisas se encaixam.

Pipes são baratos

No Unix, as ferramentas são geralmente combinadas colocando-as em linha, com a saída de cada ferramenta na série sendo usada como entrada para a próxima ferramenta. A coisa pela qual os dados fluem é chamada de pipe (N.T.: "cano, encanamento" em inglês). Em alguns sistemas, mover dados de um programa para outro é um processo custoso. No Unix, os pipes são baratos.

O Unix favorece soluções iniciais que são baratas em tempo de programação, mesmo se a eficiência das mesmas não está maximizada. Obviamente, um programa cuidadosamente projetado para uma dada tarefa provavelmente irá ter melhor performance que uma série de programas genéricos que se comunicam via pipes (obviamente, este programa irá exigir cuidadoso ajuste -- um programa mal feito pode fazer suas tarefas de maneira pobre e acabar sendo mais lento).

Usuários Unix experientes não tem receio de acrescentar uma ferramenta à pipeline que só faça algo trivial: é mais barato que fazer à mão, e não irá te atrasar de uma forma que possa ser sentida enquanto você está resolvendo o problema. Se você repete uma tarefa com certa freqüência, e tem um problema de performance, então você tenta aperfeiçoar a solução. Mas você não se preocupa com isto quando está examinando um problema pela primeira vez.

Por exemplo, com o exemplo acima, de "contar as ocorrências de foo", eu poderia ter usado grep -c '^foo$' ao invés de grep '^foo$' | wc -l. Só que isto não me ocorreu no momento em que eu estava escrevendo o exemplo, de forma que vamos deixar assim. A diferença de performance é difícil de medir com o tipo de dados que serão usados. Entretanto, este exemplo mostra que geralmente há mais de uma forma de resolver um problema.

Em algums sistemas, os pipes são implementados usando arquivos temporários ocultos, e o primeiro programa em um pipe deve completar sua tarefa antes que o segundo possa iniciar a execução. Sistemas Unix não fazem isto -- todos os programas em um pipe são executados simultaneamente, e processam a saída conforme ela vai chegando.

Componentes distribuídos usando sockets de rede dependem da mesma funcionalidade -- ambos os lados de um soquete podem estar trabalhando ao mesmo tempo. De fato, algumas das ferramentas Unix fornecem pipes que são executados em outra máquina em uma rede.

Monte suas próprias ferramentas

Se ocê descobre que precisa de um resultado parcial em específico frequentemente quando está trabalhando em algo, você pode pegar a parte da linha de comando que executa a tarefa e salvar ela em um arquivo. Agindo assim, você está escrevendo um programa. Como o próprio shell é uma linguagem de programação, as linhas de comando são também programas, você pode salvar um destes programas e usá-lo como qualquer outro. Você não precisa saber programar em C para programar um sistema Unix.

Por exemplo, digamos que eu frequentemente queira uma lista de palavras em um arquivo, ordenadas pela freqüência em que aparecem. Posso criar um shell script parecido com o seguinte:

	#!/bin/sh
	cat $* |
	tr -cs a-zA-Z '' |
	sort |
	uniq -c |
	sort -n |
	awk '{print $2}'

Novamente, nenhum destes utilitários está fazendo algo muito complicado, mas a série completa acaba fazendo algo bem poderoso.

Entretanto, o utilitário resultante pode ser usado em outro pipeline. Nota para leitores que não estão acostumados ao Unix: Este é o programa completo -- não há necessidade de wrappers IDL, de frameworks, de templates, makefiles, nada mais. Se você escrever ele e marcar como executável, ele é um programa, e irá fazer exatamente o que você quer que ele faça. Não há necessidade de alguma ferramenta especial para fazer este programa funcionar como um componente. Ele já é um componente.

Alguns de vocês podem não pensar que esta é uma aplicação útil, mas é exatamente a mesma coisa que, por exemplo, colocar em ordem uma base de clientes pelo número de vezes que eles não fizeram seus pagamentos. Você fornece para a aplicação a lista de pessoas que não pagou, e a saída é a lista de pessoas, ordenada pela quantia de vezes que elas deixaram de pagar.

Digamos que eu queira descobrir as palavras menos e mais frequentes em um dado arquivo. Eu posso usar:

	freq | tail

para encontrar as palavras mais comuns, ou

	freq | head

para encontrar as menos comum. Assim que eu encontrar uma combinação útil, eu posso tratar ela como mais um degrau, e não somente como o objetivo.

Ferramentas como Plug-ins

Quando eu estava enviando o primeiro rascunho deste artigo a meu editor, eu notei que não havia quebras de linhas no arquivo que eu acabara de ler em meu cliente de email. Por sorte, existe uma ferramenta chamada fmt que quebra as linhas. Então eu disse ao vi para passar todo o arquivo -- da linha em que eu estava até o fim -- por um programa externo e trocar o conteúdo do arquivo com a saída do programa.

Este é um grande modelo. Ao invés de fornecer todas as funcionalidades que você pode querer de um editor, o Unix geralmente te dá uma forma de passar o conteúdo de um arquivo por uma ferramenta, produzindo a saída corrigida. Quer realizar cálculos aritméticos? Passe a linha atual por uma calculadora. Esta é uma das coisas que o Unix faz bem. Na maioria dos sistema, apenas saber que um componente existe não torna prático ao usuário ligar o mesmo a um processador de texto e aplicar o mesmo a um bloco de texto.

Desafie suas expectativas

No rimeiro artigo desta série, eu falei sobre como outras ferramentas que precisam de "serviços" de edição podem simplesmente passar a tarefa para o editor preferido do usuário. Este é encontrado em uma variável de ambiente chamada $EDITOR.

O usuário pode configurar $EDITOR para apontar para qualquer programa. E se não for um editor interativo?

Mesmo assim, tudo funciona bem. Imagine que mantemos o banco de dados de recursos humanos em um arquivo texto delimitado por tabulações, com várias seções: as listas de empregados, as listas de departamentos, e a seção de avaliação de performance. Queremos remover todos os registros que estão associados com os empregados que deixaram a companhia (note que, como vimos antes, não importa se realmente os dados estão delimitados por tabulações, nós podemos exportar e importar para que fiquem temporariamente delimitados por tabulações).

Ou talvez mantenhamos um banco de dados de contatos de clientes, e precisamos ficar de acordo com uma norma federal que exija que removamos toda informação de clientes que tenham assinado um pedido para serem retirados da lista.

Podemos fazer isto à mão. Vai demorar um pouco, mas pode ser feito.

Ou então, podemos criar este shell script:

	#!/bin/sh
	grep -v XYZ $i > $i.new
	mv $i.new $i

Se eu configurar $EDITOR para apontar para este script, e rodar vipw, o vipw vai tratar todo o "trabalho" de alterar o arquivo password. Mas a edição real, ao invés de ser um processo doloroso que envolva ler todas as linhas de um arquivo de algumas centenas de linhas, vai levar apenas alguns segundos (na verdade, em versões antigas do vipw, isto seria um problema, por que o programa assumia que a data e hora do arquivo sempre seria alterada se o arquivo realmente fosse alterado).

Essencialmente, para o vipw, $EDITOR é somente outro plugin. Não importa o que ele é, desde que ele edite um arquivo in place.

Trocando modos

E se o editor não fizer edição in place? Em arquiteturas de componentes tradicionais, cada componente tem que fornecer toda interface que você pode precisar. No Unix, a alteração da interface é uma questão de ter uma ferramenta que troque a interface de outra ferramenta.

Obviamente, para um componente enorme em uma arquitetura de componentes tradicional, esta tarefa quase passa despercebida. Entretanto, isto torna os componentes menores proibitivamente caros. Acrescentar 1% ao código de um enorme e poderoso aplicativo é algo que está "no radar". Triplicar o custo de escrever um componente pequeno significa que você não irá escrevê-lo, ou que você acabará escrevendo componentes que tentem fazer tudo de uma só vez, ao invés de escrever componentes que sigam os excelentes exemplos do uniq e do sort.

Pegue o nosso exemplo acima emq ue usamos o grep no arquivo de senhas. Se o grep tivesse um modo de edição in place, você poderia usar o mesmo como seu editor. Mas ele não tem, logo, você precisa de um wrapper.

O Unix vem com uma variedade de wrappers que executam algumas traduções importantes. Outros podem ser facilmente escritos, como vimos neste artigo (e veremos mais no próximo).

O mais poderoso, e confuso, de todos os wrappers Unix é provavelmente o xargs. O programa xargs recebe, como entrada, uma lista de arquivos, e passa estes arquivos por um comando informado. Ele funciona particularmente bem em conjunto com um utilitários chamado find, que produz listas de arquivos (na verdade, o find também é capaz de fazer isto, mas é menos eficiente na maioria dos casos).

Por exemplo, digamos que queremos procurar pela string "foo" em todos os arquivos fonte de C no nosso diretório atual, ou em algum subdiretório do mesmo:

	find . -name "*.c" -print | xargs grep foo

Talvez eu queira saber quais arquivos contém a string?

	find . -name "*.c" -print | xargs grep -l foo

Note que, no ambiente Unix, o comando find apenas lista arquivos. Ele não tenta encontrar arquivos baseado no seu conteúdo. Se você quer procurar no conteúdo, precisa passar a lista de arquivos que quer pesquisar em uma ferramenta que saiba como pesquisar no conteúdo. No Unix, a ferramenta que sabe como encontrar arquivos faz o que sabe melhor e deixa outras tarefas para outras ferramentas que são boas em outras coisas.

Digamos que você queira rodar um dado programa sobre uma série de arquivos, mas a saída tem que ser in place, ao invés de uma stream.

Uma versão simples desta ferramenta pode pegar os nomes na entrada padrão, e o comando para ser executado como um argumento. Poderia se parecer com isto:

	#!/bin/sh
	while read i
	do $* $i.new && mv $i.new $i
	done

Note que este programa não é seguro nem confiável. Para uma aplicação "real", você vai querer fazer mais testes, e proteger-se contra alguns erros comuns. Entretanto, se você conhece a entrada e o conjunto de dados, você pode fazer isto e isto vai funcionar bem.

Não tem certeza que está tudo certo? troque a linha do meio para

	do echo "$* $i.new && mv $i.new $i"

e veja se a lista de comandos gerados parece estar correta. Melhor ainda, se você quer executar um caso de teste, rode o utilitário por head -1 | sh e veja se ele faz o que você queria no primeiro arquivo.

O próprio shell está pronto para ser usado como uma ferramenta. Ele recebe comandos na entrada padrão, e coloca a saída destes comandos na saída padrão.

Em outras palavras, todas as ferramentas são projetadas para serem usadas para construir outras ferramentas, e as ferramentas resultantes são elas mesmas ainda projetadas para funcionarem bem com outras ferramentas. Se uma ferramenta não funciona da forma que você deseja, provavelmente há uma ferramenta para executar a "tradução" das interfaces. Se não, você pode escrever uma, facilmente.

Na Parte 3, iremos discutir a construção de novas ferramentas.

Recursos

Sobre o autor

Quando criança, Peter Seebach pensou que "rm" era um nome perfeitamente intuitivo para o comando que "ReMove" um arquivo. Desde então tem sido um defensor incondicional do Unix. Você pode entrar em contato com ele em unixcomponents@seebs.plethora.net.

1