Papo de botequim parte VIII
- 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:
| Corpo do Programa |
| Variáveis Globais |
| Funções |
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:
- 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;
- 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;
- 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;
- 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);
- 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!

Copyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki-SL?
Send feedback