Feeds:
Posts
Comentários

– 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!


– 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
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
% Imprime um %. Não existe nenhuma conversão

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
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)
t Avança para a próxima marca de tabulação

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 “%cn” “1 caracter”
1
Saltou linha mas ainda não listou a cadeia inteira
$ printf “%c caracteren” 1
1 caractere
Esta é a forma correta o %c recebeu o 1
$ a=2
$ printf “%c caracteresn” $a
2 caracteres
O %c recebeu o valor da variável $a
$ printf “%10c caracteresn” $a
2 caracteres

$ printf “%10cn” $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 “%dn” 32
32

$ printf “%10dn” 32
32
Preenche com brancos à esquerda e não com zeros
$ printf “%04dn” 32
0032
04 após % significa 4 dígitos com zeros à esquerda
$ printf “%en” $(echo “scale=2 ; 100/6” | bc)
1.666000e+01
O default do %e é 6 decimais
$ printf “%.2en” `echo “scale=2 ; 100/6” | bc`
1.67e+01
O .2 especificou duas decimais
$ printf “%fn” 32.3
32.300000
O default do %f é 6 decimais
$ printf “%.2fn” 32.3
32.30
O .2 especificou duas decimais
$ printf “%.3fn” `echo “scale=2 ; 100/6” | bc`
33.330
O bc devolveu 2 decimais. o printf colocou 0 à direita
$ printf “%on” 10
12
Converteu o 10 para octal
$ printf “%03on” 27
033
Assim a conversão fica com mais jeito de octal, né?
$ printf “%sn” Peteleca
Peteleca

$ printf “%15sn” Peteleca
Peteleca
Peteleca com 15 caracteres enchidos com brancos
$ printf “%-15sNevesn” Peteleca
Peteleca Neves
O menos (-) encheu à direita com brancos
$ printf “%.3sn” Peteleca
Pet
3 trunca as 3 primeiras
$ printf “%10.3san” Peteleca
Peta
Pet com 10 caracteres concatenado com a (após o s)
$ printf “EXEMPLO %xn” 45232
EXEMPLO b0b0
Transformou para hexa mas os zeros não combinam
$ printf “EXEMPLO %Xn” 45232
EXEMPLO B0B0
Assim disfarçou melhor (repare o X maiúsculo)
$ printf “%X %XL%Xn” 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
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
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.
  • 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
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
${cadeia/%subcad1/subcad2} Se subcad1 combina com o fim 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!


– 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!


– E aí rapaz, tudo bom? – Belê, mas você lembra que me pediu para fazer um programa que quando o tamanho de uma tela variasse, no seu centro apareceria dinamicamente, em vídeo reverso, a quantidade de linhas e colunas do jeito que o Linux faz normalmente? Pois é eu fiz mas a aparência não ficou igual.- Não estou nem aí para a aparência, o que eu queria é que você exercitasse o que aprendemos. Deixe-me ver o que você fez.

$ cat tamtela.sh
#!/bin/bash
#
# Coloca no centro da tela, em video reverso,
# a quantidade de colunas e linhas
# quando o tamanho da tela eh alterado.
#
trap Muda 28 # 28 = sinal gerado pela mudanca no tamanho
# da tela e Muda eh a funcao que fara isso.

Bold=$(tput bold) # Modo de enfase
Rev=$(tput rev) # Modo de video reverso
Norm=$(tput sgr0) # Restaura a tela ao padrao default

Muda ()
{
clear
Cols=$(tput cols)
Lins=$(tput lines)
tput cup $(($Lins / 2)) $(((Cols – 7) / 2)) # Centro da tela
echo $Bold$Rev$Cols X $Lins$Norm
}

clear
read -n1 -p “Mude o tamanho da tela ou tecle algo para terminar “

– Perfeito! Que se dane a aparência, depois vou te ensinar uns macetes para melhorá-la o que vale é que o programa está funcionando e está bem enxuto.

– Poxa, perdi o maior tempo tentando descobrir como aumentar o fonte …

– Deixe isso para lá e hoje vamos ver umas coisas bastante interessantes e úteis.

Named Pipes

Um outro tipo de pipe é o named pipe, que também é chamado de FIFO. FIFO é um acrônimo de First In First Out que se refere à propriedade em que a ordem dos bytes entrando no pipe é a mesma que a da saída. O name em named pipe é, na verdade, o nome de um arquivo. Os arquivos tipo named pipes são exibidos pelo comando ls como qualquer outro, com poucas diferenças, veja:

$ ls -l pipe1
prw-r-r– 1 julio dipao 0 Jan 22 23:11 pipe1|

O p na coluna mais à esquerda indica que fifo1 é um named pipe. O resto dos bits de controle de permissões, quem pode ler ou gravar o pipe, funcionam como um arquivo normal. Nos sistemas mais modernos uma barra vertical (|) colocado ao fim do nome do arquivo, é outra dica, e nos sistemas LINUX, onde a opção de cor está habilitada, o nome do arquivo é escrito em vermelho por default.

Nos sistemas mais antigos, os named pipes são criados pelo programa mknod, normalmente situado no diretório /etc.

Nos sistemas mais modernos, a mesma tarefa é feita pelo mkfifo. O programa mkfifo recebe um ou mais nomes como argumento e cria pipes com estes nomes. Por exemplo, para criar um named pipe com o nome pipe1, faça:

$ mkfifo pipe1

Como sempre, a melhor forma de mostrar como algo funciona é dando exemplos. Suponha que nós tenhamos criado o named pipe mostrado anteriormente. Vamos agora trabalhar com duas sessões ou duas consoles virtuais ou uma de cada. Em uma delas faça:

$ ls -l > pipe1

e em outra faça:

$ cat < pipe1

Voilá! A saída do comando executado na primeira console foi exibida na segunda. Note que a ordem em que os comandos ocorreram não importa.

Se você prestou atenção, reparou que o primeiro comando executado, parecia ter “pendurado, congelado”. Isto acontece porque a outra ponta do pipe ainda não estava conectada, e então o sistema operacional suspendeu o primeiro processo até que o segundo “abrisse” o pipe. Para que um processo que usa pipe não fique em modo de wait, é necessário que em uma ponta do pipe tenha um processo “tagarela” e na outra um “ouvinte” e no exemplo que demos, o ls era o “falador” e o cat era o “orelhão”.

Uma aplicação muito útil dos named pipes é permitir que programas sem nenhuma relação possam se comunicar entre si, os named pipes também são usados para sincronizar processos, já que em um determinado ponto você pode colocar um processo para “ouvir” ou para “falar” em um determinado named pipe e ele daí só sairá, se outro processo “falar” ou “ouvir” aquele pipe.

Você já viu que o uso desta ferramenta é ótimo para sincronizar processos e para fazer bloqueio em arquivos de forma a evitar perda/corrupção de informações devido a atualizações simultâneas (concorrência). Vejamos exemplos para ilustrar estes casos.

Sincronização de processos.

Suponha que você dispare paralelamente dois programas (processos) cujos diagramas de blocos de suas rotinas são como a figura a seguir:

Gráfico do Named Pipe Os dois processos são disparados em paralelo e no BLOCO1 do Programa1 as três classificações são disparadas da seguinte maneira:

    for Arq in BigFile1 BigFile2 BigFile3
    do
        if  sort $Arq
        then
            Manda=va
        else
            Manda=pare
            break
        fi
    done
    echo $Manda > pipe1
    [ $Manda = pare ] &&
        {
        echo Erro durante a classificação dos arquivos
        exit 1
        }
    ...

Assim sendo, o comando if testa cada classificação que está sendo efetuada. Caso ocorra qualquer problema, as classificações seguintes serão abortadas, uma mensagem contendo a cadeia pare é enviada pelo pipe1 e programa1 é descontinuado com um fim anormal.

Enquanto o Programa1 executava o seu primeiro bloco (as classificações) o Programa2 executava o seu BLOCO1, processando as suas rotinas de abertura e menu paralelamente ao Programa1, ganhando desta forma um bom intervalo de tempo.

O fragmento de código do Programa2 a seguir, mostra a transição do seu BLOCO1 para o BLOCO2:

    OK=`cat pipe1`
    if  [ $OK = va ]
    then
        ...
        Rotina de impressão
        ...
    else    #  Recebeu "pare" em OK
        exit 1
    fi

Após a execução de seu primeiro bloco, o Programa2 passará a “ouvir” o pipe1, ficando parado até que as classificações do Programa1 terminem, testando a seguir a mensagem passada pelo pipe1 para decidir se os arquivos estão íntegros para serem impressos, ou se o programa deverá ser descontinuado. Desta forma é possível disparar programas de forma assíncrona e sincronizá-los quando necessário, ganhando bastante tempo de processamento.

Bloqueio de arquivos

Suponha que você escreveu uma CGI (Common Gateway Interface) em Shell para contar quantos hits recebe uma determinada URL e a rotina de contagem está da seguinte maneira:

    Hits="$(cat page.hits 2> /dev/null)" || Hits=0
    echo $((Hits=Hits++)) > page.hits

Desta forma se a página receber dois ou mais acessos concorrentes, um ou mais poderá(ão) ser perdido(s), basta que o segundo acesso seja feito após a leitura da arquivo page.hits e antes da sua gravação, isto é, basta que o segundo acesso seja feito após o primeiro ter executado a primeira linha do script e antes de executar a segunda.

Então o que fazer? Para resolver o problema de concorrência vamos utilizar um named pipe. Criamos o seguinte script que será o daemon que receberá todos os pedidos para incrementar o contador. Note que ele vai ser usado por qualquer página no nosso site que precise de um contador.

$ cat contahits.sh
#!/bin/bash

PIPE=”/tmp/pipe_contador” # arquivo named pipe
# dir onde serao colocados os arquivos contadores de cada pagina
DIR=”/var/www/contador”

[ -p “$PIPE” ] || mkfifo “$PIPE”

while :
do
for URL in $(cat < $PIPE)
do
FILE=”$DIR/$(echo $URL | sed ‘s,.*/,,’)”
# OBS1: no sed acima, como precisava procurar
# uma barra,usamos vírgula como separador.
# OBS2: quando rodar como daemon comente a proxima linha
echo “arquivo = $FILE”

n=”$(cat $FILE 2> /dev/null)” || n=0
echo $((n=n+1)) > “$FILE”
done
done

Como só este script altera os arquivos, não existe problema de concorrência.

Este script será um daemon, isto é, rodará em background. Quando uma página sofrer um acesso, ela escreverá a sua URL no arquivo de pipe. Para testar, execute este comando:

    echo "teste_pagina.html" > /tmp/pipe_contador

Para evitar erros, em cada página que quisermos adicionar o contador acrescentamos a seguinte linha:

    <!--#exec cmd="echo $REQUEST_URI > /tmp/pipe_contador"-->

Note que a variável $REQUEST_URI contém o nome do arquivo que o navegador (browser) requisitou.

Este último exemplo, é fruto de uma idéia que troquei com o amigo e mestre em Shell, Thobias Salazar Trevisan que escreveu o script e colocou-o em sua excelente URL. Aconselho a todos que querem aprender Shell a dar uma olhada nela (Dê uma olhada e inclua-a nos favoritos).

Ahhh! Você pensa que o assunto sobre named pipes está esgotado? Enganou-se. Vou mostrar um uso diferente a partir de agora.

Substituição de processos

Acabei de mostrar um monte de dicas sobre named pipes, agora vou mostrar que o Shell também usa os named pipes de uma maneira bastante singular, que é a substituição de processos (process substitution). Uma substituição de processos ocorre quando você põe um comando ou um pipeline de comandos entre parênteses e um < ou um > grudado na frente do parêntese da esquerda. Por exemplo, teclando-se o comando:

$ cat <(ls -l)

Resultará no comando ls -l executado em um subshell como é normal (por estar entre parênteses), porém redirecionará a saída para um named pipe temporário, que o Shell cria, nomeia e depois remove. Então o cat terá um nome de arquivo válido para ler (que será este named pipe e cujo dispositivo lógico associado é /dev/fd/63), e teremos a mesma saída que a gerada pela listagem do ls -l, porém dando um ou mais passos que o usual, isto é, mais onerosa para o computador.

Como poderemos constatar isso? Fácil… Veja o comando a seguir:

$ ls -l >(cat)
l-wx—— 1 jneves jneves 64 Aug 27 12:26 /dev/fd/63 -> pipe:[7050]

É… Realmente é um named pipe.

Você deve estar pensando que isto é uma maluquice de nerd, né? Então suponha que você tenha 2 diretórios: dir e dir.bkp e deseja saber se os dois estão iguais (aquela velha dúvida: será que meu backup está atualizado?). Basta comparar os dados dos arquivos dos diretórios com o comando cmp, fazendo:

$ cmp <(cat dir/*) <(cat dir.bkp/*) || echo backup furado

ou, melhor ainda:

$ cmp <(cat dir/*) <(cat dir.bkp/*) >/dev/null || echo backup furado

Da forma acima, a comparação foi efetuada em todas as linhas de todos os arquivos de ambos os diretórios. Para acelerar o processo, poderíamos compara somente a listagem longa de ambos os diretórios, pois qualquer modificação que um arquivo sofra, é mostrada na data/hora de alteração e/ou no tamanho do arquivo. Veja como ficaria:

$ cmp <(ls -l dir) <(ls -l dir.bkp) >/dev/null || echo backup furado

Este é um exemplo meramente didático, mas são tantos os comandos que produzem mais de uma linha de saída, que serve como guia para outros. Eu quero gerar uma listagem dos meus arquivos, numerando-os e ao final dar o total de arquivos do diretório corrente:

    while read arq
    do
        ((i++)) # assim nao eh necessario inicializar i
        echo "$i: $arq"
    done < <(ls)
    echo "No diretorio corrente (`pwd`) existem $i arquivos"

Tá legal, eu sei que existem outras formas de executar a mesma tarefa. Usando o comando while, a forma mais comum de resolver esse problema seria:

    ls | while read arq
    do
        ((i++)) # assim nao eh necessario inicializar i
        echo "$i: $arq"
    done
    echo "No diretorio corrente (`pwd`) existem $i arquivos"

Quando executasse o script, pareceria estar tudo certo, porém no comando echo após o done, você verá que o valor de $i foi perdido. Isso deve-se ao fato desta variável estar sendo incrementada em um subshell criado pelo pipe (|) e que terminou no comando done, levando com ele todas as variáveis criadas no seu interior e as alterações feitas em todas as variáveis, inclusive as criadas externamente.

Somente para te mostrar que uma variável criada fora do subshell e alterada em seu interior perde as alterações feitas ao seu final, execute o script a seguir:

    #!/bin/bash
    LIST=""                  # Criada no shell principal
    ls | while read FILE     # Inicio do subshell
    do
        LIST="$FILE $LIST"   # Alterada dentro do subshell
    done                     # Fim do subshell
    echo :$LIST:

Ao final da execução você verá que aperecerão apenas dois dois-pontos (::). Mas no início deste exemplo eu disse que era meramente didático porque existem formas melhores de fazer a mesma tarefa. Veja só estas duas:

$ ls | ln

ou então, usando a própria substituição de processos:

$ cat -n <(ls)

Um último exemplo: você deseja comparar arq1 e arq2 usando o comando comm, mas este comando necessita que os arquivos estejam classificados. Então a melhor forma de proceder é:

$ comm <(sort arq1) <(sort arq2)

Esta forma evita que você faça as seguintes operações:

$ sort arq1 > /tmp/sort1
$ sort arq2 > /tmp/sort2
$ comm /tmp/sort1 /tmp/sort2
$ rm -f /tmp/sort1 /tmp/sort2

Pessoal, o nosso Papo de Botequim chegou ao fim frown . Curti muito aqui e recebi diversos elogios pelo trabalho desenvolvido ao longo de doze meses e, o melhor de tudo, fiz muitas amizades e tomei muitos chopes de graça com os leitores que encontrei pelos congressos e palestras que ando fazendo pelo nosso querido Brasil.

O que vou escrever aqui não está combinado nem sei se será publicado, mas como os editores desta revista são dois malucos beleza (ambos Rafael), é bem capaz de deixarem passar. É o seguinte: se quiserem que o Papo de Botequim continue, entulhem a caixa postal da Linux Magazine pedindo por isso e desde já escolham o próximo tema entre sed + expressões regulares ou linguagem awk.

De qualquer forma, caso não consigamos sensibilizar a direção da revista, me despeço de todos mandando um grande abraço aos barbudos e beijos às meninas e agradecendo os mais de 100 e-mails que recebi (todos elogiosos) e todos devidamente respondidos.

À saúde de todos nós: Tim, Tim.

– Chico, fecha a minha conta porque vou mudar de botequim.

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!

Tira Gosto

Shell em pedaços pequenos e gostosos


Em construção para sempre


Esta página, apesar de estar no escopo do Papo de Botequim, nunca foi publicada na Linux Magazine. Trata-se de artigos que escrevi para outras mídias, dicas úteis que li pela internet afora (e neste caso com os devidos créditos), contribuições deste pessoal do Software Livre, maravilhoso e sempre pronto a ajudar e da imperdível Lista de Shell Script



Passando parâmetros com xargs

Existe um comando, cuja função primordial é construir listas de parâmetros e passá-la para a execução de outros programas ou instruções. Este comando é o xargs e deve ser usado da seguinte maneira:

    xargs [comando [argumento inicial]]

xargs combina o argumento inicial com os argumentos recebidos da entrada padrão, de forma a executar o comando especificado uma ou mais vezes.

Exemplo:

Vamos procurar em todos os arquivos abaixo de um determinado diretório uma cadeia de caracteres usando o comando find com a opção -type f para pesquisar somente os arquivos normais, desprezando diretórios, arquivos especiais, arquivos de ligações, etc, e vamos torná-la mais genérica recebendo o nome do diretório inicial e a cadeia a ser pesquisada como parâmetros. Para isso fazemos:

$ cat grepr
#
# Grep recursivo
# Pesquisa a cadeia de caracteres definida em $2 a partir do diretorio $1
#
find $1 -type f -print|xargs grep -l “$2”

Na execução deste script procuramos, a partir do diretório definido na variável $1, todos os arquivos que continham a cadeia definida na variável $2.

Exatamente a mesma coisa poderia ser feito se a linha do programa fosse a seguinte:

    find $1 -type f -exec grep -l "$2" {} \;

O primeiro processo tem duas grandes desvantagens sobre o anterior:

  • A primeira é bastante visível: o tempo de execução deste método é muito superior ao daquele, isso porque o grep será feito em cada arquivo que lhe for passado pelo find, um-a-um, ao passo que com o xargs, será passada toda, ou na pior das hipóteses, a maior parte possível, da lista de arquivos gerada pelo find;
  • Dependendo da quantidade de arquivos encontrados que atendem ao find, poderemos ganhar aquela famosa e fatídica mensagem de erro Too many arguments indicando um estouro da pilha de execução do grep. Como foi dito no item anterior, se usarmos o xargs ele passará para o grep a maior quantidade de parâmetros possível, suficiente para não causar este erro, e caso necessário executará o grep mais de uma vez.
Pinguim com placa de atenção Aê pessoal do linux que usa o ls colorido que nem porta de tinturaria: nos exemplos a seguir que envolvem esta instrução, você devem usar a opção --color=none, senão existem grandes chances dos resultados não ocorrerem como o esperado.

Vamos agora analisar um exemplo que é mais ou menos o inverso deste que acabamos de ver. Desta vez, vamos fazer um script para remover todos os arquivos do diretório corrente, pertencentes a um determinado usuário.

A primeira idéia que surge é, como no caso anterior, usar um comando find, da seguinte maneira:

    find . -user cara -exec rm -f {} \;

Quase estaria certo, o problema é que desta forma você removeria não só os arquivos do cara no diretório corrente, mas também de todos os outros subdiretórios “pendurados” neste. Vejamos então como fazer:

    ls -l | grep " cara " | cut -c55- | xargs rm

Desta forma, o grep selecionou os arquivos que continham a cadeia cara no diretório corrente listado pelo ls -l. O comando cut pegou somente o nome dos arquivos, passando-os para a remoção pelo rm usando o comando xargs como ponte

O xargs é também uma excelente ferramenta de criação de one-liners (scripts de somente uma linha). Veja este para listar todos os donos de arquivos (inclusive seus links) “pendurados” no diretório /bin e seus subdiretórios.

$ find /bin -type f -follow | \
xargs ls -al | tr -s ‘ ‘ | cut -f3 -d’ ‘ | sort -u

Muitas vezes o /bin é um link (se não me engano, no Solaris o é) e a opção -follows obriga o find a seguir o link. O comando xargs alimenta o ls -al e a seqüência de comandos seguinte é para pegar somente o 3º campo (dono) e classificá-lo devolvendo somente uma vez cada dono (opção -u do comando sort, que equivale ao comando uniq).

Opções do xargs

Você pode usar as opções do xargs para construir comandos extremamente poderosos.

Opção -i

Para exemplificar isso e começar a entender as principais opções desta instrução, vamos supor que temos que remover todos as arquivos com extensão .txt sob o diretório corrente e apresentar os seus nomes na tela. Veja o que podemos fazer:

$ find . -type f -name “*.txt” | \
xargs -i bash -c “echo removendo {}; rm {}”

A opção -i do xargs troca pares de chaves ({}) pela cadeia que está recebendo pelo pipe (|). Então neste caso as chaves ({}) serão trocadas pelos nomes dos arquivos que satifaçam ao comando find.

Opção -n

Olha só a brincadeira que vamos fazer com o xargs:

$ ls | xargs echo > arq.ls
$ cat arq.ls
arq.ls arq1 arq2 arq3

$ cat arq.ls | xargs -n1
arq.ls
arq1
arq2
arq3

Quando mandamos a saída do ls para o arquivo usando o xargs, comprovamos o que foi dito anteriormente, isto é, o xargs manda tudo que é possível (o suficiente para não gerar um estouro de pilha) de uma só vez. Em seguida, usamos a opção -n 1 para listar um por vez. Só para dar certeza veja o exemplo a seguir, quando listaremos dois em cada linha:

$ cat arq.ls | xargs -n 2
arq.ls arq1
arq2 arq3

Mas a linha acima poderia (e deveria) ser escrita sem o uso de pipe (|), da seguinte forma:

$ xargs -n 2 < arq.ls

Opção -p

Outra opção legal do xargs é a -p, na qual o sistema pergunta se você realmente deseja executar o comando. Digamos que em um diretório você tenha arquivos com a extensão .bug e .ok, os .bug estão com problemas que após corrigidos são salvos como .ok. Dá uma olhadinha na listagem deste diretório:

$ ls dir
arq1.bug
arq1.ok
arq2.bug
arq2.ok

arq9.bug
arq9.ok

Para comparar os arquivos bons com os defeituosos, fazemos:

$ ls | xargs -p -n2 diff -c
diff -c arq1.bug arq1.ok ?…y
….
diff -c arq9.bug arq9.ok ?…y

Opção -t

Para finalizar, o xargs também tem a opção -t, onde vai mostrando as instruções que montou antes de executá-las. Gosto muito desta opção para ajudar a depurar o comando que foi montado.

Resumo

Então podemos resumir o comando de acordo com a tabela a seguir:

Opção Ação
-i Substitui o par de chaves ({}) pelas cadeias recebidas
-nNum Manda o máximo de parâmetros recebidos, até o máximo de Num para o comando a ser executado
-lNum Manda o máximo de linhas recebidas, até o máximo de Num para o comando a ser executado
-p Mostra a linha de comando montada e pergunta se deseja executá-la
-t Mostra a linha de comando montada antes de executá-la

Here Strings

Acabei de dar um treinamento no Instituto de Pesquisas Eldorado, onde peguei uma turma de 12 alunos de altíssima qualidade. Devido ao preparo do pessoal, o nível do curso foi tão bom que cheguei a abordar alguns conceitos pouco usados como o redirecionamento por here strings. Então pensei: se é uma coisa tão útil, porque não mostrá-la a todos? Ai vai …

Primeiro um programador com complexo de inferioridade criou o redirecionamento de entrada e representou-o com um sinal de menor (<) para representar seus sentimento. Em seguida, outro sentindo-se pior ainda, criou o here document representando-o por dois sinais de menor (<<) porque sua fossa era maior. O terceiro, pensou: “estes dois não sabem o que é estar por baixo”… Então criou o here strings representado por três sinais de menor (<<<).

Brincadeiras a parte, o here strings é utilíssimo e, não sei porque, é um perfeito desconhecido. Na pouquíssima literatura que há sobre o tema, nota-se que o here strings é freqüentemente citado como uma variante do here document, teoria com a qual discordo pois sua aplicabilidade é totalmente diferente daquela. Sua sintaxe é simples:

    $ comando <<< $cadeia

Onde cadeia é expandida e alimenta a entrada primária (stdin) de comando.

Como sempre, vamos direto aos exemplos dos dois usos mais comuns para que vocês próprios tirem suas conclusões.

  • Uso #1. Substituindo a famigerada construção echo "cadeia" | comando, que força um fork, criando um subshell e onerando o tempo de execução.

Exemplos:

$ a=”1 2 3″
$ cut -f 2 -d ‘ ‘ <<< $a # Normalmente faz-se: echo $a | cut -f 2 -d ‘ ‘
2

$ echo $NomeArq
Meus Documentos
# Arrrghhh!
$ tr “A-Z ” “a-z_” <<< $NomeArq # Substituindo o echo $NomeArq | tr “A-Z ” “a-z_”
meus_documentos

$ bc <<<“3 * 2”
6

$ bc <<<“scale = 4; 22 / 7”
3.1428

Para mostrar a melhoria no desempenho, vamos fazer um loop de 500 vezes usando o exemplo dados para o comando tr: Veja agora esta seqüência de comandos com medidas de tempo:

$ time for ((i=1; i<= 500; i++)); { tr “A-Z ” “a-z_” <<< $NomeArq >/dev/null; }

real 0m3.508s
user 0m2.400s
sys 0m1.012s

$ time for ((i=1; i<= 500; i++)); { echo $NomeArq | tr “A-Z ” “a-z_” >/dev/null; }

real 0m4.144s
user 0m2.684s
sys 0m1.392s

Veja agora esta seqüência de comandos com medidas de tempo:

$ time for ((i=1;i<=100;i++)); { who | cat > /dev/null; }

real 0m1.435s
user 0m1.000s
sys 0m0.380s

$ time for ((i=1;i<=100;i++)); { cat <(who) > /dev/null; }

real 0m1.552s
user 0m1.052s
sys 0m0.448s

$ time for ((i=1;i<=100;i++)); { cat <<< $(who) > /dev/null; }

real 0m1.514s
user 0m1.056s
sys 0m0.412s

Observando este quadro você verá que no primeiro usamos a forma convencional, no segundo usamos um named pipe temporário para executar uma substituição de processos e no terceiro usamos here strings. Notará também que ao contrário do exemplo anterior, aqui o uso de here strings não foi o mais veloz. Mas repare bem que neste último caso o comando who está sendo executado em um subshell e isso onerou o processo como um todo.

Vejamos uma forma rápida de inserir uma linha como cabeçalho de um arquivo:

$ cat num
1 2
3 4
5 6
7 8
9 10

$ cat – num <<< “Impares Pares”
Impares Pares
1 2
3 4
5 6
7 8
9 10
  • Uso #2. Outra forma legal de usar o here strings é casando-o com um comando read, não perdendo de vista o que aprendemos sobre IFS (veja aqui, na explicação do comando for). O comando cat com as opções -vet mostra o <ENTER> como $, o <TAB> como ^I e os outros caracteres de controle com a notação ^L onde L é uma letra qualquer. Vejamos então o conteúdo de uma variável e depois vamos ler cada um de seus campos:

Exemplos:

$ echo “$Linha”
Leonardo Mello (21)3313-1329

$ cat -vet <<< “$Linha”
Leonardo Mello^I(21)3313-1329$
# Os separadores sao branco e <TAB> (^I)
$ read Nom SNom Tel <<< “$Linha”
$ echo “${Nom}_$S{Nom}_$Tel” # Vamos ver se ele leu cada um dos campos
Leonardo_Mello_(21)3313-1329
# Leu porque os separadores casavam com o IFS

Também podemos ler direto para um vetor (array) veja:

$ echo $Frutas
Pera:Uva:Maçã

$ IFS=:
$ echo $Frutas
Pera Uva Maçã
# Sem as aspas o shell mostra o IFS como branco
$ echo “$Frutas”
Pera:Uva:Maçã
# Ahhh, agora sim!
$ read -a aFrutas <<< “$Frutas” # A opção -a do read, lê para um vetor
$ for i in 0 1 2
> do
> echo ${aFrutas[$i]} # Imprimindo cada elemento do vetor
> done
Pera
Uva
Maçã

Rotatório Peczenyj

Estava, como faço todo dia, dando um lida nos e-mails da “Lista de Shell Script” , quando vi uma “descoberta” totalmente inusitada do Tiago Barcellos Peczenyj. Quando resolvi montar esta coletânea de dicas, me lembrei disso e pedi-lhe para me encaminhar aquele e-mail novamente. O texto a a seguir é o e-mail que ele me mandou, só inseri o último exemplo e tirei as abreviaturas.

Julio descobri uma forma para o Shell criar combinaçãoes fazendo rotação com os elementos estipulados. Podemos gerar todos os binários de 0000 a 1111 da seguinte forma:

$ A={0,1}
$ eval echo $A$A$A$A
0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

Uma aplicação pratica que vejo é para combinar valores diferentes sem ter que encadear loops nem usar o seq

$ A={`seq -s , -f “_%g” 3`}
$ eval echo -e $A$A$A |tr ‘ _’ ‘\n ‘ | grep -vE ‘.+?(\b[0-9]+\b).+?\1’
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

Neste caso eu combinei os números de 1 a 3 eliminando repetições com o grep. Usei um tr ‘podre’ para melhor tratar os dados, saltando linha.

O grep é simples como você pode notar, eu vejo se uma determinada parte da combinação (.+?(\b[0-9]+\b).+?) existe em outra parte (\1), se existe eu não deixo imprimir por causa da opção -v, assim

    1 1 2
    1 2 1
    1 1 1

não serão impressos. Agora vai o meu exemplo: o one-liner a seguir gerará todas as permissões possíveis (em octal) para o arquivo arq (o exemplo foi interrompido porque existem 512 combinações de permissões possíveis).

$ A={`seq -s , 0 7`}
$ eval echo -e $A$A$A | tr ‘ ‘ ‘\n’ | xargs -i bash -c “chmod {} arq; ls -l arq”
———- 1 julio julio 100 2006-11-27 11:50 arq
———x 1 julio julio 100 2006-11-27 11:50 arq
——–w- 1 julio julio 100 2006-11-27 11:50 arq
——–wx 1 julio julio 100 2006-11-27 11:50 arq
——-r– 1 julio julio 100 2006-11-27 11:50 arq
——-r-x 1 julio julio 100 2006-11-27 11:50 arq
——-rw- 1 julio julio 100 2006-11-27 11:50 arq
——-rwx 1 julio julio 100 2006-11-27 11:50 arq
. . . . . . . . . . . . . . . . . .
-rwxrwxrw- 1 julio julio 100 2006-11-27 11:50 arq
-rwxrwxrwx 1 julio julio 100 2006-11-27 11:50 arq

Vamos ver este exemplo passo-a-passo para entendê-lo:

$ echo $A
{0,1,2,3,4,5,6,7}

$ eval echo -e $A$A$A
000 001 002 003 004 005 006 007 010 … … 767 770 771 772 773 774 775 776 777

$ eval echo -e $A$A$A | tr ‘ ‘ ‘\n’ # O tr trocará cada espaco em branco por um <ENTER>
000
001
002
003
. . .
774
775
776
777

A seguir o xargs (clique para dicas do xargs) executa o comando bash -c (que serve para executar uma linha de comandos) que por sua vez executa o chmod e o ls -l para mostrar que as permissões estão sendo alteradas.

Aritmética em Shell

Antigamente usávamos o comando expr para fazer operações aritméticas e muita gente ainda usa, pois é compatível com quaquer ambiente.

Exemplo:

$ expr 7 \* 5 / 3 # 7 vezes 5 = 35 dividido por 3 = 11
14

Neste artigo porém, vamos ver outras formas não tanto conhecidas, porém mais simples de usar, mais elaboradas e com precisão maior.

O uso do bc

Uma forma bacana de fazer cálculos em Shell – usada normalmente quando a expressão aritmética é mais complexa, ou quando é necessário trabalharmos com casas decimais – é usar a instrução calculadora do UNIX/LINUX. O bc. Veja como: Exemplo:

$ echo “(2 + 3) * 5” | bc # Parênteses usados para dar precedência
25

Para trabalhar com números reais (números não necessariamente inteiros), especifique a precisão (quantidade de decimais) com a opção scale do comando bc. Assim vejamos o penúltimo exemplo:

$ echo “scale=2; 7*5/3” | bc
11.66

Outros exemplos:

$ echo “scale=3; 33.333*3” | bc
99.999

$ num=5
$ echo “scale=2; ((3 + 2) * $num + 4) / 3” | bc
9.66

Obviamente todos os exemplos acima no caso de linux, poderiam (e deveriam) ser escritos usando Here Strings. Veja os últimos como ficariam:

$ bc <<< “scale=3; 33.333*3”
99.999

$ num=5
$ bc <<< “scale=2; ((3 + 2) * $num + 4) / 3”
9.66

Uma vez apareceu na lista (excelente por sinal) de Shell script no Yahoo (http://br.groups.yahoo.com/group/shell-script/) um cara com a seguinte dúvida: “eu tenho um arquivo cujos campos estão separados por e o terceiro deles possui números. Como posso calcular a soma de todos os números desta coluna do arquivo?”

Mandei a seguinte resposta:

$ echo $(cut -f3 num | tr ‘\n’ +)0 | bc
20.1

Vamos por partes para entender melhor e primeiramente vamos ver como era o arquivo que fiz para teste:

$ cat num
a b 3.2
a z 4.5
w e 9.6
q w 2.8

Como pode-se ver, está dentro do padrão do problema, onde eu tenho como terceiro campo números reais. Vamos ver o que faria a primeira parte da linha de comandos, onde eu transformo os caracteres (new-line) em um sinal de mais (+):

$ cut -f3 num | tr ‘\n’ +
3.2+4.5+9.6+2.8+

Se eu mandasse desse jeito para o bc, ele me devolveria um erro por causa daquele sinal de mais (+) solto no final do texto. A minha saída foi colocar um zero no final, pois somando zero o resultado não se alterará. Vamos ver então como ficou:

$ echo $(cut -f3 num | tr -s ‘\n’ +)0
3.2+4.5+9.6+2.8+0

Isso é o que se costuma chamar one-liner, isto é, códigos que seriam complicados em outras linguagens (normalmente seria necessário criar contadores e fazer um loop de leitura somando o terceiro campo ao contador) e em Shell são escritos em uma única linha.

Há também gente que chama isso de método KISS, que é o acrônimo de Keep It Simple Stupid. smile

Mas o potencial de uso desta calculadora não se encerra aí, existem diversas facilidades por ela propiciadas. Veja só este exemplo:

$ echo “obase=16; 11579594” | bc
B0B0CA

$ echo “ibase=16; B0B0CA” | bc # B, zero, B, zero, C, e A
11579594

Nestes exemplos vimos como fazer mudanças de base de numeração com o uso do bc. Na primeira explicitamos a base de saída (obase) como 16 (hexadecimal) e na segunda, dissemos que a base da entrada (ibase) era 10 (decimal).

Outras formas de trabalhar com inteiros

Outra forma muito legal de fazer cálculos é usar a notação $((exp aritmética)). É bom ficar atento, porém, ao fato desta sintaxe não ser universalizada. O Bourne Shell (sh), por exemplo, não a reconhece.

Exemplo:

Usando o mesmo exemplo que já havíamos usado:

$ echo $(((2+3)*5)) # Os parênteses mais internos priorizaram o 2+3
25

Agora olha só esta maluquice:

$ tres=3
$ echo $(((2+tres)*5)) # Variável tres não precedida pelo $
25

$ echo $(((2+$tres)*5)) # Variável tres precedida pelo $
25

Ué!! Não é o cifrão ($) precedente que caracteriza uma variável? Sim, porém em todos os sabores UNIX que testei, sob bash ou ksh, ambas as formas de construção produzem uma boa aritmética.

Preste a atenção nesta seqüência:

$ unset i # $i mooorreu!
$ echo $((i++))
0

$ echo $i
1

$ echo $((++i))
2

$ echo $i
2

Repare que apesar da variável não estar definida, pois foi feito um unset nela, nenhum dos comandos acusou erro, porque, como estamos usando construções aritméticas, sempre que uma variável não existe, é inicializada com zero (0).

Repare que o i++ produziu zero (0). Isto ocorre porque este tipo de construção chama-se pós-incrementação, isto é, primeiramente o comando é executado e só então a variável é incrementada. No caso do ++i, foi feita uma pré-incrementação: primeiro incrementou e somente após o comando foi executado.

Também são válidos:

$ echo $((i+=3))
5

$ echo $i
5

$ echo $((i*=3))
15

$ echo $i
15

$ echo $((i%=2))
1

$ echo $i
1

Estas três operações seriam o mesmo que:

    i=$((i+3))
    i=$((i*3))
    i=$((i%2))

E isto seria válido para todos os operadores aritméticos o que em resumo produziria a tabela a seguir:

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

Mas o auge desta forma de construção com duplo parênteses é o seguinte:

$ echo $var
50

$ var=$((var>40 ? var-40 : var+40))
$ echo $var
10

$ var=$((var>40 ? var-40 : var+40))
$ echo $var
50

Este tipo de construção deve ser lido da seguinte forma: caso a variável var seja maior que 40 (var>40), então (?) faça var igual a var menos 40 (var-40), senão (:) faça var igual a var mais 40 (var+40). O que quis dizer é que os caracteres ponto-de-interrogação (?) e dois-pontos (:) fazem o papel de “então” e “senão”, servindo desta forma para montar uma operação aritmética condicional.

Da mesma forma que usamos a expressão $((...)) para fazer operações aritméticas, também poderíamos usar a intrínseca (built-in) let ou construção do tipo $[...].

Os operadores são os mesmos para estas três formas de construção, o que varia um pouco é a operação aritmética condicional com o uso do let. Vejamos como seria:

$ echo $var
50

$ let var=’var>40 ? var-40 : var+40′
$ echo $var
10

$ let var=’var>40 ? var-40 : var+40′
$ echo $var
50

Baseando

Se você quiser trabalhar com bases diferentes da decimal, basta usar o formato:

    base#numero

Onde base é um número decimal entre 2 e 64 representando o sistema de numeração, e numero é um número no sistema numérico definido por base. Se base# for omitida, então 10 é assumida como default. Os algarismos maiores que 9 são representados por letras minúsculas, maiúsculas, @ e _, nesta ordem.

Se base for menor ou igual a 36, maiúsculas ou minúsculas podem ser usadas indiferentemente para definir algarismos maiores que 9 (não está mal escrito, os algarismos do sistema hexadecimal, por exemplo, variam entre 0 (zero) e F). Vejamos como isso funciona:

$ echo $[2#11]
3

$ echo $((16#a))
10

$ echo $((16#A))
10

$ echo $((2#11 + 16#a))
13

$ echo $[64#a]
10

$ echo $[64#A]
36

$ echo $((64#@))
62

$ echo $((64#_))
63

Nestes exemplos usei as notações $((...)) e $[...] indistintamente, para demonstrar que ambas funcionam.

Funciona também uma mudança automática para a base decimal, desde que você esteja usando a convenção numérica do C, isto é, em 0xNN, o NN será tratado como um hexadecimal e em 0NN, o NN será visto como um octal. Veja o exemplo:

Exemplo

$ echo $((10)) # decimal
10

$ echo $((010)) # octal
8

$ echo $((0x10)) # hexadecimal
16

$ echo $((10+010+0x10)) # Decimal + octal + hexadecimal
64

Ah, já ia me esquecendo! As expressões aritméticas com os formatos $((...)), $[...] e com o comando let usam os mesmos operadores usados na instrução expr, além dos operadores unários (++, --, +=, *=, ...) e condicionais que acabamos de ver.

Testes usando expressões regulares

No Papo de Botequim 004, nós falamos tudo sobre comandos condicionais, mas faltou um que não existia àquela época. Neste mesmo Papo de Botequim, na seção E tome de test nós chegamos a falar de uma construção do tipo:

    [[ Expressao ]] && cmd

Onde o comando cmd será executado caso a expressão condicional Expressao seja verdadeira. Disse ainda que Expressao poderia ser estipulada de acordo com as regras de Geração de Nome de Arquivos (File Name Generation). A partir do bash versão 3, foi incorporado a esta forma de teste um operador representado por =~, cuja finalidade é fazer comparações com Expressões Regulares.

Exemplo:

$ echo $BASH_VERSION # Conferindo se a versão do Bash é igual ou superior a 3.0.0
3.2.17(15)-release

$ Cargo=Senador
$ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && echo É político
É político

$ Cargo=Senadora
$ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && echo É político
É político

$ Cargo=Diretor
$ [[ $Cargo =~ ^(Governa|Sena|Verea)dora?$ ]] && echo É político
$

Vamos dar uma esmiuçada na Expressão Regular ^(Governa|Sena|Verea)dora?$: ela casa com tudo que começa (^) por Governa, ou (|) Sena, ou (|) Verea, seguido de dor e seguido de um a opcional (?). O cifrão ($) serve para marcar o fim. Em outras palavras esta Expressão Regular casa com Governador, Senador, Vereador, Governadora, Senadora e Vereadora.

Colorindo a tela

Como você já havia visto no Papo de Botequim 007, o comando tput serve para fazer quase tudo referente a formatação de tela, mas o que não foi dito é que com ele também pode-se usar cores de frente (dos caracteres) e de fundo. Existem também outras formas de fazer o mesmo, acho porém, esta que veremos agora, mais intuitiva (ou menos “desintuitiva”). A tabela a seguir mostra os comandos para especificarmos os padrões de cores de frente (foreground) ou de fundo (background):

Obtendo cores com o comando tput
Comando Efeito
tput setaf n Especifica n como a cor de frente (foreground)
tput setab n Especifica n como a cor de fundo (background)

Bem, agora você já sabe como especificar o par de cores, mas ainda não sabe as cores. A tabela a seguir mostra os valores que o n (da tabela anterior) deve assumir para cada cor:

Valores das cores com o comando tput
Valor Cor
0 Preto
1 Vermelho
2 Verde
3 Marrom
4 Azul
5 Púrpura
6 Ciano
7 Cinza claro

Neste ponto você já pode começar a brincar com as cores.

– Mas peraí, ainda são muito poucas!

– É, tem toda razão… O problema é que ainda não lhe disse que se você colocar o terminal em modo de ênfase (tput bold), estas cores geram outras oito. Vamos montar então a tabela definitiva de cores:

Valores das cores com o comando tput
Valor Cor Cor após tput bold
0 Preto Cinza escuro
1 Vermelho Vermelho claro
2 Verde Verde claro
3 Marron Amarelo
4 Azul Azul Brilhante
5 Púrpura Rosa
6 Ciano Ciano claro
7 Cinza claro Branco

Exemplo

Como exemplo, vejamos um script que mudará a cor de sua tela de acordo com sua preferência.

$ cat mudacor.sh
#!/bin/bash
tput sgr0
clear

# Carregando as 8 cores básicas para um vetor
Cores=(Preto Vermelho Verde Marrom Azul Púrpura Ciano “Cinza claro”)

# Listando o menu de cores
echo ”
Opc Cor
= ===”
# A linha a seguir significa: para i começando de 1;
#+ enquanto i menor ou igual ao tamanho do vetor Cores;
#+ incremente o valor de i de 1 em 1
for ((i=1; i<=${#Cores[@]}; i++))
{
printf “%02d %s\n” $i “${Cores[i-1]}”
}

CL=
until [[ $CL == 0[1-8] || $CL == [1-8] ]]
do
read -p ”
Escolha a cor da letra: ” CL
done

# Para quem tem bash a partir da versao 3.2
#+ o test do until acima poderia ser feito
#+ usando-se Expressoes Regulares. Veja como:
#+ until [[ $CL =~ 0?[1-8] ]]
#+ do
#+ read -p ”
#+ Escolha a cor da letra: ” CL
#+ done

CF=
until [[ $CF == 0[1-8] || $CF == [1-8] ]]
do
read -p ”
Escolha a cor de fundo: ” CF
done

let CL– ; let CF– # Porque as cores variam de zero a sete
tput setaf $CL
tput setab $CF
clear

Ganhando o jogo com mais coringas

Estava eu lendo meus e-mails quando recebo um do Tiago enviado para a lista de Shell Script (já falei da lista e do Tiago no Rotatório Peczenyj). A seguir o conteúdo do e-mail:

Não sei se é conhecimento de todos mas o shell possui, alem do globbing normal (a expansão *, ? e [a-z] de nomes de arquivos e diretórios), um globbing extendido.

Acho que, em alguns casos, podera ser BEM util, eliminando um pipe para um grep por exemplo.

São eles:

    ?(padrao)

Casa zero ou uma ocorrência de um determinado padrao

    *(padrao)

Casa zero ou mais ocorrências de um determinado padrao

    +(padrao)

Casa uma ou mais ocorrências de um determinado padrao

    @(padrao)

Casa com exatamente uma ocorrência de um determinado padrao

    !(padrao)

Casa com qualquer coisa, exceto com padrao Para poder utilizá-lo precisa executar o shopt conforme o exemplo abaixo:

$ shopt -s extglob
$ ls
file filename filenamename fileutils

$ ls file?(name)
file filename

$ ls file*(name)
file filename filenamename

$ ls file+(name)
filename filenamename

$ ls file@(name)
filename

$ ls file!(name) # divertido esse
file filenamename fileutils

$ ls file+(name|utils)
filename filenamename fileutils

$ ls file@(name|utils) # “lembra” um {name,utils}
filename fileutils

Usando o awk para pesquisar por equivalência

Aí vai mais uma que o Tiago mandou para a lista de Shell Script do Yahoo (já falei da lista e do Tiago no Rotatório Peczenyj e no Ganhando o jogo com mais coringa)

Quem ja não passou por isso: Procurar uma palavra, porém uma letra acentuada, ou não, atrapalhou a busca?

Não descobri como fazer o grep ou sed aceitarem algo semelhante, mas o gawk aceita classes de equivalência!

Melhor explicar com um exemplo, onde vou listar o número da linha e a ocorrência casada:

$ cat dados
éco
eco
èco
êco
ëco
eço

$ awk ‘/^eco/{print NR,$1}’ dados
2 eco

$ awk ‘/^e[[=c=]]o/{print NR,$1}’ dados
2 eco
6 eço

$ awk ‘/^[[=e=]]co/{print NR,$1}’ dados
1 éco
2 eco
3 èco
4 êco
5 ëco

Ou seja, usar [=X=] permite que a expressão encontre a letra X estando acentuada ou não (é sensivel à localização corrente!).

A sintaxe é parecida com a das classes POSIX, trocando os dois-pontos (:) antes e depois da classe por sinais de igual (=).

Achei curioso e deve servir para algum caso semelhante ao descrito.

find – Procurando arquivo por características

Se você está como o Mr. Magoo, procurando em vão um arquivo, use o comando find que serve para procurar arquivos não só pelo nome, como por diversas outras características. Sua sintaxe é a seguinte:

    find [caminho ...] expressão [ação]

Parâmetros:

caminho Caminhos de diretório a partir do qual (porque ele é recursivo, sempre tentará entrar pelos subdiretórios “pendurados” neste) irá procurar pelos arquivos;
expressão Define quais critérios de pesquisa. Pode ser uma combinação entre vários tipos de procura;
ação Define que ação executar com os arquivos que atender aos critérios de pesquisa definidos por expressão.

Os principais critérios de pesquisa definidos por expressão são:

-name Procura arquivos que tenham o nome especificado. Aqui podem ser usados metacaracteres ou caracteres curingas, porém estes caracteres deverão estar entre aspas, apóstrofos ou imediatamente precedidos por uma contrabarra isso porque quem tem de expandir os coringas é o find. Se fosse o Shell que os expandisse, isto seria feito somente com relação ao diretório corrente, o que jogaria por terra a característica recursiva do find;
-user Procura arquivos que tenham usuário como dono;
-group Procura arquivos que tenham grupo como grupo dono;
-type c Procura por arquivos que tenham o tipo c, correspondente à letra do tipo do arquivo. Os tipos aceitos estão na tabela a seguir:
  Valores de c Tipo de arquivo procurado
  b Arquivo especial acessado a bloco
  c Arquivo especial acessado a caractere
  d Diretório
  p Named pipe (FIFO)
  f Arquivo normal
  l Link simbólico
  s Socket
-size ±n[unid] Procura por arquivos que usam mais (+n) de n unidades unid de espaço ou a menos (-n) de n unidades unid de espaço.
  Unidades Valor
  b Bloco de 512 bytes (valor default)
  c Caracteres
  k Kilobytes (1024 bytes)
  w Palavras (2 bytes)
-atime ±d Procura por arquivos que foram acessados há mais (+d) de d dias ou a menos (-d) de d dias;
-ctime ±d Procura por arquivos cujo status mudou há mais (+d) de d dias ou a menos (-d) de d dias;
-mtime ±d Procura por arquivos cujos dados foram modificados há mais (+d) de d dias ou a menos (-d) de d dias;

Para usar mais de um critério de pesquisa, faça:
expressão1 expressão2
ou
expressão1 –a expressão2
para atender aos critérios especificados por expressão1 e expressão2;
expressão1 –o expressão2
para atender aos critérios especificados por expressão1 ou expressão2.

As principais ações definidas para ação são:

-print Esta opção faz com que os arquivos encontrados sejam exibidos na tela. Esta é a opção default no Linux. Nos outros sabores Unix que conheço, se nenhuma ação for especificada, ocorrerá um erro;
-exec cmd {} \; Executa o comando cmd. O escopo de comando é considerado encerrado quando um ponto-e-vírgula (;) é encontrado. A cadeia {} é substituída pelo nome de cada arquivo que satisfaz ao critério de pesquisa e a linha assim formada é executada. Assim como foi dito para a opção –name, o ponto-e-vírgula (;) deve ser precedido por uma contrabarra (\), ou deve estar entre aspas ou apóstrofos;
-ok cmd {} \; O mesmo que o anterior porém pergunta se pode executar a instrução cmd sobre cada arquivo que atende ao critério de pesquisa;
-printf formato Permite que se escolha os campos que serão listados e formata a saída de acordo com o especificado em formato.

Exemplos:

Para listar na tela (-print) todos os arquivos, a partir do diretório corrente, terminados por .sh, faça:

$ find . -name \*.sh Ação não especificada –print é default
./undelete.sh
./ntod.sh
estes quatro primeiros arquivos foram
./dton.sh
encontrados no diretório corrente.
./graph.sh
./tstsh/cotafs.sh
./tstsh/data.sh
Estes quatro foram encontrados no
./tstsh/velha.sh
diretório tstsh, sob o diretório corrente
./tstsh/charascii.sh

Preciso abrir espaço em um determinado file system com muita urgência, então vou remover arquivos com mais de um megabyte e cujo último acesso foi há mais de 60 dias. Para isso, vou para este file system e faço:

$ find . –type f –size +1000000c –atime +60 –exec rm {} \;

Repare que no exemplo acima usei três critérios de pesquisa, a saber:

-type f Todos os arquivos regulares (normais)
-size +1000000c Tamanho maior do que 1000000 de caracteres (+1000000c)
-atime +60 Último acesso há mais de 60 (+60) dias.

Repare ainda que entre estes três critérios foi usado o conector e, isto é, arquivos regulares e maiores que 1MByte e sem acesso há mais de 60 dias.

Para listar todos os arquivos do disco terminados por .sh ou .txt, faria:

$ find / -name \*.sh –o –name \*.txt –print

Neste exemplo devemos salientar além das contrabarras (\) antes dos asteriscos (*), o uso do –o para uma ou outra extensão e que o diretório inicial era o raiz (/); assim sendo, esta pesquisa deu-se no disco inteiro (o que freqüentemente é bastante demorado).

Com o printf é possível formatar a saída do comando find e especificar os dados desejados. A formatação do printf é muito semelhante à do mesmo comando na linguagem C e interpreta caracteres de formatação precedidos por um símbolo de percentual (%). Vejamos seus efeitos sobre a formatação:

Caractere Significado
%f Nome do arquivo (caminho completo não aparece)
%F Indica a qual tipo de file system o arquivo pertence
%g Grupo ao qual o arquivo pertence
%G Grupo ao qual o arquivo pertence (GID- Numérico)
%h Caminho completo do arquivo (tudo menos o nome)
%i Número do inode do arquivo (em decimal)
%m Permissão do arquivo (em octal)
%p Nome do arquivo
%s Tamanho do arquivo
%u Nome de usuário (username) do dono do arquivo
%U Número do usuário (UID) do dono do arquivo

Também é possível formatar datas e horas obedecendo às tabelas a seguir:

Caractere Significado
%a Data do último acesso
%c Data de criação
%t Data de alteração

Os três caracteres anteriores produzem uma data semelhante ao do comando date.

Veja um exemplo:

$ find . -name “.b*” -printf ‘%t %p\n’
Mon Nov 29 11:18:51 2004 ./.bash_logout
Tue Nov 1 09:44:16 2005 ./.bash_profile
Tue Nov 1 09:45:28 2005 ./.bashrc
Fri Dec 23 20:32:31 2005 ./.bash_history

Nesse exemplo, o %p foi o responsável por colocar os nomes dos arquivos. Caso fosse omitido, somente as datas seriam listadas. Observe ainda que ao final foi colocado um /n. Sem ele não haveria salto de linha e a listagem anterior seria uma grande tripa.

Essas datas também podem ser formatadas, para isso basta passar as letras da tabela anterior para maiúsculas (%A, %C e %T) e usar um dos formatadores das duas tabelas a seguir:

Tabela de formatação de tempo
Caractere Significado
H Hora (00..23)
I Hora (01..12)
k Hora (0..23)
l Hora (1..12)
M Minuto (00..59)
p AM or PM
r Horário de 12 horas (hh:mm:ss) seguido de AM ou PM
S Segundos (00 … 61)
T Horário de 24-horas (hh:mm:ss)
Z Fuso horário (na Cidade Maravilhosa BRST)
Tabela de formatação de datas
Caractere Significado
a Dia da semana abreviado (Dom…Sab)
A Dia da semana por extenso (Domingo…Sábado)
b Nome do mês abreviado (Jan…Dez)
B Dia do mês por extenso (Janeiro…Dezembro)
c Data e hora completa (Fri Dec 23 15:21:41 2005)
d Dia do mês (01…31)
D Data no formato mm/dd/aa
h Idêntico a b
j Dia seqüencial do ano (001…366)
m Mês (01…12)
U Semana seqüencial do ano. Domingo como 1º dia da semana (00…53)
w Dia seqüencial da semana (0..6)
W Semana seqüencial do ano. Segunda-feira como 1º dia da semana (00…53)
x Representação da data no formato do país (definido por $LC_ALL)
y Ano com 2 dígitos (00…99)
Y Ano com 4 dígitos

Para melhorar a situação, vejamos uns exemplos; porém, vejamos primeiro quais são os arquivos do diretório corrente que começam por .b:

$ ls -la .b*
-rw——- 1 d276707 ssup 21419 Dec 26 17:35 .bash_history
-rw-r–r– 1 d276707 ssup 24 Nov 29 2004 .bash_logout
-rw-r–r– 1 d276707 ssup 194 Nov 1 09:44 .bash_profile
-rw-r–r– 1 d276707 ssup 142 Nov 1 09:45 .bashrc

Para listar esses arquivos em ordem de tamanho, podemos fazer:

$ find . -name “.b*” -printf ‘%s\t%p\n’ | sort -n
24 ./.bash_logout
142 ./.bashrc
194 ./.bash_profile
21419 ./.bash_history

No exemplo que acabamos de ver, o \t foi substituído por um na saída de forma a tornar a listagem mais legível. Para listar os mesmos arquivos classificados por data e hora da última alteração:

$ find . -name “.b*” -printf ‘%TY-%Tm-%Td %TH:%TM:%TS %p\n’ | sort
2004-11-29 11:18:51 ./.bash_logout
2005-11-01 09:44:16 ./.bash_profile
2005-11-01 09:45:28 ./.bashrc
2005-12-26 17:35:13 ./.bash_history

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!