logo titulo
anterior indice proximo

Programação com o shell


O shell, específicamente o bash, no nosso caso, é muito mais que uma simples interface para interpretação e execução de comandos. Ele é uma linguagem de programação poderosa, com estruturas de controle de alto nível, capaz de resolver muitos problemas com uma perda insignificante de velocidade na maioria das aplicações.

Já vimos como o shell executa interativamente. Na realidade a grande maioria dos comandos que estivemos analisando aqui foram comandos submtetidos ao shell para execução. Como poderíamos ampliar esse conceito, colocando comandos mais usados em um arquivo para posterior execução? Vejamos um simples exemplo:

#!/bin/sh
echo "O último comando executado retornou $?"
echo "O nome deste programa é $0"
echo "O PID deste processo é $$"
echo "O número de argumentos é $#"
echo "Eles são $*"
echo "O primeiro é $1"
exit       

Editando este programa no arquivo mostra-args e transformando ele em executável com chmod +x mostra-args, podemos testá-lo com: ./mostra-args Linux invade o Brasil. Isto retornará a seguinte saida:

~$ mostra-args Linux invade o Brasil
O último comando executado retornou 0
O nome deste programa é ./mostra-args
O PID deste processo é 1305
O número de argumentos é 4
Eles são Linux invade o Brasil 
O primeiro é Linux

A primeira linha do script contém uma sequência "mágica" de caracteres que indica ao shell que este programa é executável, e que deve ser invocado com o programa /bin/sh (que é o próprio bash!). As linhas com "#" são comentários, exceto pelo caso especial "#!" quando no começo do arquivo. Os parâmetros com nome $1 a $9 são os argumentos passados ao programa, $0 é o nome do comando (o arquivo onde foi colocado este script acima), $* é a lista de todos os argumentos (exceto o nome do programa), e $# é o número de argumentos passados ao programa, $? é o resultado (produzido com exit <resultado>) do último comando executado, e $$ é o PID (Process id) do process corrente.

repetindo para cada argumento

Observe este outro script: (programa à esquerda, execução à direita)

#!/bin/sh
# putdelim -- devolve parametros como 
# lista delimitada "",""...

for x in $*
do 
echo -n \"$x\", 
done
echo -e "\b " 
exit
~$ putdelim recife s.paulo brasilia
"recife","s.paulo","brasilia"

A construção for variavel in lista do ... done repete todo o interior (entre do e done), atribuindo para cada repetição um dos valores da lista para a variavel. Observe que o do tem que estar em linha separada do for. O echo no interior do loop (repeticão) usa a chave -n para evitar o avanço de linha (newline) no final da impressão, enquanto que o echo final usa a chave -e para permitir a interpretação de caracteres especiais como o \b (backspace), usado neste caso para apagar a última vírgula deixada pelo loop.

executando condicionalmente

#!/bin/sh
# tst-hora-almoco

if [ `date +%H%M` -eq 1200 ]
then
echo Hora de almoçar!
fi
exit
~$ tst-hora-almoco
Hora de almoçar! 

Agora introduzimos outra estrutura de controle if ... then ... fi. O teste é uma função que tem duas formas: [ expressão ] ou test expressão. Preferimos a primeira, mais elegante. O comando date +%H%M devolve 4 dígitos com a hora e minuto corrente concatenados., que é usado para testar igualdade (chave -eq do comando test) com o valor 1200. Se a hora atual coincidir com 12:00 h, a mensagem será mostrada, senão nada acontecerá. A estrutura if pode ter múltiplas cláusulas com elif .. then ou uma cláusula alternativa com else ....

O comando test suporta muitos tipos de testes distintos, não só para comparações entre variáveis, mas também para testar strings (cadeias de caracteres) e várias propriedades de arquivos. A tabela abaixo sumariza essas operações.

-op arquivo arquivo existe e é/tem... arq1 -op arq2 significa... comp. diversas (arg = argumento)
-b

-c

-d

-e

-f

-g

-k

-L

-p

-r

-s

-S

-u

-w

-x

-O

-G

dispositivo de bloco

dispositivo de caracter

diretório

existe (e mais nada)

arquivo normal

sgid (set group id)

bit stick ligado

link simbólico

pipe

readable

tamanho > 0

socket

suid (set user id)

writable

executável

propriedade do EUID atual

propriedade do EGID atual

-nt

-ot

-ef

arq1 mais novo que arq2

arq1 mais velho que arq2

são hard link um do outro
(mesmo disp. e inode)

-z string

-n string

string1 = string2

string1 != string2

! expr

expr1 -a expr2

expr1 -o expr2

arg1 -eq arg2

arg1 -ne arg2

arg1 -lt arg2

arg1 -le arg2

arg1 -gt arg2

arg1 -ge arg2

string com tamanho 0

string com tamanho > 0

strings iguais

strings diferentes

expressão falsa

expr1 AND expr2

expr1 OR expr2

arg1 = arg2

arg1 != arg2

arg1 < arg2

arg1 <= arg2

arg1 > arg2

arg1 >= arg2

Comandos do shell podem estar na mesma linha, se separados por ";". Veja como ficaria o if se reescrevessemos com vários comandos na mesma linha:

if [ `date +%H%M` -eq 1200 ]; then echo Hora de almoçar! ; fi exit

Esse tipo de "economia de espaço" deve ser evitada a todo custo, pois torna o script quase ilegível. Note que o then não tem vírgula após, pois ele não é realmente um comando.

obtendo dados interativamente

#!/bin/sh
# pergunta-nome
 
echo -n "Entre seu nome:" 
read NOME SOBRENOME
echo "Prazer em encontrá-lo $NOME!"
exit
~$ pergunta-nome

Aqui introduzimos o comando read, que lê stdin um ou mais argumentos e armazena em variáveis (no caso NOME e SOBRENOME) do environment. Os separadores contidos na variável IFS são usados para quebrar a linha em tokens (palavras. "Token" é ficha). Note também que, para obter o nome do usuário, não seria necessário essa pergunta, pois ele está disponível em $LOGNAME.

outras estruturas de controle

#!/bin/sh
# le-arquivos -- le os vários arquivos 
# intoduzidos como argumentos e os 
# mostra na saida 
 
LINE=         # linha obtida do arquivo
IFS=          # sem separadores      
while [ $# -gt 0 ] 
do      
   if [ ! -r "$1" ]; then
      echo "Não consigo encontrar $1" >&2 
      exit 1     
   else      
      while read LINE           
         do 
            echo "$LINE"
         done < $1
   fi      
   shift 
done
exit
~$ le-arquivos fortune1 fortune2
"I am not an Economist.  
 I am an honest man!"
      -- Paul McCracken
The truth is what is;
  what should be is a dirty lie.
      -- Lenny Bruce

Neste exemplo, o while <teste> do ... done é uma estrutura que repete os comandos no seu interior enquanto o <teste> for verdade. Existe também o until, que repete igualmente mas enquanto o <teste> for falso. O comando read é usado para ler a entrada do stdin, mas nesse exemplo, stdin está redirecionado para o arquivo (linha com done < $1).
Finalmente, o comando shift permite ler mais que 9 argumentos (variáveis $1 ... $9), deslocando os seus valores. A variável $9 assumirá o valor da próxima (seria $10, que não é possível), e a $1 será perdida.

Observe que este programa (le-arquivos) é similar ao comando cat, já existente.

o comando select

#!/bin/sh
# sel-edit -- seleciona um arquivo 
# fonte (*.c *.h) e edita

select FNAME in *.c *.h
   do
     joe $FNAME
   done
exit
~$ sel-edit
1) checklist.c    7) kvptyaux.c  13) ptytest.c
2) dialog.c       8) menubox.c   14) rc.c
3) guage.c        9) mouse.c     15) tcl_curses.c
4) inputbox.c    10) msgbox.c    16) textbox.c
5) kvadap_main.c 11) pcnt.c      17) util.c
6) kvpty.c       12) ptypair.c   18) yesno.c
#?

Como podemos ver, este comando gera um menu com todos os argumentos dado na lista (no caso *.c *.h) e cria um prompt diferente (que pode ser editado na variável PS3 do environment). Então o usuário deve escolher uma opção do menu e os comandos entre do ... done serão executados com a variável (no caso FNAME) atribuida o valor correspondente à opção do menu escolhida. O comando é repetido continuadamente até o usuário introduzir eof (Ctrl-d).

múltiplos casos

Agora iremos elaborar um programa para nos lembrar dos feriados mais importantes. Usaremos uma nova estrutura de controle, o case, que é uma espécide de múltiplo if...fi. Sua forma genérica é case <var> in <pad1>) cmds1 ;; <pad2>) cmds2 ;; esac.

#!/bin/sh
# feriados -- mostra principais
# feriados nacionais

HOJE=`date +%d/%m`

case $HOJE in    
   01/01) echo Confraternização universal ;;    
   24/06) echo São João ;;    
   07/09) echo Independência ;;    
   15/11) echo Procl. República ;;    
   25/12) echo Natal ;;        
       *) echo Um dia como qualquer outro ;;
esac
exit
~$ feriados
Um dia como qualquer outro

O comando date +%m/%d irá gerar a data no formato dd/mm (exemplo, hoje é 15/01), que é armazenada na variável HOJE.
O padrão "*", evidentemente, "casa" com qualquer coisa, por isso foi usado como última opção.

funções

O bash, com qualquer linguagem procedural, possui funções. Seu formato genérico é <nome da função> ( ) { ... }. Adicionalmente, funções podem ter variáveis locais, precedendo a tribuição da palavra local. Funçoes do shell podem ser recursivas, como mostra a função fatorial abaixo. Edite o arquivo em qualquer arquivo (pode até mesmo ter o nome fat, mas não necessariamente) e execute alguns exemplos. Esta função cresce rapidamente, portanto não use valores que não caibam em um número inteiro, senão os valores obtidos serão inconsistentes (zero). Se for requerido maior precisão, use o programa bc para fazer os cálculos.

#!/bin/sh 
# função fatorial

fat () {     
   local num=$1;     
   if [ "$num" = 1 ] ; then         
      echo 1         
      return ;     
   fi;     
   echo $(( $num * $(fat $(( $num - 1 )) ) )) 
}

echo `fat $1` 
exit
~$ fat 5
120
~$ fat 23
862453760

outros comandos importantes do bash


Seria necessário um livro inteiro somente sobre o bash para cobrir de forma compreensiva as suas características. Demos apenas uma amostra do que pode ser feito com este shell, mas gostaríamos de deixar claro alguns comandos importantes que foram deixados de fora e sua finalidade.

break
vai para o final de um loop (while, until ou for), abortando a execução deste. Com um parâmetro "n" aborta n loops de profundidade.
continue
retorna para o início do loop mais interno, continuando no próximo ciclo.
return
devolve um valor a partir de uma função. O default é retornar o valor do último comando executado.
wait
aguarda a conclusão de um processo "filho"
trap
captura um sinal (signal) e executa um comando quando este é enviado, na forma trap <cmando> <sinal>.
exec
troca o programa corrente pelo fornecido (não cria novo processo), efetuando redirecionamentos. É útil também para abertura de novos arquivos de dentro de um script: exec 3< carta.txt abrirá o arquivo para leitura com o descritor 3.

Outro exemplo: exec 3< .profile; cat <&3 irá listar o arquivo, mas usando outro descritor (3) para a leitura.

eval
força a re-avaliação dos argumentos. (uma nova rodade de interpretação)
Exemplo:
~$ MEU_TERMINAL= \$TERM
~$ echo $MEU_TERMINAL
$TERM
~$ eval echo $MEU_TERMINAL
xterm


rpragana
Fri Jan 15 16:45:56 EDT 1999