PROGRAMAÇÃO BOURNE-SHELL
Para além da utilização no dia-a-dia, o Shell pode ser usado para criar programas que se transformam em novos comandos. Estes programas são designados geralmente "shell scripts" ou "shell procedures". Este manual pretende ensinar a forma de criar e executar estes programas, utilizando comandos, variáveis, parâmetros posicionais, códigos de resultado e estruturas de controlo de fluxo básicas.
A programação em Shell é muito útil para criar pequenos programas que realizam tarefas do dia-a-dia, que poderiam ser realizadas à mão utilizando os comandos já existentes no UNIX.
Quer o Bourne-Shell quer o C-Shell são apropriados para realizar programação, mas o Bourne-Shell executa mais rápidamente os programas e o C-Shell não existe em todas as máquinas UNIX. Por isto optamos e aconcelhamos a que toda a programação shell seja feita em Bourne-Shell, de modo a que os programas sejam mais port'aveis e mais rápidos. Todo este manual foi realizado com esta ideia em mente.
Vamos começar por criar um pequeno script que efectue as seguintes tarefas:
- Mostrar o nome do directório corrente.
- Listar o conteúdo do directório corrente.
- Indicar que terminou a tarefa.
Estas tarefas poderiam ser executadas manualmente através da utilização dos comandos "pwd", "ls" e "echo". Para automatizar esta tarefa basta criar um ficheiro com os comandos dentro. Utilize o seu editor preferido para criar um ficheiro chamado "dl" (directory list) com o seguinte conteúdo:
|
$
cat dl |
|
pwd |
|
ls |
|
echo
"fim do script" |
|
$ |
Figura 1. Visualização do script dl
Se já acabou, já tem o seu primeiro programa escrito em shell! Para o executar basta, a partir do shell, digitar o seguinte comando:
|
sh dl |
Figura 2. Execução de um shell script
Os comandos contidos no ficheiro "dl" são executados sequêncialmente pelo shell: o nome do directório é mostrado primeiro, seguido do resultado do comando ls, e finalmente o echo diz "fim do script.".
Se dl é um comando útil, é possivel transformá-lo num ficheiro executável, utilizando o comando chmod:
|
$
ls -l dl -rw-------
1 Manel ic 29 Jan 4 14:49 dl $
chmod u+x dl $
ls -l dl -rwx------
1 Manel ic 29 Jan 4 14:49 dl $
dl /usr/users/ic/fernandez/tx SU2 dl fim
do script. |
Figura 3. dl como um ficheiro executável
O passo seguinte é tornar os nossos novos comandos acessíveis a partir de ualquer directório, tal como os comandos comuns do UNIX, que estão localizados nos directórios /bin e /usr/bin. Começamos por arrumá-los cuidadosamente num sub-directório bin do nosso directório.
Seguidamente alteramos a variável de ambiente PATH de modo a que o shell passe a procurar comandos também neste nosso sub-directório.
|
$
cd $
mkdir bin $
mv dl bin/dl $
ls -l bin total
1 -rwx------
1 Manel ic 29 Jan 4 14:49 dl $
echo $PATH /bin:/usr/bin $
echo $HOME /usr/users/ic/fernandez $
PATH=$HOME/bin:$PATH $
echo $PATH /usr/users/ic/fernandez:/bin:/usr/bin $ |
Figura 4. Preparação do ambiente de trabalho
Agora, se tudo correu bem, será possivel chamar o novo comando "dl" de qualquer directório. Experimente.
O nome "bin" dado ao nosso sub-directório não é obrigatório. Podiamos ter-lhe chamado "progs" ou "scripts" ou até "xkyzz.dir", mas optámos por escolher um nome antigo da tradição UNIX - "bin" - que é usado para directórios que contêm programas executáveis. Mas se optar por mudar o nome ao seu não se esqueça de alterar também a variável PATH.
É possivel dar qualquer nome ao script, desde que este seja um nome de ficheiro v'alido. No entanto, não é nada aconcelhável [1] dar-lhes nomes de comandos já existentes no sistema. Por exemplo, se tivéssemos chamado "mv" ao nosso programa, cada vez que tent'assemos mudar o nome a um ficheiro o shell executaria este nosso script. Dificilmente seria o que desejávamos...
Outro problema surgiria se lhe cham'assemos "ls", já que o próprio script invoca o comando ls. Seria criado um ciclo infinito, já que cada chamada ao nosso programa "ls" invocaria outro comando "ls" e nunca terminaria nenhum deles. Após algum tempo, o sistema emitiria a seguinte mensagem de erro: "Too many processes, cannot fork" que significa que foram criados demasiados processos, atingindo o limite imposto pelo sistema.
O que aconteceu? Digitámos o nosso novo comando - ls. O shell leu o ficheiro e executou o primeiro comando - pwd. Então leu o segundo comando - ls - e tentou executar outra vez o nosso comando ls. Isto criou um ciclo infinito, porque cada um dos comandos "ls" executa outro comando "ls" e só pára quando um deles não consegue chamar o seguinte, porque o sistema já não deixa.
Os criadores do UNIX, sábiamente, limitaram o número de vezes que um ciclo infinito deste género executa. Uma forma de evitar que isto aconteça, mantendo o nome "ls" do nosso comando é indicar o nome completo do comando "ls" do sistema no script:
|
$
cat bin/ls pwd /bin/ls echo
"fim do script." $ |
Figura 5. Comando 'ls' alterado
A partir de Então, a única forma de executar o verdadeiro comando "ls" seria invocando-o com o seu nome completo: "/bin/ls".
As variáveis são os objectos básicos manipulados pelos programas de shell. O outro tipo de objectos são os ficheiros. Vamos discutir aqui três tipos de variáveis e como podem ser usados:
- Parâmetros posisionais.
- Parâmetros especiais.
- Variáveis identificadas (ou simplesmente: variáveis).
Um parâmetro posicional é uma variável dentro de um shell script cujo valor é fornecido pelo utilizador que chama o programa com argumentos na linha de comando. Cada argumento corresponde a um parâmetro posicional e estes estão numerados pela ordem com que foram colocados na linha de comando. Os parâmetros posicionais são referidos utilizando um sinal "$" seguido do número de ordem: $1, $2, $3, e assim por diante.
Um programa em shell pode referenciar até nove parâmetros posicionais. Se um programa em shell fôr chamado do seguinte modo:
|
$ myprog um dois três quatro cinco seis sete oito nove |
Então o parâmetro $1, dentro do programa terá o valor "um", o parâmetro $2 terá o valor "dois", e assim por diante.
Vamos criar um script chamado pp (no nosso directório bin) para verificar o funcionamento dos parâmetros posicionais. Este programa vai conter comandos echo de forma a mostrar cada um dos argumentos com que o programa fôr chamado.
|
$
cat bin/pp echo
O primeiro argumento é: $1 echo
O segundo argumento é: $2 echo
O terceiro argumento é: $3 echo
O quarto argumento é: $4 $ |
Figura 6. Script que mostra os argumentos
Não nos podemos esquecer de utilizar o comando "chmod u+x bin/pp". Seguidamente podemos então experimentar o seu funcionamento:
|
$
pp abc 94902 XPTO 2j%#j9-88 O
primeiro argumento é: abc O
segundo argumento é: 94902 O
terceiro argumento é: XPTO O
quarto argumento é: 2j%#j9-88 $ |
Figura 7. Utilização dos parâmetros posicionais
Com o programa que se segue é possivel enviar via "mail" uma mensagem de aniversário para outro utilizador [2] da máquina.
|
$
cat bin/parabens banner
Parabens! | mail $1 $
parabens suzana $ |
Figura 8. Exemplo útil de script com argumentos
A Suzana, quando entrar no sistema, recebe uma agradável mensagem de parabéns enviada por nós.
O comando "who" lista informação de quem está a trabalhar no sistema na altura em que é executado. Como criar um pequeno comando chamado "quem" que informe se determinado utilizador está a trabalhar correntemente?
Crie o seguinte ficheiro:
|
$
cat bin/quem who
| grep $1 $ |
Figura 9. Programa 'QUEM'
O comando "who" lista todos os utilizadores, e o "grep" procura uma linha contendo o que vier no primeiro argumento.
Tente utilizar o seu nome (o "logname", evidentemente, já que o sistema não conhece outro) como argumento do novo comando "quem". Digamos, por exemplo, que o seu logname é "ana". Quando se digita o comando "quem ana", o programa substituirá $1 por "ana" e executará o comando "who | grep ana".
|
$ quem ana ana tty18 Jan 5 11:38 $ |
Figura 10. Utilização do comando 'quem'
Este novo comando pode ainda ser utilizado para descobrir se alguém está a trabalhar em determinado terminal. Imaginemos que queríamos saber se o terminal "tty18" estava ocupado.
|
$
quem tty18 manel tty18 Jan 5 11:38 $ |
Figura 11. Outra utilização do script 'quem'
Já que o grep encontrou uma linha com "tty18" isto significaria que este posto de trabalho estava ocupado de momento.
Embora o shell permita chamar programas com um máximo de 128 argumentos, os scripts estão restringidos a um máximo de nove parâmetros posicionais, de $1 a $9. esta restrição pode ser torneada através da utilização do comando "shift", que funciona do seguinte modo:
|
$
cat bin/shift.ex echo
O primeiro argumento é: $1 echo
O segundo argumento é: $2 echo
O terceiro argumento é: $3 echo
O quarto argumento é: $4 echo
" n n n" shift echo
"Shiftou... n" echo
Agora o primeiro argumento é: $1 echo
Agora o segundo argumento é: $2 echo
Agora o terceiro argumento é: $3 echo
Agora o quarto argumento é: $4 $ |
Figura 12. Comando 'shift'
A execução do script "shift.ex" dá o seguinte output:
|
$
shift.ex AAA BBB CCC DDD EEE FFF O
primeiro argumento é: AAA O
segundo argumento é: BBB O
terceiro argumento é: CCC O
quarto argumento é: DDD Shiftou... Agora
o primeiro argumento é: BBB Agora
o segundo argumento é: CCC Agora
o terceiro argumento é: DDD Agora
o quarto argumento é: EEE |
Figura 13. Resultado do script 'shift.ex'
O parâmetro especial "$*", descrito na secção seguinte, também pode ser usado para acesso aos valores de todos os argumentos que vêm da linha de comando.
2.2.1. Número de
Argumentos - $#
Este parâmetro, quando referido num programa de shell, contém o número de argumentos com que o programa foi invocado. O seu valor pode ser utilizado em qualquer lugar do programa.
|
$
cat bin/get.num echo
O número de argumentos é $# $
get.num a b c d O
número de argumentos é 4 $ |
Figura 14. Parâmetro especial $#
2.2.2 Todos
argumentos - $*
Este parâmetro contém uma string com todos os argumentos com que o programa foi invocado. Não está restringido aos nove parâmetros posicionais, $1 a $9.
É possivel criar um pequeno script que demonstre a utilização deste parâmetro:
|
$
cat bin/show.param echo
$* $
show.param Isto são os parâmetros deste script. Isto
são os parâmetros deste script. $ |
Figura 15. Parâmetro especial $*
Existe também outro parâmetro especial - $@ - com um significado semelhante mas que tem ligeiras diferenças que não discutiremos aqui.
2.2.3. Estado de
saída do último comando - $?
Este parâmetro contém o estado de saída do último comando executado dentro do programa. Como o leitor deve saber, no UNIX os programas terminam devolvendo um valor numérico entre 0 e 255. Se o programa correu sem problemas deve devolver 0; se encontrou alguma situação esquisita que não tenha podido controlar devolve outro valor qualquer, normalmente 1. Mais para diante neste curso falaremos de como fazer com que os nossos próprios scripts devolvam "exit status" para quem os chamou. Neste momento vamos apenas ver como consultar o "exit status" de um programa que tenhamos chamado. O exemplo que se segue não é um programa mas uma sessão de shell interactiva, o que ajuda a demonstrar que o que se faz em scripts também se pode fazer à mão, e vice-versa.
|
$
cat myfile this
is the contents of this file. $
echo $? 0 $
cat xpto cat:
cannot open xpto $
echo $? 2 $ |
Figura 16. Exit status - Parâmetro $?
2.2.4.
Número do processo - $$
Outra coisa que o leitor deve saber é que cada pedido de execução de um programa numa máquina UNIX dá origem à criação de um processo. Não vamos aqui debater a natureza dos processos, mas é fundamental que o leitor saiba que todos os processos são numerados e que se dois utilizadores pedirem simultâneamente a execução do mesmo programa, serão criados dois processo diferentes (embora parecidos).
É possivel, em shell [3] saber qual o número do processo que corresponde ao nosso programa através da consulta do parâmetro $$ como se exemplifica seguidamente:
|
$
ps PID TTY TIME COMMAND 4195 tty18 0:04 sh $
echo $$ 4195 $ |
Figura 17. Número do nosso processo - parâmetro $$
2.2.5. Número de
processos em background - $!
O parâmetro $! contém o número de processo do último comando invocado para background. Eis um exemplo:
|
$
sleep 500 & 4490 $
ps PID TTY TIME COMMAND 4195 tty18 0:04 sh 4490 tty18 0:04 sleep $
echo $! 4490 $
kill -9 $! 4490
Killed $ |
Figura 18. Parâmetro $!
Este é um nome muito esquisito para se dar áquilo que se designa sempre por "variáveis", e dificilmente o ireis encontrar no resto deste texto. Os nomes das variáveis são da responsabilidade do programador/utilizador (isto é, da nossa responsabilidade). Os valores delas também são da nossa responsabilidade. Para quem já programou noutras linguagens, isto não se afigurará muito estranho. As variáveis do shell são normalmente designadas "strings" nas outras linguagens de programação.
No exemplo seguinte, var1 é uma variável e abcdef789 é o valor que nela se vai colocar:
|
var1=abcdef789 |
Um "$" colocado antes do nome da variável indica ao shell que deve substituir a sequência $var1 pelo seu conteúdo: "abcdef789". O primeiro caracter de um nome de variável deve ser uma letra ou um underscore ("_"). O resto do nome da variável pode ser constituido por letras, underscores ou algarismos. Tal como nos nomes dos programas, não é aconcelhável utilizar nomes de comandos nos nomes das variáveis. Além disto, o shell tem alguns nomes de variáveis reservados para si. Uma breve explanação destas pode ler-se seguidamente:
|
CDPATH: |
define o caminho de pesquisa para o comando "cd". |
|
HOME: |
o directório para onde se desloca o comando "cd" se invocado sem argumentos. |
|
IFS: |
contém os separadores de campos (argumentos) para a avaliação dos argumentos de um comando (isto tem muito que se lhe diga!...). |
|
LOGNAME: |
o nome do utilizador. |
|
MAIL: |
o ficheiro que contém o correio electrónico. |
|
PATH: |
o caminho de pesquisa de comandos (já falámos dele antes). |
|
PS1: |
a string de pedido de comando (em sessões interactivas), normalmente "$ ". |
|
PS2: |
a string secundária de pedido de comando (em sessões interactivas), normalmente "> ". |
|
TERM: |
o tipo de terminal, para ser usado pelos programas que usam as bases de dados de terminais (TERMCAP e TERMINFO). |
Podem-se afectar os valores contidos pelas variáveis de vários modos:
- Utilizar o modo "var=conteúdo".
- Utilizar o comando "read var" para que o próprio shell pe,ca o valor para a variável.
- Redireccionar o output de um comando para dentro de uma variável utilizando os acentos graves ("`...`").
- Assignar um parâmetro posicional a uma variável.
As secções que se seguem discutem detalhadamente cada um destes métodos.
2.4.1 Utilização do
sinal '=' (afectação)
Qualquer variável pode ser afectada com o sinal "=" com qualquer valor. Este valor é sempre considerado como uma string de caracteres e não deve conter espaços, a menos que seja indicada entre pelicas ou aspas. Assim sendo, dizer
|
a=Joao |
|
ou |
|
a="Joao Alberto Costa Morais" |
|
ou ainda |
|
x=123 |
são utilizações válidas do operando "=" enquanto que
|
b=Joao Alberto Costa Morais |
está errado.
2.4.2 Utilização do
comando 'Read'
O comando "read" inserido num script shell indica ao shell que deve pedir o valor da variável e afectá-la. é assim possivel fazer entradas de dados interactivas com programas de shell. O formato geral para a utilização deste comando é:
|
read variável |
Se um programa utilizar o comando "echo" antes de invocar o "read" é possivel dar indicações ao utilizador do género de "Indique o nome ...", de forma a que este saiba o que lhe está a ser pedido. O comando read esperará até que o utilizador digite uma string terminada por <RETURN>, e a variável passa a conter esta string. Todas as subsequentes invocações de $variável serão substituidas pela string que o utilizador digitou.
O exemplo seguinte mostra como criar um programa que permite consultar uma agenda telefónica contida num ficheiro chamado "tellist". O nosso programa chamar-se-á "telefone".
|
$
cat bin/telefone echo
"Indique o nome a procurar ou parte deste: c" read
nome grep
nome tellist $
telefone Indique
o nome a procurar ou parte deste: Pedro Pedro
Ferreira 253 88 56 $ |
Figura 19. Programa 'telefone'
Crie um ficheiro chamado "tellist" com alguns nomes como o seguinte:
|
$
cat tellist Pedro
Ferreira 253 88 56 Palma 87 26 88 Ana
Ferreira 253 38 91 Rui
Martins 77 63 21 $ |
Figura 20. Ficheiro 'tellist' com a lista telefónica.
A sequência " c" evita que o comando "echo" termine a mensagem mudando de linha.
O exemplo que se segue é um programa chamado "faz_lista" que adiciona nomes à lista telefónica existente:
|
$
cat bin/faz_lista echo
"Nome: c" read
nome echo
"Telefone: c" read
telefone echo
$nome $telefone >> tellist echo
"Registo adicionado." $
faz_lista Nome:
Alberto Albertini Telefone:
829949182 Registo
adicionado. $ |
Figura 21. Programa de adição à lista telefónica.
O programa funciona da seguinte forma:
|
echo |
envia uma mensagem para o terminal pedindo o nome de uma pessoa. |
|
read |
lê do teclado (standard input) o nome da pessoa e afecta a variável "nome" com esse valor. |
|
echo |
envia nova mensagem a pedir o número telefónico da pessoa. |
|
read |
lê o número. |
|
echo |
com o output redireccionado, cria novo registo no ficheiro que contém a lista telefónica. |
|
echo |
envia ao utilizador uma mensagem final, que indica que o programa terminou. |
O leitor deve lembrar-se que se quizer que o output do comando "echo" seja adicionado ao fim do ficheiro da lista telefónica deve usar ">>". Se usasse apenas ">" o ficheiro conteria apenas o ultimo número de telefone que tivesse sido adicionado.
É de notar também que a variável "nome" vai ser afectada, não só pelo primeironome "Alberto" mas por toda a resposta do utilizador, até ao fim da linha - "Alberto Albertini".
2.4.3. Afectação de
uma variável com o output de um comando
É sempre possível referir o output de qualquer comando em shell através da notação
|
`comando` |
Portanto, a maneira óbvia de afectar uma variável com o output de um comando é
|
var=`comando` |
O conteúdo da variável é modificado para conter o output do comando. Experimente o seguinte programa, que lhe dará a hora do dia, com base na data do sistema (que é dada pelo comando "date", como já sabe).
|
$
cat bin/h time=`date
| cut -c12-19` echo
"São $time" $
h São
14:41 $ |
Figura 22. Script para saber as horas.
É fundamental não deixar espaços de cada lado do sinal "=". Se se deixar um espaço do lado esquerdo o shell tentará executar um hipotético comando com o nome da variável. Se se deixar um espaço à direita do sinal, este espaço vai fazer parte do valor com que se está a afectar a variável.
2.4.4 Afectação com
parâmetros posicionais
É possivel afectar uma variável com um parâmetro posicional utilizando o seguinte formato:
|
var=$1 |
A programação em shell tem algumas particularidades que a tornam flexivel:
- Comentários, que permitem documentar a função dos programas.
- Redireccionamento interno, que permite ter dentro do texto do programa o standard input de um comando.
- O comando "exit", que permite abortar um programa em qualquer ponto da sua execução devolvendo "exit status" a quem os chamou.
- Os comandos para programação de tarefas cíclicas [4] - "for" e "while".
- Os comandos condicionais - "if" e "case" - que permitem executar comandos apenas se determinadas condições existirem.
- O comando "break", que permite sair incondicionalmente de um loop.
É possivel manter comentários num programa através da utilização do caracter "#". Todo o texto existente numa linha Após o "#" é ignorado. O "#" pode estar colocado no início da linha, caso em que toda a linha é ignorada, mas também pode estar colocado após um comando, caso em que o comando é executado, mas o resto da linha ignorado.
Por exemplo, o programa que contém as seguintes linhas ignora-las-á quando fôr executado:
|
#
Este programa envia parabéns #
Este programa recebe como argumento um logname, que #
espera que seja válido. |
Figura 23.Comentários inseridos num programa.
O redireccionamento interno permite ao programador colocar no script .linhas que vão servir de input a um comando. é uma forma de providenciar input a um comando sem ter que recorrer a outro ficheiro adicional. A notação consiste no símbolo "<<" e uma string delimitadora que especifica o principio e o fim do input do comando. Esta string pode ser uma qualquer, sendo muitas vezes usado o ponto de exclamação - "!".
No exemplo que se segue, o programa "parabens", criado anteriormente, foi modificado para utilizar esta forma de redireccionamento.
|
$
cat bin/parabens mail
$1 <<! $1: Soube
que fazes anos. Parabens. Manel ! $
parabens Maria |
Figura 24. Script 'parabens' alterado.
Note-se que todo o texto entre os dois pontos de exclamação é fornecido ao "mail" como input, e que mesmo dentro deste é possivel utilizar variáveis, tal como se viu no exemplo.
A utilizadora Maria, ao entrar no sistema, saberia que tinha correio na sua caixa de correio electrónica e executaria o comando "mail":
|
From Manel Wed Jan 10 15:37 GMT 1990 Maria: Soube que fazes anos. Parabéns. Manel ? q $ |
Figura 25. Recepçao dos parabéns.
O redireccionamento interno fornece um meio útil e conveniente para usaro editor "ed" num script. Se o leitor não está familiarizado com este editor não precisa de se preocupar. O seu funcionamento é extremamente simples e ser-lhe-á muito fácil compreender o exemplo. Imagine que quer criar um programa que altere um texto contido num ficheiro. Manualmente, isso faria isso entrando no seu editor preferido e executando um comando de substituição global (desde que exista nesse editor). Vamos apresentar aqui um exemplo de como isso pode
ser automatizado, usando o editor "ed" e o redireccionamento interno. Veja-se o script que se segue:
|
$
cat bin/subst #
ENTRADA DE DADOS echo
"Ficheiro a alterar: c" read
file echo
"Texto a substituir: c" read
oldtext echo
"Texto a colocar: c" read
newtext #
SUBSTITUIÇÃO DO TEXTO ed
- $file <<! g/$oldtext/s//$newtext/g w q ! $ |
Figura 26. Comando 'subst'
Repare-se na opção "-" do "ed" que faz com que este programa trabalhe silenciosamente, não enviando mensagens para o terminal. Note-se também a forma do comando do "ed" para efectuar a substituição global:
|
g/texto_velho/s//novo_texto/g |
O programa usa três variáveis: file - que vai conter o nome do ficheiro a modificar - oldtext - o texto que vai ser mudado - e newtext - o texto a colocar. Quando o programa corre, é utilizado o comando read para obter os valores destas variáveis.
Uma vez entrados os valores nas variáveis o redireccionamento interno redirecciona a substituição global do texto, a escrita e o comando de saída, para dentro do input do "ed".
|
$
cat tellist Pedro
Ferreira 253 88 56 Palma 87 26 88 Rui
Martins 77 63 21 Ana
Ferreira 253 38 91 Alberto
Albertini 82873 10923 $
subst Ficheiro
a alterar: tellist Texto
a substituir: Palma Texto
a colocar: F. Palma $
cat tellist Pedro
Ferreira 253 88 56 F.
Palma 87 26 88 Rui
Martins 77 63 21 Ana
Ferreira 253 38 91 Alberto
Albertini 82873 10923 $ |
Figura 27. Utilização do comando 'subst'
A maior parte dos comandos devolvem estados de saída para indicar como correu a sua execução. Por convenção, se devolverem 0 (zero) a execução foi bem sucedida, se devolverem outro valor significa que aconteceu algo de extraordinário que o programa não pôde resolver. O código devolvido não é mostrado automáticamente mas está disponivel no parâmetro $?.
Já vimos mais atrás como se pode verificar o conteúdo desta variável, mas vamos agora como fazer com que os nossos scripts possam devolver também valores a quem os chamou.
Um script termina normalmente quando o último comando nele contido acaba de ser executado. No entanto, é possivel abortar um script em qualquer altura (a partir de dentro, e não com a tecla <DELETE> do terminal) através do comando "exit". Mais importante ainda, pode-se invocar o comando exit com um argumento numérico que vai ser o estado de saída do programa.
|
exit
0 ou exit
1 |
Figura 28. Exemplos de invocação do exit.
Nas secções anteriores deste curso os comandos dos scripts foram sempre executados de uma forma linear. Os primeiros comandos do ficheiro são executados primeiro e os seguintes depois. Começamos aqui o estudo das estruturas de controlo que permitirão alterar esta forma
de funcionamento, alterando a sequência de execução dos comandos, de uma forma perfeitamente controlada.
Os comandos "for" e "while" de execução ciclica, permitem que um programa execute um comando ou uma sequência de comandos repetidas vezes.
3.5.1. O ciclo 'For'
O ciclo "for" executa uma sequência de comandos uma vez por cada membro de uma lista. Tem o seguinte formato:
|
for
variável in lista_de_valores do comando_1 comando_2 comando_3 . . . ultimo_comando done |
Figura 29. Formato do ciclo 'for'
Em cada iteração do ciclo, a variável é afectada com um dos membros da lista. Dentro do corpo do ciclo (isto é, entre o "do" e o "done"), podem ser feitas referências à variável para consultar o seu conteúdo.
É mais fàcil ler um programa se as construcções ciclicas estiverem visualmente claras. Já que o shell ignora os espaços a mais, cada secção de comandos pode ser indentada tal como se viu na figura anterior. Para além disto, torna-se mais simples verificar se cada "do" tem um "done" correspondente.
A variável é à escolha do programador. Por exemplo, se lhe chamamos "var", os valores dados na lista após a palavra "in" serão colocados na variável "var" um de cada vez, e os comandos entre o "do" e o "done" executados uma vez para cada um dos valores. Qualquer referência ao valor "$var" será substituida pelo valor do momento. Se a clausula "in ..." fôr omitida, a variável será afectada com a lista de argumentos do programa - "$*".
Quando os comandos entre o "do" e o "done" tiverem sido executados para o último elemento lista será executada a linha seguinte à do "done", continuando assim a execução normal do programa. Se não existir nenhuma linha após o "done" o programa terminará normalmente.
A forma mais simples de compreender isto tudo é através de um exemplo. Vamos criar um programa que mova os ficheiros de um directório um por um para outro directório, indicando préviamente a operação. Os seguintes comandos são usados:
|
echo |
para pedir o directório. |
|
read |
para ler o nome do directório do telclado. |
|
for variável |
para obrigar o programa a entrar em ciclo, com a variável como variável de controlo. |
|
in lista |
para indicar a lista de valores que vão ser colocados na variável, um por cada ciclo. |
|
do comandos |
para indicar os comandos a executar ciclicamente. |
A figura seguinte mostra o texto do script mvall
|
$
cat bin/mvall echo
"Qual o directório destino dos ficheiros? c" read
pathname for
file in * do echo "Mudan,ca do ficheiro $file: em
curso... c" sleep 2 mv $file $pathname echo " rMudan,ca do ficheiro $file: efectuada. " done $ |
Figura 30. Script mvall.
Entre o "do" e o "done" existe uma particularidade a referir. O leitor pode estar neste momento a perguntar-se para que servirá a sequência " r" no segundo "echo", e para que será que este existe. Se tentar executar este script verificará que lhe é enviada a primeira mensagem, que passados alguns segundos é "mágicamente" substituida pela segunda. Isto é devido à sequência " c" " r", que permite voltar a escrever na mesma linha. Como já vimos, a sequência " c" evita que o cursor mude de linha após um "echo". A sequência " r" desloca o cursor para o início da linha em que este está no momento. Assim, o cursor, que após o primeiro "echo" está colocado no fim da mensagem "...em curso..." é reposto no inicio dessa linha pelo segundo "echo" e a mensagem que este envia sobrepôe-se à que já existia no ecrã.
3.5.2 O ciclo 'While'
Outra instrucção de execução cíclica é o "while", que usa dois grupos de comandos. O segundo grupo será executado ciclicamente enquanto o último comando do primeiro grupo fôr executado devolvendo "exit status" 0 (zero), significando que foi bem sucedido.
O formato genérico do "while" é o seguinte:
|
while
comando_1 comando_2 comando_3 . . . último_comando do comando_1 comando_2 comando_3 . . . último_comando done |
Figura 31. Ciclo 'while' - Formato genérico
O leitor deve estar lembrado do script que permitia introduzir dados na lista telefónica, que se chamava "fazlista". Vamos agora desenvolver uma nova versão deste script, que permitirá introduzir vários nomes e números de telefone com uma invocação apenas.
|
$
cat bin/faz_lista2 echo
"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" echo
"Introdução de dados para lista telefónica - Versão II" echo
"\n" while echo "Nome (^d para
terminar):\c" read nome do echo "Telefone:\c" read telefone echo $nome $telefone >> tellist echo "Registo adicionado." done echo
"\n" echo
"Lista criada ou alterada." $ |
Figura 32. Script 'faz_lista2'
3.5.3 O ciclo 'Until'
Este ciclo é muito semelhante ao ciclo "while" sendo apenas diferente na forma de execução. O segundo grupo de comandos será executado até que o primeiro grupo seja bem sucedido o que é exatamente ao contrário do "while". Não vamos demorar-nos muito sobre este ciclo devido à sua extraordinária semelhança com o "while". O seu formato é o que se segue:
|
until
comando_1 comando_2 comando_3 . . . último_comando do comando_1 comando_2 comando_3 . . . último_comando done |
Figura 33. Ciclo until - Formato genérico
A árvore de directórios de quase todas as máquinas UNIX contém um ficheiros especial onde se pode depositar todo o output indesejado. Por exemplo: um programa em shell, tinha que garantir que um ficheiro não existia, e portanto deveria removê-lo se existisse. Como fazer isso? A forma mais rápida seria:
|
rm file 2> /dev/null |
As mensagens de erro do comando "rm" seriam redireccionadas para o caixote do lixo do sistema - o ficheiro especial /dev/null. Se o ficheiro "file" existisse o "rm" removê-lo-ia silenciosamente, caso contrário emitiria uma mensagem de erro que não seria mostrada.
3.7.1. If...THEN...FI
O comando "if" funciona de forma muito semelhante ao ciclo "while", mas o teste ao resultado do primeiro grupo de comandos só se efectua uma única vez. O formato genérico do comando "if" é o seguinte:
|
if comando_1 comando_2 comando_3 . . . último_comando then comando_1 comando_2 comando_3 . . . último_comando fi |
Figura 34. 'if...then...fi' - Formato genérico
Eis um exemplo da utilização do comando "if":
|
$
cat bin/tem echo
"Escreva o nome do ficheiro e a palavra a pesquisar:" read
file word if
grep $word $file > /dev/null then echo "A palavra $word existe no
ficheiro $file." fi $ |
Figura 35. Um grep diferente - tem
São de notar duas coisas neste script. Uma é que a leitura das duas variáveis é feita num único "read", o que serve para relembrar uma forma menos habitual de o usar. A outra é o redireccionamento do grep, que evita que seja mostrado o output deste comando sempre que a palavra exista no ficheiro. Não se deve pensar que ao escrever
|
if grep $word $file > /dev/null |
estamos a redireccionar o "if". Isso seria um grave erro. Tudo o que vem entre a palavra "if" e a palavra "then" é um grupo de comandos, e como tal, redireccionáveis.
3.7.2.
If...THEN...ELSE...FI
Vimos que a seguir ao
"then" são escritos os comandos que devem ser executados se o
primeiro grupo, que está entre o "if" e o "then" fôr bem
sucedido e se quizessemos executar comandos caso este grupo não fosse bem
sucedido? Os construtores do Shell também
pensaram nisto e providenciaram uma extensão ao "if ... then ... fi", o "else" que significa "senão". O formato genérico é o que se segue:
|
if comando_1 comando_2 comando_3 . . . último_comando then comando_1 comando_2 comando_3 . . . último_comando else comando_1 comando_2 comando_3 . . . último_comando fi |
Figura 36. 'if...then...else...fi' - Formato genérico
É possivel alterar ligeiramente o programa "tem" de forma a torná-lo um pouco mais simpático, de forma a indicar não só a existência da palavra no ficheiro mas também a sua não-existência:
|
$
cat bin/tem echo
"Escreva o nome do ficheiro e a palavra a pesquisar:" read
file word if
grep $word $file > /dev/null then echo "A palavra $word existe no
ficheiro $file." else echo "A palavra $word NÃO existe
no ficheiro $file." fi $ |
Figura 37. Um grep diferente - 'tem' - versão II
3.7.3. CASE...ESAC
A construcçãoo "case ... esac" permite testar o conteúdo de uma variável e reagir de diferentes formas, consoante o seu valor. Enquanto o "if" se poderia traduzir para
|
se ISTO então AQUILO senão AQUELOUTRO |
o "case" poder-se-à traduzir
|
caso
VARIÁVEL seja ____
X executar comando 1 ____
Y executar comando 2 ____
Z executar comando 3 |
Figura 38. Como funciona um 'case'
Em Shell, o "case" tem a seguinte forma:
|
case
$variável in caso1)
comando_1 comando_2 . . último_comando ;; caso2) . . . esac |
Figura 39. case - formato genérico
A grande vantagem do "case" é que é possivel colocar metacaracteres nas strings de comparação, que estão representadas na figura por "caso1" e "caso2" vamos ver uma aplicação disto.
Imaginemos que um dos nossos scripts, tal como já aconteceu, recebe informação através dos seus argumentos, que têm que ser dois. Podemos usar para este exemplo o script "tem" que realizámos há algumas secções atrás. Vamos alterá-lo para que receba o nome do ficheiro e a palavra por argumentos, em vez de perguntar com o "echo" e o "read".
|
$
cat bin/tem2 #
tem - versão II - pesquisa palavras em ficheiros file=$1 word=$2 if
grep $word $file > /dev/null then echo "A palavra $word existe no
ficheiro $file." else echo "A palavra $word NÃO existe
no ficheiro $file." fi $ |
Figura 40. script 'tem' - versão usando argumentos
O que acontecerá se o utilizador
se esquecer de dar argumentos ao programa? "Culpa dele!", dirá o
programador mais descontraido. Não é uma atitude muito simpática e a melhor
forma de actuar será alterar o programa de forma a que este saiba reagir
elegantemente aos erros do
utilizador. Neste caso vamos alterar o programa, utilizando o parâmetro $# para testar a existência de argumentos e o seu número. Vamos colocar no início as seguintes linhas:
|
case
$# in 0)
echo "$0: necessários argumentos" exit 1 ;; 1)
echo "falta palavra para procurar no ficheiro '$1'" exit 1 ;; 2)
;; *)
# todos os outros casos... echo "$0: demasiados
argumentos" exit 1 ;; esac |
Figura 41. teste aos argumentos.
Vejamos o que acontece: é usado
um "case" para testar o conteudo do parâmetro $#, que contém o número
de argumentos com que o programa foi invocado. Se o utilizador não der
argumentos $# conterá 0, e será enviada para o terminal a mensagem que diz que
o programa precisa de ser invocado com argumentos. O parâmetro "$0" é
um parâmetro posicional que contém o nome do programa, a primeira palavra do
comando digitado no shell. Se o utilizador só der um argumento o programa
assume que o utilizador se esqueceu de indicar a palavra e avisa. Caso o
utilizador forneça dois argumentos o não se faz nada nesta fase, deixando todas
as acções efectuarem-se normalmente, mais tarde. No caso de existirem mais de
dois
argumentos o programa emite também uma mensagem de erro e aborta. Este caso é um exemplo da possibilidade de utilizar metacaracteres no "case", e a utilização do asteristico significa "todos os outros casos".
Em qualquer dos casos o programa funciona graciosamente e ajuda o utilizador a corrigir os seus próprios erros. Isto é simpático, e pode poupar muito trabalho ao programador, que não será tantas vezes incomodado por causa de um "comportamento esquisito" do seu programa, quando o que se trata apenas de uma utilização incorrecta.
O comando "test" verifica se certas condições são verdadeiras e devolve um "exit tatus" correspondente. Serve fundamentalmente para ser usado em ciclos "while" testes "if", que veremos mais adiante. Como o leitor já sabe, o ciclo "while" esta o "exit status" do comando e executa ciclos enquanto este fôr zero, que ignifica execução bem sucedida.
É possivel usar o comando "test" para verificar vários tipos de condições, como se verá seguidamente. É também importante focar que o comando "test" tem duas formas. A forma mais convencional é
|
test argumentos |
mas existe também outra forma, destinada a tornar os programas mais legíveis pelos programadores habituados às outras linguagens de programação, que é
|
[ argumentos ] |
Esta segunda forma é de longe a mais usada, e nós não iremos fugir a esta regra, mas convém não nos esquecermos de que "test" é um comando, e que "[" é apenas outra forma de o invocar.
Quando usado na forma "[" o comando "test" exige um "]" no fim da linha de comando.
Vamos agora ver qual o potencial deste comando e como o usar.
3.8.1.Atributos de
ficheiros
O comando "test" pode ser usado para testar diversos atributos de ficheiros. pode por exemplo informar-nos se o ficheiro existe, se tem dados ou está vazio, se temos permissão de escrita, etc. O exemplo que se segue é um extracto de um programa em que a dada altura se faz um teste à permissão de leitura de um ficheiro.
|
. . . #
testa a autorização de leitura if
[ -r $file ] then # processa os dados cat $file >> $tempfile else # não pode ler, aborta o processamento echo "$0: não autorizado a ler
$file." exit 1 fi . . . |
Figura 42. test - atributos de ficheiros
|
[ -r file ] |
Verdadeiro (exit status=0) se "file" existe e temos autorização de leitura. |
|
[ -w file ] |
Verdadeiro se "file" existe e temos autorização de escrita. |
|
[ -x file ] |
Verdadeiro se "file" existe e temos autorização de execução. |
|
[ -s file ] |
Verdadeiro se "file" existe e tem conteúdo (pelo menos um caracter). |
|
[ -f file ] |
Verdadeiro se "file" existe e é ficheiro regular. |
|
[ -d file ] |
Verdadeiro se "file" existe e é directório. |
|
[ -b file ] |
Verdadeiro se "file" existe e é ficheiro especial de blocos. |
|
[ -c file ] |
Verdadeiro se "file" existe e é ficheiro especial de caracteres. |
|
[ -u file ] |
Verdadeiro se "file" existe e tem o SUID BIT ligado. |
|
[ -g file ] |
Verdadeiro se "file" existe e tem o SGID BIT ligado. |
|
[ -k file ] |
Verdadeiro se "file" existe e tem o STICKY BIT ligado. |
3.8.2. Igualdade de
strings
Também é possivel usar o comando "test" para verificar a igualdade de variáveis permitindo assim testar automáticamente o seu conteúdo de uma forma mais simples do que com a utilização do comando "case". Apesar disto o "case" devemos lembrar que o "case" é bastante mais potente e que embora o "test" seja usado para a maior parte das comparações de variáveis aquele mantém o seu enorme potencial.
O uso do "test" para comparações de strings é simples. O teste seguinte
|
[ $var1 = $var2 ] |
será verdadeiro se os conteúdos forem idênticos.
O exemplo que se segue é o caso tipico de um programa que executa ciclicamente até que o utilizador digite a opção de terminar.
|
resposta="s" until
[ "$resposta" = n ] do . . . ac,c~ao a realizar . . . echo "quer continuar (s/n) ? c" read resposta done |
Figura 43 test - exemplo de comparaçãoo de strings
Uma pergunta pode surgir: porque está a variável "resposta" entre aspas durante o teste?
Isto pode não parecer óbvio mas imaginemos que o primeiro ciclo se efectuava e o utilizador respondia com um <RETURN>. A variável ficava vazia. Ao realizar o teste o programa invocaria
|
[ = n ] |
que é uma forma errada de usar o comando "test". Colocar aspas na variável evita este inconveniente. é completamente diferente, e perfeitamente aceit'avel para o "test" ser chamado assim:
|
[ "" = n ] |
Sempre que uma variável possa ter um conteúdo nulo os testes devem ser feitos colocando a variável entre aspas. Se se tiver d'uvidas usem-se as aspas.
3.8.3. Comparação
numérica
O comando "test" permite também a comparaçãoo numérica de variáveis. Como o leitor sabe, as variáveis do shell só têm um tipo: string [5]. Como tal podem conter todo o tipo de caracteres, desde a letras maiusculas e minusculas, algarismos (na informática chamados digitos) ou quaisquer outros caracteres do código ASCII. Até agora não vimos nenhuma forma de realizar operações aritméticas em programas shell (e ainda não é aqui, mas mais à frente...) mas, já que as variáveis podem conter digitos porque não comparar os valores descritos por esses digitos?
É o que faz o "test", da seguinte forma:
|
[ $var1 -eq $var2 ] |
Verdadeiro se os valores forem (numericamente) iguais. |
|
[ $var1 -ne $var2 ] |
Verdadeiro se os valores forem diferentes. |
|
[ $var1 -gt $var2 ] |
Verdadeiro se o valor $var1 fôr maior do que o valor $var2. |
|
[ $var1 -ge $var2 ] |
Verdadeiro se o valor $var1 fôr maior ou igual do que o valor $var2. |
|
[ $var1 -lt $var2 ] |
Verdadeiro se o valor $var1 fôr menor do que o valor $var2. |
|
[ $var1 -le $var2 ] |
Verdadeiro se o valor $var1 fôr menor ou igual do que o valor $var2. |
3.8.4. Expressões
lógicas
Para os programadores mais exigentes, o "test" permite adicionar condições lógicas às expressôes. Pode-se compôr diversas expressões com AND's (conjunções) OR's (disjunção) e NOT's (negação). Pode-se até utilizar parêntesis, que têm, no entanto que ser colocados entre aspas ou com uma barra (\) para que o shell não lhes dê significado especial.
|
-a |
conjunção (and). |
|
-b |
disjunção (or). |
|
! |
negação (not). |
Se por exemplo, quizessemos saber se um ficheiro tem permissões de leitura mas não de execução fariamos:
|
if
[ -r $file -a "(" -x $file ")" ] then . . . |
Figura 44. test - condições
O comando "break" pára incondicionalmente a execução de qualquer ramo dentro do qual seja encontrado, e prossegue a Execução na instrucção seguinte ao "done", "fi" ou "esac". Se não existirem instrucções após este o programa termina.
O comando "continue" faz com que o programa reinicie imediatamente um novo ciclo, ignorando o que resta dos comandos entre o "do" e o "done".
O exemplo que se segue é um excerto de um programa que tem de processar o conteúdo de um ficheiro, saltando linhas de comentário (que, tal como no Shell, são começadas por "#") e terminando o processamento, não no fim do ficheiro, mas quando encontrar a palavra "fim" isolada numa linha.
|
. . . . while
read linha do case $linha in "#"*) continue ;; fim) break ;; esac # AQUI PROCESSA A LINHA . . . done
< /tmp/datafile$$ . . . |
Figura 45. break e continue - exemplo de utilização
O comando "expr" é óptimo para realizar operações aritméticas em scripts de Shell. Como já repararam, dentro das variáveis do Shell pode-se ter qualquer conjunto de caracteres, inclusivé
algarismos. Então porque não realizar operações com os seus conteúdos? O Shell não providencia nenhum operador numérico mas existe um utilitário que permite avaliar expressões numéricas. Este comando é o "expr", e é normalmente utilizado do seguinte modo:
|
resultado=`expr $opr1 + $opr2` ou resultado=`expr $val / 3` ou ainda resultado=`expr $val + $opr1
$opr2 ` |
O leitor pode estranhar a utilização das barras na expressão; lembre-se que os parantesis e o asteristico têm significado especial para o Shell - são metacaracteres e que esse significado tem que ser "desligado" com a barra invertida ('\').
O "expr", infelizmente, não efectua operações com casas decimais, pelo que sempre que isto seja necessário ser'a preciso recorrer ao "bc", utilizado provávelmente com redireccionamento interno.
Os scripts de Shell nem sempre funcionam à primeira, e nos casos em que o script é grande é dificil encontrar o(s) ponto(s) do texto onde existem erros. Os maiores problemas não são os erros de sintaxe, já que o shell indica-os através do número de linha. O mais dificil de corrigir são os erros de lógica, em que o programa executa mas não faz o que queremos.
Para ajudar nestas situações, os programadores que fizeram o Shell, que sabem bem o que custam estas situações, criaram um mecanismo que permite visualizar a execução do programa.
Para activar este mecanismo, que permite visualizar cada comando do script antes de ser executado, basta inserir
|
set -x |
no texto do programa. Para desactivar este mecanismo deve-se usar "+x" em vez de "-x". Veja-se o exemplo.
|
. . . # aqui começa a àrea do programa que parece não
funcionar bem set -x . . . # e aqui termina set +x # resto do programa... . . . |
Figura 46. detecção de erros lógicos
Já conhecemos várias utilidades do comando "set". Começámos por usá-lo para visualizar variáveis e o seu conteúdo, vimos ainda agora que permite ligar e desligar o mecanismo de detecção de erros lógicos e vamos agora observar ainda outra utilidade: permitir alterar os valores contidos nos parâmetros posicionais $1, $2, $3, etc., e nos parâmetros especiais $* e $#. Como é que isso se faz?
|
$
cat bin/setex #
mostra parâmetros com que foi invocado echo
$* echo
numero de argumentos é $# set
`ls` echo
$* echo
agora numero de argumentos é $# |
Figura 47. alteração dos argumentos
Imaginemos agora que temos um ficheiro com dados num determinado formato e que queremos processar as linhas desse ficheiro uma a uma. Isto talvez seja um conceito novo para o leitor que não está habituado a programar, e portanto vamos tentar enquadrar este exemplo numa situação real.
Admitamos que existe uma instalação UNIX onde o administrador de sistema impôs aos utilizadores a existencia de um ficheiro com nomes de outros ficheiros ou directórios dos quais é preciso fazer backups regularmente. Se um utilizador quer fazer backups dos seus dados basta criar um ficheiro chamado "backup.lst" no seu directório pessoal. Quando o administrador fôr realizar os backups limitar-se-á a ler os nomes dos ficheiros a arquivar dos diversos "backup.lst" de cada um dos utilizadores (provavelmente usará o comando find para os encontrar) e criará uma lista geral.
|
$
cat backup.lst /usr/acct/rosa/c.progs /usr/acct/rosa/textos /usr/acct/rosa/i4gl/progs /usr/acct/rosa/bin/l /usr/acct/rosa/bin/p /usr/acct/rosa/bin/s /usr/acct/rosa/.profile $ |
Figura 48. exemplo de um ficheiro backup.lst
Como procederia o administrador de sistema para realizar os backups? Eis uma hipótese:
|
$
find / -name backup.lst -print > /tmp/lista1 $
cat `cat /tmp/lista1` > /tmp/lista2 $
tar cvF /tmp/lista2 |
Figura 49. exemplo de backups realizados à mão
O administrador de sistema que realiza os backups do modo que acima se indicou limita-se a cumprir os seus deveres sem grande entusiasmo. Não se preocupa, por exemplo com ficheiros mal preenchidos. Um utilizador pode ter um nome de directório mal escrito no seu backup.lst, não o saber e julgar que os seus dados estão a ser convenientemente salavaguardados, e enquanto isso o "tar" não consegue ler o ficheiro. Como resolver isto?
Não é necessário aumentar o
trabalho do administrador. Basta automatizar de forma conveniente esta tarefa
através de um bom script. Eis outra hipótese:
|
$ cat /usr/local/bin/userbackup # userbackup versão I - backup de ficheiros pedidos
pelos utilizadores # ficheiros temporários ERROS=/tmp/tf1$$ LISTA=/tmp/tf2$$ CORREIO=/tmp/tf3$$ true > $LISTA # fase 1: criar lista de ficheiros e directórios find / -name backup.lst -print | while read listfile do # mensagem echo
"encontrado ficheiro $listfile. processando..." #inicializar o
ficheiro de erros e a variável indicadora true > $ERROS # descobrir o
dono do ficheiro através do comando ls e do # comando set o
ls devolve este formato: # # 2
-rw-r--r-- 1 Manel iC
26 abr 11 11:31 backup.lst set `ls -sl
$listfile` dono=$4 # validar todos
os nomes contidos no ficheiro do utilizador echo
"validação dos dados em $listfile..." while read
filename do # se o nome
corresponde a um ficheiro regular ou directório if [ -f
$filename -o -d $filename ] then # verificar se
o ficheiro tem dados if [ -s
$filename ] then echo
$filename >> $LISTA else echo
"*** AVISO: $filename está vazio." >> $ERROS fi else echo "***
ERRO: $filename não é bom."
>> $ERROS fi done <
$listfile |
(continua na página seguinte)
|
if [ -s $ERROS ] then echo
"encontrados erros_. correio para o dono: $dono" echo
"\\n\\n\\nERROS no backup de `date`\\n\\n" > $CORREIO cat $ERROS
>> $CORREIO mail $dono <
$CORREIO fi done # fase 2: arquivo echo "criação do arquivo..." tar cvFf $LISTA /tmp/tar # fase 3: remoção dos temporários rm $ERROS $LISTA $CORREIO 2> /dev/null echo "fim do backup." $ |
Figura 50. userbackup: script de backups
Eis um programa onde a confusão pode aumentar devido ao seu tamanho.
É altura de usar o comando "set -x" quando não funciona. Mas não é caso para sustos. Mais adiante vamos estudar a maneira de simplificar a programação de tarefas assim...
Como curiosidade, vejamos o correio que chegou a um utilizador descuidado, que tinha o seu "backup.lst" mal preenchido:
|
$ mail From root Wed Apr 11 12:52 LIS 1990 Relatório do backup de quarta-feira 11 de abril
1990 12:52:51 *** AVISO: /tmp/jk está vazio - ignorado. *** ERRO:
/tmp/jkl não é regular nem directório. ? d $ |
Figura 51. mensagem de aviso
A variável IFS, já abordada anteriormente indica ao shell quais os caracteres que separam os diversos campos de um registo de entrada. Um registo de entrada pode ser uma linha de comando, ou uma linha que tenha que ser avaliada pelo comando "read".
Vejamos o primeiro caso: o que separa as várias palavras (campos) que constituem o comando e os seus argumentos? Normalmente espaços, mas de um modo geral são brancos isto é, espaços, tabs e newlines, em número indeterminado.
No segundo caso, se por exemplo invocarmos
|
read a b c d |
o que é que influencia o que será colocado em cada uma das variáveis? Mais uma vez os caracteres brancos
No entanto, este comportamento pode ser alterado se alterarmos a variável IFS. Dentro desta variável estão os caracteres que podem ser reconhecidos como separadores de campos na entrada (Input Field Separators). Esta variável de ambiente, que por defeito contém três caracteres (espaço, tab e newline), pode ser modificada do seguinte modo:
|
IFS=":,+=%" |
caso em que os novos separadores de campo passariam a ser os caracteres ":,+=%". Uma experiência interessante é experimentar o seguinte:
|
IFS=":" |
e verificar que se podem emitir os comandos com o sinal ":" a separar as palavras em vez de espaços, como a seguir se exemplifica.
|
$
IFS=":" $
export IFS $
ls:-l:/tmp total
666 -rw-r--r--
1 ccat iC 0 abr 10 16:19 3 -rw-------
1 Manel iC 68608 abr 11 15:24 Ex03394 -rw-------
1 Manel iC 5120 abr 11 15:22 Rx03394 -rw-r--r--
1 root root 0 abr 10 17:20 TPERROR drwxr-xr-x
2 root root 32 abr
9 12:34 WORK -rw-r--r--
1 Manel iC 190 abr 11 15:17 jk -rw-r--r--
1 Manel iC 37 abr 11 11:32 lista1 -rw-r--r--
1 Manel iC 26 abr 11 11:33 lista2 -rw-rw-r--
1 root sys 272 abr 9 10:24
sa.adrfl -rw-r--r--
1 Manel iC 0 abr 11 12:48 tf23071 -rw-r--r--
1 Manel iC 49152 abr 11 14:25 toprn.3272 -rw-r--r--
1 Manel iC 8 abr 9 14:26 xx -rw-r--r--
1 Manel iC 3 abr 9 14:31 xy $ |
Figura 52. alteração de variável IFS
Não fica muito legível pois não? No entanto, a alteração da variável IFS pode ter bastante utilidade. Vejamos o caso de um script que quer ler o ficheiro /etc/passwd.
|
. . . IFS=":" while
read nome password id gid comment dir shell do # à cautela repôe-se o IFS com espaço,
tab e newline IFS=" " # processa cada linha . . . done
< /etc/passwd IFS=" " |
Figura 53. leitura de dados de um ficheiro
Existem em Shell outros metecaracteres pouco utilizados, que são muito teis para evitar utilização grande escala dos if. Estes metacaracteres são os metacaracteres de execução condicional: "&&" e "||".
É possível executar um comando se e só se o anterior fôr bem sucedido utilizando o metacaracter "&&", da seguinte forma:
|
comando1 && comando2 |
Também é possivel executar um comando see só se o comando anterior fôr bem sucedido: forma:
|
comando1 || comando2 |
Assim, se quisermos testar a existência de um programa antes de o executar poderemos usar o método que a seguir se descreve:
|
test -x progfile && progfile |
Do mesmo modo, é possivel testar a existência de um ficheiro, e criá-lo caso não exista. No exemplo que se segue testa-se a existência de um ficheiro com os nomes dos utilizadores da máquina e, caso não exista, cria-se:
|
test -f users || cut -d: -f1 /etc/passwd > users |
Sinais são mensagens enviadas pelo Kernel ao programa. Podem ser gerados por iniciativa deste ou devido a condições estranhas. Um sinal qualquer pode ser enviado a um processo através da utilização do comando "kill(1)".
A tabela que se segue, sem pretender ser exaustiva, apresenta os sinais mais importantes. Em termos de programação em Shell os que interessam são:
|
HANGUP(1): |
gerado automáticamente pelo Kernel, este sinal é enviado aos processos associados a determinado terminal quando este é desligado ou a comunicação interrompida comunicação por modems, que ligação telefónica cortada). |
|
INTERRUPT(2): |
significa que o utilizador carregou na tecla designada para interromper a execução dos programas. Esta tecla, geralmente DELETE, pode ser designada através do comando "stty". Muitos utilizadores, que trabalham também com DOS, preferem o CTRL-C para este fim, pelo que põem no seu ".profile" a linha "stty intr '^c'" |
|
QUIT(3): |
este sinal, de importância superior ao INTERRUPT, tem um significado semelhante ao anterior (com algumas diferenças que não discutiremos aqui) e é recebido quando o utilizador carrega em CTRL- . |
|
TERMINATE(15): |
a utilização simples do comando "kill" d'a origem ao envio deste sinal, que significa que o programa deve ser imediatamente interrompido; o "shutdown" envia este sinal a todos os processos existentes na máquina momentos antes de enviar o sinal que se segue. |
|
KILL(9): |
este sinal pode ser enviado através da utilização do comando "kill -9 ..." e implica a interrupção imediata do processo a que está destinado. |
|
NOME: |
NM |
T |
DESCRIÇÃO |
|
SIGHUP |
1 |
A |
HANGUP (Terminal desligado ou linha cortada). |
|
SIGINT |
2 |
M |
INTERRUPT (Utilizador carregou em Delete, Rubout ou Ctrl-C). |
|
SIGQUIT |
3 |
M |
QUIT (Utilizador carregou em Ctrl-\). |
|
SIGKILL |
9 |
M |
KILL (Alguém matou o processo). |
|
SIGBUS |
10 |
A |
BUS ERROR (Erro interno ao programa). |
|
SIGSEGV |
11 |
A |
SEGMENTATION VIOLATION (Programa mal comportado). |
|
SIGPIPE |
13 |
A |
BROKEN PIPE (Pipe sem saída) |
|
SIGTERM |
15 |
Software Termination Signal From Kill. |
|
|
SIGUR1 |
16 |
M |
User Defined Signal 1. |
|
SIGUR2 |
17 |
M |
User Defined Signal 2. |
|
SIGCLD |
18 |
A |
Death Of A Child. |
|
SIGPWR |
19 |
A |
Power-Fail Restart. |
Figura 54. Sinais mais usados.
A resposta à recepção de sinais varia conforme a sua importância e nalguns casos pode ser alterada, como veremos mais adiante. Normalmente, quando um processo recebe um SIGUP, SIGINT, SIGQUIT, SIGTERM ou SIGKILL, a sua execução é imediatamente interrompida. Por outro lado, se receber um SIGPWR, SIGCLD ou SIGPIPE, SIGUSR1 ou SIGUSR2 a sua
execução proseguirá normalmente. Esta reacção pode ser alterada para qualquer sinal, excepto o SIGKILL, que provoca sempre a morte do processo.
Na programação Shell (nas outras os métodos variam grandemente) os sinais podem ser "apanhados", isto é, podemos modificar a atitude do programa em relação à recepção de sinais, através da utilização do comando "trap".
|
a) |
podemos igorar sinais que de outra forma causariam o aborto da execução: ex: trap "" 1 2 3 |
|
b) |
podemos executar comandos após a recepção desses sinais: ex: trap "echo INTERRUPT; rm /tmp/tempfile;exit 1" 1 2 3 |
|
c) |
podemos restaurar o comportamento standard de recepção desses sinais: ex: trap 1 2 3 |
Como o nosso leitor já se deve ter apercebido, as máquinas UNIX permitem acesso através de diversos terminais. Isto é: para se aceder a estas máquinas tanto se pode utilizar um terminal DEC VT100, como um TELEVIDEO 925, ou um IBM PC ou qualquer outro das várias dezenas (se não centenas) de terminais disponíveis no mercado. E todos têm caracteristicas diferentes.
Um programador que se preze gosta de controlar o que o utilizador vê no ecrã. Assim apaga o ecrã, move o cursor, utiliza as capacidades de mostrar caracteres em "standout", "reverse", "blink" e "underline", etc. Desde que se saiba à partida qual o terminal que vai ser usado, não é difìcil fazer isto: basta consultar o manual deste e enviar as sequências de caracteres (normalmente come,cadas por ESCAPE) que aí vêm descritas.
Mas se à partida não se sabe qual o terminal aí começam os trabalhos. é preciso preparar o programa para trabalhar com cada um dos terminais e alterá-lo sempre que surja um novo... Nada prático.
Felizmente isto não é necessário. O comando "tput", serve precisamente para tornar os programas independentes do terminal em que são invocados. A compreensão de como fuciona este comando implica o conhecimento da base de dados TERMINFO, pelo que não nos alongaremos demasiado no seu estudo. Importa principalmente indicar a forma de o utilizar, o que será feito pela tabela seguinte e pelos exemplos que se lhe seguem.
|
FUNÇÃO |
1: ARG |
OUTROS ARGUMENTOS |
|
Apagar ecrãn |
clear |
|
|
Mover cursor |
cup |
nova posição |
|
Cursor invisivel |
civis |
|
|
Cursor normal |
cnorm |
|
|
Entrar em "bold" |
smso |
|
|
Sair de "bold" |
rmso |
|
|
Entrar em "underline" |
smul |
|
|
Sair de "underline" |
rmul |
|
|
Ligar impressora[6] |
mc5 |
|
|
Desligar impressora |
mc4 |
Figura 55. tput - alguns parâmetros e significado
Se quisermos desligar o cursor devemos portanto executar
|
$ tput civis |
e se quisermos apagar o ecrã, deixando o cursor no meio daquele podemos invocar
|
$ tput clear tput cup 12 40 |
4.8.1 A necessidade
de simplificação
À medida em que os programas se vão tornando maiores, os programadores lutam com crescentes dificuldades para dominar a sua complexidade. Este tipo de problemas e as suas hipotéticas soluções têm vindo a ser discutidos ao longo do tempo por diversos peritos de análise e programação e esta discussão não cabe neste pequeno curso. No entanto, todos são unânimes em que uma das formas mais simples de combater as dificuldades causadas pelo tamanho dos programas é sub-dividir a tarefa a realizar em sub-tarefas interligadas, que por sua vez se sub-dividirão noutras sub-tarefas, e assim sempre até que se atinja um nível compreensível para o computador.
Para exemplificar imagine que tem de programar um robot para executar um telefonema. Nada simples. O que temos a fazer é sub-dividir o telefonema em pequenos passos, as diversas sub-tarefas: (1) levantar auscultador, (2) aguardar sinal de marcar, (3) colocar moedas, (4) marcar número, (5) aguardar resposta e finalmente (6) conversar.[7] Vamos agora avaliar a sub-tarefa 3 - colocar moedas. Poderiamos sub-dividi-la ainda em (7) meter a mão ao bolso, (8) tirar moedas, (9) largar moedas na calha. Por sua vez, qualquer uma das tarefas que compôem a tarefa 3 pode ser sub-dividida. A tarefa 7, por exemplo: (10) levantar ombro, (11) encolher braço(12) encostar mão à anca, (13) esticar braço até a mão entrar no bolso.
Poderíamos continuar assim a quebrar a complexidade da tarefa inicial até atingirmos um nível que fosse directamente programável no robot. Este método torna-se "obrigatório" quando se quer informatizar trabalhos um pouco mais complexos. Resta agora avaliar como poderemos aplicá-lo à programação em shell.
4.8.2. Funções
Assim como podemos criar novos comandos que poderão ser chamados em qualquer altura à mão ou noutro programa podemos também criar comandos internos a determinado programa. Estes comandos internos designam-se por funções. O exemplo que se segue apresenta o caso já contêmplado de um programa para fazer backups, desta vez com a tarefa sub-dividida em funções. Repare-se na particularidade de as instrucções relativas às funções estarem colocadas à cabeça do programa:
|
#
userbackup - versão II (estruturada) # backup de
ficheiros pedidos pelos utilizadores # ficheiros
temporários RROS=/tmp/tf1$$ LISTA=/tmp/tf2$$ CORREIO=/tmp/tf3$$ #
crialista: função para criar lista de ficheiros e directórios crialista
() { true > $LISTA find / -name backup.lst -print | while read
listfile do echo "encontrado ficheiro $listfile.
processando..." true > $ERROS set `ls -sl $listfile` dono=$4 echo "validação dos dados em
$listfile..." while read filename do if [ -f $filename -o -d $filename ] then if [ -s $filename ] then echo $filename >> $LISTA else echo "*** AVISO: $filename está
vazio." >> $ERROS fi else echo "*** ERRO: $filename não é válido." >>
$ERROS fi done < $listfile if [ -s $ERROS ] then mostraerros fi done } #
mostraerros: função para enviar correio ao dono do ficheiro # sub-tarefa da função crialista mostraerros
() { echo "encontrados erros_. correio para
o dono: $dono" echo "\n\n\nRelatório do backup de
`date`\n\n" > $CORREIO cat $ERROS >> $CORREIO mail $dono < $CORREIO } |
( contiuna na página seguinte )
|
# arquiva:
função que realiza o arquivo arquiva ()
{ echo "criação do arquivo..." tar cvFf $LISTA /tmp/tar } # limpa:
remoção dos temporários limpa () { rm $ERROS $LISTA $CORREIO 2> /dev/null } # CORPO DO
PROGRAMA crialista arquiva limpa # FIM DO
PROGRAMA |
Figura 56. userbackup2 - versão estruturada dos backups
4.8.3. Argumentos e
valor de saída
Tal como os programas independentes as funções podem ter argumentos e estados, de saída. A única diferença está na devolução deste último, que em vez de ser declarado com "exit nnn" deve sê-lo com "return nnn". Para todos os efeitos uma função pode ser tratada tal como um programa externo:
|
#
código relativo à função func
() { # utilização dos parâmetros,
exemplificada com echo echo $1 $2 $3 # devolução de exit status, neste exemplo #
condicionado pela existência de determinado ficheiro if [ -f $filename ] then return 0 else return 1 fi } #
CORPO DO PROGRAMA . . . if
func arg1 arg2 arg3 then # ficheiro existe . . . else # ficheiro não existe . . . fi |
Figura 57. utilização de funções
Com os conhecimentos adquiridos neste curso, o leitor (que pode passar a considerar-se desde já um programador se ainda não o era) tem capacidade de criar programas bastante potentes. No entanto é natural que venha a encontrar situações em que o shell, como linguagem de programação é insuficiente. Normalmente, isto acontece por falta de rapidez na execução dos programas. Não há nada que substitua uma boa linguagem de programação como o C[8] mas há alguns utilitários que podem ajudar ao programador de shell. O estudo destes programas não pode ser feito neste curso mas é nosso dever fazer-lhes referência: awk, sed, cut, paste, join, etc.
Para terminar duas recomendações: não deixe de fazer os exercícios, leia os livros da bibliografia (alguns, pelo menos), e boa sorte.
Escreva um script que indique apenas a data em tamanho grande (com a utilização do comando "banner"). Tenha cuidado para não dar ao seu script o mesmo nome que tem o comando do UNIX.
Altere o seu ".profile" de modo a manter uma lista em ficheiro com as várias horas de entrada no sistema. Isto é: cada vez que você entre no sistema, quando o shell executa o ".profile" devem ser executadas instrucções para guardar esta informação.
Escreva um programa que envie uma nota para diversos utentes. A nota deve ser pedida por teclado. Os utentes devem receber um correio a nota com o seguinte formato:
|
Caro
colega: * texto a pedir pelo teclado * Sem
outro assunto, * logname* |
Figura 58. Resultado do programa 'memo'
O programa executará do seguinte modo:
|
$
memo Joao Ana Vitor MSilva Introduza
o texto terminando com ^d: >
Estive hoje numa reunião com o chefe da informática >
da S.V., que me disse que... . . >
^D Confirma
o envio para: Joao Ana Vitor MSilva ? s Envio... Terminado. $ |
Figura 59. Utilização do comando 'memo'
Os utentes receberão um correio com o seguinte teor:
|
$
mail From
Manel Tue Jan 23 13:00 GMT 1990 Caro
Colega: Estive
hoje numa reunião com o chefe da informática da
S.V., que me disse que... . . Sem
outro assunto, Manel |
Figura 60. Correio do comando 'memo'
Mude a sua prompt para "CMD: ".
Sem usar a opção -R do comando "ls" faça um script que execute uma visualização recursiva de um directório e dos seus sub-directórios.
É preciso fazer backups. Queremos usar uma tape mas ficheiros do directório que queremos guardar (chamemos-lhe X) não cabem todos. A solução é deixar de fora os programas executáveis. Mas os scripts de shell devem ir, pelo que não basta usar o "[ -x $file ]". Como o fazer?
Realize um programa em shell que permita fazer toda a gestão de uma lista telefónica, desde a adição de registos, à consulta por nome, até à impressão da lista numa impressora. Esmere-se, e faça um programa bonito e bem arranjadinho.
Você é o chefe de um grupo de trabalho. Precisa de enviar para o seu grupo muitas comunicações e está farto de enumerar sempre todos os nomes dos seus colegas ao mail. Podia ser preguiçoso e criar um script assim:
|
mail
Joao Joana Alberto Alberta António Antónia \ Guilherme Guilhermina Mário Maria
Carlos Carla \ Manuel Manuela |
Mas a preguiça não é um dos seus defeitos. Você decidiu criar um script, ao qual chama "gmail", que funciona exactamente como o mail, mas recebe nos seus argumentos nomes de grupos, em vez de nomes de utilizadores.
Crie um programa que mova todos os ficheiros com permissão de execução (que se presumem ser programas) para o directório $HOME/bin.
Aproveite o programa que já fez num dos exercicios anteriores para realizar outro que permita fazer backups de qualquer directório ou lista de ficheiros a fornecer pelo utilizador, e permitir também realizar a reposição dos ficheiros contidos em tape. O programa deve apresentar incialmente o seguinte menu:
|
1. Cópias de Segurança 2. Restauro de Dados 3. Avaliação Prévia 4. Esclarecimentos 5. Sair Indique a sua escolha: |
Figura 61. Menú do programa de Backups.
A opção
"esclarecimentos" servirá apenas para dizer ao utilizador o que fazem
cada uma das outra opções. A opção "Avaliação prévia" servirá para
calcular o número de tapes necessárias para o backup de determinada lista de
ficheiros. Aconcelha-se a que seja usado o "cpio" para
arquivar os dados. Assuma que as tapes usadas são todas do mesmo tamanho e que têm ou 40 Mbytes ou 150 MBytes, conforme a máquina em que está a trabalhar.
Sem utilizar o comando "wc" conte os ficheiros existentes no seu directório. Automáticamente.
UNIX User's Guide, AT&T
UNIX User's Reference Manual, AT&T
UNIX, The Complete Reference, Stephen Coffin, McGraw Hill
The UNIX System Guidebook, 2nd Ed., Peter P. Silvester, Springer-Verlag
Advanced Programmer's Guide to UNIX System V, Rebecca Thomas, Lawrence Rogers,
Jean Yates, McGraw Hill
UNIX System Programming, Keith Haviland, Ben Salama, Addison-Wesley
ABERTURA..................................................................................................................... 1
PROGRAMAÇÃO EM SHELL......................................................................................... 2
1. SHELL SCRIPTS.............................................................................................. 2
1.1 Criação de um pequeno shell script........................................................ 2
1.2. Criação de um directório 'BIN' para conter os scripts............................. 4
1.3. Avisos acerca dos nomes dos shell scripts............................................. 5
2. VARIÁVEIS..................................................................................................... 7
2.1. Parâmentos posicionais........................................................................ 7
2.2. Parâmetros especiais........................................................................... 12
2.3 Variáveis identificadas.......................................................................... 16
2.4. Afectção de variáveis.......................................................................... 17
3. PASSO EM FRENTE........................................................................................ 23
3.1 Comentários......................................................................................... 23
3.2 Redireccionamento interno.................................................................... 24
3.3 Utilização do 'ED' dentro de um script.................................................... 25
3.4.Estados de saida - Exit Status................................................................ 28
3.5. Ciclos.................................................................................................. 28
3.6 O caixote do lixo: '/dev/null'................................................................... 35
3.7 Execução Condicional........................................................................... 35
3.8 'TEST': Um comando útil....................................................................... 43
3.9. Controlo incondicional: 'BREAK' e 'CONTINUE'.................................. 51
3.10. Aritmética EXPR............................................................................... 52
4. TÉCNICAS MAIS AVANÇADAS.................................................................... 54
4.1. Resolução de problemas....................................................................... 54
4.2. O comando 'SET'................................................................................. 55
4.3. Leitura ficheiros cim o 'WHILE READ'................................................ 56
4.4. A variável 'IFS'.................................................................................... 60
4.5. Execução condicional sem 'IF'.............................................................. 62
4.6. Tratamento de sinais............................................................................ 64
4.7. 'TPUT' - Programas atraentes.............................................................. 67
4.8. Funções.............................................................................................. 70
5. E AGORA?....................................................................................................... 75
6. EXERCÍCIOS.................................................................................................... 76
Exercício - I............................................................................................... 76
Exercício - II.............................................................................................. 76
Exercício - III............................................................................................ 76
Exercício - IV............................................................................................ 77
Exercício - V............................................................................................. 78
Exercício - VI............................................................................................ 78
Exercício - VII........................................................................................... 78
Exercício - VIII.......................................................................................... 78
Exercício - IX............................................................................................ 79
Exercício - X.............................................................................................. 79
Exercício - XI............................................................................................ 80
7. BIBLIOGRAFIA............................................................................................... 81
8. ÍNDICE............................................................................................................. 82
[8] Isto é uma afirmação muito discutivel...