Feeds:
Posts
Comentários

Posts Tagged ‘if’

Papo de Botequim – Parte 3

– Garçon, traga dois chopes por favor que hoje eu vou ter que falar muito.

Trabalhando com cadeias

Pelo título acima não pense você que vou lhe ensinar a ser carcereiro! Estou me referindo a cadeia de caracteres!

O Comando cut (que não é a central de trabalhadores)

Primeiro quero te mostrar, de forma eminentemente prática uma instrução simples de usar e muito útil: o comando cut, Esta instrução é usada para cortar um determinado pedaço de um arquivo e tem duas formas distintas de uso

O comando cut com a opção -c

Com esta opção, o comando tem a seguinte sintaxe:
cut -c PosIni-PosFim [arquivo]

Onde:
PosIni = Posição inicial
PosFim = Posição final

$ cat numeros
1234567890
0987654321
1234554321
9876556789

$ cut -c1-5 numeros
12345
09876
12345
98765

$ cut -c-6 numeros
123456
098765
123455
987655

$ cut -c4- numeros
4567890
7654321
4554321
6556789

$ cut -c1,3,5,7,9 numeros
13579
08642
13542
97568

$ cut -c -3,5,8- numeros
1235890
0986321
1235321
9875789

Como dá para ver, no duro mesmo existem quatro sintaxes distintas: na primeira (-c 1-5), eu especifiquei uma faixa, na segunda (-c -6), especifiquei tudo até uma posição, na terceira (-c 4-) de uma determinada posição em diante e na quarta (-c 1,3,5,7,9), determinadas posições. A última (-c -3,5,8-) foi só para mostrar que podemos misturar tudo.

O comando cut com a opção -f

Mas não pense você que acabou por aí! Como você deve ter percebido esta forma de cut é útil para arquivos com campos de tamanho fixo, mas atualmente o que mais existe são arquivos com campos de tamanho variáveis, onde cada campo termina com um delimitador. Vamos dar uma olhada no arquivo musicas que começamos a preparar no nosso papo na última vez que viemos aqui no botequim.

$ 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

Então, recapitulando, o seu “leiaute” é o seguinte:
nome do album^interprete1~nome da musica1:...:interpreten~nome da musican

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 nome do intérprete será separado por um til (~) do nome da música.

Então para pegarmos os dados referentes a todas as segundas músicas do arquivo musicas, devemos fazer:

$ cut -f2 -d: musicas
Artista2~Musica2
Artista4~Musica4
Artista6~Musica5
Artista8~Musica8

Ou seja, cortamos o segundo campo (-f de field em inglês) delimitado (-d) por dois-pontos (:). Mas, se quisermos somente os intérpretes, devemos fazer:

$ cut -f2 -d: musicas | cut -f1 -d~
Artista2
Artista4
Artista6
Artista8

Para entender isso, vamos pegar a primeira linha de musicas:

$ head -1 musicas
album 1^Artista1~Musica1:Artista2~Musica2

Então observe o que foi feito:

Delimitador do primeiro cut (:)

album 1^Artista1~Musica1:Artista2~Musica2

Desta forma, no primeiro cut, o primeiro campo do delimitador (-d) dois-pontos (:) é album 1^Artista1~Musica1 e o segundo, que é o que nos interessa, é Artista2~Musica2.

Vamos então ver o que aconteceu no segundo cut:

Novo delimitador (~)

Artista2~Musica2

Agora, primeiro campo do delimitador (-d) til (~), que é o que nos interessa, é Artista2 e o segundo é Musica2.

Se o raciocínio que fizemos para a primeira linha for aplicado no restante do arquivo, chegaremos à resposta anteriormente dada.

Se tem cut tem paste

Como já era de se esperar, o comando paste serve para colar, só que aqui no Shell o que ele cola são arquivos. Só para começar a entendê-lo, vamos fazer assim::

    paste arq1 arq2

Desta forma ele mandará para a saída padrão (stdout) cada um dos registros de arq1 ao lado dos registros de arq2 correspondentes e caso nenhum delimitador seja especificado, ele usará por default o <TAB>.

O paste é um comando pouco usado por sua sintaxe ser pouco conhecida. Vamos brincar com 2 arquivos criados da seguinte forma:

$ seq 10 > inteiros
$ seq 2 2 10 > pares

Para ver o conteúdo dos arquivos criados, vamos usar o paste na sua forma careta que mostramos acima:

$ paste inteiros pares
1 2
2 4
3 6
4 8
5 10
6
7
8
9
10

Quem está em pé, deita

Agora vamos transformar a coluna do pares em linha:

$ paste -s pares
2 4 6 8 10

Usando separadores

Como já foi dito, o separador default do paste é o <TAB>, mas isso pode ser alterado com a opção -d. Então para calcular a soma do conteúdo de pares primeiramente faríamos:

$ paste -s -d’+’ pares # também poderia ser -sd’+’
2+4+6+8+10

e depois passaríamos esta linha para a calculadora (bc) e então ficaria:

$ paste -sd’+’ pares | bc
30

Assim sendo, para calcular o fatorial do número contido em $Num, basta:

$ seq $Num | paste -sd’*’ | bc

Com o comando paste você também pode montar formatações exóticas como esta a seguir:

$ ls | paste -s -d’ttn’
arq1 arq2 arq3
arq4 arq5 arq6

O que aconteceu foi o seguinte: foi especificado para o comando paste que ele transformaria linhas em colunas (pela opção -s) e que os seus separadores (é…! Ele aceita mais de um, mas somente um após cada coluna criada pelo comando) seriam uma <TAB>, outra <TAB> e um <ENTER>, gerando desta forma a saída tabulada em 3 colunas.

Agora que você já entendeu isto, veja como fazer a mesma coisa, porém de forma mais fácil e menos bizarra e tosca, usando o mesmo comando mas com a seguinte sintaxe:

$ ls | paste – – –
arq1 arq2 arq3
arq4 arq5 arq6

E isto acontece porque se ao invés de especificarmos os arquivos colocarmos o sinal de menos (-), o comando paste os substitui pela saída ou entrada padrão conforme o caso. No exemplo anterior os dados foram mandados para a saída padrão (stdout), porque o pipe (|) estava desviando a saída do ls para a entrada padrão (stdin) do paste, mas veja o exemplo a seguir:

$ cat arq1
predisposição
privilegiado
profissional

$ cat arq2
encher
mário
motor

$ cut -c-3 arq1 | paste -d “” – arq2
preencher
primário
promotor

Neste caso, o cut devolveu as três primeiras letras de cada registro de arq1, o paste foi montado para não ter separador (-d"") e receber a entrada padrão (desviada pelo pipe) no traço (-) gerando a saída juntamente com arq2.

O Comando tr

Outro comando muito interessante é o tr que serve para substituir, comprimir ou remover caracteres. Sua sintaxe segue o seguinte padrão:

    tr [opções] cadeia1 [cadeia2]

O comando tr copia o texto da entrada padrão (stdin), troca as ocorrência dos caracteres de cadeia1 pelo seu correspondente na cadeia2 ou troca múltiplas ocorrências dos caracteres de cadeia1 por somente um caracter, ou ainda remove caracteres da cadeia1.

As principais opções do comando são:

Principais Opções do comando tr
Opção Significado
-s Comprime n ocorrências de cadeia1 em apenas uma
-d Remove os caracteres de cadeia1

Trocando caracteres com tr

Primeiro vou te dar um exemplo bem bobo:

$ echo bobo | tr o a
baba

Isto é, troquei todas as ocorrências da letra o pela letra a.

Suponha que em um determinado ponto do meu script eu peça ao operador para teclar s ou n (sim ou não), e guardo sua resposta na variável $Resp. Ora o conteúdo de $Resp pode estar com letra maiúscula ou minúscula, e desta forma eu teria que fazer diversos testes para saber se a resposta dada foi S, s, N ou n. Então o melhor é fazer:

$ Resp=$(echo $Resp | tr SN sn)

e após este comando eu teria certeza que o conteúdo de $Resp seria um s ou um n.

Se o meu arquivo ArqEnt está todo escrito com letras maiúsculas e desejo passá-las para minúsculas eu faço:

$ tr A-Z a-z < ArqEnt > /tmp/$$
$ mv -f /tmp/$$ ArqEnt

Note que neste caso usei a notação A-Z para não escrever ABCD...YZ. Outro tipo de notação que pode ser usada são as escape sequences (prefiro escrever no bom e velho português, mas nesse caso como eu traduziria? Seqüências de escape? Meio sem sentido, né? Mas vá lá…) que também são reconhecidas por outros comandos e também na linguagem C, e cujo significado você verá a seguir:

Escape Sequences
Seqüência Significado Octal
t Tabulação 11
n Nova linha 12
v Tabulação Vertical 13
f Nova Página 14
r Início da linha <^M> 15
\ Uma barra invertida 134

Removendo caracteres com tr

Então deixa eu te contar um “causo”: um aluno que estava danado comigo, resolveu complicar a minha vida e em um exercício prático valendo nota que passei para ser feito no computador, me entregou o script com todos os comandos separados por ponto-e-vírgula (lembra que eu disse que o ponto-e-vírgula servia para separar diversos comandos em uma mesma linha?).

Vou dar um exemplo simplificado e idiota de uma “tripa” assim:

$ cat confuso
echo leia Programação Shell Linux do Julio Cezar Neves > livro;cat livro;pwd;ls;rm -f lixo 2>/dev/null;cd ~

Eu executava o programa e ele funcionava:

$ confuso
leia Programação Shell Linux do Julio Cezar Neves
/home/jneves/LM
confuso livro musexc musicas musinc muslist numeros

Mas nota de prova é coisa séria (e nota de dólar é mais ainda :)) então, para entender o que o aluno havia feito, o chamei e em sua frente executei o seguinte comando:

$ tr “;” “n” < confuso
echo leia Programação Shell Linux do Julio Cezar Neves
pwd
ls
rm -f lixo 2>/dev/null

cd ~

O cara ficou muito desapontado, porque em 2 ou 3 segundos eu desfiz a gozação que ele perdera horas para fazer.
Mas preste atenção! Se eu estivesse em uma máquina com Unix, eu teria feito:

$ tr “;” “12” < confuso

Xpremendo com tr

Agora veja a diferença entre os dois comandos date: o que fiz hoje e outro que foi executado há duas semanas:

$ date # Hoje
Sun Sep 19 14:59:54 2004

$ date # Há duas semanas
Sun Sep 5 10:12:33 2004

Para pegar a hora eu deveria fazer:

$ date | cut -f 4 -d ‘ ‘
14:59:54

Mas duas semanas antes ocorreria o seguinte:

$ date | cut -f 4 -d ‘ ‘
5

Mas observe porque:

$ date # Há duas semanas
Sun Sep 5 10:12:33 2004

Como você pode notar, existem 2 caracteres em branco antes do 5 (dia), o que estraga tudo porque o terceiro pedaço está vazio e o quarto é o dia (5). Então o ideal seria comprimir os espaços em brancos sucessivos em somente um espaço para poder tratar as duas cadeias resultantes do comando date da mesma forma, e isso se faz assim:

$ date | tr -s ” “a
Sun Sep 5 10:12:33 2004

Como você pode ver não existem mais os dois espaços, então agora eu poderia cortar:

$ date | tr -s ” ” | cut -f 4 -d ” “
10:12:33

Olha só como o Shell já está quebrando o galho. Veja este arquivo que foi baixado de uma máquina com aquele sistema operacional que pega vírus:

$ cat -ve ArqDoDOS.txt
Este arquivo^M$
foi gerado pelo^M$
DOS/Rwin e foi^M$
baixado por um^M$
ftp mal feito.^M$

E agora eu quero te dar duas dicas:

Pinguim com placa de dica Dica #1 – A opção -v do cat mostra os caracteres de controle invisíveis, com a notação ^L, onde ^ é a tecla control e L é a respectiva letra. A opção -e mostra o final da linha como um cifrão ($).
Pinguim com placa de dica Dica #2 – Isto ocorre porque no formato DOS (ou rwin), o fim dos registros é formado por um carriage-return (r) e um line-feed (n). No Linux porém o final do registro tem somente o line-feed.

Vamos então limpar este arquivo.

$ tr -d ‘r’ < ArqDoDOS.txt > /tmp/$$
$ mv -f /tmp/$$ ArqDoDOS.txt

Agora vamos ver o que aconteceu:

$ cat -ve ArqDoDOS.txt
Este arquivo$
foi gerado pelo$
DOS/Rwin e foi$
baixado por um$
ftp mal feito.$

Bem a opção -d do tr remove o caractere especificado de todo o arquivo. Desta forma eu removi os caracteres indesejados salvando em um arquivo de trabalho e posteriormente renomeei-o para a sua designação original.
Obs: No Unix eu deveria fazer:

$ tr -d ’15’ < ArqDoDOS.txt > /tmp/$$
Pinguim com placa de atenção Isto aconteceu porque o ftp foi feito do modo binário (ou image), isto é, sem a interpretação do texto. Se antes da transmissão do arquivo tivesse sido estipulada a opção ascii do ftp, isto não teria ocorrido.

– Olha, depois desta dica tô começando a gostar deste tal de Shell, mas ainda tem muita coisa que não consigo fazer.

– Pois é, ainda não te falei quase nada sobre programação em Shell, ainda tem muita coisa para aprender, mas com o que aprendeu, já dá para resolver muitos problemas, desde que você adquira o “modo Shell de pensar”. Você seria capaz de fazer um script para me dizer quais são as pessoas que estão “logadas” há mais de um dia no seu servidor?

– Claro que não! Para isso seria necessário eu conhecer os comandos condicionais que você ainda não me explicou como funcionam.

– Deixa eu tentar mudar um pouco a sua lógica e trazê-la para o “modo Shell de pensar”, mas antes é melhor tomarmos um chope… Ô Chico, traz mais dois…

– Agora que já molhei a palavra, vamos resolver o problema que te propus. Repare como funciona o comando who:

$ who
jneves pts/1 Sep 18 13:40
rtorres pts/0 Sep 20 07:01
rlegaria pts/1 Sep 20 08:19
lcarlos pts/3 Sep 20 10:01

E veja também o date:

$ date
Mon Sep 20 10:47:19 BRT 2004

Repare que o mês e o dia estão no mesmo formato em ambos os comandos.

Pinguim com placa de dica Algumas vezes um comando tem a saída em português e o outro em inglês. Quando isso ocorrer, você pode usar o seguinte artifício:

$ date
Mon Sep 20 10:47:19 BRT 2004

$ LANG=pt_BR date
Seg Set 20 10:47:19 BRT 2004

Desta forma passando a saída do comando date para português.

Ora, se em algum registro do who eu não encontrar a data de hoje, é sinal que o cara está “logado” há mais de um dia, já que ele não pode ter se “logado” amanhã… Então vamos guardar o pedaço que importa da data de hoje para procurá-la na saída do who:

$ Data=$(date | cut -c 5-10)

Eu usei a construção $(...), para priorizar a execução dos comandos antes de atribuir a sua saída à variável $Data. Vamos ver se funcionou:

$ echo $Data
Sep 20

Beleza! Agora, o que temos que fazer é procurar no comando who os registros que não possuem esta data.

– Ah! Eu acho que estou entendendo! Você falou em procurar e me ocorreu o comando grep, estou certo?

– Certíssimo! Só que eu tenho que usar o grep com aquela opção que ele só lista os registros nos quais ele não encontrou a cadeia. Você se lembra que opção é essa?

– Claro, é a opção -v

– Isso! Tá ficando bão! Então vamos ver:

$ who | grep -v “$Data”
jneves pts/1 Sep 18 13:40

– E se eu quisesse mais um pouco de perfumaria eu faria assim:

$ who | grep -v “$Data” | cut -f1 -d ‘ ‘
jneves

– Viu? Não foi necessário usar nenhum comando condicional, até porque o nosso mais usado comando condicional, o famoso if, não testa condição, mas sim instruções, como veremos agora.

Comandos Condicionais

Veja as linhas de comando a seguir:

$ ls musicas
musicas

$ echo $?
0

$ ls ArqInexistente
ls: ArqInexistente: No such file or directory

$ echo $?
1

$ who | grep jneves
jneves pts/1 Sep 18 13:40 (10.2.4.144)

$ echo $?
0

$ who | grep juliana
$ echo $?

1

– O que é esse $? faz aí? Começando por cifrão ($) parece ser uma variável, certo?

– Sim é uma variável que contém o código de retorno da última instrução executada. Posso te garantir que se esta instrução foi bem sucedida, $? terá o valor zero, caso contrário seu valor será diferente de zero.

O Comando if

O que o nosso comando condicional if faz é testar a variável $?. Então vamos ver a sua sintaxe:

    if cmd
    then
        cmd1
        cmd2
        cmdn
    else
        cmd3
        cmd4
        cmdm
    fi

ou seja: caso comando cmd tenha sido executado com sucesso, os comandos do bloco do then (cmd1, cmd2 e cmdn) serão executados, caso contrário, os comandos executados serão os do bloco opcional do else (cmd3, cmd4 e cmdm), terminando com um fi.

Vamos ver na prática como isso funciona usando um scriptizinho que serve para incluir usuários no /etc/passwd:

$ cat incusu
#!/bin/bash
# Versão 1
if grep ^$1 /etc/passwd
then
echo Usuario ‘$1’ já existe
else
if useradd $1
then
echo Usuário ‘$1’ incluído em /etc/passwd
else
echo “Problemas no cadastramento. Você é root?”
fi
fi

Repare que o if está testando direto o comando grep e esta é a sua finalidade. Caso o if seja bem sucedido, ou seja, o usuário (cujo nome está em $1) foi encontrado em /etc/passwd, os comandos do bloco do then serão executados (neste exemplo é somente o echo) e caso contrário, as instruções do bloco do else é que serão executadas, quando um novo if testa se o comando useradd foi executado a contento, criando o registro do usuário em /etc/passwd, ou não quando dará a mensagem de erro.
Vejamos sua execução, primeiramente passando um usuário já cadastrado:

$ incusu jneves
jneves:x:54002:1001:Julio Neves:/home/jneves:/bin/bash
Usuario ‘jneves’ ja existe

Como já vimos diversas vezes, mas é sempre bom insistir no tema para que você já fique precavido, no exemplo dado surgiu uma linha indesejada, ela é a saída do comando grep. Para evitar que isso aconteça, devemos desviar a saída desta instrução para /dev/null, ficando assim:

$ cat incusu
#!/bin/bash
# Versão 2
if grep ^$1 /etc/passwd > /dev/null # ou: if grep -q ^$1 /etc/passwd
then
echo Usuario ‘$1’ já existe
else
if useradd $1
then
echo Usuário ‘$1’ incluído em /etc/passwd
else
echo “Problemas no cadastramento. Você é root?”
fi
fi

Agora vamos testá-lo como usuário normal (não root):

$ incusu ZeNinguem
./incusu[6]: useradd: not found
Problemas no cadastramento. Você é root?

Epa, aquele erro não era para acontecer! Para evitar que isso aconteça devemos mandar também a saída de erro (strerr, lembra?) do useradd para /dev/null, ficando na versão final assim:

$ cat incusu
#!/bin/bash
# Versão 3
if grep ^$1 /etc/passwd > /dev/null
then
echo Usuario ‘$1’ já existe
else
if useradd $1 2> /dev/null
then
echo Usuário ‘$1’ incluído em /etc/passwd
else
echo “Problemas no cadastramento. Você é root?”
fi
fi

Depois destas alterações e de fazer um su – (me tornar root) vejamos o seu comportamento:

$ incusu botelho
Usuário ‘botelho’ incluido em /etc/passwd

E novamente:

$ incusu botelho
Usuário ‘botelho’ já existe

Lembra que eu falei que ao longo dos nossos papos e chopes os nossos programas iriam se aprimorando? Então vejamos agora como poderíamos melhorar o nosso programa para incluir músicas:

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 3)
#
if grep “^$1$” musicas > /dev/null
then
echo Este álbum já está cadastrado
else
echo $1 >> musicas
sort musicas -o musicas
fi

Como você viu, é uma pequena evolução da versão anterior, assim, antes de incluir um registro (que pela versão anterior poderia ser duplicado), testamos se o registro começava (^) e terminava ($) igual ao parâmetro passado ($1). O uso do circunflexo (^) no início da cadeia e cifrão ($) no fim, são para testar se o parâmetro passado (o álbum e seus dados) são exatamente iguais a algum registro anteriormente cadastrado e não somente igual a um pedaço de algum dos registros.
Vamos executá-lo passando um álbum já cadastrado:

$ musinc “album 4^Artista7~Musica7:Artista8~Musica8”
Este álbum já está cadastrado

E agora um não cadastrado:

$ musinc “album 5^Artista9~Musica9:Artista10~Musica10”
$ 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
album 5^Artista9~Musica9:Artista10~Musica10

– Como você viu, o programa melhorou um pouquinho, mas ainda não está pronto. À medida que eu for te ensinando a programar em shell, nossa CDteca irá ficando cada vez melhor.

– Entendi tudo que você me explicou, mas ainda não sei como fazer um if para testar condições, ou seja o uso normal do comando.

– Cara, para isso existe o comando test, ele é que testa condições. O comando if testa o comando test. Mas isso está meio confuso e como já falei muito, estou precisando de uns chopes para molhar a palavra. Vamos parando por aqui e na próxima vez te explico direitinho o uso do test e de diversas outras sintaxes do if.

– Falou! Acho bom mesmo porque eu também já tô ficando zonzo e assim tenho tempo para praticar esse monte de coisas que você me falou hoje.

– Para fixar o que você aprendeu, tente fazer um scriptizinho para informar se um determinado usuário, que será passado como parâmetro esta logado (arghh!) ou não.

– Aê Chico, mais dois chopes por favor…

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 »

Papo de Botequim – Parte 4

– E aí cara, tentou fazer o exercício que te pedi para revigorar as idéias?

– Claro, que sim! Em programação, se você não treinar, não aprende. Você me pediu para fazer um scriptizinho para informar se um determinado usuário, que será passado como parâmetro esta logado (arghh!) ou não. Eu fiz o seguinte:

$ cat logado
#!/bin/bash
# Pesquisa se uma pessoa está logada ou não
if who | grep $1
then
echo $1 está logado
else
echo $1 não se encontra no pedaço
fi

– Calma rapaz! Já vi que você chegou cheio de tesão, primeiro vamos pedir os nossos chopes de praxe e depois vamos ao Shell. Chico traz dois chopes, um sem colarinho!

– Agora que já molhamos os nossos bicos, vamos dar uma olhadinha na execução do seu bacalho:

$ logado jneves
jneves pts/0 Oct 18 12:02 (10.2.4.144)
jneves está logado

Realmente funcionou. Passei o meu login como parâmetro e ele disse que eu estava logado, porém ele mandou uma linha que eu não pedi. Esta linha é a saída do comando who, e para evitar que isso aconteça é só mandá-la para o buraco negro que a esta altura você já sabe que é o /dev/null. Vejamos então como ficaria:

$ cat logado
#!/bin/bash
# Pesquisa se uma pessoa está logada ou não (versão 2)
if who | grep $1 > /dev/null
then
echo $1 está logado
else
echo $1 não se encontra no pedaço
fi

Agora vamos aos testes:

$ logado jneves
jneves está logado
$ logado chico
chico não se encontra no pedaço
Pinguim com placa de atenção Ah, agora sim! Lembre-se desta pegadinha, a maior parte dos comandos tem uma saída padrão e uma saída de erros (o grep é uma das poucos exceções, já que não dá mensagem de erro quando não acha uma cadeia) e é necessário estarmos atentos para redirecioná-las para o buraco negro quando necessário.

Bem, agora vamos mudar de assunto: na última vez que nos encontramos aqui no Botequim, eu estava te mostrando os comandos condicionais e, quando já estávamos de goela seca falando sobre o if, você me perguntou como se testa condições. Vejamos então o

O Comando test

Bem, todos estamos acostumados a usar o if testando condições, e estas são sempre, maior, menor, maior ou igual, menor ou igual, igual e diferente. Bem, em Shell para testar condições, usamos o comando test, só que ele é muito mais poderoso que o que estamos habituados. Primeiramente vou te mostrar as principais opções (existem muitas outras) para testarmos arquivos em disco:

Opções do Comando test para arquivos
Opção Verdadeiro se:
-e arq arq existe
-s arq arq existe e tem tamanho maior que zero
-f arq arq existe e é um arquivo regular
-d arq arq existe e é um diretório;
-r arq arq existe e com direito de leitura
-w arq arq existe e com direito de escrita
-x arq arq existe e com direito de execução

Veja agora as principais opções para teste de cadeias de caracteres:

Opções do comando test para cadeias de caracteres
Opção Verdadeiro se:
-z cadeia Tamanho de cadeia é zero
-n cadeia Tamanho de cadeia é maior que zero
cadeia A cadeia cadeia tem tamanho maior que zero
c1 = c2 Cadeia c1 e c2 são idênticas

E pensa que acabou? Engano seu! Agora é que vem o que você está mais acostumado, ou seja as famosas comparações com numéricos. Veja a tabela:

Opções do comando test para números
Opção Verdadeiro se: Significado
n1 -eq n2 n1 e n2 são iguais equal
n1 -ne n2 n1 e n2 não são iguais not equal
n1 -gt n2 n1 é maior que n2 greater than
n1 -ge n2 n1 é maior ou igual a n2 greater or equal
n1 -lt n2 n1 é menor que n2 less than
n1 -le n2 n1 é menor ou igual a n2 less or equal

Além de tudo, some-se a estas opções as seguintes facilidades:

Operadores
Operador Finalidade
Parênteses ( ) Agrupar
Exclamação ! Negar
-a E lógico
-o OU lógico

Ufa! Como você viu tem coisa prá chuchu, e como eu te disse no início, o nosso if é muito mais poderoso que o dos outros. Vamos ver em uns exemplos como isso tudo funciona, primeiramente testaremos a existência de um diretório:

Exemplos:

    if  test -d lmb
    then
        cd lmb
    else
        mkdir lmb
        cd lmb
    fi

No exemplo, testei se existia um diretório lmb definido, caso negativo (else), ele seria criado. Já sei, você vai criticar a minha lógica dizendo que o script não está otimizado. Eu sei, mas queria que você o entendesse assim, para então poder usar o ponto-de-espantação (!) como um negador do test. Veja só:

    if  test ! -d lmb
    then
        mkdir lmb
    fi
    cd lmb

Desta forma o diretório lmb seria criado somente se ele ainda não existisse, e esta negativa deve-se ao ponto-de-exclamação (!) precedendo a opção -d. Ao fim da execução deste fragmento de script, o programa estaria com certeza dentro do diretório lmb.

Vamos ver dois exemplos para entender a diferença comparação entre números e entre cadeias.

    cad1=1
    cad2=01
    if  test $cad1 = $cad2
    then
        echo As variáveis são iguais.
    else
        echo As variáveis são diferentes.
    fi

Executando o fragmento de programa acima vem:

As variáveis são diferentes.

Vamos agora alterá-lo um pouco para que a comparação seja numérica:

    cad1=1
    cad2=01
    if  test $cad1 -eq $cad2
    then
        echo As variáveis são iguais.
    else
        echo As variáveis são diferentes.
    fi

E vamos executá-lo novamente:

As variáveis são iguais.

Como você viu nas duas execuções obtive resultados diferentes porque a cadeia 01 é realmente diferente da cadeia 1, porém, a coisa muda quando as variáveis são testadas numericamente, já que o número 1 é igual ao número 01.

Exemplos:

Para mostrar o uso dos conectores -o (OU) e -a (E), veja um exemplo animal feito direto no prompt (me desculpem os zoólogos, mas eu não entendendo nada de reino, filo, classe, ordem, família, gênero e espécie, desta forma o que estou chamando de família ou de gênero tem grande chance de estar incorreto):

$ Familia=felinae
$ Genero=gato
$ if test $Familia = canidea -a $Genero = lobo -o $Familia = felina -a $Genero = leão
> then
> echo Cuidado
> else
> echo Pode passar a mão
> fi
Pode passar a mão

Neste exemplo caso o animal fosse da família canídea E (-a) do gênero lobo, OU (-o) da familia felina E (-a) do gênero leão, seria dado um alerta, caso contrário a mensagem seria de incentivo.

Pinguim com placa de dica Os sinais de maior (>) no início das linhas internas ao if são os prompts de continuação (que estão definidos na variável $PS2) e quando o Shell identifica que um comando continuará na linha seguinte, automaticamente ele o coloca até que o comando seja encerrado.

Vamos mudar o exemplo para ver se continua funcionando:

$ Familia=felino
$ Genero=gato
$ if test $Familia = felino -o $Familia = canideo -a $Genero = onça -o $Genero = lobo
> then
> echo Cuidado
> else
> echo Pode passar a mão
> fi
Cuidado

Obviamente a operação redundou em erro, isto foi porque a opção -a tem precedência sobre a -o, e desta forma o que primeiro foi avaliado foi a expressão:

$Familia = canideo -a $Genero = onça

Que foi avaliada como falsa, retornando o seguinte:

$Familia = felino -o FALSO -o $Genero = lobo

Que resolvida vem:

VERDADEIRO -o FALSO -o FALSO

Como agora todos conectores são -o, e para que uma série de expressões conectadas entre si por diversos OU lógicos seja verdadeira, basta que uma delas seja, a expressão final resultou como VERDADEIRO e o then foi executado de forma errada. Para que isso volte a funcionar façamos o seguinte:

$ if test ($Familia = felino -o $Familia = canideo) -a ($Genero = onça -o $Genero = lobo)
> then
> echo Cuidado
> else
> echo Pode passar a mão
> fi
Pode passar a mão

Desta forma, com o uso dos parênteses agrupamos as expressões com o conector -o, priorizando as suas execuções e resultando:

VERDADEIRO -a FALSO

Para que seja VERDADEIRO o resultado duas expressões ligadas pelo conector -a é necessário que ambas sejam verdadeiras, o que não é o caso do exemplo acima. Assim o resultado final foi FALSO sendo então o else corretamente executado.

Se quisermos escolher um CD que tenha faixas de 2 artistas diferentes, nos sentimos tentados a usar um if com o conector -a, mas é sempre bom lembrarmos que o bash nos dá muito recursos, e isso poderia ser feito de forma muito mais simples com um único comando grep, da seguinte maneira:

$ grep Artista1 musicas | grep Artista2

Da mesma forma para escolhermos CDs que tenham a participação do Artista1 e do Artista2, não é necessário montarmos um if com o conector -o. O egrep (ou grep -E, sendo este mais aconselhável) também resolve isso para nós. Veja como:

$ egrep (Artista1|Artista2) musicas

Ou (nesse caso específico) o próprio grep puro e simples poderia nos quebrar o galho:

$ grep Artista[12] musicas

No egrep acima, foi usada uma expressão regular, onde a barra vertical (|) trabalha como um OU lógico e os parênteses são usados para limitar a amplitude deste OU. Já no grep da linha seguinte, a palavra Artista deve ser seguida por um dos valores da lista formada pelos colchetes ([ ]), isto é, 1 ou 2.

– Tá legal, eu aceito o argumento, o if do Shell é muito mais poderoso que os outros caretas, mas cá pra nós, essa construção de if test ... é muito esquisita, é pouco legível.

– É você tem razão, eu também não gosto disso e acho que ninguém gosta. Acho que foi por isso, que o Shell incorporou outra sintaxe que substitui o comando test.

Exemplos:

Para isso vamos pegar aquele exemplo para fazer uma troca de diretórios, que era assim:

    if  test ! -d lmb
    then
        mkdir lmb
    fi
    cd lmb

e utilizando a nova sintaxe, vamos fazê-lo assim:

    if  [ ! -d lmb ]
    then
        mkdir lmb
    fi
    cd lmb

Ou seja, o comando test pode ser substituído por um par de colchetes ([ ]), separados por espaços em branco dos argumentos, o que aumentará enormemente a legibilidade, pois o comando if irá ficar com a sintaxe semelhante à das outras linguagens e por isso este será o modo que o comando test será usado daqui para a frente.

Querida, Encolheram o Comando Condicional

Se você pensa que acabou, está muito enganado. Repare a tabela (tabela verdade) a seguir:

Valores Booleanos E OU
VERDADEIRO-VERDADEIRO VERDADEIRO VERDADEIRO
VERDADEIRO-FALSO FALSO VERDADEIRO
FALSO-VERDADEIRO FALSO VERDADEIRO
FALSO-FALSO FALSO FALSO

Ou seja, quando o conector é E e a primeira condição é verdadeira, o resultado final pode ser VERDADEIRO ou FALSO, dependendo da segunda condição, já no conector OU, caso a primeira condição seja verdadeira, o resultado sempre será VERDADEIRO e se a primeira for falsa, o resultado dependerá da segunda condição.

Ora, os caras que desenvolveram o interpretador não são bobos e estão sempre tentando otimizar ao máximo os algoritmos. Portanto, no caso do conector E, a segunda condição não será avaliada, caso a primeira seja falsa, já que o resultado será sempre FALSO. Já com o OU, a segunda será executada somente caso a primeira seja falsa.

Aproveitando disso, criaram uma forma abreviada de fazer testes. Batizaram o conector E de && e o OU de || e para ver como isso funciona, vamos usá-los como teste no nosso velho exemplo de trocarmos de diretório, que em sua última versão estava assim:

    if  [ ! -d lmb ]
    then
        mkdir lmb
    fi
    cd lmb

Isso também poderia ser escrito da seguinte maneira:

    [ ! -d lmb ] && mkdir lmb
    cd lmb

Ou ainda retirando a negação (!):

    [ -d lmb ] || mkdir lmb
    cd lmb

No primeiro caso, se o primeiro comando (o test que está representado pelos colchetes) for bem sucedido, isto é, não existir o diretório lmb, o mkdir será efetuado porque a primeira condição era verdadeira e o conector era E.

No exemplo seguinte, testamos se o diretório lmb existia (no anterior testamos se ele não existia) e caso isso fosse verdade, o mkdir não seria executado porque o conector era OU. Outra forma:

cd lmb || mkdir lmb

Neste caso, se o cd fosse mal sucedido, seria criado o diretório lmb mas não seria feito o cd para dentro dele. Para executarmos mais de um comando desta forma, é necessário fazermos um grupamento de comandos, e isso se consegue com o uso de chaves ({ }). Veja como seria o correto:

    cd lmb ||
        {
        mkdir lmb
        cd lmb
        }

Ainda não está legal, porque caso o diretório não exista, o cd dará a mensagem de erro correspondente. Então devemos fazer:

    cd lmb 2> /dev/null ||
        {
        mkdir lmb
        cd lmb
        }

Como você viu o comando if nos permitiu fazer um cd seguro de diversas maneiras. É sempre bom lembrarmos que o seguro a que me referi é no tocante ao fato de que ao final da execução você sempre estará dentro de lmb, desde que você tenha permissão entrar em lmb, permissão para criar um diretório em ../lmb, haja espaço em disco, …

E tome de test

Ufa! Você pensa que acabou? Ledo engano! Ainda tem uma forma de test a mais. Essa é legal porque ela te permite usar padrões para comparação. Estes padrões atendem às normas de Geração de Nome de Arquivos (File Name Generation, que são ligeiramente parecidas com as Expressões Regulares, mas não podem ser confundidas com estas). A diferença de sintaxe deste para o test que acabamos de ver é que esse trabalha com dois pares de colchete da seguinte forma:

[[ expressão ]]

Onde expressão é uma das que constam na tabela a seguir:

Expressões Condicionais Para Padrões
Expressão Retorna
cadeia == padrão
cadeia1 = padrao
Verdadeiro se cadeia1 casa com padrão
cadeia1 != padrao Verdadeiro se cadeia1 não casa com padrao.
cadeia1 < cadeia1 Verdadeiro se cadeia1 vem antes de cadeia1 alfabeticamente.
cadeia1 > cadeia1 Verdadeiro se cadeia1 vem depois de cadeia1 alfabeticamente
expr1 && expr2 “E” lógico, verdadeiro se ambos expr1 e expr2 são verdadeiros
expr1 ¦¦ expr2 “OU” lógico, verdadeiro se expr1 ou expr2 for verdadeiro
$ echo $H
13

$ [[ $H == [0-9] || $H == 1[0-2] ]] || echo Hora inválida
Hora inválida

$H=12
$ [[ $H == [0-9] || $H == 1[0-2] ]] || echo Hora inválida
$

Neste exemplo, testamos se o conteúdo da variável $H estava compreendido entre zero e nove ([0-9]) ou (||) se estava entre dez e doze (1[0-2]), dando uma mensagem de erro caso não fosse.

Exemplos:

Para saber se uma variável tem o tamanho de um e somente um caractere, faça:

$ var=a
$ [[ $var == ? ]] && echo var tem um caractere
var tem um caractere

$ var=aa
$ [[ $var == ? ]] && echo var tem um caractere
$

Como você pode imaginar, este uso de padrões para comparação, aumenta muito o poderio do comando test. No início deste papo, antes do último chope, afirmamos que o comando if do interpretador Shell é mais poderoso que o seu similar em outras linguagens. Agora que conhecemos todo o seu espectro de funções, diga-me: você concorda ou não com esta assertiva?

Acaso Casa com case

Vejamos um exemplo didático: dependendo do valor da variável $opc o script deverá executar uma uma das opções: inclusão, exclusão, alteração ou fim. Veja como ficaria este fragmento de script:

    if  [ $opc -eq 1 ]
    then
        inclusao
    elif [ $opc -eq 2 ]
    then
        exclusao
    elif [ $opc -eq 3 ]
    then
        alteracao
    elif [ $opc -eq 4 ]
    then
        exit
    else
        echo Digite uma opção entre 1 e 4
    fi

Neste exemplo você viu o uso do elif com um else if, esta á a sintaxe válida e aceita, mas poderíamos fazer melhor, e isto seria com o comando case, que tem a sintaxe a seguir:

    case $var in
        padrao1) cmd1
                 cmd2
                 cmdn ;;
        padrao2) cmd1
                 cmd2
                 cmdn ;;
        padraon) cmd1
                 cmd2
                 cmdn ;;
    esac

Onde a variável $var é comparada aos padrões padrao1, ..., padraon e caso um deles atenda, o bloco de comandos cmd1, ..., cmdn correspondente é executado até encontrar um duplo ponto-e-vírgula (;;), quando o fluxo do programa se desviará para instrução imediatamente após o esac.

Na formação dos padrões, são aceitos os seguintes caracteres:

Caracteres Para Formação de Padrões
Caractere Significado
* Qualquer caractere ocorrendo zero ou mais vezes
? Qualquer caractere ocorrendo uma vez
[...] Lista de caracteres
¦ OU lógico

Para mostrar como fica melhor, vamos repetir o exemplo anterior, só que desta vez usaremos o case e não o if ... elif ... else ... fi.

    case $opc in
        1) inclusao ;;
        2) exclusao ;;
        3) alteracao ;;
        4) exit ;;
        *) echo Digite uma opção entre 1 e 4
    esac

Como você deve ter percebido, eu usei o asterisco como a última opção, isto é, se o asterisco atende a qualquer coisa, então ele servirá para qualquer coisa que não esteja no intervalo de 1 a 4. Outra coisa a ser notada é que o duplo ponto-e-vírgula não é necessário antes do esac.

Exemplos:

Vamos agora fazer um script mais radical. Ele te dará bom dia, boa tarde ou boa noite dependendo da hora que for executado, mas primeiramente veja estes comandos:

$ date
Tue Nov 9 19:37:30 BRST 2004

$ date +%H
19

O comando date informa a data completa do sistema, mas ele tem diversas opções para seu mascaramento. Neste comando, a formatação começa com um sinal de mais (+) e os caracteres de formatação vêm após um sinal de percentagem (%), assim o %H significa a hora do sistema. Dito isso vamos ao exemplo:

$ cat boasvindas.sh
#!/bin/bash
# Programa bem educado que
# dá bom-dia, boa-tarde ou
# boa-noite conforme a hora
Hora=$(date +%H)
case $Hora in
0? | 1[01]) echo Bom Dia
;;
1[2-7] ) echo Boa Tarde
;;
* ) echo Boa Noite
;;
esac
exit

Peguei pesado, né? Que nada vamos esmiuçar a resolução caso-a-caso (ou seria case-a-case? smile )

0? | 1[01] – Significa zero seguido de qualquer coisa (?), ou (|) um seguido de zero ou um ([01]) ou seja, esta linha pegou 01, 02, … 09, 10 e 11;

1[2-7] – Significa um seguido da lista de dois a sete, ou seja, esta linha pegou 12, 13, … 17;

* – Significa tudo que não casou com nenhum dos padrões anteriores.

– Cara, até agora eu falei muito e bebi pouco. Agora eu vou te passar um exercício para você fazer em casa e me dar a resposta da próxima vez que nos encontrarmos aqui no botequim, tá legal?

– Tá, mas antes informe ao pessoal que está acompanhando este curso conosco como eles podem te encontrar para fazer críticas, contar piada, convidar para o chope, curso ou palestra ou até mesmo para falar mal dos políticos.

– É fácil, meu e-mail é julio.neves@gmail.com, mas pare de me embromar que eu não vou esquecer de te passar o script para fazer. É o seguinte: quero que você faça um programa que receberá como parâmetro o nome de um arquivo e que quando executado salvará este arquivo com o nome original seguido de um til (~) e colocará este arquivo dentro do vi (o melhor editor que se tem notícia) para ser editado. Isso é para ter sempre a última cópia boa deste arquivo caso o cara faça alterações indevidas. Obviamente, você fará as críticas necessárias, como verificar se foi passado um parâmetro, se o arquivo passado existe, … Enfim, o que te der na telha e você achar que deve constar do script. Deu prá entender?

– Hum, hum…

– Chico! Traz mais um sem colarinho que o cara aqui já está dando para entender! smile

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 »

Papo de Botequim – Parte 6

Comandos de Loop ou Laço (Continuação)

– Fala cara! E aí, já tá sabendo tudo do comando for? Eu te deixei um exercício para treinar, se não me engano era para contar a quantidade de palavras de um arquivo… Você fez? – Claro! Tô empolgadão com essa linguagem, eu fiz da forma que você pediu, isto é sem usar o comando wc porque senão era mais mole ainda. Olha só como eu fi…

– Êpa! Perai! Você realmente está fissurado na linguagem, mas eu tô sequinho pra tomar um chope. Aê Chico, traz dois por favor. Um sem colarinho!

– Como eu ia dizendo olha a forma que eu fiz. É muito fácil…

$ cat contpal.sh
#!/bin/bash
# Script meramente pedagógico cuja
# função é contar a qtd de palavras
# de um arquivo. Supõe-se que as
# palavras estão separadas entre si
# por espaço, ou .

if [ $# -ne 1 ]
then
echo uso: $0 /caminho/do/arquivo
exit 2
fi
Cont=0
for Palavra in $(cat $1)
do
Cont=$((Cont+1))
done
echo O arquivo $1 tem $Cont palavras.

Ou seja, o programa começa como sempre verificando se a passagem de parâmetros foi correta, em seguida o comando for se incumbe de pegar cada uma das palavras (lembre-se que o $IFS padrão (default) é branco, <TAB> e <ENTER>, que é exatamente o que desejamos para separar as palavras), incrementando a variável $Cont.

Vamos relembrar como é o arquivo ArqDoDOS.txt.

$ cat ArqDoDOS.txt
Este arquivo
foi gerado pelo
DOS/Rwin e foi
baixado por um
ftp mal feito.

Agora vamos testar o programa passando este arquivo como parâmetro:

$ contpal.sh ArqDoDOS.txt
O arquivo ArqDoDOS.txt tem 14 palavras

– Beleza, funcionou legal!

Um Pouco Mais de for e Matemática

Voltando à vaca fria, na última vez que aqui estivemos, terminamos o nosso papo mostrando o loop de for a seguir:

    for ((; i<=9;))
    do
        let i++
        echo -n "$i "
    done

Uma vez que chegamos neste ponto, creio ser bastante interessante citar que o Shell trabalha com o conceito de “Expansão Aritmética” (Arithmetic Expansion), da qual vou falar rapidamente porque na seção Tira Gosto isso está muito bem mastigado.

A expansão aritmética é acionada por uma construção da forma:

$((expressão))

ou

let expressão

No último for citado usei a expansão das duas formas, mas não poderíamos seguir adiante sem saber que a expressão pode ser de uma das listadas a seguir:

Expansão Aritmética
Expressão Resultado
id++ id-- pós-incremento e pós-decremento de variáveis
++id -–id pré-incremento e pré-decremento de variáveis
** exponenciação
* / % multiplicação, divisão, resto da divisão
+ - adição, subtração
<= >= < > comparação
== != igualdade, desigualdade
&& E lógico
|| OU lógico

– Mas você pensa que o papo de loop (ou laço) se encerra no comando for? Ledo engano amigo, vamos a partir de agora ver mais dois.

O comando while

Todos os programadores conhecem este comando, porque ele é comum a todas as linguagens e nelas, o que normalmente ocorre é que um bloco de comandos é executado, enquanto (enquanto em ingles é while) uma determinada condição for verdadeira. Pois bem, isto é o que ocorre nas linguagens caretas! Em programação Shell, o bloco de comandos é executado enquanto um comando for verdadeiro. E é claro, se quiser testar uma condição use o comando while junto com o comando test, exatamente como você aprendeu a fazer no if, lembra?

Então a sintaxe do comando fica assim:

    while comando
    do
        cmd1
        cmd2
        ...
        cmdn
    done

e desta forma o bloco de comandos formado pelas instruções cmd1, cmd2,… e cmdn é executado enquanto a execução da instrução comando for bem sucedida.

Suponha a seguinte cena: tem uma tremenda gata me esperando e eu preso no trabalho sem poder sair porque o meu chefe, que é um pé no saco (aliás chefe-chato é uma redundância, né?:), ainda estava na sua sala, que fica bem na minha passagem para a rua.

Ele começou a ficar com as antenas (provavelmente instaladas na cabeça dele pela esposa) ligadas depois da quinta vez que passei pela sua porta e olhei para ver se já havia ido embora. Então voltei para a minha mesa e fiz, no servidor, um script assim:

$ cat logaute.sh
#!/bin/bash

# Espero que a Xuxa não tenha
# copyright de xefe e xato 🙂

while who | grep xefe
do
sleep 30
done
echo O xato se mandou, não hesite, dê exit e vá a luta

Neste scriptizinho, o comando while testa o pipeline composto pelo who e pelo grep e que será verdadeiro enquanto o grep localizar a palavra xefe na saída do who. Desta forma, o script dormirá por 30 segundos enquanto o chefe estiver logado (Argh!). Assim que ele se desconectar do servidor, o fluxo do script sairá do loop e dará a tão ansiada mensagem de liberdade.

Quando o executei adivinha o que aconteceu?

$ logaute.sh
xefe pts/0 Jan 4 08:46 (10.2.4.144)
xefe pts/0 Jan 4 08:47 (10.2.4.144)

xefe pts/0 Jan 4 08:52 (10.2.4.144)

Isto é a cada 30 segundos seria enviado para a tela a saída do grep, o que não seria legal já que poluiria a tela do meu micro e a mensagem esperada poderia passar desapercebida. Para evitar isso já sabemos que a saída do pipeline tem que ser redirecionada para /dev/null.

$ cat logaute.sh
#!/bin/bash

# Espero que a Xuxa não tenha
# copyright de xefe e xato 🙂

while who | grep xefe > /dev/null
do
sleep 30
done
echo O xato se mandou, não hesite, dê exit e vá a luta

Agora quero montar um script que receba o nome (e eventuais parâmetros) de um programa que será executado em background e que me informe do seu término. Mas, para você entender este exemplo, primeiro tenho de mostar uma nova variável do sistema. Veja estes comandos diretos no prompt:

$ sleep 10&
[1] 16317

$ echo $!
16317
[1]+ Done sleep 10

$ echo $!
16317

Isto é, criei um processo em background para dormir por 10 segundos, somente para mostrar que a variável $! guarda o PID (Process IDentification) do último processo em background, mas repare após a linha do done, que a variável reteve o valor mesmo após o término deste processo.

Bem sabendo isso já fica mais fácil de monitorar qualquer processo em background. Veja só como:

$ cat monbg.sh
#!/bin/bash

# Executa e monitora um
# processo em background

$1 & # Coloca em backgroud
while ps | grep -q $!
do
sleep 5
done
echo Fim do Processo $1

Este script é bastante similar ao anterior, mas tem uns macetes a mais, veja só: ele tem que ser executado em background para não prender o prompt mas o $! será o do programa passado como parâmetro já que ele foi colocado em background após o monbg.sh propriamente dito. Repare também a opção -q (quiet) do grep, ela serve para tranformá-lo num comando mineiro, isto é, para o grep “trabalhar em silêncio”. O mesmo resultado poderia ser obtido se a linha fosse while ps | grep $! > /dev/null, como nos exemplos que vimos até agora.

Pinguim com placa de dica Não esqueça: o Bash disponibiliza a variável $! que possui o PID (Process IDentification) do último processo executado em background.

Vamos melhorar o musinc, que é o nosso programa para incluir registros no arquivo musicas, mas antes preciso te ensinar a pegar um dado da tela, e já vou avisando: só vou dar uma pequena dica do comando read (que é quem pega o dado da tela) que seja o suficiente para resolver este nosso problema. Em uma outra rodada de chope vou te ensinar tudo sobre o assunto, inclusive como formatar tela, mas hoje estamos falando sobre loops.

A sintaxe do comando read que nos interessa por hoje é a seguinte:

$ read -p “prompt de leitura” var

Onde prompt de leitura é o texto que você quer que apareça escrito na tela, e quando o operador teclar o dado, ele irá para a variável var. Por exemplo:

$ read -p “Título do Álbum: ” Tit

Bem, uma vez entendido isso, vamos à especificação do nosso problema: faremos um programa que inicialmente lerá o nome do álbum e em seguida fara um loop de leitura, pegando a música e o artista. Este loop termina quando for informada uma música vazia, isto é, ao ser solicitada a digitação da música, o operador dá um simples <ENTER>. Para facilitar a vida do operador, vamos oferecer como default o mesmo nome do artista da música anterior (já que é normal que o álbum seja todo do mesmo artista) até que ele deseje alterá-lo. Vamos ver como ficou:

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 4)
#
clear
read -p “Título do Álbum: ” Tit
[ “$Tit” ] || exit 1 # Fim da execução se título vazio
if grep “^$Tit^” musicas > /dev/null
then
echo Este álbum já está cadastrado
exit 1
fi
Reg=”$Tit^”
Cont=1
oArt=
while true
do
echo Dados da trilha $Cont:
read -p “Música: ” Mus
[ “$Mus” ] || break # Sai se vazio
read -p “Artista: $oArt // ” Art
[ “$Art” ] && oArt=”$Art” # Se vazio Art anterior
Reg=”$Reg$oArt~$Mus:” # Montando registro
Cont=$((Cont + 1))
# A linha anterior tb poderia ser ((Cont++))
done
echo “$Reg” >> musicas
sort musicas -o musicas

Este exemplo, começa com a leitura do título do álbum, que se não for informado, terminará a execução do programa. Em seguida um grep procura no início (^) de cada registro de musicas, o título informado seguido do separador (^) (que está precedido de uma contrabarra () para protegê-lo da interpretação do Shell).

Para ler os nomes dos artistas e as músicas do álbum, foi montado um loop de while simples, cujo único destaque é o fato de estar armazenando o artista da música anterior na variável $oArt que só terá o seu conteúdo alterado, quando algum dados for informado para a variável $Art, isto é, quando não teclou-se um simples <ENTER> para manter o artista anterior.

O que foi visto até agora sobre o while foi muito pouco. Este comando é muito utilizado, principalmente para leitura de arquivos, porém nos falta bagagem para prosseguir. Depois que aprendermos a ler, veremos esta instrução mais a fundo.

Pinguim com placa de dica Leitura de arquivo significa ler um-a-um todos os registros, o que é sempre uma operação lenta. Fique atento para não usar o while quando seu uso for desnecessário. O Shell tem ferramentas como o sed e a família grep que vasculham arquivos de forma otimizada sem ser necessário o uso de comandos de loop para fazê-lo registro a registro (ou até palavra a palavra).

O comando until

O comando until funciona exatamente igual ao while, porém ao contrário. Disse tudo mas não disse nada, né? É o seguinte: ambos testam comandos; ambos possuem a mesma sintaxe e ambos atuam em loop, porém enquanto o while executa o bloco de intruções do loop enquanto um comando for bem sucedido, o until executa o bloco do loop até que o comando seja bem sucedido. Parece pouca coisa mas a diferença é fundamental.

A sintaxe do comando é praticamente a mesma do while. Veja:

    until comando
    do
        cmd1
        cmd2
        ...
        cmdn
    done

E desta forma o bloco de comandos formado pelas instruções cmd1, cmd2,… e cmdn é executado até que a execução da instrução comando seja bem sucedida.

Como eu te disse, o while e until funcionam de forma antagônica e isso é muito fácil de demonstrar: em uma guerra sempre que se inventa uma arma, o inimigo busca uma solução para neutralizá-la. Baseado neste principio belicoso que o meu chefe, desenvolveu, no mesmo servidor que eu executava o logaute.sh um script para controlar o meu horário de chegada.

Um dia deu um problema da rede, ele me pediu para dar uma olhada no micro dele e me deixou sozinho em sua sala. Imediatamente comecei a bisbilhotar seus arquivos – porque guerra é guerra – e veja só o que descobri:

$cat chegada.sh
#!/bin/bash

until who | grep julio
do
sleep 30
done
echo $(date “+ Em %d/%m às %H:%Mh”) >> relapso.log

Olha que safado! O cara estava montando um log com os horários que eu chegava, e ainda por cima chamou o arquivo que me monitorava de relapso.log! O que será que ele quis dizer com isso?

Neste script, o pipeline who | grep julio, será bem sucedido somente quando julio for encontrado no comando who, isto é, quando eu me “logar” no servidor. Até que isso aconteça, o comando sleep, que forma o bloco de instruções do until, porá o programa em espera por 30 segundos. Quando este loop encerrar-se, será dada uma mensagem para o relapso.log (ARGHH!). Supondo que no dia 20/01 eu me loguei às 11:23 horas, a mensagem seria a seguinte:

Em 20/01 às 11:23h

Quando vamos cadastrar músicas, o ideal seria que pudéssemos cadastrar diversos CDs, e na última versão que fizemos do musinc, isso não ocorre, a cada CD que cadastramos o programa termina. Vejamos como melhorá-lo:

$ cat musinc
#!/bin/bash
# Cadastra CDs (versao 5)
#
Para=
until [ “$Para” ]
do
clear
read -p “Título do Álbum: ” Tit
if [ ! “$Tit” ] # Se titulo vazio…
then
Para=1 # Liguei flag de saída
else
if grep “^$Tit^” musicas > /dev/null
then
echo Este álbum já está cadastrado
exit 1
fi
Reg=”$Tit^”
Cont=1
oArt=
while [ “$Tit” ]
do
echo Dados da trilha $Cont:
read -p “Música: ” Mus
[ “$Mus” ] || break # Sai se vazio
read -p “Artista: $oArt // ” Art
[ “$Art” ] && oArt=”$Art” # Se vazio Art anterior
Reg=”$Reg$oArt~$Mus:” # Montando registro
Cont=$((Cont + 1))
# A linha anterior tb poderia ser ((Cont++))
done
echo “$Reg” >> musicas
sort musicas -o musicas
fi
done

Nesta versão, um loop maior foi adicionado antes da leitura do título, que só terminará quando a variável $Para deixar de ser vazia. Caso o título do álbum não seja informado, a variável $Para receberá valor (no caso coloquei 1 mas poderia ter colocado qualquer coisa. O importante é que não seja vazia) para sair deste loop, terminando desta forma o programa. No resto, o script é idêntico à sua versão anterior.

Atalhos no loop

Nem sempre um ciclo de programa, compreendido entre um do e um done, sai pela porta da frente. Em algumas oportunidades, temos que colocar um comando que aborte de forma controlada este loop. De maneira inversa, algumas vezes desejamos que o fluxo de execução do programa volte antes de chegar ao done. Para isto, temos respectivamente, os comandos break (que já vimos rapidamente nos exemplos do comado while) e continue, que funcionam da seguinte forma:

O que eu não havia dito anteriormente é que nas suas sintaxes genéricas eles aparecem da seguinte forma:

break [qtd loop]

e

continue [qtd loop]

Onde qtd loop representa a quantidade dos loops mais internos sobre os quais os comandos irão atuar. Seu valor default é 1.

Fluxograma Duvido que você nunca tenha deletado um arquivo e logo após deu um tabefe na testa se xingando porque não devia tê-lo removido. Pois é, na décima vez que fiz esta besteira, criei um script para simular uma lixeira, isto é, quando mando remover um (ou vários) arquivo(s), o programa “finge” que removeu-o, mas no duro o que fez foi mandá-lo(s) para o diretório /tmp/LoginName_do_usuario. Chamei este programa de erreeme e no /etc/profile coloquei a seguinte linha:

alias rm=erreeme

O programa era assim:

$ cat erreeme
#/bin/bash
#
# Salvando Copia de Arquivo Antes de Remove-lo
#

if [ $# -eq 0 ] # Tem de ter um ou mais arquivos para remover
then
echo “Erro -> Uso: erreeme arq [arq] … [arq]”
echo ” O uso de metacaracteres e’ permitido. Ex. erreeme arq*”
exit 1
fi

MeuDir=”/tmp/$LOGNAME” # Variavel do sist. Contém o nome do usuário.
if [ ! -d $MeuDir ] # Se não existir o meu diretório sob o /tmp…
then
mkdir $MeuDir # Vou cria-lo
fi

if [ ! -w $MeuDir ] # Se não posso gravar no diretório…
then
echo Impossivel salvar arquivos em $MeuDir. Mude permissao…
exit 2
fi

Erro=0 # Variavel para indicar o cod. de retorno do prg
for Arq # For sem o “in” recebe os parametros passados
do
if [ ! -f $Arq ] # Se este arquivo não existir…
then
echo $Arq nao existe.
Erro=3
continue # Volta para o comando for
fi

DirOrig=`dirname $Arq` # Cmd. dirname informa nome do dir de $Arq
if [ ! -w $DirOrig ] # Verifica permissão de gravacaoo no diretório
then
echo Sem permissao de remover no diretorio de $Arq
Erro=4
continue # Volta para o comando for
fi

if [ “$DirOrig” = “$MeuDir” ] # Se estou “esvaziando a lixeira”…
then
echo $Arq ficara sem copia de seguranca
rm -i $Arq # Pergunta antes de remover
[ -f $Arq ] || echo $Arq removido # Será que o usuario removeu?
continue
fi

cd $DirOrig # Guardo no fim do arquivo o seu diretorio
pwd >> $Arq # original para usa-lo em um script de undelete
mv $Arq $MeuDir # Salvo e removo
echo $Arq removido
done
exit $Erro # Passo eventual numero do erro para o codigo de retorno

Como você pode ver, a maior parte do script é formada por pequenas criticas aos parâmetros informados, mas como o script pode ter recebido diversos arquivos para remover, a cada arquivo que não se encaixa dentro do especificado, há um continue, para que a sequência volte para o loop do for de forma a receber outros arquivos.

Quando você está no Windows (com perdão da má palavra) e tenta remover aquele monte de lixo com nomes esquisitos como HD04TG.TMP, se der erro em um deles, os outros não são removidos, não é? Então, o continue foi usado para evitar que um impropério desses ocorra, isto é, mesmo que dê erro na remoção de um arquivo, o programa continuará removendo os outros que foram passados.

– Eu acho que a esta altura você deve estar curioso para ver o programa que restaura o arquivo removido, não é? Pois então aí vai vai um desafio: faça-o em casa e me traga para discutirmos no nosso próximo encontro aqui no boteco.

– Poxa, mas nesse eu acho que vou dançar, pois não sei nem como começar…

– Cara, este programa é como tudo que se faz em Shell, extremamente fácil, é para ser feito em, no máximo 10 linhas. Não se esqueça que o arquivo está salvo em /tmp/$LOGNAME e que a sua última linha é o diretório em que ele residia antes de ser “removido”. Também não se esqueça de criticar se foi passado o nome do arquivo a ser removido.

– É eu vou tentar, mas sei não…

– Tenha fé irmão, eu tô te falando que é mole! Qualquer dúvida é só me passar um e-mail para julio.neves@gmail.com. Agora chega de papo que eu já estou de goela seca de tanto falar. Me acompanha no próximo chope ou já vai sair correndo para fazer o script que passei?

– Deixa eu pensar um pouco…

– Chico, traz mais um chope enquanto ele pensa!

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 »

Papo de Botequim – Parte 7


– Cumequié rapaz! Derreteu os pensamentos para fazer o scriptizinho que eu te pedi? – É, eu realmente tive de colocar muita pensação na tela preta mas acho que consegui! Bem, pelo menos no testes que fiz a coisa funcionou, mas você tem sempre que botar chifres em cabeça de cachorro!- Não é bem assim, programar em shell é muito fácil, o que vale são as dicas e macetes que não são triviais. As correções que te faço, são justamente para mostrá-los. Mas vamos pedir dois chopes enquanto dou uma olhadela no teu script.- Aê Chico, traz dois. Não esqueça que um é sem colarinho.


$ cat restaura

#!/bin/bash
#
# Restaura arquivos deletados via erreeme
#

if [ $# -eq 0 ]
then
echo “Uso: $0 ”
exit 1
fi
# Pega nome do diretório original na última linha
Dir=`tail -1 /tmp/$LOGNAME/$1`
# O grep -v exclui última linha e cria o
# arquivo com diretorio e nome originais
grep -v $Dir /tmp/$LOGNAME/$1 > $Dir/$1
# Remove arquivo que jah estava moribundo
rm /tmp/$LOGNAME/$1

– Peraí, deixa ver se entendi. Primeiramente você coloca na variável Dir a última linha do arquivo cujo nome é formado por /tmp/nome do operador ($LOGNAME)/parâmetro passado com nome do arquivo a ser restaurado ($1). Em seguida o grep -v que você montou exclui a linha em que estava o nome do diretório, isto é, sempre a última e manda o restante do arquivo, que seria assim o arquivo já limpo, para o diretório original e depois remove o arquivo da “lixeira”; S E N S A C I O N A L! Impecável! Zero erro! Viu? você já está pegando as manhas do shell!

– Então vamulá chega de lesco-lesco e blá-blá-blá, de que você vai falar hoje?

– É tô vendo que o bichinho do Shell te pegou. Que bom, mas vamos ver como se pode (e deve) ler dados e formatar telas e primeiramente vamos entender um comando que te dá todas as ferramentas para você formatar a sua tela de entrada de dados.

O comando tput

O maior uso deste comando é para posicionar o cursor na tela, mas também é muito usado para apagar dados da tela, saber a quantidade de linhas e colunas para poder posicionar corretamente um campo, apagar um campo cuja crítica detectou como errado. Enfim, quase toda a formatação da tela é feita por este comando.

Uns poucos atributos do comando tput podem eventualmente não funcionar se o modelo de terminal definido pela variável $TERM não tiver esta facilidade incorporada.

Na tabela a seguir, apresenta os principais atributos do comando e os efeitos executados sobre o terminal, mas veja bem existem muito mais do que esses, veja só:

$ tput it
8

Neste exemplo eu recebi o tamanho inicial da <TAB> ( Initial T ab), mas me diga: para que eu quero saber isso? Se você quiser saber tudo sobre o comando tput (e olha que é coisa que não acaba mais), veja em: http://www.cs.utah.edu/dept/old/texinfo/tput/tput.html#SEC4.

Principais Opções do Comando tput
Opções do tput Efeito
cup lin col CUrsor Position – Posiciona o cursor na linha lin e coluna col. A origem é zero
bold Coloca a tela em modo de ênfase
rev Coloca a tela em modo de vídeo reverso
smso Idêntico ao anterior
smul A partir desta instrução, os caracteres teclados aparecerão sublinhados na tela
blink Os caracteres teclados aparecerão piscando
sgr0 Após usar um dos atributos acima, use este para restaurar a tela ao seu modo normal
reset Limpa o terminal e restaura suas definições de acordo com o terminfo ou seja, o terminal volta ao padrão definido pela variável $TERM
lines Devolve a quantidade de linhas da tela no momento da instrução
cols Devolve a quantidade de colunas da tela no momento da instrução
el Erase Line – Apaga a linha a partir da posição do cursor
ed Erase Display – Apaga a tela a partir da posição do cursor
il n Insert Lines – Insere n linhas a partir da posição do cursor
dl n Delete Lines – Remove n linhas a partir da posição do cursor
ech n Erase CHaracters – Apaga n caracteres a partir da posição do cursor
sc Save Cursor position – Salva a posição do cursor
rc Restore Cursor position – Coloca o cursor na posição marcada pelo último sc

Vamos fazer um programa bem besta (e portanto fácil) para mostrar alguns atributos deste comando. É o famoso e famigerado Alô Mundo só que esta frase será escrita no centro da tela e em vídeo reverso e após isso, o cursor voltará para a posição em que estava antes de escrever esta tão criativa frase. Veja:

$ cat alo.sh
#!/bin/bash
# Script bobo para testar
# o comando tput (versao 1)

Colunas=`tput cols` # Salvando quantidade colunas
Linhas=`tput lines` # Salvando quantidade linhas
Linha=$((Linhas / 2)) # Qual eh a linha do meio da tela?
Coluna=$(((Colunas – 9) / 2)) # Centrando a mensagem na tela
tput sc # Salvando posicao do cursor
tput cup $Linha $Coluna # Posicionando para escrever
tput rev # Video reverso
echo Alô Mundo
tput sgr0 # Restaura video ao normal
tput rc # Restaura cursor aa posição original

Como o programa já está todo comentado, acho que a única explicação necessária seria para a linha em que é criada a variável Coluna e o estranho ali é aquele número 9, mas ele é o tamanho da cadeia que pretendo escrever (Alô Mundo).

Desta forma este programa somente conseguiria centrar cadeias de 9 caracteres, mas veja isso:

$ var=Papo
$ echo ${#var}
4

$ var=”Papo de Botequim”
$ echo ${#var}
16

Ahhh, melhorou! Então agora sabemos que a construção ${#variavel} devolve a quantidade de caracteres de variavel. Assim sendo, vamos otimizar o nosso programa para que ele escreva em vídeo reverso, no centro da tela a cadeia passada como parâmetro e depois o cursor volte à posição que estava antes da execução do script.

$ cat alo.sh
#!/bin/bash
# Script bobo para testar
# o comando tput (versao 2)

Colunas=`tput cols` # Salvando quantidade colunas
Linhas=`tput lines` # Salvando quantidade linhas
Linha=$((Linhas / 2)) # Qual eh a linha do meio da tela?
Coluna=$(((Colunas – ${#1}) / 2)) #Centrando a mensagem na tela
tput sc # Salvando posicao do cursor
tput cup $Linha $Coluna # Posicionando para escrever
tput rev # Video reverso
echo $1
tput sgr0 # Restaura video ao normal
tput rc # Restaura cursor aa posição original

Este script é igual ao anterior, só que trocamos o valor fixo da versão anterior (9), por ${#1}, onde este 1 é o $1 ou seja, esta construção devolve o tamanho do primeiro parâmetro passado para o programa. Se o parâmetro que eu quiser passar tiver espaços em branco, teria que colocá-lo todo entre aspas, senão o $1 seria somente o primeiro pedaço. Para evitar este aborrecimento, é só substituir o $1 por $*, que como sabemos é o conjunto de todos os parâmetros. Então aquela linha ficaria assim:

    Coluna=`$(((Colunas - ${#*}) / 2))` #Centrando a mensagem na tela

e a linha echo $1 passaria a ser echo $*. Mas não esqueça de qdo executar, passar a frase que vc desja centrar como parâmetro.

E agora podemos ler os dados da tela

Bem a partir de agora vamos aprender tudo sobre leitura, só não posso ensinar a ler cartas e búzios porque se eu soubesse, estaria rico, num pub londrino tomando scotch e não em um boteco desses tomando chope. Mas vamos em frente.

Da última vez que nos encontramos aqui eu já dei uma palinha sobre o comando read. Para começarmos a sua analise mais detalhada. veja só isso:

$ read var1 var2 var3
Papo de Botequim

$ echo $var1
Papo

$ echo $var2
de

$ echo $var3
Botequim

$ read var1 var2
Papo de Botequim

$ echo $var1
Papo

$ echo $var2
de Botequim

Como você viu, o read recebe uma lista separada por espaços em branco e coloca cada item desta lista em uma variável. Se a quantidade de variáveis for menor que a quantidade de itens, a última variável recebe o restante.

Eu disse lista separada por espaços em branco? Agora que você já conhece tudo sobre o $IFS (Inter Field Separator) que eu te apresentei quando falávamos do comando for, será que ainda acredita nisso? Vamos testar direto no prompt:

$ oIFS=”$IFS”
$ IFS=:
$ read var1 var2 var3
Papo de Botequim

$ echo $var1
Papo de Botequim

$ echo $var2$ echo $var3
$ read var1 var2 var3
Papo:de:Botequim

$ echo $var1
Papo

$ echo $var2
de

$ echo $var3
Botequim

$ IFS=”$oIFS”

Viu, estava furado! O read lê uma lista, assim como o for, separada pelos caracteres da variável $IFS. Então veja como isso pode facilitar a sua vida:

$ grep julio /etc/passwd
julio:x:500:544:Julio C. Neves – 7070:/home/julio:/bin/bash

$ oIFS=”$IFS” # Salvando IFS
$ IFS=:
$ grep julio /etc/passwd | read lname lixo uid gid coment home shell
$ echo -e “$lnamen$uidn$gidn$comentn$homen$shell”
julio
500
544
Julio C. Neves – 7070
/home/julio
/bin/bash

$ IFS=”$oIFS” # Restaurando IFS

Como você viu, a saída do grep foi redirecionada para o comando read que leu todos os campos de uma só tacada. A opção -e do echo foi usada para que o n fosse entendido como um salto de linha (new line), e não como um literal.

Sob o Bash existem diversas opções do read que servem para facilitar a sua vida. Veja a tabela a seguir:

Opções do comando read no Bash
Opção Ação
-p prompt Escreve o prompt antes de fazer a leitura
-n num Lê até num caracteres
-t seg Espera seg segundos para que a leitura seja concluída
-s O que está sendo teclado não aparece na tela

E agora direto aos exemplos curtos para demonstrar estas opções.

Para ler um campo “Matrícula”:

$ echo -n “Matricula: “; read Mat # -n nao salta linha
Matricula: 12345

$ echo $Mat
12345

Ou simplificando com a opção -p:

$ read -p “Matricula: ” Mat
Matricula: 12345

$ echo $Mat
12345

Para ler uma determinada quantidade de caracteres:

$ read -n5 -p”CEP: ” Num ; read -n3 -p- Compl
CEP: 12345-678
$
$ echo $Num
12345

$ echo $Compl
678

Neste exemplo fizemos dois read: um para a primeira parte do CEP e outra para o seu complemento, deste modo formatando a entrada de dados. O cifrão ($) após o último algarismo teclado, é porque o read não tem o new-line implícito por default como o tem o echo.

Para ler que até um determinado tempo se esgote (conhecido como time out):

$ read -t2 -p “Digite seu nome completo: ” Nom || echo ‘Eta moleza!’
Digite seu nome completo: JEta moleza!

$ echo $Nom$

Obviamente isto foi uma brincadeira, pois só tinha 3 segundos para digitar o meu nome completo e só me deu tempo de teclar um J (aquele colado no Eta), mas serviu para mostrar duas coisas:

  1. O comando após o par de barras verticais (||) (o ou lógico, lembra-se?) será executado caso a digitação não tenha sido concluída no tempo estipulado;
  2. A variável Nom permaneceu vazia. Ela será valorada somente quando o <ENTER> for teclado.

Para ler um dado sem ser exibido na tela:

$ read -sp “Senha: “
Senha:
$ echo $REPLY
segredo 🙂

Aproveitei um erro para mostrar um macete. Quando escrevi a primeira linha, esqueci de colocar o nome da variável que iria receber a senha, e só notei quando ia listar o seu valor. Felizmente a variável $REPLY do Bash, possui a última cadeia lida e me aproveitei disso para não perder a viagem. Teste você mesmo o que acabei de fazer.

Mas o exemplo que dei, era para mostrar que a opção -s impede o que está sendo teclado de ir para a tela. Como no exemplo anterior, a falta do new-line fez com que o prompt de comando ($) permanecesse na mesma linha.

Bem, agora que sabemos ler da tela vejamos como se lê os dados dos arquivos.

Vamos ler arquivos?

Como eu já havia lhe dito, e você deve se lembrar, o while testa um comando e executa um bloco de instruções enquanto este comando for bem sucedido. Ora quando você está lendo um arquivo que lhe dá permissão de leitura, o read só será mal sucedido quando alcançar o EOF (end of file), desta forma podemos ler um arquivo de duas maneiras:

1 – Redirecionando a entrada do arquivo para o bloco do while assim:

    while read Linha
    do
        echo $Linha
    done < arquivo

2 – Redirecionando a saída de um cat para o while, da seguinte maneira:

    cat arquivo |
    while read Linha
    do
        echo $Linha
    done

Cada um dos processos tem suas vantagens e desvantagens:

Vantagens do primeiro processo:

  • É mais rápido;
  • Não necessita de um subshell para assisti-lo;

Desvantagem do primeiro processo:

  • Em um bloco de instruções grande, o redirecionamento fica pouco visível o que por vezes prejudica a vizualização do código;

Vantagem do segundo processo:

  • Como o nome do arquivo está antes do while, é mais fácil a vizualização do código.

Desvantagens do segundo processo:

  • O Pipe (|) chama um subshell para interpretá-lo, tornando o processo mais lento, pesado e por vezes problemático (veja o exemplo a seguir).

Para ilustrar o que foi dito, veja estes exemplos a seguir:

$ cat readpipe.sh
#!/bin/bash
# readpipe.sh
# Exemplo de read passando arquivo por pipe.

Ultimo=”(vazio)”
cat $0 | # Passando o arq. do script ($0) p/ while
while read Linha
do
Ultimo=”$Linha”
echo “-$Ultimo-”
done
echo “Acabou, Último=:$Ultimo:”

Vamos ver sua execução:

$ readpipe.sh
-#!/bin/bash-
-# readpipe.sh-
-# Exemplo de read passando arquivo por pipe.-

-Ultimo=”(vazio)”-
-cat $0 | # Passando o arq. do script ($0) p/ while-
-while read Linha-
-do-
-Ultimo=”$Linha”-
-echo “-$Ultimo-“-
-done-
-echo “Acabou, Último=:$Ultimo:”-
Acabou, Último=:(vazio):

Como você viu, o script lista todas as suas próprias linhas com um sinal de menos (-) antes e outro depois de cada, e no final exibe o conteúdo da variável $Ultimo. Repare no entanto que o conteúdo desta variável permanece como (vazio).

– Ué será que a variável não foi atualizada?

– Foi, e isso pode ser comprovado porque a linha echo "-$Ultimo-" lista corretamente as linhas.

– Então porque isso aconteceu?

– Por que como eu disse, o bloco de instruções redirecionado pelo pipe (|) é executado em um subshell e lá as variáveis são atualizadas. Quando este subshell termina, as atualizações das variáveis vão para os píncaros do inferno junto com ele. Repare que vou fazer uma pequena mudança nele, passando o arquivo por redirecionamento de entrada (<) e as coisas passarão a funcionar na mais perfeita ordem:

$ cat redirread.sh
#!/bin/bash
# redirread.sh
# Exemplo de read passando arquivo por pipe.

Ultimo=”(vazio)”
while read Linha
do
Ultimo=”$Linha”
echo “-$Ultimo-”
done < $0 # Passando o arq. do script ($0) p/ while
echo “Acabou, Último=:$Ultimo:”

E veja a sua perfeita execução:

$ redirread.sh
-#!/bin/bash-
-# redirread.sh-
-# Exemplo de read passando arquivo por pipe.-

-Ultimo=”(vazio)”-
-while read Linha-
-do-
-Ultimo=”$Linha”-
-echo “-$Ultimo-“-
-done < $0 # Passando o arq. do script ($0) p/ while-
-echo “Acabou, Último=:$Ultimo:”-
Acabou, Último=:echo “Acabou, Último=:$Ultimo:”:

Bem amigos da Rede Shell, para finalizar o comando read só falta mais um pequeno e importante macete que vou mostrar utilizando um exemplo prático. Suponha que você queira listar na tela um arquivo e a cada dez registros esta listagem pararia para que o operador pudesse ler o conteúdo da tela e ela só voltasse a rolar (scroll) após o operador digitar qualquer tecla. Para não gastar papel (da Linux Magazine) pra chuchu, vou fazer esta listagem na horizontal e o meu arquivo (numeros), tem 30 registros somente com números seqüênciais. Veja:

$ seq 30 | xargs -i echo lin {} > numeros # Criando arquivo numeros
$ paste -sd’:’ numeros # Este paste é para exibir o conteúdo sem ser um
#+ por linha, usando dois-pontos como separador
lin 1:lin 2:lin 3:lin 4:lin 5:lin 6:lin 7:lin 8:lin 9:lin 10:lin 11:lin 12:lin 13:lin 14:lin 15:l
in 16:lin 17:lin 18:lin 19:lin 20:lin 21:lin 22:lin 23:lin 24:lin 25:lin 26:lin 27:lin 28:lin 29:
lin 30

$ cat 10porpag.sh
#!/bin/bash
# Prg de teste para escrever
# 10 linhas e parar para ler
# Versão 1

while read Num
do
let ContLin++ # Contando…
echo -n “$Num ” # -n para nao saltar linha
((ContLin % 10)) > /dev/null || read
done < numeros

Na tentativa de fazer um programa genérico criamos a variável $ContLin (por que na vida real, os registros não são somente números seqüenciais) e parávamos para ler quando o resto da divisão por 10 fosse zero (mandando a saída para /dev/null de forma a não aparecer na tela, sujando-a). Porém, quando fui executar deu a seguinte zebra:

$ 10porpag.sh
lin 1 lin 2 lin 3 lin 4 lin 5 lin 6 lin 7 lin 8 lin 9 lin 10 lin 12 lin 13 lin 14 lin 15 lin 16 l
in 17 lin 18 lin 19 lin 20 lin 21 lin 23 lin 24 lin 25 lin 26 lin 27 lin 28 lin 29 lin 30
$

Repare que faltou a linha lin 11 e a listagem não parou no read. O que houve foi que toda a entrada do loop estava redirecionada do arquivo numeros e desta forma, a leitura foi feita em cima deste arquivo, desta forma perdendo a lin 11 (e também a lin 22 ou qualquer linha múltipla de 11).

Vamos mostrar então como deveria ficar para funcionar a contento:

$ cat 10porpag.sh
#!/bin/bash
# Prg de teste para escrever
# 10 linhas e parar para ler
# Versão 2

while read Num
do
let ContLin++ # Contando…
echo -n “$Num ” # -n para nao saltar linha
((ContLin % 10)) > /dev/null || read < /dev/tty
done < numeros

Observe que agora a entrada do read foi redirecionada por /dev/tty, que nada mais é senão o terminal corrente, explicitando desta forma que aquela leitura seria feita do teclado e não de numeros. É bom realçar que isto não acontece somente quando usamos o redirecionamento de entrada, se houvéssemos usado o redirecionamento via pipe (|), o mesmo teria ocorrido.

Veja agora a sua execução:

$ 10porpag.sh
lin 1 lin 2 lin 3 lin 4 lin 5 lin 6 lin 7 lin 8 lin 9 lin 10
lin 11 lin 12 lin 13 lin 14 lin 15 lin 16 lin 17 lin 18 lin 19 lin 20
lin 21 lin 22 lin 23 lin 24 lin 25 lin 26 lin 27 lin 28 lin 29 lin 30

Isto está quase bom mas falta um pouco para ficar excelente. Vamos melhorar um pouco o exemplo para que você o reproduza e teste (mas antes de testar aumente o número de registros de numeros ou reduza o tamanho da tela, para que haja quebra).

$ cat 10porpag.sh
#!/bin/bash
# Prg de teste para escrever
# 10 linhas e parar para ler
# Versão 3

clear
while read Num
do
((ContLin++)) # Contando…
echo “$Num”
((ContLin % (`tput lines` – 3))) ||
{
read -n1 -p”Tecle Algo ” < /dev/tty # para ler qq caractere
clear # limpa a tela apos leitura
}
done < numeros

A mudança substancial feita neste exemplo é com relação à quebra de página, já que ela é feita a cada quantidade-de-linhas-da-tela (tput lines) menos (-) 3, isto é, se a tela tem 25 linha, listará 22 registros e parará para leitura. No comando read também foi feita uma alteração, inserido um -n1 para ler somente um caractere sem ser necessariamente um <ENTER> e a opção -p para dar a mensagem.

– Bem meu amigo, por hoje é só porque acho que você já está de saco cheio…

– Num tô não, pode continuar…

– Se você não estiver eu estou… Mas já que você está tão empolgado com o Shell, vou te deixar um exercício de apredizagem para você melhorar a sua CDteca que é bastante simples. Reescreva o seu programa que cadastra CDs para montar toda a tela com um único echo e depois vá posicionando à frente de cada campo para receber os valores que serão teclados pelo operador.

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 »