Feeds:
Posts
Comentários

Posts Tagged ‘regexp’

Papo de Botequim – Parte 2

– 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:

  • grep
  • egrep
  • fgrep

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.
Pinguim com placa de atenção 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
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
Variável Significado
$0 Contém o nome do programa
$# Contém a quantidade de parâmetros passados
$* Contém o conjunto de todos os parâmetros (muito parecido com $@)

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:

  1. 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;
  2. 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.

Pinguim com placa de dica 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!

Anúncios

Read Full Post »


– E aí rapaz, tudo bom? – Belê, mas você lembra que me pediu para fazer um programa que quando o tamanho de uma tela variasse, no seu centro apareceria dinamicamente, em vídeo reverso, a quantidade de linhas e colunas do jeito que o Linux faz normalmente? Pois é eu fiz mas a aparência não ficou igual.- Não estou nem aí para a aparência, o que eu queria é que você exercitasse o que aprendemos. Deixe-me ver o que você fez.

$ cat tamtela.sh
#!/bin/bash
#
# Coloca no centro da tela, em video reverso,
# a quantidade de colunas e linhas
# quando o tamanho da tela eh alterado.
#
trap Muda 28 # 28 = sinal gerado pela mudanca no tamanho
# da tela e Muda eh a funcao que fara isso.

Bold=$(tput bold) # Modo de enfase
Rev=$(tput rev) # Modo de video reverso
Norm=$(tput sgr0) # Restaura a tela ao padrao default

Muda ()
{
clear
Cols=$(tput cols)
Lins=$(tput lines)
tput cup $(($Lins / 2)) $(((Cols – 7) / 2)) # Centro da tela
echo $Bold$Rev$Cols X $Lins$Norm
}

clear
read -n1 -p “Mude o tamanho da tela ou tecle algo para terminar “

– Perfeito! Que se dane a aparência, depois vou te ensinar uns macetes para melhorá-la o que vale é que o programa está funcionando e está bem enxuto.

– Poxa, perdi o maior tempo tentando descobrir como aumentar o fonte …

– Deixe isso para lá e hoje vamos ver umas coisas bastante interessantes e úteis.

Named Pipes

Um outro tipo de pipe é o named pipe, que também é chamado de FIFO. FIFO é um acrônimo de First In First Out que se refere à propriedade em que a ordem dos bytes entrando no pipe é a mesma que a da saída. O name em named pipe é, na verdade, o nome de um arquivo. Os arquivos tipo named pipes são exibidos pelo comando ls como qualquer outro, com poucas diferenças, veja:

$ ls -l pipe1
prw-r-r– 1 julio dipao 0 Jan 22 23:11 pipe1|

O p na coluna mais à esquerda indica que fifo1 é um named pipe. O resto dos bits de controle de permissões, quem pode ler ou gravar o pipe, funcionam como um arquivo normal. Nos sistemas mais modernos uma barra vertical (|) colocado ao fim do nome do arquivo, é outra dica, e nos sistemas LINUX, onde a opção de cor está habilitada, o nome do arquivo é escrito em vermelho por default.

Nos sistemas mais antigos, os named pipes são criados pelo programa mknod, normalmente situado no diretório /etc.

Nos sistemas mais modernos, a mesma tarefa é feita pelo mkfifo. O programa mkfifo recebe um ou mais nomes como argumento e cria pipes com estes nomes. Por exemplo, para criar um named pipe com o nome pipe1, faça:

$ mkfifo pipe1

Como sempre, a melhor forma de mostrar como algo funciona é dando exemplos. Suponha que nós tenhamos criado o named pipe mostrado anteriormente. Vamos agora trabalhar com duas sessões ou duas consoles virtuais ou uma de cada. Em uma delas faça:

$ ls -l > pipe1

e em outra faça:

$ cat < pipe1

Voilá! A saída do comando executado na primeira console foi exibida na segunda. Note que a ordem em que os comandos ocorreram não importa.

Se você prestou atenção, reparou que o primeiro comando executado, parecia ter “pendurado, congelado”. Isto acontece porque a outra ponta do pipe ainda não estava conectada, e então o sistema operacional suspendeu o primeiro processo até que o segundo “abrisse” o pipe. Para que um processo que usa pipe não fique em modo de wait, é necessário que em uma ponta do pipe tenha um processo “tagarela” e na outra um “ouvinte” e no exemplo que demos, o ls era o “falador” e o cat era o “orelhão”.

Uma aplicação muito útil dos named pipes é permitir que programas sem nenhuma relação possam se comunicar entre si, os named pipes também são usados para sincronizar processos, já que em um determinado ponto você pode colocar um processo para “ouvir” ou para “falar” em um determinado named pipe e ele daí só sairá, se outro processo “falar” ou “ouvir” aquele pipe.

Você já viu que o uso desta ferramenta é ótimo para sincronizar processos e para fazer bloqueio em arquivos de forma a evitar perda/corrupção de informações devido a atualizações simultâneas (concorrência). Vejamos exemplos para ilustrar estes casos.

Sincronização de processos.

Suponha que você dispare paralelamente dois programas (processos) cujos diagramas de blocos de suas rotinas são como a figura a seguir:

Gráfico do Named Pipe Os dois processos são disparados em paralelo e no BLOCO1 do Programa1 as três classificações são disparadas da seguinte maneira:

    for Arq in BigFile1 BigFile2 BigFile3
    do
        if  sort $Arq
        then
            Manda=va
        else
            Manda=pare
            break
        fi
    done
    echo $Manda > pipe1
    [ $Manda = pare ] &&
        {
        echo Erro durante a classificação dos arquivos
        exit 1
        }
    ...

Assim sendo, o comando if testa cada classificação que está sendo efetuada. Caso ocorra qualquer problema, as classificações seguintes serão abortadas, uma mensagem contendo a cadeia pare é enviada pelo pipe1 e programa1 é descontinuado com um fim anormal.

Enquanto o Programa1 executava o seu primeiro bloco (as classificações) o Programa2 executava o seu BLOCO1, processando as suas rotinas de abertura e menu paralelamente ao Programa1, ganhando desta forma um bom intervalo de tempo.

O fragmento de código do Programa2 a seguir, mostra a transição do seu BLOCO1 para o BLOCO2:

    OK=`cat pipe1`
    if  [ $OK = va ]
    then
        ...
        Rotina de impressão
        ...
    else    #  Recebeu "pare" em OK
        exit 1
    fi

Após a execução de seu primeiro bloco, o Programa2 passará a “ouvir” o pipe1, ficando parado até que as classificações do Programa1 terminem, testando a seguir a mensagem passada pelo pipe1 para decidir se os arquivos estão íntegros para serem impressos, ou se o programa deverá ser descontinuado. Desta forma é possível disparar programas de forma assíncrona e sincronizá-los quando necessário, ganhando bastante tempo de processamento.

Bloqueio de arquivos

Suponha que você escreveu uma CGI (Common Gateway Interface) em Shell para contar quantos hits recebe uma determinada URL e a rotina de contagem está da seguinte maneira:

    Hits="$(cat page.hits 2> /dev/null)" || Hits=0
    echo $((Hits=Hits++)) > page.hits

Desta forma se a página receber dois ou mais acessos concorrentes, um ou mais poderá(ão) ser perdido(s), basta que o segundo acesso seja feito após a leitura da arquivo page.hits e antes da sua gravação, isto é, basta que o segundo acesso seja feito após o primeiro ter executado a primeira linha do script e antes de executar a segunda.

Então o que fazer? Para resolver o problema de concorrência vamos utilizar um named pipe. Criamos o seguinte script que será o daemon que receberá todos os pedidos para incrementar o contador. Note que ele vai ser usado por qualquer página no nosso site que precise de um contador.

$ cat contahits.sh
#!/bin/bash

PIPE=”/tmp/pipe_contador” # arquivo named pipe
# dir onde serao colocados os arquivos contadores de cada pagina
DIR=”/var/www/contador”

[ -p “$PIPE” ] || mkfifo “$PIPE”

while :
do
for URL in $(cat < $PIPE)
do
FILE=”$DIR/$(echo $URL | sed ‘s,.*/,,’)”
# OBS1: no sed acima, como precisava procurar
# uma barra,usamos vírgula como separador.
# OBS2: quando rodar como daemon comente a proxima linha
echo “arquivo = $FILE”

n=”$(cat $FILE 2> /dev/null)” || n=0
echo $((n=n+1)) > “$FILE”
done
done

Como só este script altera os arquivos, não existe problema de concorrência.

Este script será um daemon, isto é, rodará em background. Quando uma página sofrer um acesso, ela escreverá a sua URL no arquivo de pipe. Para testar, execute este comando:

    echo "teste_pagina.html" > /tmp/pipe_contador

Para evitar erros, em cada página que quisermos adicionar o contador acrescentamos a seguinte linha:

    <!--#exec cmd="echo $REQUEST_URI > /tmp/pipe_contador"-->

Note que a variável $REQUEST_URI contém o nome do arquivo que o navegador (browser) requisitou.

Este último exemplo, é fruto de uma idéia que troquei com o amigo e mestre em Shell, Thobias Salazar Trevisan que escreveu o script e colocou-o em sua excelente URL. Aconselho a todos que querem aprender Shell a dar uma olhada nela (Dê uma olhada e inclua-a nos favoritos).

Ahhh! Você pensa que o assunto sobre named pipes está esgotado? Enganou-se. Vou mostrar um uso diferente a partir de agora.

Substituição de processos

Acabei de mostrar um monte de dicas sobre named pipes, agora vou mostrar que o Shell também usa os named pipes de uma maneira bastante singular, que é a substituição de processos (process substitution). Uma substituição de processos ocorre quando você põe um comando ou um pipeline de comandos entre parênteses e um < ou um > grudado na frente do parêntese da esquerda. Por exemplo, teclando-se o comando:

$ cat <(ls -l)

Resultará no comando ls -l executado em um subshell como é normal (por estar entre parênteses), porém redirecionará a saída para um named pipe temporário, que o Shell cria, nomeia e depois remove. Então o cat terá um nome de arquivo válido para ler (que será este named pipe e cujo dispositivo lógico associado é /dev/fd/63), e teremos a mesma saída que a gerada pela listagem do ls -l, porém dando um ou mais passos que o usual, isto é, mais onerosa para o computador.

Como poderemos constatar isso? Fácil… Veja o comando a seguir:

$ ls -l >(cat)
l-wx—— 1 jneves jneves 64 Aug 27 12:26 /dev/fd/63 -> pipe:[7050]

É… Realmente é um named pipe.

Você deve estar pensando que isto é uma maluquice de nerd, né? Então suponha que você tenha 2 diretórios: dir e dir.bkp e deseja saber se os dois estão iguais (aquela velha dúvida: será que meu backup está atualizado?). Basta comparar os dados dos arquivos dos diretórios com o comando cmp, fazendo:

$ cmp <(cat dir/*) <(cat dir.bkp/*) || echo backup furado

ou, melhor ainda:

$ cmp <(cat dir/*) <(cat dir.bkp/*) >/dev/null || echo backup furado

Da forma acima, a comparação foi efetuada em todas as linhas de todos os arquivos de ambos os diretórios. Para acelerar o processo, poderíamos compara somente a listagem longa de ambos os diretórios, pois qualquer modificação que um arquivo sofra, é mostrada na data/hora de alteração e/ou no tamanho do arquivo. Veja como ficaria:

$ cmp <(ls -l dir) <(ls -l dir.bkp) >/dev/null || echo backup furado

Este é um exemplo meramente didático, mas são tantos os comandos que produzem mais de uma linha de saída, que serve como guia para outros. Eu quero gerar uma listagem dos meus arquivos, numerando-os e ao final dar o total de arquivos do diretório corrente:

    while read arq
    do
        ((i++)) # assim nao eh necessario inicializar i
        echo "$i: $arq"
    done < <(ls)
    echo "No diretorio corrente (`pwd`) existem $i arquivos"

Tá legal, eu sei que existem outras formas de executar a mesma tarefa. Usando o comando while, a forma mais comum de resolver esse problema seria:

    ls | while read arq
    do
        ((i++)) # assim nao eh necessario inicializar i
        echo "$i: $arq"
    done
    echo "No diretorio corrente (`pwd`) existem $i arquivos"

Quando executasse o script, pareceria estar tudo certo, porém no comando echo após o done, você verá que o valor de $i foi perdido. Isso deve-se ao fato desta variável estar sendo incrementada em um subshell criado pelo pipe (|) e que terminou no comando done, levando com ele todas as variáveis criadas no seu interior e as alterações feitas em todas as variáveis, inclusive as criadas externamente.

Somente para te mostrar que uma variável criada fora do subshell e alterada em seu interior perde as alterações feitas ao seu final, execute o script a seguir:

    #!/bin/bash
    LIST=""                  # Criada no shell principal
    ls | while read FILE     # Inicio do subshell
    do
        LIST="$FILE $LIST"   # Alterada dentro do subshell
    done                     # Fim do subshell
    echo :$LIST:

Ao final da execução você verá que aperecerão apenas dois dois-pontos (::). Mas no início deste exemplo eu disse que era meramente didático porque existem formas melhores de fazer a mesma tarefa. Veja só estas duas:

$ ls | ln

ou então, usando a própria substituição de processos:

$ cat -n <(ls)

Um último exemplo: você deseja comparar arq1 e arq2 usando o comando comm, mas este comando necessita que os arquivos estejam classificados. Então a melhor forma de proceder é:

$ comm <(sort arq1) <(sort arq2)

Esta forma evita que você faça as seguintes operações:

$ sort arq1 > /tmp/sort1
$ sort arq2 > /tmp/sort2
$ comm /tmp/sort1 /tmp/sort2
$ rm -f /tmp/sort1 /tmp/sort2

Pessoal, o nosso Papo de Botequim chegou ao fim frown . Curti muito aqui e recebi diversos elogios pelo trabalho desenvolvido ao longo de doze meses e, o melhor de tudo, fiz muitas amizades e tomei muitos chopes de graça com os leitores que encontrei pelos congressos e palestras que ando fazendo pelo nosso querido Brasil.

O que vou escrever aqui não está combinado nem sei se será publicado, mas como os editores desta revista são dois malucos beleza (ambos Rafael), é bem capaz de deixarem passar. É o seguinte: se quiserem que o Papo de Botequim continue, entulhem a caixa postal da Linux Magazine pedindo por isso e desde já escolham o próximo tema entre sed + expressões regulares ou linguagem awk.

De qualquer forma, caso não consigamos sensibilizar a direção da revista, me despeço de todos mandando um grande abraço aos barbudos e beijos às meninas e agradecendo os mais de 100 e-mails que recebi (todos elogiosos) e todos devidamente respondidos.

À saúde de todos nós: Tim, Tim.

– Chico, fecha a minha conta porque vou mudar de botequim.

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!

Read Full Post »