Programação em Shell (bash)

Iniciação à programação em Shell

O objectivo da programação em shell, é simplificar a execução de múltiplos comandos, e a realização de ciclos repetitivos tirando partido dos potentes comandos existentes no UNIX. Estes "programas" apesar de serem muitas vezes mais lentos a executar que os programas escritos em C/C++ ou ADA, têm a vantagem de não necessitarem de ser compilados. E como na maior parte das vezes, as capacidades que necessitamos podem ser conseguidas com um ou mais comandos UNIX, os shell scripts tornam a vida de um administrador muito mais simples.

O primeiro passo na criação de um script, é fazer um ficheiro de texto com o script. Para dizer à shell, que o ficheiro de texto é um script, devemos escrever nos dois primeiros caracteres da 1ª linha o código #!, que é o magic code referente a script (ver comando file). A seguir a este código deve-se indicar o interpretador para o script, no nosso caso /bin/bash, seguido dos parâmetros de configuração da shell (ex. -noprofile, para não carregar os ficheiros de profile). Os scripts perl e tcl/tk, etc. também utilizam este código, sendo somente alterado o interpretador.

A partir deste momento (se a flag x estiver activa), podemos executar o script directamente na linha de comando (./nome_do_script). A definição do interpretador do script é importante, pois se corrermos dentro de uma shell csh um script feito para bash sem a indicação da shell a utilizar, irão ocorrer erros devidos à diferente sintaxe destas shells (experimentem correr o script ola_mundo com a csh ou a tcsh).

O carácter # é também um carácter especial, que indica à shell que tudo o que está até ao fim da linha é comentário. No entanto, alguns comandos que utilizam este carácter (ex. ${#variável} - indica o tamanho em caracteres do conteúdo da variável)

De seguida apresenta-se um exemplo de como deve ser um script. Apesar de não serem necessários, e muitas vezes os programadores não os utilizarem, os comentários podem ser muito úteis mais tarde, quando quisermos saber para que serve um script, ou alguma parte do código. Ver o que fazer e o que não fazer nesta página.

#!/bin/bash -noprofile
# Autor:                 Paulo Matos
# Data:                  4/11/2002
# Programa:              Olá Mundo
# Versão:                1.0
# Modo de Utilização:    ola_mundo.sh [<device>]
# Função:                Escrever Olá Mundo para o device passado como parâmetro ou para o
#                        device devolvido pelo comando tty
#
# Ficheiros:             Ficheiros que o script utiliza
#
# Notas:                 Notas sobre mais pormenorizadas que as descritas em Função 
#                        sobre o script
# Historia:              Revisões e alterações efectuadas ao script
#

echo Olá Mundo > ${1:-`tty`} 2> /dev/null     # escreve olá mundo para o device e os erros para o "lixo"

Variáveis

O essencial sobre variáveis foi dito na página relativa à shell. No entanto ainda existem algumas coisas relativas à programação que são explicadas de seguida.

Variável Significado
$0 nome do script executado
$1 até $9 os primeiros 9 parâmetros passados ao script
$# número de parâmetros passados ao script
$* e $@ indicam todos os argumentos passados ao script (ver o script param.sh)
$! pid do último comando executado
$? valor devolvido pelo último comando executado

Como só é possível aceder a 9 parâmetros de cada vez, existe o comando shift, que atribui ao parâmetro $0 o $1, ao $1 o $2, e assim sucessivamente. No caso de ainda existirem mais de 9 parâmetros, ao $9 é atribuído o 10º parâmetro.

A partir do momento que é efectuado um shift, não é mais possível aceder ao parâmetro eliminado. O comando shift altera também as variáveis $#, $* e $@. No script param.sh, podem-se ver as diferenças entre a utilização de $*, "$*", $@, "$@" e shift.

#!/bin/bash
#  nome:         param.sh
#  utilização:   param.sh [<lista de parâmetros>]
#  exemplo:      param.sh texto 123 numero   olá   "teste com aspas"   'teste com    plicas   !!!'
echo '---- $* ----'
i=1
for valor in $*
do
        echo "arg $i: $valor"
        i=$[$i+1]
done

echo '---- "$*" ----'
i=1
for valor in "$*"
do
        echo "arg $i: $valor"
        i=$[$i+1]
done

echo '---- $@ ----'
i=1
for valor in $@
do
        echo "arg $i: $valor"
        i=$[$i+1]
done

echo '---- "$@" ----'
i=1
for valor in "$@"
do
        echo "arg $i: $valor"
        i=$[$i+1]
done

echo "---- shift ----"
i=1
while [ "$1" ]		# equivalente a teste "$1"  (ver comando test, ou executar help test)
do
        echo "arg $i: $1"
        i=$[$i+1]
        shift
done

Comando Read

Sempre que o utilizador necessita ler dados do stdin, pode utilizar o comando read

#!/bin/bash
# Nome:     read.sh

echo -n "escreva qualquer coisa:"   # ver a sintaxe do echo com o comando help echo
read texto                          # ver a sintaxe do read com o comando help read
echo "Escreveu:$texto"

Comando Test

Em todos as comandos que aceitam condições (ex. if, while) deve ser posto um comando nessa condição, sendo avaliado o valor devolvido por esse comando.

if ls "$1" > /dev/null 2>&1; then
    echo true
else
    echo false
fi

No exemplo anterior, se o comando ls devolver true (0) o código relativo ao then é executado, se devolver false (1) é executado o código relativo ao else.

Muitas das vezes o que queremos usar numa condição, é um teste a uma variável, uma comparação de valores, ou ver se um ficheiro existe e se pode ser escrito. Para conseguir isto, existe o comando test, que pode ser simplificado na sua escrita para [ (como utilizado no script param.sh)

Devem ter em atenção que como a separação de argumentos para o comando test é efectuada pela shell se uma variável tiver o valor "olá mundo", esta variável deve ser passada ao test entre aspas, pois de outro modo serão passados dois parâmetros e será gerado um erro. Do mesmo modo todos os argumentos devem ser separados por espaços pois de outro modo são identificados como um único parâmetro.

Existem vários switches que podem ser utilizados com o comando test e que podem ver nesta página. Na tabela seguinte está um resumo dos switches do comando test.

Expressão Verdadeira se
-z string tamanho de string é 0
-n string tamanho de string não é 0
string1 = string2 a string1 é igual à string2
string1 != string2 a string1 é diferente à string2
string a string não é nula
int1 -eq int2 o int1é igual ao int2
int1 -ne int2 o int1é diferente ao int2
int1 -gt int2 o int1é maior ao int2
int1 -ge int2 o int1é maior ou igual ao int2
int1 -lt int2 o int1é menor ao int2
int1 -le int2 o int1é menor ou igual ao int2
-r ficheiro o ficheiro existe e pode ser lido
-w ficheiro o ficheiro existe e pode ser escrito
-x ficheiro o ficheiro existe e pode ser executado
-f ficheiro o ficheiro existe e é um ficheiro normal
-d ficheiro o ficheiro existe e é um directório
-h ficheiro o ficheiro existe e é um link simbólico
-c ficheiro o ficheiro existe e é um device da caracteres
-b ficheiro o ficheiro existe e é um device de blocos
-p ficheiro o ficheiro existe e é um named pipe
-u ficheiro o ficheiro existe e é tem o setuid activo
-g ficheiro o ficheiro existe e é tem o setgid activo
-k ficheiro o ficheiro existe e é tem o seticky bit activo
-s ficheiro o ficheiro existe e tem um tamanho maior do que 0
! o resultado inverso de uma expressão
-a o operador lógico e (and)
-o o operador lógico ou (or)
( expr ) agrupa uma expressão, como os parêntesis têm significado especial para a shell, tem de ser de lhes ser retirado esse significado para o comando test.

if - then - else

De seguida são apresentadas as duas sintaxes possíveis para o comando if. Podem ser encadeados vários if. Devem ter em atenção que se o then ficar na mesma linha do if, têm separá-lo da condição com um ; (tentem descobrir porquê)

if comando; then
   comando_if_1
   comando_if_2
   .... 
else                   # opcional
   comando_else_1
   comando_else_2
   ....
fi                     # obrigatório
if comando; then
   comando_if_1
   comando_if_2
   .... 
elif comando; then
   comando_elif_1
   comando_elif_2
   ....
fi

Se o comando devolver 0 (true), é executado o código do then, se devolver 1 (false) é executado o código do else (se existir else).

No exemplo seguinte foi escrito o if numa só linha, sendo necessário colocar o ; a separar os vários "sub comandos".

if ls "fich.txt" > /dev/null 2>&1; then echo true; else echo false; fi

case

De seguida é apresentada as sintaxe do comando case. Nesta página existem vários exemplos com case.

case palavra in
    padrao11 [| padrao12...])  comando11
                               comando12
                               ....
                         ;;
    padrao21 [| padrao22...])  comando21
                               comando22
                               ....
                         ;;
    .....
esac

A seguir é apresentado um exemplo de um case, sendo utilizado o | para separar as alternativas de padrões.

echo -n "Escreva a sua resposta (s/n)?"
read resposta
case "$resposta" in
    s* | S*)  echo "A resposta foi sim"
    		;;
    [nN]*)    echo "A resposta foi não"
    		;;
    *)      echo "A resposta foi talvez :)"  # isto é um else
    		;;
esac

A seguir é apresentado o case anterior, condensado numa só linha.

case "$resposta" in s* | S*) echo "A resposta foi sim" ;; \
[nN]*) echo "A resposta foi não" ;; *) echo "A resposta foi \
talvez :)" ;; esac

Ciclos while e until

A seguir é apresentada a sintaxe dos ciclos while e until.

while comando
do
   comando1
   comando2
   .... 
done
until comando
do
   comando1
   comando2
   .... 
done

A seguir são apresentados exemplos do comando while e until.

i=10
while [ $i -ne 0 ]
do
    echo "$i"; i=$[$i-1]
done
i=10; while [ $i -ne 0 ]; do echo "$i"; i=$[$i-1];done
i=10; until [ $i -eq 0 ]; do echo "$i"; i=$[$i-1];done

O stdin (ou o stdout) de um ciclo while ou until, pode ser redireccionado para (de) um ficheiro tal como demonstrado no exemplo seguinte.

while read buffer
do
    echo -e "`echo $buffer | cut -d" " -f9` \t\t `echo $buffer | cut -d" " -f5`"
done < fich.lst      # ficheiro com uma listagem longa de um directório

Nesta página podem ser vistos exemplos dos ciclos while e until

Ciclo for

A seguir é apresentada a sintaxe do ciclo for. Se a opção in lista_de_valores for omitida, o for utiliza a variável "$@".

for variavel [in lista_de_valores]
do
   comando1
   comando2
   .... 
done

A seguir são apresentados alguns exemplos do comando for.

for numeros in "1 um" "2 dois" "3 tres" "4 quatro" "5 cinco" "6 seis"
do
    set -- $numeros     # faz o parsing da variável $numeros e atribui às variáveis posicionais
                        # se necessitar dos argumentos originais, estes devem ser copiados.
                        # args_originais=("$@") antes de entrar no ciclo
    echo "$1($2)"
done
for i in 10 9 8 7 6 5 4 3 2 1; do echo "$i"; done

Nesta página podem ser vistos exemplos do ciclo for

Interromper e continuar ciclos

Há alturas em que é necessário sair de um ciclo a meio, ou saltar de novo para o teste do ciclo.

Para interromper um ciclo é utilizada o comando break, que pode ter como parâmetro o número de níveis a sair (para ciclos encadeados). Ao sair de um ciclo a execução normal do script, é retomada depois o código do(s) ciclo(s).

Para continuar na próxima iteração de um ciclo, existe o comando continue que também pode receber um inteiro como parâmetro, que indica qual é o número de níveis que sai (ao contrário do break não continua a execução normal, mas vai para o teste do ciclo).

A seguir estão exemplos simples do break e do continue. Mais exemplos podem ser obtidos nesta página.

for i in 10 9 8 7 6 5 4 3 2 1
do
    if [ $i -lt 6 -a $i -gt 3 ]; then 
    	continue;
    fi
    echo "$i"
done
for i in 10 9 8 7 6 5 4 3 2 1
do
    if [ $i -lt 6 -a $i -gt 3 ]; then 
    	break;
    fi
    echo "$i"
done

Funções em scripting

Em programação shell, também é possível criar funções, e inclusive executar funções revulsivas. A seguir é apresentada um exemplo de uma função. Repare que a variável $num dentro da função é definida como local, sendo por isso diferente da variável $num existente fora da função.

#!/bin/bash

ERRO=-1

function factorial       # também podia ser definida como factorial()
{
   local num=$[$1-1]

   if [ $1 -lt 0 -o $1 -gt 12 ]; then
      return $ERRO  # devolve o valor definido para os erros (podiam ser diferentes)
   fi

   if [ $1 -eq 0 ]; then
        return 1
     else
        factorial $num
        res_fac=$[$?*($num+1)]  # o valor num é sempre local
   fi

   return $res_fac   # devolve o resultado
}

number()
{
    echo $1|grep -E -q -e "^[-+]?[0-9]*$"

    # o resultado devolvido, é o resultado do último comando executado
}

num=${1:-0}

if number $num; then
    factorial $num
    res_factorial=$?
    if [ "$res_factorial" -ne $ERRO ]; then
        echo "o factorial de $num e' $res_factorial"
    else
        echo "Erro no factorial, número negativo ou maior que 12"
    fi
else
    echo "o argumento passado não é um número"
fi

Esperar por programas em background

Quando estamos a criar scripts, pode ser necessário executar comandos em background, e mais à frente esperar que esse comando acabe para executar outro. O comando wait pid permite ficar à espera que o comando com o PID igual a pid acabe de executar. Se não for utilizado o argumento pid, é utilizado o pid do último comando executado ($!).

echo "vou correr um sleep de 10 segundos em background"
sleep 10 &
pid_sleep10=$!
echo "vou correr um sleep de 4 segundos em background"
sleep 4 &
pid_sleep4=$!
echo "estou a fazer outra coisa qualquer"
echo "vou esperar que o sleep de 4 acabe"
wait $pid_sleep4
echo "o sleep 4 acabou"
echo "vou esperar que o sleep de 10 acabe"
wait $pid_sleep10
echo "o sleep 10 acabou"

Responder a sinais do kill

Quando um determinado script recebe um sinal enviado pelo kill, existe a possibilidade de executar algum código relativo a esse sinal. O comando trap <comandos> <sinais> permite ao script quando recebe um determinado sinal, enviado pelo kill, executar os comandos (ex. apagar ficheiros temporários). Não é possível fazer um trap a um sinal SIGKILL(9). Existem vários exemplos do trap nesta página

#!/bin/bash

trap 'echo "recebi um SIGHUP"' SIGHUP                         # igual a 1 ou HUP
trap 'rm fich[12].tmp;echo "saida normal do programa"' EXIT   # sinal de saida normal
trap 'echo "saida com SIGQUIT ou SIGILL";exit 1' QUIT ILL     # trata 2 sinais
trap 'echo "Control-C disabled."' 2                           # trata o ctrl-c SIGINT

: > fich1.tmp       # Cria um ficheiro vazio. o comando : não faz nada.
: > fich2.tmp

i=0
while [ $i -lt 35 ]
do
  i=$[$i+1]
  echo $i
  if [ $i -eq 15 ]; then
      trap SIGINT			# desactiva trap para o SIGINT
      echo "O Control-C já funciona"
  fi
  sleep 1
done

exit 0

O comando eval

Este comando já foi apresentado na secção relativa à shell e serve para avaliar um comando 2 vezes. Uma das utilidades deste em scripting é guardar nomes de variáveis em variáveis, e aceder a estas variáveis em run-time. Nesta página existem alguns exemplos com o comando eval.

nome_var=var1
var1="Este é o conteudo da variavel var1"

echo "resultado do comando sem eval=\$$nome_var"
eval echo "resultado do comando com eval=\$$nome_var"

Programação com sintaxe C (bash >= 2.04)

Apesar da notação aqui apresentada ser a mais usual em scripts para a bash, nas versões mais recentes da bash, existe uma notação alternativa parecida com a do C, que se pode aplicar a atribuições, condições aritméticas, ciclos for e ciclos while.

Esta notação utiliza uma série de macros, e é conseguida utilizando as sintaxe (( ... )), não sendo necessário incluir o $ para referir variáveis.

Ultima alteração: terça-feira, 01 de Abril de 2003 às 16:06