Feeds:
Posts
Comentários

Posts Tagged ‘exit’

Papo de Botequim – Parte 5

– Fala cara! E as idéias estão em ordem? Já fundiu a cuca ou você ainda aguenta mais Shell?

– Guento! Tô gostando muito! Gostei tanto que até caprichei no exercício que você passou. Lembra que você me pediu para fazer um programa que receberia como parâmetro o nome de um arquivo e que quando executado salvaria este arquivo com o nome original seguido de um til (~) e colocaria este arquivo dentro do vi?

– Claro que lembro, me mostre e explique como você fez.

$ cat vira
#!/bin/bash
#
# vira – vi resguardando arquivo anterior
# == = =

# Verificando se foi passado 1 parametro
if [ “$#” -ne 1 ]
then
echo “Erro -> Uso: $0 ”
exit 1
fi

Arq=$1
# Caso o arquivo não exista, nao ha copia para ser salva
if [ ! -f “$Arq” ]
then
vi $Arq
exit 0
fi

# Se nao puder alterar o arquivo vou usar o vi para que?
if [ ! -w “$Arq” ]
then
echo “Voce nao tem direito de gravacao em $Arq”
exit 2
fi

# Ja que esta tudo OK, vou salvar a copia e chamar o vi
cp -f $Arq $Arq~
vi $Arq
exit 0

– É, beleza! Mas me diz uma coisa: porque você terminou o programa com um exit 0?

– Ahhh! Eu descobri que o número após o exit resultará no código de retorno do programa (o $?, lembra?), e desta forma, como foi tudo bem sucedido, ele encerraria com o $? = 0. Porém se você observar, verá que caso o programa não tenha recebido o nome do arquivo ou caso o operador não tivesse direito de gravação sobre este arquivo, o código de retorno ($?) seria diferente do zero.

– Grande garoto, aprendeu legal, mas é bom deixar claro que exit 0, simplesmente exit ou não colocar exit, produzem igualmente um código de retorno ($?) igual a zero. Agora vamos falar sobre as instruções de loop ou laço, mas antes vou passar o conceito de bloco de programa.

Até agora já vimos alguns blocos de programa. Quando te mostrei um exemplo para fazer um cd para dentro de um diretório que era assim:

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

O fragmento contido entre as duas chaves ({}), forma um bloco de comandos. Também neste exercício que acabamos de ver, em que salvamos o arquivo antes de editá-lo, existem vários blocos de comandos compreendidos entre os then e os fi do if.

Um bloco de comandos também pode estar dentro de um case, ou entre um do e um done.

– Peraí Julio, que do e done é esse, não me lembro de você ter falado nisso e olha estou prestando muita atenção…

– Pois é, ainda não havia falado porque não havia chegado o momento propício. Todas as instruções de loop ou laço, executam os comandos do bloco compreendido entre o do e o done.

Comandos de Loop (ou laço)

As instruções de loop ou laço são o for, o while e o until que passarei a te explicar uma-a-uma a partir de agora.

O comando for

Se você está habituado a programar, certamente já conhece o comando for, mas o que você não sabe é que o for, que é uma instrução intrinseca do Shell (isto significa que o código fonte do comando faz parte do código fonte do Shell, ou seja em bom programês é um built-in), é muito mais poderoso que os seus correlatos das outras linguagens.

Vamos entender a sua sintaxe, primeiramente em português e depois como funciona no duro.

    para var em val1 val2 ... valn
    faça
        cmd1
        cmd2
        cmdn
    feito

Onde a variável var assume cada um dos valores da lista val1 val2 ... valn e para cada um desses valores executa o bloco de comandos formado por cmd1, cmd2 e cmdn

Agora que já vimos o significado da instrução em português, vejamos a sintaxe correta:

Primeira sintaxe do comando for

    for var in val1 val2 ... valn
    do
        cmd1
        cmd2
        cmdn
    done

Vamos direto para os exemplos, para entender direito o funcionamento deste comando. Vamos escrever um script para listar todos os arquivos do nosso diretório separados por dois-pontos, mas primeiro veja:

$ echo *
ArqDoDOS.txt1 confuso incusu logado musexc musicas musinc muslist

Isto é, o Shell viu o asterisco (*) expandindo-o com o nome de todos os arquivos do diretório e o comando echo jogou-os para a tela separados por espaços em branco. Visto isso vamos ver como resolver o problema a que nos propuzemos:

$ cat testefor1
#!/bin/bash
# 1o. Prog didático para entender o for

for Arq in *
do
echo -n $Arq: # A opcao -n eh para nao saltar linha
done

Então vamos executá-lo:

$ testefor1
ArqDoDOS.txt1:confuso:incusu:logado:musexc:musicas:musinc:muslist:
$

Como você viu o Shell transformou o asterísco (que odeia ser chamado de asterístico) em uma lista de arquivos separados por espaços em branco. quando o for viu aquela lista, ele disse: “Opa, lista separadas por espaços é comigo mesmo!”

O bloco de comandos a ser executado era somente o echo, que com a opção -n listou a variável $Arq seguida de dois-pontos (:), sem saltar a linha. O cifrão ($) do final da linha da execução é o prompt. que permaneceu na mesma linha também em função da opção -n. Outro exemplo simples (por enquanto):

$ cat testefor2
#!/bin/bash
# 2o. Prog didático para entender o for

for Palavra in Papo de Botequim
do
echo $Palavra
done

E executando vem:

$ testefor2
Papo
de
Botequim

Como você viu, este exemplo é tão bobo e simples como o anterior, mas serve para mostrar o comportamento básico do for.

Veja só a força do for: ainda estamos na primeira sintaxe do comando e já estou mostrando novas formas de usá-lo. Lá atrás eu havia falado que o for usava listas separadas por espaços em branco, mas isso é uma meia verdade, era só para facilitar a compreensão.

No duro, as listas não são obrigatóriamente separadas por espaços mas antes de prosseguir, deixa eu te mostrar como se comporta uma variável do sistema chamada de $IFS. Repare seu conteúdo:

$ echo “$IFS” | od -h
0000000 0920 0a0a
0000004

Isto é, mandei a variável (protegida da interpretação do Shell pelas aspas) para um dump hexadecimal (od -h) e resultou:

Conteúdo da Variável $IFS
Hexadecimal Significado
09 <TAB>
20 <ESPAÇO>
0a <ENTER>

Onde o último 0a foi proveniente do <ENTER> dado ao final do comando. Para melhorar a explicação, vamos ver isso de outra forma:

$ echo “:$IFS:” | cat -vet
: ^I$
:$

Preste atenção na dica a seguir para entender a construção deste comando cat:

Pinguim com placa de dica No comando cat, a opção -e representa o <ENTER> como um cifrão ($) e a opção -t representa o <TAB> como um ^I. Usei os dois-pontos (:) para mostrar o início e o fim do echo. E desta forma, mais uma vez pudemos notar que os três caracteres estão presentes naquela variável.

Agora veja você, IFS significa Inter Field Separator ou, traduzindo, separador entre campos. Uma vez entendido isso, eu posso afirmar (porque vou provar) que o comando for não usa listas separadas por espaços em branco, mas sim pelo conteúdo da variável $IFS, cujo valor padrão (default) são esses caracteres que acabamos de ver. Para comprovarmos isso, vamos mostrar um script que recebe o nome do artista como parâmetro e lista as músicas que ele executa, mas primeiramente vamos ver como está o nosso arquivo musicas:

$ cat musicas
album 1^Artista1~Musica1:Artista2~Musica2
album 2^Artista3~Musica3:Artista4~Musica4
album 3^Artista5~Musica5:Artista6~Musica6
album 4^Artista7~Musica7:Artista1~Musica3
album 5^Artista9~Musica9:Artista10~Musica10

Em cima deste “leiaute” foi desenvolvido o script a seguir:

$ cat listartista
#!/bin/bash
# Dado um artista, mostra as suas musicas

if [ $# -ne 1 ]
then
echo Voce deveria ter passado um parametro
exit 1
fi

IFS=”
:”

for ArtMus in $(cut -f2 -d^ musicas)
do
echo “$ArtMus” | grep $1 && echo $ArtMus | cut -f2 -d~
done

O script, como sempre, começa testando se os parâmetros foram passados corretamente, em seguida o IFS foi setado para <ENTER> e dois-pontos (:) (como demonstram as aspas em linha diferentes), porque é ele que separa os blocos Artistan~Musicam. Desta forma, a variável $ArtMus irá receber cada um destes blocos do arquivo (repare que o for já recebe os registros sem o álbum em virtude do cut na sua linha). Caso encontre o parâmetro ($1) no bloco, o segundo cut listará somente o nome da música. Vamos executá-lo:

$ listartista Artista1
Artista1~Musica1
Musica1
Artista1~Musica3
Musica3
Artista10~Musica10
Musica10

Êpa! Aconteceram duas coisas indesejáveis: os blocos também foram listados e a Musica10 idem. Além do mais, o nosso arquivo de músicas está muito simples, na vida real, tanto a música quanto o artista têm mais de um nome. Suponha que o artista fosse uma dupla sertaneja chamada Perereca & Peteleca (não gosto nem de dar a idéia com receio que isso se torne realidade:). Neste caso o $1 seria Perereca e o resto deste lindo nome seria ignorado na pesquisa.

Para que isso não ocorresse, eu deveia passar o nome do artista entre aspas (") ou alterar $1 por $@ (que significa todos os parâmetros passados), que é a melhor solução, mas neste caso eu teria que modificar a crítica dos parâmetros e o grep. A nova crítica não seria se eu passei um parâmetro, mas pelo menos um parâmetro e quanto ao grep, veja só o que resultaria após a substituição do $* (que entraria no lugar do $1) pelos parâmetros:

    echo "$ArtMus" | grep perereca & peteleca

O que resultaria em erro. O correto seria:

    echo "$ArtMus" | grep -i "perereca & peteleca"

Onde foi colocado a opção -i para que a pesquisa ignorasse maiúsculas e minúsculas e as aspas também foram inseridas para que o nome do artista fosse visto como uma só cadeia monolítica.

Ainda falta consertar o erro dele ter listado o Artista10. Para isso o melhor é dizer ao grep que a cadeia está no início de $ArtMus (a expressão regular para dizer que está no início é ^) e logo após vem um til (~). É necessário também que se redirecione a saída do grep para /dev/null para que os blocos não sejam mais listados. Veja então a nova (e definitiva) cara do programa:

$ cat listartista
#!/bin/bash
# Dado um artista, mostra as suas musicas
# versao 2

if [ $# -eq 0 ]
then
echo Voce deveria ter passado pelo menos um parametro
exit 1
fi

IFS=”
:”

for ArtMus in $(cut -f2 -d^ musicas)
do
echo “$ArtMus” | grep -i “^$@~” > /dev/null && echo $ArtMus | cut -f2 -d~
done

Que executando vem:

$ listartista Artista1
Musica1
Musica3

Segunda sintaxe do comando for

    for var
    do
        cmd1
        cmd2
        cmdn
    done

– Ué, sem o in como ele vai saber que valor assumir?

– Pois é, né? Esta construção a primeira vista parece xquisita mas é bastante simples. Neste caso, var assumirá um-a-um cada um dos parâmetros passados para o progama.

Vamos logo aos exemplos para entender melhor. Vamos fazer um script que receba como parâmetro um monte de músicas e liste seus autores:

$ cat listamusica
#!/bin/bash
# Recebe parte dos nomes de musicas como parametro e
# lista os interpretes. Se o nome for composto, deve
# ser passado entre aspas.
# ex. “Eu nao sou cachorro nao” “Churrasquinho de Mae”
#
if [ $# -eq 0 ]
then
echo Uso: $0 musica1 [musica2] … [musican]
exit 1
fi
IFS=”
:”
for Musica
do
echo $Musica
Str=$(grep -i “$Musica” musicas) ||
{
echo ” Não encontrada”
continue
}
for ArtMus in $(echo “$Str” | cut -f2 -d^)
do
echo ” $ArtMus” | grep -i “$Musica” | cut -f1 -d~
done
done

Da mesma forma que os outros, começamos o exercício com uma crítica sobre os parâmetros recebidos, em seguida fizemos um for em que a variável $Musica receberá cada um dos parâmetros passados, colocando em $Str todos os álbuns que contém as músicas passadas. Em seguida, o outro for pega cada bloco Artista~Musica nos registros que estão em $Str e lista cada artista que execute aquela música.

Como sempre vamos executá-lo para ver se funciona mesmo:

$ listamusica musica3 Musica4 “Eguinha Pocotó”
musica3
Artista3
Artista1
Musica4
Artista4
Eguinha Pocotó
Não encontrada

A listagem ficou feinha porque ainda não sabemos formatar a saída, mas qualquer dia desses, quando você souber posicionar o cursor, fazer negrito, trabalhar com cores e etc, faremos esta listagem novamente usando todas estas perfumarias e ela ficará muito fashion.

A esta altura dos acontecimentos você deve estar se perguntando: “E aquele for tradicional das outras linguagens em que ele sai contando a partir de um número, com um determinado incremento até alcançar uma condição?”

E é aí que eu te respondo: “Eu não te disse que o nosso for é mais porreta que os outros?” Para fazer isso existem duas formas:

1 – Com a primeira sintaxe que vimos, como nos exemplos a seguir direto no prompt:

$ for i in $(seq 9)
> do
> echo -n “$i ”
> done
1 2 3 4 5 6 7 8 9

Neste a variável i assumiu os inteiros de 1 a 9 gerados pelo comando seq e a opção -n do echo foi usada para não saltar linha a cada número listado (sinto-me ecologicamente correto por não gastar um monte de papel da revista quando isso pode ser evitado). Ainda usando o for com seq:

$ for i in $(seq 3 9)
> do
> echo -n “$i ”
> done
4 5 6 7 8 9

Ou ainda na forma mais completa do seq:

$ for i in $(seq 0 3 9)
> do
> echo -n “$i ”
> done
0 3 6 9

2 – A outra forma de fazer o desejado é com uma sintaxe muito semelhante ao for da linguagem C, como veremos a seguir.

Terceira sintaxe do comando for

    for ((var=ini; cond; incr))
    do
        cmd1
        cmd2
        cmdn
    done

Onde:

var=ini – Significa que a variável var começará de um valor inicial ini;
cond – Siginifica que o loop ou laço do for será executado enquanto var não atingir a condição cond;
incr – Significa o incremento que a variável var sofrerá em cada passada do loop.

Como sempre vamos aos exemplos que a coisa fica mais fácil:

$ for ((i=1; i<=9; i++))
> do
> echo -n “$i ”
> done
1 2 3 4 5 6 7 8 9

Neste caso a variável i partiu do valor inicial 1, o bloco de comando (neste caso somente o echo) será executado enquanto i menor ou igual (<=) a 9 e o incremento de i será de 1 a cada passada do loop.

Repare que no for propriamente dito (e não no bloco de comandos) não coloquei um cifrão ($) antes do i, e a notação para incrementar (i++) é diferente do que vimos até agora. Isto é porque o uso de parênteses duplos (assim como o comando let) chama o interpretador aritmético do Shell, que é mais tolerante.

Como me referi ao comando let, só para mostrar como ele funciona e a versatilidade do for, vamos fazer a mesma coisa, porém omitindo a última parte do escopo do for, passando-a para o bloco de comandos.

$ for ((; i<=9;))
> do
> let i++
> echo -n “$i ”
> done
1 2 3 4 5 6 7 8 9

Repare que o incremento saiu do corpo do for e passou para o bloco de comandos, repare também que quando usei o let, não foi necessário sequer inicializar a variável $i. Veja só os comandos a seguir dados diretamente no prompt para mostrar o que acabo de falar:

$ echo $j$ let j++
$ echo $j
1

Ou seja, a variável $j sequer existia e no primeiro let assumiu o valor 0 (zero) para, após o incremento, ter o valor 1.

Veja só como as coisas ficam simples:

$ for arq in *
> do
> let i++
> echo “$i -> $Arq”
> done
1 -> ArqDoDOS.txt1
2 -> confuso
3 -> incusu
4 -> listamusica
5 -> listartista
6 -> logado
7 -> musexc
8 -> musicas
9 -> musinc
10 -> muslist
11 -> testefor1
12 -> testefor2

– Pois é amigo, tenho certeza que você já tomou um xarope do comando for. Por hoje chega, na próxima vez que nos encontrarmos falaremos sobre outras instruções de loop, mas eu gostaria que até lá você fizesse um pequeno script para contar a quantidade de palavras de um arquivo texto, cujo nome seria recebido por parâmetro.

OBS: Essa contagem tem de ser feita usando o comando for para se habituar ao seu uso. Não vale usar o wc -w.

– Aê Chico! Traz a saideira.

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 8


– E aê cara tudo bem? – Legal!, eu queria te mostrar o que fiz mas já sei que você vai querer molhar o bico primeiro né?- Só pra contrariar, hoje vou deixar você mostrar logo o seu “bacalho”. Vai mostra aí o que você fez.- Poxa o exercício que você passou é muito grande. Veja só como eu resolvi:

$ cat musinc5
#!/bin/bash
# Cadastra CDs (versao 5)
#
clear
LinhaMesg=$((`tput lines` – 3)) # Linha que msgs serão dadas para operador
TotCols=$(tput cols) # Qtd colunas da tela para enquadrar msgs
echo ”
Inclusao de Músicas
======== == =======
Título do Álbum:
| Este campo foi
Faixa: < criado somente para
| orientar o preenchimento
Nome da Música:
Intérprete:” # Tela montada com um único echo
while true
do
tput cup 5 38; tput el # Posiciona e limpa linha
read Album
[ ! “$Album” ] && # Operador deu <ENTER>
{
Msg=”Deseja Terminar? (S/n)”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1))
read -n1 SN
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
[ $SN = “N” -o $SN = “n” ] && continue # $SN é igual a N ou (-o) n?
clear; exit # Fim da execução
}
grep “^$Album^” musicas > /dev/null &&
{
Msg=”Este álbum já está cadastrado”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
read -n1
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
continue # Volta para ler outro álbum
}
Reg=”$Album^” # $Reg receberá os dados para gravação
oArtista= # Variavel que guarda artista anterior
while true
do
((Faixa++))
tput cup 7 38
echo $Faixa
tput cup 9 38 # Posiciona para ler musica
read Musica
[ “$Musica” ] || # Se o operador tiver dado …
{
Msg=”Fim de Álbum? (S/n)”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1)
read -n1 SN
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
[ “$SN” = N -o “$SN” = n ]&&continue # $SN é igual a N ou (-o) n?
break # Sai do loop para gravar
}
tput cup 11 38 # Posiciona para ler Artista
[ “$oArtista” ]&& echo -n “($oArtista) ” # Artista anterior é default
read Artista
[ “$Artista” ] && oArtista=”$Artista”
Reg=”$Reg$oArtista~$Musica:” # Montando registro
tput cup 9 38; tput el # Apaga Musica da tela
tput cup 11 38; tput el # Apaga Artista da tela
done
echo “$Reg” >> musicas # Grava registro no fim do arquivo
sort musicas -0 musicas # Classifica o arquivo
done

– É o programa tá legal, tá todo estruturadinho, mas gostaria de alguns poucos comentários sobre o que você fez:

  • Só para relembrar, as seguintes construções: [ ! $Album ] && e [ $Musica ] || representam a mesma coisa, isto é, no caso da primeira, o testamos se a variável $Album não (!) tem nada dentro, então (&&) … Na segunda, testamos se $Musica tem dado, senão (||) …
  • Se você reclamou do tamanho dele, é porque ainda não dei algumas dicas. Repare que a maior parte do script é para dar mensagens centradas na penúltima linha da tela. Repare ainda que algumas mensagens pedem um S ou um N e outras são só de advertência. Seria o caso típico do uso de funções, que seriam escritas somente uma vez e chamada a execução de diversos pontos do script. Vou montar duas funções para resolver estes casos e vamos incorporá-las ao seu programa para ver o resultado final.

Funções

– Chico! Agora traz dois chopes, sendo um sem colarinho, para me dar inspiração.

    Pergunta ()
        {
        #  A função recebe 3 parâmetros na seguinte ordem:
        #  $1 - Mensagem a ser dada na tela
        #  $2 - Valor a ser aceito com resposta default
        #  $3 - O outro valor aceito
        #  Supondo que $1=Aceita?, $2=s e $3=n, a linha a
        #  seguir colocaria em Msg o valor "Aceita? (S/n)"
        local Msg="$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)"
        local TamMsg=${#Msg}
        local Col=$(((TotCols - TamMsg) / 2))  # Centra msg na linha
        tput cup $LinhaMesg $Col
        echo "$Msg"
        tput cup $LinhaMesg $((Col + TamMsg + 1))
        read -n1 SN
        [ ! $SN ] && SN=$2                     # Se vazia coloca default em SN
        echo $SN | tr A-Z a-z                  # A saída de SN será em minúscula
        tput cup $LinhaMesg $Col; tput el      # Apaga msg da tela
        return                                 # Sai da função
        }

Como podemos ver, uma função é definida quando fazemos nome_da_função () e todo o seu corpo está entre chaves ({}). Assim como conversamos aqui no Boteco sobre passagem de parâmetros, as funções os recebem da mesma forma, isto é, são parâmetros posicionais ($1, $2, ..., $n) e todas as regras que se aplicam a passagem de parâmetros para programas, também valem para funções, mas é muito importante realçar que os parâmetros passados para um programa não se confundem com aqueles que este passou para suas funções. Isso significa, por exemplo, que o $1 de um script é diferente do $1 de uma de suas funções

Repare que as variáveis $Msg, $TamMsg e $Col são de uso restrito desta rotina, e por isso foram criadas como local. A finalidade disso é simplesmente para economizar memória, já que ao sair da rotina, elas serão devidamente detonadas da partição e caso não tivesse usado este artifício, permaneceriam residentes.

A linha de código que cria local Msg, concatena ao texto recebido ($1) um abre parênteses, a resposta default ($2) em caixa alta, uma barra, a outra resposta ($3) em caixa baixa e finaliza fechando o parênteses. Uso esta convenção para, ao mesmo tempo, mostrar as opções disponíveis e realçar a resposta oferecida como default.

Quase ao fim da rotina, a resposta recebida ($SN) é passada para caixa baixa de forma que no corpo do programa não se precise fazer este teste.

Veja agora como ficaria a função para dar uma mensagem na tela:

    function MandaMsg
        {
        # A função recebe somente um parâmetro
        # com a mensagem que se deseja exibir,
        # para não obrigar ao programador passar
        # a msq entre aspas, usaremos $* (todos
        # os parâmetro, lembra?) e não $1.
        local Msg="$*"
        local TamMsg=${#Msg}
        local Col=$(((TotCols - TamMsg) / 2)) # Centra msg na linha
        tput cup $LinhaMesg $Col
        echo "$Msg"
        read -n1
        tput cup $LinhaMesg $Col; tput el     # Apaga msg da tela
        return                                # Sai da função
        }

Esta é uma outra forma de definir uma função: não a chamamos como no exemplo anterior usando uma construção com a sintaxe nome_da_função (), mas sim como function nome_da_função. Quanto ao mais, nada difere da anterior, exceto que, como consta dos comentários, usamos a variável $* que como já sabemos é o conjunto de todos os parâmetros passados, para que o programador não precise usar aspas envolvendo a mensagem que deseja passar para a função.

Para terminar com este blá-blá-blá vamos ver então as alterações que o programa necessita quando usamos o conceito de funções:

$ cat musinc6
#!/bin/bash
# Cadastra CDs (versao 6)
#

# Área de variáveis globais
LinhaMesg=$((`tput lines` – 3)) # Linha que msgs serão dadas para operador
TotCols=$(tput cols) # Qtd colunas da tela para enquadrar msgs

# Área de funções
Pergunta ()
{
# A função recebe 3 parâmetros na seguinte ordem:
# $1 – Mensagem a ser dada na tela
# $2 – Valor a ser aceito com resposta default
# $3 – O outro valor aceito
# Supondo que $1=Aceita?, $2=s e $3=n, a linha
# abaixo colocaria em Msg o valor “Aceita? (S/n)”
local Msg=”$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)”
local TamMsg=${#Msg}
local Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1))
read -n1 SN
[ ! $SN ] && SN=$2 # Se vazia coloca default em SN
echo $SN | tr A-Z a-z # A saída de SN será em minúscula
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
return # Sai da função
}
function MandaMsg
{
# A função recebe somente um parâmetro
# com a mensagem que se deseja exibir,
# para não obrigar ao programador passar
# a msq entre aspas, usaremos $* (todos
# os parâmetro, lembra?) e não $1.
local Msg=”$*”
local TamMsg=${#Msg}
local Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
read -n1
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela
return # Sai da função
}

# O corpo do programa propriamente dito começa aqui
clear
echo ”
Inclusao de Músicas
======== == =======
Título do Álbum:
| Este campo foi
Faixa: < criado somente para
| orientar o preenchimento
Nome da Música:
Intérprete:” # Tela montada com um único echo
while true
do
tput cup 5 38; tput el # Posiciona e limpa linha
read Album
[ ! “$Album” ] && # Operador deu
{
Pergunta “Deseja Terminar” s n
[ $SN = “n” ] && continue # Agora só testo a caixa baixa
clear; exit # Fim da execução
}
grep -iq “^$Album^” musicas 2> /dev/null &&
{
MandaMsg Este álbum já está cadastrado
continue # Volta para ler outro álbum
}
Reg=”$Album^” # $Reg receberá os dados de gravação
oArtista= # Guardará artista anterior
while true
do
((Faixa++))
tput cup 7 38
echo $Faixa
tput cup 9 38 # Posiciona para ler musica
read Musica
[ “$Musica” ] || # Se o operador tiver dado …
{
Pergunta “Fim de Álbum?” s n
[ “$SN” = n ] && continue # Agora só testo a caixa baixa
break # Sai do loop para gravar dados
}
tput cup 11 38 # Posiciona para ler Artista
[ “$oArtista” ]&& echo -n “($oArtista) ” # Artista anterior é default
read Artista
[ “$Artista” ] && oArtista=”$Artista”
Reg=”$Reg$oArtista~$Musica:” # Montando registro
tput cup 9 38; tput el # Apaga Musica da tela
tput cup 11 38; tput el # Apaga Artista da tela
done
echo “$Reg” >> musicas # Grava registro no fim do arquivo
sort musicas -o musicas # Classifica o arquivo
done

Repare que a estruturação do script está conforme o gráfico a seguir:

Variáveis Globais
Funções
Corpo do Programa

Esta estruturação é devido ao Shell ser uma linguagem interpretada e desta forma o programa é lido da esquerda para a direita e de cima para baixo e uma variável para ser vista simultaneamente pelo script e suas funções deve ser declarada (ou inicializada) antes de qualquer coisa. As funções por sua vez devem ser declaradas antes do corpo do programa propriamente dito porque no ponto em que o programador mencionou seu nome, o interpretador Shell já o havia antes localizado e registrado que era uma função.

Uma coisa bacana no uso de funções é fazê-las o mais genérico possível de forma que elas sirvam para outras aplicações, sem necessidade de serem reescritas. Essas duas que acabamos de ver têm uso generalizado, pois é difícil um script que tenha uma entrada de dados pelo teclado que não use uma rotina do tipo da MandaMsg ou não interage com o operador por algo semelhante à Pergunta.

Conselho de amigo: crie um arquivo e cada função nova que você criar, anexe-a a este arquivo. Ao final de um tempo você terá uma bela biblioteca de funções que lhe poupará muito tempo de programação.

O comando source

Vê se você nota algo de diferente na saída do ls a seguir:

$ ls -la .bash_profile
-rw-r–r– 1 Julio unknown 4511 Mar 18 17:45 .bash_profile

Não olhe a resposta não, volte a prestar atenção! Bem, já que você está mesmo sem saco de pensar e prefere ler a resposta, vou te dar uma dica: acho que você sabe que o .bash_profile é um dos programas que são automaticamente “executados” quando você se loga (ARRGGHH! Odeio este termo). Agora que te dei esta dica olhe novamente para a saída do ls e me diga o que há de diferente nela.

Como eu disse o .bash_profile é “executado” em tempo de logon e repare que não tem nenhum direito de execução. Isso se dá porque o se você o executasse como qualquer outro script careta, quando terminasse sua execução todo o ambiente por ele gerado morreria junto com o Shell sob o qual ele foi executado (você se lembra que todos os scripts são executados em subshells, né?).

Pois é. É para coisas assim que existe o comando source, também conhecido por . (ponto). Este comando faz com que não seja criado um novo Shell (um subshell) para executar o programa que que lhe é passado como parâmetro.

Melhor um exemplo que 453 palavras. Veja este scriptizinho a seguir:

$ cat script_bobo
cd ..
ls

Ele simplesmente deveria ir para o diretório acima do diretório atual. Vamos executar uns comandos envolvendo o script_bobo e vamos analisar os resultados:

$ pwd
/home/jneves

$ script_bobo
jneves juliana paula silvie

$ pwd
/home/jneves

Se eu mandei ele subir um diretório, porque não subiu? Subiu sim! O subshell que foi criado para executar o script tanto subiu que listou os diretórios dos quatro usuários abaixo do /home, só que assim que o script acabou, o subshell foi para o beleleu e com ele todo o ambiente criado. Olha agora como a coisa muda:

$ source script_bobo
jneves juliana paula silvie

$ pwd
/home

$ cd –
/home/jneves

$ . script_bobo
jneves juliana paula silvie

$ pwd
/home

Ahh! Agora sim! Sendo passado como parâmetro do comando source ou . (ponto), o script foi executado no Shell corrente deixando neste, todo o ambiente criado. Agora damos um rewind para o início da explicação sobre este comando. Lá falamos do .bash_profile, e a esta altura você já deve saber que a sua incumbência é, logo após o login, deixar o ambiente de trabalho preparado para o usuário, e agora entendemos que é por isso mesmo que ele é executado usando este artifício.

E agora você deve estar se perguntando se é só para isso que este comando serve, e eu lhe digo que sim, mas isso nos traz um monte de vantagens e uma das mais usadas é tratar funções como rotinas externas. Veja uma outra forma de fazer o nosso programa para incluir CDs no arquivo musicas:

$ cat musinc7
#!/bin/bash
# Cadastra CDs (versao 7)
#

# Área de variáveis globais
LinhaMesg=$((`tput lines` – 3)) # Linha que msgs serão dadas para operador
TotCols=$(tput cols) # Qtd colunas da tela para enquadrar msgs

# O corpo do programa propriamente dito começa aqui
clear
echo ”
Inclusao de Músicas
======== == =======
Título do Álbum:
| Este campo foi
Faixa: < criado somente para
| orientar o preenchimento
Nome da Música:
Intérprete:” # Tela montada com um único echo
while true
do
tput cup 5 38; tput el # Posiciona e limpa linha
read Album
[ ! “$Album” ] && # Operador deu
{
source pergunta.func “Deseja Terminar” s n
[ $SN = “n” ] && continue # Agora só testo a caixa baixa
clear; exit # Fim da execução
}
grep -iq “^$Album^” musicas 2> /dev/null &&
{
. mandamsg.func Este álbum já está cadastrado
continue # Volta para ler outro álbum
}
Reg=”$Album^” # $Reg receberá os dados de gravação
oArtista= # Guardará artista anterior
while true
do
((Faixa++))
tput cup 7 38
echo $Faixa
tput cup 9 38 # Posiciona para ler musica
read Musica
[ “$Musica” ] || # Se o operador tiver dado …
{
. pergunta.func “Fim de Álbum?” s n
[ “$SN” = n ] && continue # Agora só testo a caixa baixa
break # Sai do loop para gravar dados
}
tput cup 11 38 # Posiciona para ler Artista
[ “$oArtista” ] && echo -n “($oArtista) ” # Artista anter. é default
read Artista
[ “$Artista” ] && oArtista=”$Artista”
Reg=”$Reg$oArtista~$Musica:” # Montando registro
tput cup 9 38; tput el # Apaga Musica da tela
tput cup 11 38; tput el # Apaga Artista da tela
done
echo “$Reg” >> musicas # Grava registro no fim do arquivo
sort musicas -o musicas # Classifica o arquivo
done

Agora o programa deu uma boa encolhida e as chamadas de função foram trocadas por arquivos externos chamados pergunta.func e mandamsg.func, que assim podem ser chamados por qualquer outro programa, desta forma reutilizando o seu código.

Por motivos meramente didáticos as execuções de pergunta.func e mandamsg.func estão sendo comandadas por source e por . (ponto) indiscriminadamente, embora prefira o source por ser mais visível desta forma dando maior legibilidade ao código e facilitando sua posterior manutenção.

Veja agora como ficaram estes dois arquivos:

$ cat pergunta.func
# A função recebe 3 parâmetros na seguinte ordem:
# $1 – Mensagem a ser dada na tela
# $2 – Valor a ser aceito com resposta default
# $3 – O outro valor aceito
# Supondo que $1=Aceita?, $2=s e $3=n, a linha
# abaixo colocaria em Msg o valor “Aceita? (S/n)”
Msg=”$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
tput cup $LinhaMesg $((Col + TamMsg + 1))
read -n1 SN
[ ! $SN ] && SN=$2 # Se vazia coloca default em SN
echo $SN | tr A-Z a-z # A saída de SN será em minúscula
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela

$ cat mandamsg.func
# A função recebe somente um parâmetro
# com a mensagem que se deseja exibir,
# para não obrigar ao programador passar
# a msq entre aspas, usaremos $* (todos
# os parâmetro, lembra?) e não $1.
Msg=”$*”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Centra msg na linha
tput cup $LinhaMesg $Col
echo “$Msg”
read -n1
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela

Em ambos os arquivos, fiz somente duas mudanças que veremos nas observações a seguir, porém tenho mais três a fazer:

  1. As variáveis não estão sendo mais declaradas como local, porque está é uma diretiva que só pode ser usada no corpo de funções e portanto estas variáveis permanecem no ambiente do Shell, poluindo-o;
  2. O comando return não está mais presente mas poderia estar sem alterar em nada a lógica, uma vez que ele só serviria para indicar um eventual erro via um código de retorno previamente estabelecido (por exemplo return 1, return 2, …), sendo que o return e return 0 são idênticos e significam rotina executada sem erros;
  3. O comando que estamos acostumados a usar para gerar código de retorno é o exit, mas a saída de uma rotina externa não pode ser feita desta forma, porque por estar sendo executada no mesmo Shell que o script chamador, o exit simplesmente encerraria este Shell, terminando a execução de todo o script;
  4. De onde surgiu a variável LinhaMesg? Ela veio do musinc7, porque ela havia sido declarada antes da chamada das rotinas (nunca esquecendo que o Shell que está interpretando o script e estas rotinas é o mesmo);
  5. Se você decidir usar rotinas externas, não se avexe, abunde os comentários (principalmente sobre a passagem dos parâmetros) para facilitar a manutenção e seu uso por outros programas no futuro.

– Bem, agora você já tem mais um monte de novidade para melhorar os scripts que fizemos você se lembra do programa listartista no qual você passava o nome de um artista como parâmetro e ele devolvia as suas músicas? Ele era assim:

$ cat listartista
#!/bin/bash
# Dado um artista, mostra as suas musicas
# versao 2

if [ $# -eq 0 ]
then
echo Voce deveria ter passado pelo menos um parametro
exit 1
fi

IFS=”
:”
for ArtMus in $(cut -f2 -d^ musicas)
do
echo “$ArtMus” | grep -i “^$*~” > /dev/null && echo $ArtMus | cut -f2 -d~
done

– Claro que me lembro!…

– Então para firmar os conceitos que te passei, faça ele com a tela formatada, em loop, de forma que ele só termine quando receber um <ENTER> puro no nome do artista. Ahhh! Quando a listagem atingir a antepenúltima linha da tela, o programa deverá dar uma parada para que o operador possa lê-las, isto é, suponha que a tela tenha 25 linhas. A cada 22 músicas listadas (quantidade de linhas menos 3) o programa aguardará que o operador tecle algo para então prosseguir. Eventuais mensagens de erro devem ser passadas usando a rotina mandamsg.func que acabamos de desenvolver.

– Chico, manda mais dois, o meu é com pouca pressão…

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 »


– E aê amigo, te dei a maior moleza, né? Um exerciciozinho muito simples…- É mais nos testes que eu fiz, e de acordo com o que você ensinou sobre substituição de parâmetros, achei que deveria fazer outras alterações nas funções que desenvolvemos para torná-las de uso geral como você me disse que todas as funções deveriam ser, quer ver?- Claro né mané, se te pedi para fazer é porque estou afim de te ver aprender, mas peraí, dá um tempo!- Chico! Manda dois, um sem colarinho!- Vai, mostra aí o que você fez.- Bem, além do que você pediu, eu reparei que o programa que chamava a função, teria de ter previamente definidas a linha em que seria dada a mensagem e a quantidade de colunas. O que fiz foi incluir duas linhas – nas quais empreguei substituição de parâmetros – que caso uma destas variáveis não fosse informada, a própria função atribuiria. A linha de mensagem seria três linhas acima do fim da tela e o total de colunas seria obtido pelo comando tput cols. Veja como ficou:

$ cat pergunta.func
# A funcao recebe 3 parametros na seguinte ordem:
# $1 – Mensagem a ser dada na tela
# $2 – Valor a ser aceito com resposta default
# $3 – O outro valor aceito
# Supondo que $1=Aceita?, $2=s e $3=n, a linha
# abaixo colocaria em Msg o valor “Aceita? (S/n)”
TotCols=${TotCols:-$(tput cols)} # Se nao estava definido, agora esta
LinhaMesg=${LinhaMesg:-$(($(tput lines)-3))} # Idem
Msg=”$1 (`echo $2 | tr a-z A-Z`/`echo $3 | tr A-Z a-z`)”
TamMsg=${#Msg}
Col=$(((TotCols – TamMsg) / 2)) # Para centrar Msg na linha
tput cup $LinhaMesg $Col
read -n1 -p “$Msg ” SN
SN=${SN:-$2} # Se vazia coloca default em SN
SN=$(echo $SN | tr A-Z a-z) # A saida de SN serah em minuscula
tput cup $LinhaMesg $Col; tput el # Apaga msg da tela

– Gostei, você já se antecipou ao que eu ia pedir. Só pra gente encerrar este papo de substituição de parâmetros, repare que a legibilidade está horrorível, mas a performance, isto é, velocidade de execução, está ótima. Como funções são coisas muito pessoais, já que cada um usa as suas, e quase não dão manutenção, eu sempre opto pela performance.

– Hoje vamos sair daquela chatura que foi o nosso último papo e vamos voltar à lógica saindo da decoreba, mas volto a te lembrar, tudo que eu te mostrei da outra vez aqui no Boteco do Chico é válido e quebra um galhão, guarde aqueles guardanapos que rabiscamos que, mais cedo ou mais tarde, vão te ser muito úteis.

O comando eval

– Vou te dar um problema que eu duvido que você resolva:

$ var1=3
$ var2=var1

– Te dei estas duas variáveis, e quero que você me diga como eu posso, só me referindo a $var2, listar o valor de $var1 (3).

– A isso é mole, é só fazer:

    echo $`echo $var2`

– Repare que eu coloquei o echo $var2 entre crases (`), que desta forma terá prioridade de execução e resultará em var1, montando echo$var1 que produzirá 3

– A é? Então execute para ver se está correto.

$ echo $`echo $var2`
$var1

– Ué! Que foi que houve? O meu raciocínio me parecia bastante lógico…

– O seu raciocínio realmente foi lógico, o problema é que você esqueceu de uma das primeiras coisas que te falei aqui no Boteco e vou repetir. O Shell usa a seguinte ordem para resolver uma linha de comandos:

  • Resolve os redirecionamentos;
  • Substitui as variáveis pelos seus valores;
  • Resolve e substitui os meta caracteres;
  • Passa a linha já toda esmiuçada para execução.

Desta forma, quando chegou na fase de resolução de variáveis, que como eu disse é anterior à execução, a única variável existente era $var2 e por isso a tua solução produziu como saída $var1. O comando echo identificou isso como uma cadeia e não como uma variável.

Problemas deste tipo são relativamente freqüentes e seriam insolúveis caso não existisse a instrução eval, cuja sintaxe é:

    eval cmd

Onde cmd é uma linha de comando qualquer que você poderia inclusive executar direto no prompt do terminal. Quando você põe o eval na frente, no entanto, o que ocorre é que o Shell trata cmd como se seus dados fossem parâmetros do eval e em seguida o eval executa a linha recebida, submetendo-a ao Shell, dando então na prática duas passadas em cmd.

Desta forma se executássemos o comando que você propôs colocando o eval à sua frente, teríamos a saída esperada, veja:

$ eval echo $`echo $var2`
3

Este exemplo também poderia ter sido feito da seguinte maneira:

$ eval echo $$var2
3

Na primeira passada a contrabarra () seria retirada e $var2 seria resolvido produzindo var1, para a segunda passada teria sobrado echo $var1, que produziria o resultado esperado.

Agora vou colocar um comando dentro de var2:

$ var2=ls

Vou executar:

$ $var2
10porpag1.sh alo2.sh listamusica logaute.sh
10porpag2.sh confuso listartista mandamsg.func
10porpag3.sh contpal.sh listartista3 monbg.sh
alo1.sh incusu logado

Agora vamos colocar em var2 o seguinte: ls $var1; e em var1 vamos colocar l*, vejamos:

$ var2=’ls $var1′
$ var1=’l*’
$ $var2
ls: $var1: No such file or directory

$ eval $var2
listamusica listartista listartista3 logado logaute.sh

Novamente, no tempo de substituição das variáveis, $var1 ainda não havia se apresentado ao Shell para ser resolvida, desta forma só nos resta executar o comando eval para dar as duas passadas necessárias.

Uma vez um colega de uma excelente lista sobre Shell Script, colocou uma dúvida: queria fazer um menu que numerasse e listasse todos os arquivos com extensão .sh e quando o operador escolhesse uma opção, o programa correspondente seria executado. A minha proposta foi a seguinte:

$ cat fazmenu
#!/bin/bash
#
# Lista numerando os programas com extensão .sh no
# diretório corrente e executa o escolhido pelo operador
#
clear; i=1
printf “%11st%snn” Opção Programa
CASE=’case $opt in’
for arq in *.sh
do
printf “t%03dt%sn” $i $arq
CASE=”$CASE
“$(printf “%03d)t %s;;” $i $arq)
i=$((i+1))
done
CASE=”$CASE
*) . erro;;
esac”
read -n3 -p “Informe a opção desejada: ” opt
echo
eval “$CASE”

Parece complicado porque usei muito printf para formatação da tela, mas é bastante simples, vamos entendê-lo: o primeiro printf foi colocado para fazer o cabeçalho e logo em seguida comecei a montar dinamicamente a variável $CASE, na qual ao final será feito um eval para execução do programa escolhido. Repare no entanto que dentro do loop do for existem dois printf: o primeiro serve para formatar a tela e o segundo para montar o case (se antes do comando read você colocar uma linha echo "$CASE", verá que o comando case montado dentro da variável está todo indentado. Frescura, né? :). Na saída do for, foi adicionada uma linha à variável $CASE, para no caso de se fazer uma opção inválida, ser executada uma função externa para dar mensagens de erro.

Vamos executá-lo para ver a saída gerada:

$ fazmenu.sh
Opcao Programa

001 10porpag1.sh
002 10porpag2.sh
003 10porpag3.sh
004 alo1.sh
005 alo2.sh
006 contpal.sh
007 fazmenu.sh
008 logaute.sh
009 monbg.sh
010 readpipe.sh
011 redirread.sh
Informe a opção desejada:

Neste programa seria interessante darmos uma opção de término, e para isso seria necessário a inclusão de uma linha após o loop de montagem da tela e alterarmos a linha na qual fazemos a atribuição final do valor da variável $CASE. Vejamos como ele ficaria:

$ cat fazmenu
#!/bin/bash
#
# Lista numerando os programas com extensão .sh no
# diretório corrente e executa o escolhido pelo operador
#
clear; i=1
printf “%11st%snn” Opção Programa
CASE=’case $opt in’
for arq in *.sh
do
printf “t%03dt%sn” $i $arq
CASE=”$CASE
“$(printf “%03d)t %s;;” $i $arq)
i=$((i+1))
done
printf “t%dt%snn” 999 “Fim do programa” # Linha incluida
CASE=”$CASE
999) exit;; # Linha alterada
*) ./erro;;
esac”
read -n3 -p “Informe a opção desejada: ” opt
echo
eval “$CASE”

Sinais de Processos

Existe no Linux uma coisa chamada sinal (signal). Existem diversos sinais que podem ser mandados para (ou gerados por) processos em execução. Vamos de agora em diante dar uma olhadinha nos sinais mandados para os processos e mais à frente vamos dar uma passada rápida pelos sinais gerados por processos.

Sinais assassinos

Para mandar um sinal a um processo, usamos normalmente o comando kill, cuja sintaxe é:

    kill -sig PID

Onde PID é o identificador do processo (Process IDentification ou Process ID). Além do comando kill, algumas seqüências de teclas também podem gerar sig. A tabela a seguir mostra os sinais mais importantes para monitorarmos:

Sinais Mais Importantes
Sinal Gerado por:
0 EXIT Fim normal do programa
1 SIGHUP Quando recebe um kill -HUP
2 SIGINT Interrupção pelo teclado (<CTRL+C>)
3 SIGQUIT Interrupção pelo teclado (<CTRL+>)
15 SIGTERM Quando recebe um kill ou kill -TERM

Além destes sinais, existe o famigerado -9 ou SIGKILL que, para o processo que o está recebendo, equivale a meter o dedo no botão de desliga do computador o que seria altamente indesejável já que muitos programas necessitam “limpar o meio de campo” ao seu término. Se o seu final ocorrer de forma prevista, ou seja se tiver um término normal, é muito fácil de fazer esta limpeza, porém se o seu programa tiver um fim brusco muita coisa pode ocorrer:

  • É possível que em um determinado espaço de tempo, o seu computador esteja cheio de arquivos de trabalho inúteis
  • Seu processador poderá ficar atolado de processos zombies e defuncts gerados por processos filhos que perderam os pais;
  • É necessário liberar sockets abertos para não deixar os clientes congelados;
  • Seus bancos de dados poderão ficar corrompidos porque sistemas gerenciadores de bancos de dados necessitam de um tempo para gravar seus buffers em disco (commit).

Enfim, existem mil razões para não usar um kill com o sinal -9 e para monitorar fins anormais de programas.

O trap não atrapalha

Para fazer a monitoração descrita acima existe o comando trap cuja sintaxe é:

    trap "cmd1; cmd2; cmdn" S1 S2 ... SN

ou

    trap 'cmd1; cmd2; cmdn' S1 S2 ... SN

Onde os comandos cmd1, cmd2, cmdn serão executados caso o programa receba os sinais S1 S2SN.

As aspas (“) ou os apóstrofos (‘) só são necessários caso o trap possua mais de um comando cmd associado. Cada um dos cmd pode ser também uma função interna, uma externa ou outro script.

Para entender o uso de aspas (“) e apóstrofos (‘) vamos recorrer a um exemplo que trata um fragmento de um script que faz um ftp para uma máquina remota ($RemoComp), na qual o usuário é $Fulano, sua senha é $Segredo e vai transmitir o arquivo contido em $Arq. Suponha ainda que estas quatro variáveis foram recebidas em uma rotina anterior de leitura e que este script é muito usado por diversas pessoas da instalação. Vejamos este trecho de código:

ftp -ivn $RemoComp << FimFTP >> /tmp/$$ 2>> /tmp/$$
    user $Fulano $Segredo
    binary
    get $Arq
FimFTP

Repare que, tanto as saídas do dos diálogos do ftp, como os erros encontrados, estão sendo redirecionados para /tmp/$$, o que é uma construção bastante normal para arquivos temporários usados em scripts com mais de um usuário, porque $$ é a variável que contém o número do processo (PID), que é único, e com este tipo de construção evita-se que dois ou mais usuários disputem a posse e os direitos sobre o arquivo.

Caso este ftp seja interrompido por um kill ou um <CTRL+C>, certamente deixará lixo no disco. É exatamente esta a forma como mais se usa o comando trap. Como isto é trecho de um script, devemos, logo no seu início, como um de seus primeiros comandos, fazer:

    trap "rm -f /tmp/$$ ; exit" 0 1 2 3 15

Desta forma, caso houvesse uma interrupção brusca (sinais 1, 2, 3 ou 15) antes do programa encerrar (no exit dentro do comando trap), ou um fim normal (sinal 0), o arquivo /tmp/$$ seria removido.

Caso na linha de comandos do trap não houvesse a instrução exit, ao final da execução desta linha o fluxo do programa retornaria ao ponto em que estava quando recebeu o sinal que originou a execução deste trap.

Este trap poderia ser subdividido, ficando da seguinte forma:

    trap "rm -f /tmp/$$" 0
    trap "exit" 1 2 3 15

Assim ao receber um dos sinais o programa terminaria, e ao terminar, geraria um sinal 0, que removeria o arquivo. Caso seu fim seja normal, o sinal também será gerado e o rm será executado.

Note também que o Shell pesquisa a linha de comandos uma vez quanto o trap é interpretado (e é por isso que é usual colocá-lo no início do programa) e novamente quando um dos sinais listados é recebido. Então, no último exemplo, o valor de $$ será substituído no momento que o comando trap foi lido da primeira vez, já que as aspas (") não protegem o cifrão ($) da interpretação do Shell.

Se você desejasse que a substituição fosse realizada somente quando recebesse o sinal, o comando deveria ser colocado entre apóstrofos ('). Assim, na primeira interpretação do trap, o Shell não veria o cifrão ($), porém os apóstrofos (') seriam removidos e finalmente o Shell poderia substituir o valor da variável. Neste caso, a linha ficaria da seguinte maneira:

    trap 'rm -f /tmp/$$ ; exit' 0 1 2 3 15

Suponha dois casos: você tem dois scripts que chamaremos de script1, cuja primeira linha será um trap e script2, sendo este último colocado em execução pelo primeiro, e por serem dois processos, terão dois PID distintos.

  • 1º Caso: O ftp encontra-se em script1
    Neste caso, o argumento do comando trap deveria vir entre aspas (") porque caso ocorresse uma interrupção (<CTRL+C> ou <CTRL+>) no script2, a linha só seria interpretada neste momento e o PID do script2 seria diferente do encontrado em /tmp/$$ (não esqueça que $$ é a variável que contém o PID do processo ativo);
  • 2º Caso: O ftp acima encontra-se em script2
    Neste caso, o argumento do comando trap deveria estar entre apóstrofos ('), pois caso a interrupção se desse durante a execução de script1, o arquivo não teria sido criado, caso ocorresse durante a execução de script2, o valor de $$ seria o PID deste processo, que coincidiria com o de /tmp/$$.

O comando trap, quando executado sem argumentos, lista os sinais que estão sendo monitorados no ambiente, bem como a linha de comando que será executada quando tais sinais forem recebidos.

Se a linha de comandos do trap for nula (vazia), isto significa que os sinais especificados devem ser ignorados quando recebidos. Por exemplo, o comando:

    trap "" 2

Especifica que o sinal de interrupção (<CTRL+C>) deve ser ignorado. No caso citado, quando não se deseja que sua execução seja interrompida. No último exemplo note que o primeiro argumento deve ser especificado para que o sinal seja ignorado, e não é equivalente a escrever o seguinte, cuja finalidade é retornar o sinal 2 ao seu estado padrão (default):

    trap 2

Se você ignora um sinal, todos os Subshells irão ignorar este sinal. Portanto, se você especifica qual ação deve ser tomada quando receber um sinal, então todos os Subshells irão também tomar a ação quando receberem este sinal, ou seja, os sinais são automaticamente exportados. Para o sinal que temos mostrado (sinal 2), isto significa que os Subshells serão encerrados.

Suponha que você execute o comando:

    trap "" 2

e então execute um Subshell, que tornará a executar outro script como um Subshell. Se for gerado um sinal de interrupção, este não terá efeito nem sobre o Shell principal nem sobre os Subshell por ele chamados, já que todos eles ignorarão o sinal.

Outra forma de restaurar um sinal ao seu default é fazendo:

    trap - sinal

Em korn shell (ksh) não existe a opção -s do comando read para ler uma senha. O que costumamos fazer é usar o comando stty com a opção -echo que inibe a escrita na tela até que se encontre um stty echo para restaurar esta escrita. Então, se estivéssemos usando o interpretador ksh, a leitura da senha teria que ser feita da seguinte forma:

    echo -n "Senha: "
    stty -echo
    read Senha
    stty echo

O problema neste tipo de construção é que caso o operador não soubesse a senha, ele provavelmente daria um <CTRL+C> ou um <CTRL+> durante a instrução read para descontinuar o programa e, caso ele agisse desta forma, o que quer que ele escrevesse, não apareceria na tela do seu terminal. Para evitar que isso aconteça, o melhor a fazer é:

    echo -n "Senha: "
    trap "stty echo
          exit" 2 3
    stty -echo
    read Senha
    stty echo
    trap 2 3

Para terminar este assunto, abra uma console gráfica e escreva no prompt de comando o seguinte:

$ trap “echo Mudou o tamanho da janela” 28

Em seguida, pegue o mouse (arghh!!) e arraste-o de forma a variar o tamanho da janela corrente. Surpreso? É o Shell orientado a eventos… smile

Mais unzinho porque não pude resistir. Agora escreva assim:

$ trap “echo já era” 17

Em seguida faça:

$ sleep 3 &

Você acabou de criar um subshell que irá dormir durante três segundos em background. Ao fim deste tempo, você receberá a mensagem já era, porque o sinal 17 é emitido a cada vez que um subshell termina a sua execução.

Para devolver estes sinais aos seus defaults, faça:

$ trap 17 28

Ou

$ trap – 17 28

Acabamos de ver mais dois sinais que não são tão importante como os que vimos anteriormente, mas vou registrá-los na tabela a seguir:

Sinais Não Muito Importantes
Sinal Gerado por:
17 SIGCHLD Fim de um processo filho
28 SIGWINCH Mudança no tamanho da janela gráfica

Muito legal este comando, né? Se você descobrir algum caso bacana de uso de sinais, por favor me informe por e-mail porque é muito rara a literatura sobre o assunto.

Comando getopts

O comando getopts recupera as opções e seus argumentos de uma lista de parâmetros de acordo com a sintaxe POSIX.2, isto é, letras (ou números) após um sinal de menos (-) seguidas ou não de um argumento; no caso de somente letras (ou números) elas podem ser agrupadas. Você deve usar este comando para “fatiar” opções e argumento passados para o seu script.

Sintaxe:

    getopts cadeiadeopcoes nome

A cadeiadeopcoes deve explicitar uma cadeia de caracteres com todas as opções reconhecidas pelo script, assim se ele reconhece as opções -a -b e -c, cadeiadeopcoes deve ser abc. Se você deseja que uma opção seja seguida por um argumento, ponha dois-pontos (:) depois da letra, como em a:bc. Isto diz ao getopts que a opção -a tem a forma:

    -a argumento

Normalmente um ou mais espaços em branco separam o parâmetro da opção; no entanto, getopts também manipula parâmetros que vêm colados à opção como em:

    -aargumento

cadeiadeopcoes não pode conter interrogação (?).

O nome constante da linha de sintaxe acima, define uma variável que cada vez que o comando getopts for executado, receberá a próxima opção dos parâmetros posicionais e a colocará na variável nome.

getopts coloca uma interrogação (?) na variável definida em nome se achar uma opção não definida em cadeiadeopcoes ou se não achar o argumento esperado para uma determinada opção.

Como já sabemos, cada opção passada por uma linha de comandos tem um índice numérico, assim, a primeira opção estará contida em $1, a segunda em $2, e assim por diante. Quando o getopts obtém uma opção, ele armazena o índice do próximo parâmetro a ser processado na variável OPTIND.

Quando uma opção tem um argumento associado (indicado pelo : na cadeiadeopcoes), getopts armazena o argumento na variável OPTARG. Se uma opção não possui argumento ou o argumento esperado não foi encontrado, a variável OPTARG será “matada” (unset).

O comando encerra sua execução quando:

  • Encontra um parâmetro que não começa por menos (-);
  • O parâmetro especial -- marca o fim das opções;
  • Quando encontra um erro (por exemplo, uma opção não reconhecida).

O exemplo abaixo é meramente didático, servindo para mostrar, em um pequeno fragmento de código o uso pleno do comando.

$ cat getoptst.sh
#!/bin/sh

# Execute assim:
#
# getoptst.sh -h -Pimpressora arq1 arq2
#
# e note que as informacoes de todas as opcoes sao exibidas
#
# A cadeia ‘P:h’ diz que a opcao -P eh uma opcao complexa
# e requer um argumento, e que h eh uma opcao simples que nao requer
# argumentos.

while getopts ‘P:h’ OPT_LETRA
do
echo “getopts fez a variavel OPT_LETRA igual a ‘$OPT_LETRA'”
echo ” OPTARG eh ‘$OPTARG'”
done
used_up=`expr $OPTIND – 1`
echo “Dispensando os primeiros $OPTIND-1 = $used_up argumentos”
shift $used_up
echo “O que sobrou da linha de comandos foi ‘$*'”

Para entendê-lo melhor, vamos executá-lo como está sugerido em seu cabeçalho:

$ getoptst.sh -h -Pimpressora arq1 arq2
getopts fez a variavel OPT_LETRA igual a ‘h’
OPTARG eh ”
getopts fez a variavel OPT_LETRA igual a ‘P’
OPTARG eh ‘impressora’
Dispensando os primeiros $OPTIND-1 = 2 argumentos
O que sobrou da linha de comandos foi ‘arq1 arq2’

Desta forma, sem ter muito trabalho, separei todas as opções com seus respectivos argumentos, deixando somente os parâmetros que foram passados pelo operador para posterior tratamento.

Repare que se tivéssemos escrito a linha de comando com o argumento (impressora) separado da opção (-P), o resultado seria exatamente o mesmo, exceto pelo $OPTIND, já que neste caso ele identifica um conjunto de três opções/argumentos e no anterior somente dois. Veja só:

$ getoptst.sh -h -P impressora arq1 arq2
getopts fez a variavel OPT_LETRA igual a ‘h’
OPTARG eh ”
getopts fez a variavel OPT_LETRA igual a ‘P’
OPTARG eh ‘impressora’
Dispensando os primeiros $OPTIND-1 = 3 argumentos
O que sobrou da linha de comandos foi ‘arq1 arq2’

Repare, no exemplo a seguir, que se passarmos uma opção inválida, a variável $OPT_LETRA receberá um ponto-de-interrogação (?) e a $OPTARG será “apagada” (unset).

$ getoptst.sh -f -Pimpressora arq1 arq2 # A opção �f não é valida
./getoptst.sh: illegal option — f
getopts fez a variavel OPT_LETRA igual a ‘?’
OPTARG eh ”
getopts fez a variavel OPT_LETRA igual a ‘P’
OPTARG eh ‘impressora’
Dispensando os primeiros $OPTIND-1 = 2 argumentos
O que sobrou da linha de comandos foi ‘arq1 arq2’

– Me diz uma coisa: você não poderia ter usado um case para evitar o getopts?

– Poderia sim, mas para que? Os comandos estão aí para serem usados… O exemplo dado foi didático, mas imagine um programa que aceitasse muitas opções e seus parâmetros poderiam ou não estar colados às opções, suas opções também poderiam ou não estar coladas, ia ser um case infernal e com getopts é só seguir os passos acima.

– É… Vendo desta forma acho que você tem razão. É porque eu já estou meio cansado com tanta informação nova na minha cabeça. Vamos tomar a saideira ou você ainda quer explicar alguma particularidade do Shell?

– Nem um nem outro, eu também já cansei mas hoje não vou tomar a saideira porque estou indo dar aula na UniRIO, que é a primeira universidade federal que está preparando no uso de Software Livre, seus alunos do curso de graduação em informática.

Mas antes vou te deixar um problema para te encucar: quando você varia o tamanho de uma tela, no seu centro não aparece dinamicamente em vídeo reverso a quantidade de linhas e colunas? Então! Eu quero que você reproduza isso usando a linguagem Shell.

– Chico, traz rapidinho a minha conta! Vou contar até um e se você não trouxer eu me mando!

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 »