Papo de Botequim Parte II
- Garçom! Traz um "chops" e dois "pastel". O meu amigo hoje não vai beber por que ele finalmente está sendo apresentado a um verdadeiro sistema operacional e ainda tem muita coisa a aprender!
- E então, amigo, tá entendendo tudo que te expliquei até agora?
- Entendendo eu tô, mas não vi nada prático nisso...
- Calma rapaz, o que te falei até agora, serve como base ao que há de vir daqui pra frente. Vamos usar estas ferramentas que vimos para montar programas estruturados, que o
Shell permite. Você verá porque até na TV já teve programa chamado "O
Shell é o Limite".
- Para começar vamos falar dos comandos da família
grep.
-
grep? Não conheço nenhum termo em inglês com este nome...
- É claro,
grep é um acrônimo
Global Regular Expression Print, que usa expressões regulares para pesquisar a ocorrência de cadeias de caracteres na entrada definida (se bem que há uma lenda sobre como este comando foi nomeado: no editor de textos "ed", o avô do "vim", o comando usado para buscas era g/_expressao regular_/p, ou no inglês g/_re_/p.). Por falar em expressões regulares (ou
regexp), o
Aurélio Marinho Jargas tem todas as dicas em sua página (inclusive tutorias) que abordam o tema. Se você está mesmo a fim de aprender a programar em
Shell,
Perl,
Python, ... Acho bom você ler estes artigos para te ajudar no que está para vir.
Eu fico com o grep, você com a gripe
Esse negócio de gripe é brincadeira! É só um pretexto para pedir umas caipirinhas. Mas voltando à vaca fria, eu te falei que o
grep procura cadeia de caracteres dentro de uma entrada definida, mas o que vem a ser uma "entrada definida"? Bem, existem várias formas de definir a entrada do comando
grep. Vejamos:
Pesquisando em um arquivo:
$ grep rafael /etc/passwd
Pesquisando em vários arquivos:
$ grep grep *.sh
Pesquisando na saida de comando:
$ who | grep Pelegrino
No 1º exemplo, o mais simples, procurei a palavra rafael em qualquer lugar do arquivo
/etc/passwd. Se quisesse procurá-la como um login name, isto é, somente no início dos registros deste arquivo, eu deveria fazer:
$ grep '^rafael' /etc/passwd
E para que serve este circunflexo e os apóstrofos, você vai me perguntar. O circunflexo (
^), se você tivesse lido os artigos anteriores sobre expressões regulares que te falei, saberia que servem para limitar a pesquisa ao início de cada linha, e os apóstrofos (
') servem para o
Shell não interpretar este circunflexo, deixando-o passar incólume para o comando
grep.
Olha que legal! O
grep aceita como entrada, a saída de outro comando redirecionado por um
pipe (isto é muito comum em
Shell e é um tremendo acelerador de execução de comando já que atua como se a saída de um programa fosse guardada em disco e o segundo programa lesse este arquivo gerado), desta forma, no 3º exemplo, o comando
who listou as pessoas "logadas" na mesma máquina que você (não se esqueça jamais: o
Linux é multiusuário) e o
grep foi usado para verificar se o Pelegrino estava trabalhando ou "coçando".
A família grep
Este comando
grep é muito conhecido, pois é usado com muita freqüência, o que muitas pessoas desconhecem é que existem três comandos na família
grep, que são:
A principais características diferenciais entre os 3 são:
- O
grep pode ou não usar expressões regulares simples, porém no caso de não usá-las, o fgrep é melhor, por ser mais rápido;
- O
egrep ("e" de extended, extendido) é muito poderoso no uso de expressões regulares. Por ser o mais lento da família, só deve ser usado quando for necessária a elaboração de uma expressão regular não aceita pelo grep;
- O
fgrep ("f" de fast, rápido, ou de "file", arquivo) como o nome diz é o rapidinho da família, executa o serviço de forma muito veloz (por vezes é cerca de 30% mais veloz que o grep e 50% mais que o egrep), porém não permite o uso de expressões regulares na pesquisa.

Tudo que foi dito acima sobre velocidade, só se aplica à família de comandos
grep do
Unix. No
Linux o
grep é sempre mais veloz, já que os outros dois (
fgrep e
egrep) são
scripts em
Shell que chamam o primeiro e, já vou adiantando,
não gosto nem um pouquinho desta solução.
- Agora que você já conhece as diferenças entre os membros da família, me diga: o que você acha dos três exemplos que eu dei antes das explicações?
- Eu achei que o
fgrep resolveria o teu problema de forma mais veloz do que o
grep.
- Perfeito! Tô vendo que você está atento! Está entendendo tudo que estou te explicando! Então vamos ver mais exemplos para clarear de vez as diferenças de uso dos membros da família.
Exemplos
Eu sei que em um arquivo existe um texto falando sobre
Linux só não tenho certeza se está escrito com
L maiúsculo ou
l minúsculo. Posso fazer de duas formas:
$ egrep (Linux | linux) arquivo.txt
ou
$ grep [Ll]inux arquivo.txt
No primeiro caso, a expressão regular complexa
"(Linux | linux)" usa os parênteses para agrupar as opções e a barra vertical (
|) como um "ou" lógico, isto é, estou procurando
Linux ou
linux.
No segundo, a expressão regular
[Ll]inux significa: começado por
L ou
l seguido de
inux. Por esta expressão ser mais simples, o
grep consegue resolvê-la, portanto acho melhor usar a segunda forma, já que o
egrep tornaria a pesquisa mais lenta.
Outro exemplo. Para listar todos os subdiretórios do diretório corrente, basta:
$ ls -l | grep '^d'
drwxr-xr-x 3 root root 4096 Dec 18 2000 doc
drwxr-xr-x 11 root root 4096 Jul 13 18:58 freeciv
drwxr-xr-x 3 root root 4096 Oct 17 2000 gimp
drwxr-xr-x 3 root root 4096 Aug 8 2000 gnome
drwxr-xr-x 2 root root 4096 Aug 8 2000 idl
drwxrwxr-x 14 root root 4096 Jul 13 18:58 locale
drwxrwxr-x 12 root root 4096 Jan 14 2000 lyx
drwxrwxr-x 3 root root 4096 Jan 17 2000 pixmaps
drwxr-xr-x 3 root root 4096 Jul 2 20:30 scribus
drwxrwxr-x 3 root root 4096 Jan 17 2000 sounds
drwxr-xr-x 3 root root 4096 Dec 18 2000 xine
No exemplo que acabamos de ver, o circunflexo (
^) serviu para limitar a pesquisa à primeira posição da saída do
ls longo. Os apóstrofos foram colocados para o
Shell não "ver" o circunflexo (
^).
Vamos ver mais um. Sabemos que as quatro primeiras posições possíveis de um
ls -l de um arquivo comum (arquivo comum! Não é diretório, nem
link, nem ...) devem ser:
| Posição |
1ª |
2ª |
3ª |
4ª |
| |
|
|
- |
| Valores Possíveis |
- |
r |
w |
x |
| |
- |
- |
s (suid) |
Assim sendo, para descobrir todos os arquivos executáveis em um determinado diretório eu deveria fazer:
$ ls -la | egrep '^-..(x|s)'
-rwxr-xr-x 1 root root 2875 Jun 18 19:38 rc
-rwxr-xr-x 1 root root 857 Aug 9 22:03 rc.local
-rwxr-xr-x 1 root root 18453 Jul 6 17:28 rc.sysinit
Onde novamente usamos o circunflexo (
^) para limitar a pesquisa ao início de cada linha, então as linhas listadas serão as que começam por um traço (
-), seguido de qualquer coisa (o ponto quando usado como uma expressão regular significa qualquer coisa), novamente seguido de qualquer coisa, vindo a seguir um
x ou um
s.
Obteríamos o mesmo resultado se fizéssemos:
$ ls -la | grep '^-..[xs]'
e agilizaríamos a pesquisa.
Vamos Montar uma "cdteca"
Vamos começar a desenvolver programas, acho que a montagem de um banco de dados de músicas é bacana para efeito didático (e útil nesses tempos de
downloads de mp3 e "queimadores" de CDs). Não se esqueça que, da mesma forma que vamos desenvolver um monte de programas para organizar os seus CDs de música, com pequenas adaptações, você pode fazer o mesmo com os CDs de
software que vêm com a
Linux Magazine e outros que você compra ou queima, disponibilizando este banco de
software para todos que trabalham com você (o
Linux é multiusuário, e como tal deve ser explorado), desta forma ganhando muitos pontos com seu adorado chefe.
- Péra ai! De onde eu vou receber os dados dos CDs?
- Inicialmente, vou lhe mostrar como o seu programa pode receber parâmetros de quem o estiver executando e em breve, ensinarei a ler os dados pela tela ou de um arquivo.
Passando parâmetros
O layout do arquivo musicas será o seguinte:
nome do álbum^intérprete1~nome da música1:..:intérprete~nome da música
isto é, o nome do álbum será separado por um circunflexo (
^) do resto do registro, que é formado por diversos grupos compostos pelo intérprete de cada música do CD e a respectiva música interpretada. Estes grupos são separados entre si por dois-pontos (
:) e internamente, o intérprete será separado por um til (
~) do nome da música.
Eu quero escrever um programa que chamado
musinc, que incluirá registros no meu arquivo musicas. Eu passarei o conteúdo de cada álbum como parâmetro na chamada do programa fazendo assim:
$ musinc "álbum^interprete~musica:interprete~musica:..."
Desta forma o programa
musinc estará recebendo os dados de cada álbum como se fosse uma variável. A única diferença entre um parâmetro recebido e uma variável é que os primeiros recebem nomes numéricos (nome numérico fica muito esquisito, né? O que quis dizer é que seus nomes são formados por um e somente um algarismo), isto é
$1, $2, $3, ..., $9. Vamos, antes de tudo, fazer um teste:
Exemplos
$ cat teste
#!/bin/bash
# Programa para testar passagem de parametros
echo "1o. parm -> $1"
echo "2o. parm -> $2"
echo "3o. parm -> $3"
Vamos executá-lo:
$ teste passando parametros para testar
bash: teste: cannot execute
Ops! Esqueci-me de torná-lo executável. Vou fazê-lo de forma a permitir que todos possam executá-lo e em seguida vou testá-lo:
$ chmod 755 teste
$ teste passando parametros para testar
1o. parm -> passando
2o. parm -> parametros
3o. parm -> para
Repare que a palavra
testar, que seria o quarto parâmetro, não foi listada. Isto deu-se justamente porque o programa teste só listava os três primeiros parâmetros. Vamos executá-lo de outra forma:
$ teste "passando parametros" para testar
1o. parm -> passando parametros
2o. parm -> para
3o. parm -> testar
As aspas não deixaram o
Shell ver o espaço em branco entre as palavras e considerou-as um único parâmetro.
Macetes paramétricos
Já que estamos falando em passagem de parâmetros deixa eu te dar mais umas dicas:
| Significado das Principais Variáveis Referentes aos Parâmetros |
$* |
Contém o conjunto de todos os parâmetros (muito parecido com $@) |
| Variável |
Significado |
$0 |
Contém o nome do programa |
$# |
Contém a quantidade de parâmetros passados |
Exemplos
Vamos alterar o programa
teste para usar as variáveis que acabamos de ver. Vamos fazê-lo assim:
$ cat teste
#!/bin/bash
# Programa para testar passagem de parametros (2a. Versao)
echo O programa $0 recebeu $# parametros
echo "1o. parm -> $1"
echo "2o. parm -> $2"
echo "3o. parm -> $3"
echo Todos de uma só \"tacada\": $*
Repare que antes das aspas eu usei uma barra invertida para o escondê-las da interpretação do
Shell (se não usasse as contrabarras as aspas não apareceriam). Vamos executá-lo:
$ teste passando parametros para testar
O programa teste recebeu 4 parametros
1o. parm -> passando
2o. parm -> parametros
3o. parm -> para
Todos de uma só "tacada": passando parametros para testar
Conforme eu disse, os parâmetros recebem números de
1 a
9, mas isso não significa que não posso usar mais de 9 parâmetros significa somente que só posso endereçar 9. Vamos testar isso:
Exemplo:
$ cat teste
#!/bin/bash
# Programa para testar passagem de parametros (3a. Versao)
echo O programa $0 recebeu $# parametros
echo "11o. parm -> $11"
shift
echo "2o. parm -> $1"
shift 2
echo "4o. Parm -> $1"
Vamos executá-lo:
$ teste passando parametros para testar
O programa teste recebeu 4 parametros que são:
11o. parm -> passando1
2o. parm -> parametros
4o. parm -> testar
Duas coisas muito interessantes neste
script:
- Para mostrar que os nomes dos parâmetros variam de
$1 a $9 eu fiz um echo $11 e o que aconteceu? O Shell interpretou como sendo $1 seguido do algarismo 1 e listou passando1;
- O comando
shift cuja sintaxe é shift n, podendo o n assumir qualquer valor numérico (porém seu default é 1 como no exemplo dado), despreza os n primeiros parâmetros, tornando o parâmetro de ordem n+1, o primeiro ou seja, o $1.
Bem, agora que você já sabe mais sobre passagem de parâmetros do que eu, vamos voltar à nossa "cdteca" para fazer o
script de inclusão de CDs no meu banco chamado
musicas. O programa é muito simples (como tudo em
Shell) e vou listá-lo para você ver:
Exemplos
$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 1)
#
echo $1 >> musicas
O
script é fácil e funcional, limito-me a anexar ao fim do arquivo
musicas o parâmetro recebido. Vamos cadastrar 3 álbuns para ver se funciona (para não ficar "enchendo lingüiça", vou supor que em cada CD só existem 2 músicas):
$ musinc "album 3^Artista5~Musica5:Artista6~Musica5"
$ musinc "album 1^Artista1~Musica1:Artista2~Musica2"
$ musinc "album 2^Artista3~Musica3:Artista4~Musica4"
Listando o conteúdo de musicas.
$ cat musicas
album 3^Artista5~Musica5:Artista6~Musica6
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
Não está funcional como achava que deveria ficar... podia ter ficado melhor. Os álbuns estão fora de ordem, dificultando a pesquisa. Vamos alterar nosso
script e depois testá-lo novamente:
$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 2)
#
echo $1 >> musicas
sort musicas -o musicas
Vamos cadastrar mais um:
$ musinc "album 4^Artista7~Musica7:Artista8~Musica8"
Agora vamos ver o que aconteceu com o arquivo
musicas:
$ cat musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
album 3^Artista5~Musica5:Artista6~Musica5
album 4^Artista7~Musica7:Artista8~Musica8
Simplesmente inseri uma linha que classifica o arquivo
musicas dando a saída nele mesmo (para isso serve a opção
-o), após cada álbum ser anexado.
Oba! Agora está legal e quase funcional. Mas atenção, não se desespere! Esta não é a versão final. O programa ficará muito melhor e mais amigável, em uma nova versão que desenvolveremos após aprendermos a adquirir os dados da tela e formatar a entrada.
Exemplos
Ficar listando com o comando
cat não está com nada, vamos então fazer um programa chamado
muslist para listar um álbum cujo nome será passado como parâmetro:
$ cat muslist
#!/bin/bash
# Consulta CDs (versao 1)
#
grep $1 musicas
Vamos executá-lo, procurando pelo
album 2. Como já vimos antes, para passar a cadeia
album 2 é necessário protegê-la da interpretação do
Shell, para que ele não a interprete como dois parâmetros. Vamos fazer assim:
$ muslist "álbum 2"
grep: can't open 2
musicas: album 1^Artista1~Musica1:Artista2~Musica2
musicas: album 2^Artista3~Musica3:Artista4~Musica4
musicas: album 3^Artista5~Musica5:Artista6~Musica6
musicas: album 4^Artista7~Musica7:Artista8~Musica8
Que lambança! Onde está o erro? Eu tive o cuidado de colocar o parâmetro passado entre aspas, para o
Shell não dividi-lo em dois!
É, mas repare como está o
grep executado:
grep $1 musicas
Mesmo colocando
álbum 2 entre aspas, para que fosse encarado como um único parâmetro, quando o
$1 foi passado pelo
Shell para o comando
grep, transformou-se em dois argumentos. Desta forma o conteúdo final da linha, que o comando
grep executou foi o seguinte:
grep album 2 musicas
Como a sintaxe do
grep é:
=grep
[arq1, arq2, ..., arqn]=
o grep entendeu que deveria procurar a cadeia de caracteres album nos arquivos 2 e musicas, Por não existir o arquivo 2 gerou o erro, e por encontrar a palavra album em todos os registros de musicas, listou a todos.

Sempre que a
cadeia de caracteres a ser passada para o comando
grep possuir brancos ou
TAB, mesmo que dentro de variáveis, coloque-a sempre entre aspas para evitar que as palavras após o primeiro espaço em branco ou
TAB sejam interpretadas como nomes de arquivos.
Por outro lado, é melhor ignorarmos maiúsculas e minúsculas na pesquisa. Resolveríamos os dois problemas se o programa tivesse a seguinte forma:
$ cat muslist
#!/bin/bash
# Consulta CDs (versao 2)
#
grep -i "$1" musicas
Neste caso, usamos a opção -i do grep, que como já vimos, serve para ignorar maiúsculas e minúsculas, e colocamos o $1 entre aspas, para que o grep continuasse a ver a cadeia de caracteres resultante da expansão da linha pelo Shell como um único argumento de pesquisa.
$ muslist "album 2"
album2^Artista3~Musica3:Artista4~Musica4
Agora repare que o grep localiza a cadeia pesquisada em qualquer lugar do registro, então da forma que estamos fazendo, podemos pesquisar por álbum, por música, por intérprete ou até por um pedaço de qualquer um destes. Quando conhecermos os comandos condicionais, montaremos uma nova versão de muslist que permitirá especificar por qual campo pesquisar.
Aí você vai me dizer:
- Poxa, mas é um saco ter que colocar o argumento de pesquisa entre aspas na hora de passar o nome do álbum. Esta forma não é nem um pouco amigável!
- Tem razão, e por isso vou te mostrar uma outra forma de fazer o que você pediu:
$ cat muslist
#!/bin/bash
# Consulta CDs (versao 3)
#
grep -i "$*" musicas
$ muslist album 2
album 2^Artista3~Musica3:Artista4~Musica4
Desta forma, o $*, que significa todos os parâmetros, será substituído pela cadeia album 2 (de acordo com o exemplo anterior, fazendo o que você queria.
Não se esqueça o problema do Shell não é se você pode ou não fazer uma determinada coisa. O problema é decidir qual é a melhor forma de fazê-la, já que para desempenhar qualquer tarefa, a quantidade de opções é enorme.
Ah! Em um dia de verão você foi à praia, esqueceu o CD no carro, aquele "solzinho" de 40 graus empenou o seu CD e agora você precisa de uma ferramenta para removê-lo do banco de dados? Não tem problema, vamos desenvolver um script chamado musexc, para excluir estes CDs.
Antes de desenvolver o "bacalho", quero te apresentar a uma opção bastante útil da família de comandos grep. É a opção -v, que quando usada lista todos os registros da entrada, exceto o(s) localizado(s) pelo comando. Vejamos:
Exemplos
$ grep -v "album 2" musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 3^Artista5~Musica5:Artista6~Musica6
album 4^Artista7~Musica7:Artista8~Musica8
Conforme eu expliquei antes, o grep do exemplo listou todos os registros de musicas exceto o referente a album 2, porque atendia ao argumento do comando. Estamos então prontos para desenvolver o script para remover aquele CD empenado da sua "CDteca". Ele tem a seguinte cara:
$ cat musexc
#!/bin/bash
# Exclui CDs (versao 1)
#
grep -v "$1" musicas > /tmp/mus$$
mv -f /tmp/mus$$ musicas
Na primeira linha mandei para /tmp/mus$$ o arquivo musicas, sem os registros que atendessem a consulta feita pelo comando grep. Em seguida, movi (que, no duro, equivale a renomear) /tmp/mus$$ por cima do antigo musicas.
Usei o arquivo /tmp/mus$$ como arquivo de trabalho, porque como já havia citado no artigo anterior, o $$ contém o PID (Process Identification ou identificação do processo) e desta forma cada um que editar o arquivo musicas o fará em um arquivo de trabalho diferente, desta forma evitando colisões no uso.
- Aê cara, estes programas que fizemos até aqui estão muito primários em virtude da falta de ferramentas que ainda temos. Mas é bom, enquanto eu tomo mais um chope, você ir para casa praticar em cima dos exemplos dados porque, eu prometo, chegaremos a desenvolver um sistema bacana para controle dos seus CDs.
- Quando nos encontrarmos da próxima vez, vou te ensinar como funcionam os comandos condicionais e aprimoraremos mais um pouco estes scripts.
- Por hoje chega! Já falei demais e preciso molhar a palavra porque estou de goela seca!
- Garçom! Mais um sem colarinho!
Vou aproveitar também para mandar o meu jabá: diga para os amigos que quem estiver afim de fazer um curso porreta de programação em Shell que mande um e-mail para a nossa gerencia de treinamento para informar-se.
Qualquer dúvida ou falta de companhia para um chope ou até para falar mal dos políticos é só mandar um e-mail para mim.
Valeu!