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 IX



- Tá bom, já sei que você vai querer chope antes de começar, mas tô tão afim de te mostrar o que fiz que já vou até pedindo a rodada e em seguida vou te mostrar.

- Aê Chico, manda dois. O dele é sem colarinho pra não deixar cheiro ruim neste bigodão...

- Enquanto o chope não chega deixa eu te relembrar que você me pediu para refazer o listartista com a tela formatada, em loop, de forma que ele só termine quando receber um <ENTER> puro no nome do artista. Eventuais mensagens de erros e perguntas deveriam ser dadas na antepenúltima linha da tela utilizando as rotina mandamsg.func e pergunta.func que acabamos de desenvolver.

- Primeiramente eu dei uma encolhida no mandamsg.func e no pergunta.func, que ficaram assim:

$ 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 msg 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 read -n1 -p "$Msg "
$ 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 read -n1 -p "$Msg " SN [ ! $SN ] && SN=$2 # Se vazia coloca default em SN 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

- E agora aí vai o grandão:

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

LinhaMesg=$((`tput lines` - 3)) # Linha que msgs serão dadas para operador TotCols=$(tput cols) # Qtd colunas da tela para enquadrar msgs

clear echo " +----------------------------------------------------+  | Lista Todas as Músicas de um Determinado Artista |  | ----- ----- -- ------- -- -- ----------- ------- |  | |  | Informe o Artista: | +----------------------------------------------------+" while true do tput cup 5 51; tput ech 31 # ech=Erase chars (31 caracteres para não apagar barra vertical) read Nome if [ ! "$Nome" ] # $Nome estah vazio? then . pergunta.func "Deseja Sair?" s n [ $SN = n ] && continue break fi

fgrep -iq "^$Nome~" musicas || # fgrep não interpreta ^ como expressão regular { . mandamsg.func "Não existe música deste artista" continue }

tput cup 7 29; echo '| |' LinAtual=8 IFS=" :" for ArtMus in $(cut -f2 -d^ musicas) # Exclui nome do album do if echo "$ArtMus" | grep -iq "^$Nome~" then tput cup $LinAtual 29 echo -n '| ' echo $ArtMus | cut -f2 -d~ tput cup $LinAtual 82 echo '|' let LinAtual++ if [ $LinAtual -eq $LinhaMesg ] then . mandamsg.func "Tecle Algo para Continuar..." tput cup 7 0; tput ed # Apaga a tela a partir da linha 7 tput cup 7 29; echo '| |' LinAtual=8 fi fi done tput cup $LinAtual 29; echo '| |' tput cup $((++LinAtual)) 29 read -n1 -p "+-----------Tecle Algo para Nova Consulta------------+" tput cup 7 0; tput ed # Apaga a tela a partir da linha 7 done

- Poxa, você chegou com a corda toda! Mas gostei da forma que você resolveu o problema e estruturou o programa. Foi mais trabalhoso mas a apresentação ficou legal e você explorou bastante as opções do tput. Vamos testar o resultado com um álbum do Emerson, Lake & Palmer que tenho cadastrado:

              +----------------------------------------------------+
              |  Lista Todas as Músicas de um Determinado Artista  |
              |  ----- ----- -- ------- -- -- ----------- -------  |
              |                                                    |
              |  Informe o Artista: Emerson, Lake & Palmer         |
              +----------------------------------------------------+
              |                                                    |
              |  Jerusalem                                         |
              |  Toccata                                           |
              |  Still ... You Turn Me On                          |
              |  Benny The Bouncer                                 |
              |  Karn Evil 9                                       |
              |                                                    |
              +-----------Tecle Algo para Nova Consulta------------+

Envenenando a escrita

- Ufa! Agora você já sabe tudo sobre leitura, mas sobre escrita está apenas engatinhando. Já sei que você vai me perguntar:

- Ora, não é com o comando echo e com os redirecionamentos de saída que se escreve?

É, com estes comandos você escreve 90% das coisas necessárias, porém se precisar de escrever algo formatado eles lhe darão muito trabalho. Para formatar a saída veremos agora uma instrução muito interessante - é o printf - sua sintaxe é a seguinte:

    printf formato [argumento...]

Onde:
formato - é uma cadeia de caracteres que contem 3 tipos de objeto:

  1. caracteres simples;
  2. caracteres para especificação de formato;
  3. seqüência de escape no padrão da linguagem C.
Argumento - é a cadeia a ser impressa sob o controle do formato.

Cada um dos caracteres utilizados para especificação de formato é precedido pelo caracter % e logo a seguir vem a especificação de formato de acordo com a tabela:

Tabela dos Caracteres de Formatação do printf
%  Imprime um %. Não existe nenhuma conversão  
  Letra     A expressão será impressa como:
c  Simples caractere  
d  Número no sistema decimal  
e  Notação científica exponencial  
f  Número com ponto decimal (float)  
g O menor entre os formatos %e e %f com supressão dos zeros não significativos
o  Número no sistema octal  
s  Cadeia de caracteres  
x  Número no sistema hexadecimal  

As seqüências de escape padrão da linguagem C são sempre precedidas por um contra-barra (\) e as reconhecidas pelo comando printf são:

Sequencias de Escape do printf
t   Avança para a próxima marca de tabulação  
  Seqüência     Efeito  
a   Soa o beep  
b   Volta uma posição (backspace)  
f   Salta para a próxima página lógica (form feed)  
n   Salta para o início da linha seguinte (line feed)  
r   Volta para o início da linha corrente (carriage return)  

Não acabou por aí não! Tem muito mais coisa sobre a instrução, mas como é muito cheio de detalhes e, portanto, chato para explicar e, pior ainda para ler ou estudar, vamos passar direto aos exemplos com seus comentários, que não estou aqui para encher o saco de ninguém.

$ printf "%c" "1 caracter" 1$ Errado! Só listou 1 caractere e não saltou linha ao final $ printf "%c\n" "1 caracter" 1 Saltou linha mas ainda não listou a cadeia inteira $ printf "%c caractere\n" 1 1 caractere Esta é a forma correta o %c recebeu o 1 $ a=2 $ printf "%c caracteres\n" $a 2 caracteres O %c recebeu o valor da variável $a $ printf "%10c caracteres\n" $a          2 caracteres $ printf "%10c\n" $a caracteres          2 c

Repare que nos dois últimos exemplos, em virtude do %c, só foi listado um caracter de cada cadeia. O 10 à frente do c, não significa 10 caracteres. Um número seguindo o sinal de percentagem (%) significa o tamanho que a cadeia terá após a execução do comando.

E tome de exemplo:

$ printf "%d\n" 32 32 $ printf "%10d\n" 32 32 Preenche com brancos à esquerda e não com zeros $ printf "%04d\n" 32 0032 04 após % significa 4 dígitos com zeros à esquerda $ printf "%e\n" $(echo "scale=2 ; 100/6" | bc) 1.666000e+01 O default do %e é 6 decimais $ printf "%.2e\n" `echo "scale=2 ; 100/6" | bc` 1.67e+01 O .2 especificou duas decimais $ printf "%f\n" 32.3 32.300000 O default do %f é 6 decimais $ printf "%.2f\n" 32.3 32.30 O .2 especificou duas decimais $ printf "%.3f\n" `echo "scale=2 ; 100/6" | bc` 33.330 O bc devolveu 2 decimais. o printf colocou 0 à direita $ printf "%o\n" 10 12 Converteu o 10 para octal $ printf "%03o\n" 27 033 Assim a conversão fica com mais jeito de octal, né? $ printf "%s\n" Peteleca Peteleca $ printf "%15s\n" Peteleca Peteleca Peteleca com 15 caracteres enchidos com brancos $ printf "%-15sNeves\n" Peteleca Peteleca Neves O menos (-) encheu à direita com brancos $ printf "%.3s\n" Peteleca Pet 3 trunca as 3 primeiras $ printf "%10.3sa\n" Peteleca Peta Pet com 10 caracteres concatenado com a (após o s) $ printf "EXEMPLO %x\n" 45232 EXEMPLO b0b0 Transformou para hexa mas os zeros não combinam $ printf "EXEMPLO %X\n" 45232 EXEMPLO B0B0 Assim disfarçou melhor (repare o X maiúsculo) $ printf "%X %XL%X\n" 49354 192 10 C0CA C0LA

O último exemplo não é marketing e é bastante completo, vou comentá-lo passo-a-passo:

  1. O primeiro %X converteu 49354 em hexadecimal resultando C0CA (leia-se "cê", "zero", "cê" e "a");
  2. Em seguida veio um espaço em branco seguido por outro %XL. O %X converteu o 192 dando como resultado C0 que com o L fez C0L;
  3. E finalmente o último %X transformou o 10 em A.

Conforme vocês podem notar, a instrução printf é bastante completa e complexa (ainda bem que o echo resolve quase tudo).

Creio que quando resolvi explicar o printf através de exemplos, acertei em cheio pois não saberia como enumerar tantas regrinhas sem tornar a leitura enfadonha.

Principais Variáveis do Shell

O Bash possui diversas variáveis que servem para dar informações sobre o ambiente ou alterá-lo. Seu número é muito grande e não pretendo mostrar todas, mas uma pequena parte que pode lhe ajudar na elaboração de scripts. Então aí vão as principais:

Principais variáveis do Bash
TMOUT Se tiver um valor maior do que zero, este valor será tomado como o padrão de timeout do comando read. No prompt, este valor é interpretado como o tempo de espera por uma ação antes de expirar a sessão. Supondo que a variável contenha 30, o Shell dará logout após 30 segundos de prompt sem nenhuma ação.
      Variável Conteúdo
CDPATH Contém os caminhos que serão pesquisados para tentar localizar um diretório especificado. Apesar desta variável ser pouco conhecida, seu uso deve ser incentivado por poupar muito trabalho, principalmente em instalações com estrutura de diretórios com bastante níveis.
HISTSIZE Limita o número de instruções que cabem dentro do arquivo de histórico de comandos (normalmente .bash_history mas efetivamente é o que está armazenado na variável $HISTFILE). Seu valor default é 500.  
HOSTNAME O nome do host corrente (que também pode ser obtido com o comando uname -n).
LANG Usada para determinar a língua falada no pais (mais especificamente categoria do locale).
LINENO O número da linha do script ou da função que está sendo executada, seu uso principal é para dar mensagens de erro juntamente com as variáveis $0 (nome do programa) e $FUNCNAME (nome da função em execução)
LOGNAME Armazena o nome de login do usuário.
MAILCHECK Especifica, em segundos, a freqüência que o Shell verificará a presença de correspondências nos arquivos indicados pela variáveis $MAILPATH ou $MAIL. O tempo padrão é 60 segundos. Uma vez este tempo expirado, o Shell fará esta verificação antes de exibir o próximo prompt primário (definido em $PS1). Se esta variável estiver sem valor ou com um valor menor ou igual a zero, a verificação de novas correspondências não será efetuada.
PATH Caminhos que serão pesquisados para tentar localizar um arquivo especificado. Como cada script é um arquivo, caso use o diretório corrente (.) na sua variável $PATH, você não necessitará de usar o ./scrp para que scrp seja executado. Basta fazer scrp. Este é o modo que procedo aqui no Botequim.
PIPESTATUS É uma variável do tipo vetor (array) que contém uma lista valores de código de retorno do último pipeline executado, isto é, um array que abriga cada um dos $? de cada instrução do último pipeline.
PROMPT_COMMAND Se esta variável receber uma instrução, toda vez que você der um <ENTER> direto no prompt principal ($PS1), este comando será executado. É útil quando se está repetindo muito uma determinada instrução.
PS1 É o prompt principal. No "Papo de Botequim" usamos os seus defaults: $ para usuário comum e # para root, mas é muito freqüente que ele esteja customizado. Uma curiosidade é que existe até concurso de quem programa o $PS1 mais criativo. (clique para dar uma googlada)
PS2 Também chamado prompt de continuação, é aquele sinal de maior (>) que aparece após um <ENTER> sem o comando ter sido encerrado.
PWD Possui o caminho completo ($PATH) do diretório corrente. Tem o mesmo efeito do comando pwd.
RANDOM Cada vez que esta variável é acessada, devolve um número inteiro, que é um randômico entre 0 e 32767.
REPLY Use esta variável para recuperar o último campo lido, caso ele não tenha nenhuma variável associada.
SECONDS Esta variável contém a quantidade de segundos que o Shell corrente está de pé. Use-a somente para esnobar um usuários daquilo que chamam de sistema operacional, mas necessita de boots freqüentes. smile

  • CDPATH
$ echo $CDPATH .:..:~:/usr/local $ pwd /home/jneves/LM $ cd bin $ pwd /usr/local/bin

Como /usr/local estava na minha variável $CDPATH, e não existia o diretório bin em nenhum dos seus antecessores (., .. e ~), o cd foi executado para /usr/local/bin

  • LANG
$ date Thu Apr 14 11:54:13 BRT 2005 $ LANG=pt_BR date Qui Abr 14 11:55:14 BRT 2005

Com a especificação da variável LANG=pt_BR (português do Brasil), a data passou a ser informada no padrão brasileiro. É interessante observarmos que não foi usado ponto-e-vírgula (;) para separar a atribuição de LANG do comando date.

  • PIPESTATUS
$ who jneves pts/0 Apr 11 16:26 (10.2.4.144) jneves pts/1 Apr 12 12:04 (10.2.4.144) $ who | grep ^botelho $ echo ${PIPESTATUS[*]} 0 1

Neste exemplo mostramos que o usuário botelho não estava "logado", em seguida executamos um pipeline que procurava por ele. Usa-se a notação [*] em um array para listar todos os seus elementos, e desta forma vimos que a primeira instrução (who) foi bem sucedida (código de retorno 0) e a seguinte (grep), não (código de retorno 1).

  • RANDOM

Para gerar randomicamente um inteiro entre 0 e 100, fazemos:

$ echo $((RANDOM%101)) 73

Ou seja pegamos o resto da divisão por 101 do número randômico gerado, porque o resto da divisão de qualquer número por 101 varia entre 0 e 100.

  • REPLY
$ read -p "Digite S ou N: " Digite S ou N: N $ echo $REPLY N

Eu sou do tempo que memória era um bem precioso que custava muuuuito caro. Então para pegar um S ou um N, não costumo a alocar um espaço especial e assim sendo, pego o que foi digitado na variável $REPLY.

Expansão de parâmetros

Bem, muito do que vimos até agora são comandos externos ao Shell. Eles quebram o maior galho, facilitam a visualização, manutenção e depuração do código, mas não são tão eficientes quanto os intrínsecos (built-ins). Quando o nosso problema for performance, devemos dar preferência ao uso dos intrínsecos e a partir de agora vou te mostrar algumas técnicas para o teu programa pisar no acelerador.

Na tabela e exemplos a seguir, veremos uma série de construções chamadas expansão (ou substituição) de parâmetros (Parameter Expansion), que substituem instruções como o cut, o expr, o tr, o sed e outras de forma mais ágil.

Expansão de parâmetros
  ${cadeia/%subcad1/subcad2}   Se subcad1 combina com o fim de $cadeia, então é trocado por subcad2
  Expressão   Resultado esperado
  ${var:-padrao}   Se var não tem valor, o resultado da expressão é padrao
  ${#cadeia}   Tamanho de $cadeia
  ${cadeia:posicao}   Extrai uma subcadeia de $cadeia a partir de posicao. Origem zero
  ${cadeia:posicao:tamanho}   Extrai uma subcadeia de $cadeia a partir de posicao com tamanho igual a tamanho. Origem zero
  ${cadeia#expr}   Corta a menor ocorrência de $cadeia à esquerda da expressão expr
  ${cadeia##expr}   Corta a maior ocorrência de $cadeia à esquerda da expressão expr
  ${cadeia%expr}   Corta a menor ocorrência de $cadeia à direita da expressão expr
  ${cadeia%%expr}   Corta a maior ocorrência de $cadeia à direita da expressão expr
  ${cadeia/subcad1/subcad2}   Troca em $cadeia a primeira ocorrência de subcad1 por subcad2
  ${cadeia//subcad1/subcad2}   Troca em $cadeia todas as ocorrências de subcad1 por subcad2
  ${cadeia/#subcad1/subcad2}   Se subcad1 combina com o início de $cadeia, então é trocado por subcad2

  • Se em uma pergunta o S é oferecido como valor default (padrão) e a saída vai para a variável $SN, após ler o valor podemos fazer:
    SN=$(SN:-S}

Desta forma se o operador deu um simples <ENTER> para confirmar que aceitou o valor default, após executar esta instrução, a variável terá o valor S, caso contrário, terá o valor digitado.

  • Para sabermos o tamanho de uma cadeia:
$ cadeia=0123 $ echo ${#cadeia} 4

  • Para extrair de uma cadeia da posição um até o final fazemos:
$ cadeia=abcdef $ echo ${cadeia:1} bcdef

Repare que a origem é zero e não um.

  • Na mesma variável $cadeia do exemplo acima, para extrair 3 caracteres a partir da 2ª posição:
$ echo ${cadeia:2:3} cde

Repare que novamente que a origem da contagem é zero e não um.

  • Para suprimir tudo à esquerda da primeira ocorrência de uma cadeia, faça:
$ cadeia="Papo de Botequim" $ echo ${cadeia#*' '} de Botequim $ echo "Conversa "${cadeia#*' '} Conversa de Botequim

Neste exemplo foi suprimido à esquerda tudo que casasse com a menor ocorrência da expressão *' ', ou seja, tudo até o primeiro espaço em branco.

Estes exemplos também poderiam ser escritos sem protegermos o espaço da interpretação do Shell (mas prefiro protegê-lo para facilitar a legibilidade do código), veja:

$ echo ${cadeia#* } de Botequim $ echo "Conversa "${cadeia#* } Conversa de Botequim

Repare que na construção de expr é permitido o uso de metacaracteres.

  • Utilizando o mesmo valor da variável $cadeia, observe como faríamos para termos somente Botequim:
$ echo ${cadeia##*' '} Botequim $ echo "Vamos 'Chopear' no "${cadeia##*' '} Vamos 'Chopear' no Botequim

Desta vez suprimimos à esquerda de cadeia a maior ocorrência da expressão expr. Assim como no caso anterior, o uso de metacaracteres é permitido.

Outro exemplo mais útil: para que não apareça o caminho (path) completo do seu programa (que, como já sabemos está contido na variável $0) em uma mensagem de erro, inicie o seu texto da seguinte forma:

    echo Uso: ${0##*/} texto da mensagem de erro

Neste exemplo seria suprimido à esquerda tudo até a última barra (/) do caminho (path), desta forma sobrando somente o nome do programa.

  • O uso do percentual (%) é como se olhássemos o jogo-da-velha (#) no espelho, isto é, são simétricos. Então vejamos um exemplo para provar isso:

$ echo $cadeia Papo de Botequim $ echo ${cadeia%' '*} Papo de $ echo ${cadeia%%' '*} Papo

  • Para trocar primeira ocorrência de uma subcadeia em uma cadeia por outra:
$ echo $cadeia Papo de Botequim $ echo ${cadeia/de/no} Papo no Botequim $ echo ${cadeia/de /} Papo Botequim

Neste caso preste a atenção quando for usar metacaracteres, eles são gulosos! Eles sempre combinarão com a maior possibilidade, veja o exemplo a seguir onde a intenção era trocar Papo de Botequim por Conversa de Botequim:

$ echo $cadeia Papo de Botequim $ echo ${cadeia/*o/Conversa} Conversatequim

A idéia era pegar tudo até o primeiro o, mas o que foi trocado foi tudo até o último o. Isto poderia ser resolvido de diversas maneiras, veja algumas:

$ echo ${cadeia/*po/Conversa} Conversa de Botequim $ echo ${cadeia/????/Conversa} Conversa de Botequim

  • Trocando todas as ocorrências de uma subcadeia por outra. Quando fazemos:
$ echo ${cadeia//o/a} Papa de Batequim

Trocamos todos as letras o por a. Outro exemplo mais útil é para contarmos a quantidade de arquivos existentes no diretório corrente. Observe a linha a seguir:

$ ls | wc -l 30

Viu? O wc produz um monte de espaços em branco no início. Para tirá-los podemos fazer:

$ QtdArqs=$(ls | wc -l) # QtdArqs recebe a saída do comando $ echo ${QtdArqs// /} 30

No último exemplo, como eu sabia que a saída era composta de brancos e números, montei esta expressão para trocar todos os espaços por nada. Repare que após as duas primeiras barras existe um espaço em branco.

Outra forma de fazer a mesma coisa seria:

$ echo ${QtdArqs/* /} 30

  • Trocando uma subcadeia no início ou no fim de uma variável. Para trocar no início fazemos:
$ echo $Passaro quero quero $ echo "Como diz o sulista - "${Passaro/#quero/não} Como diz o sulista - não quero

Para trocar no final fazemos:

$ echo "Como diz o nordestino - "${Passaro/%quero/não} Como diz o nordestino - quero não

- Agora já chega, o papo hoje foi muito chato porque foi muita decoreba, mas o principal é você ter entendido o que te falei e, quando precisar, consulte estes guardanapos em que rabisquei estas dicas e depois guarde-os para consultas futuras. Mas voltando à vaca fria: tá na hora de tomar outro e ver o jogo do mengão. Na próxima vou te dar moleza e só vou cobrar o seguinte: pegue a rotina pergunta.func, (a que na qual falamos no início do nosso bate papo de hoje) e otimize-a para que a variável $SN receba o valor default por expansão de parâmetros, como vimos.

- Chico, vê se não esquece de mim e enche meu copo.

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.