Programação Shell Linux

Aqui temos um livro livre e completo sobre Shell

Os sedentos do "saber livre" são muito benvindos.

Comandos Shell Script

Diagrama Sintático
Júlio Neves
Júlio Neves
Home | Artigos Español EnglishAgradecimentos | Links Amigos

Comprar o livro

Changelogs

  Papo de Botequim - Parte 1  

  Papo de Botequim - Parte 2  

  Papo de Botequim - Parte 3  

  Papo de Botequim - Parte 4  

  Papo de Botequim - Parte 5  

  Papo de Botequim - Parte 6  

  Papo de Botequim - Parte 7  

  Papo de Botequim - Parte 8  

  Papo de Botequim - Parte 9  

  Papo de Botequim - Parte 10  

  Papo de Botequim - Parte 11  

  Tira Gosto  

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:

  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!

Licença Creative Commons - Atribuição e Não Comercial (CC) 2009 Pelos Frequentadores do Bar do Júlio Neves.
Todo o conteúdo desta página pode ser utilizado segundo os termos da Creative Commons License: Atribuição-UsoNãoComercial-PermanênciaDaLicença.