Capítulo 4. Programando no Shell
O uso do shell como uma linguagem de programação é uma extensão natural de sua utilidade como interpretador de comandos. As pessoas preferem programar em uma linguagem que conhecem e usam no dia-a-dia a ter que aprender uma linguagem totalmente diferente para automatizar suas tarefas.
4.1. O primeiro script shell
Vamos fazer nosso primeiro script. Tudo que queremos fazer agora é imprimir “Hello World” na tela. Nosso primeiro script fica assim:
#!/bin/bash echo Hello World
Esse scripts deve ser gravado em um arquivo com, pelo menos, permissão de execução e leitura. A primeira linha do script é especial. O marcador #! apenas indica ao kernel que o tipo do arquivo executável é um script. O kernel vai, ao detectar esse marcador, executar o arquivo usando o interpretador de comandos logo à direita (nesse caso, /bin/bash. Verifique se esse é o caminho para o Bourn Against Shell em sua máquina.
Após isso, o que segue é uma série de comandos, exatamente como poderiam ter sido digitados no prompt do shell. Claro que em um shell script, vamos usar recursos que não são normalmente usados no dia-a-dia do uso interativo do shell, como funções, laços, etc, mas é importante lembrar que a linguagem interativa e a linguagem de script são exatamente a mesma.
4.2. Comentários em shell scripts
Os comentários em shell scripts começam com o caractere # e se estendem até o fim da linha, e podem ser colocados em qualquer lugar da linha.
Lembre-se de manter os comentários pequenos e relevantes. O comentário não deve indicar como o shell script é implementado, e sim por que. Evite, portanto, comentários evidentes do tipo:
echo Hello, World! # imprime "Hello, World!"
4.3. Escapando caracteres especiais
As regras de escape de caracteres especiais são exatamente as mesmas que em um shell interativo. Você pode escapar um caractere usando ““uma seqüência de caracteres usando '' e uma seqüência de caracteres mas ainda interpretando variáveis com "". Veja os exemplos no shell interativo:
$ echo # Hello World $ echo # Hello World # Hello World $ echo '# Hello World' # Hello World $ echo $HOME /home/osvaldo $ echo $HOME $HOME $ echo '$HOME' $HOME $ echo "$HOME" /home/osvaldo
Caso haja necessidade de inserir uma string no script, é recomendado que essa string esteja escapada dentro de ". Apenas caso haja absoluta certeza que a expansão de variáveis dentro dessa string seja necessária deve-se usar "".
4.4. Lendo entrada do teclado
Uma maneira simples de ler entrada do teclado é usando o comando read. Rodando read VARIAVEL, o comando read guarda a entrada do teclado em $VARIAVEL.
4.5. Variáveis de parâmetros
Vamos melhorar nosso primeiro exemplo. Ao invés de imprimir Hello, World, vamos imprimir o que quer que o usuário passe como parâmetro. O script ficaria assim:
#!/bin/bash echo $*
A variável $* é uma variável especial que só faz sentido em scripts. Ela expande para todos os argumentos passados ao script. Veja como ficaria a execução desse script:
$ ./exemplo2 Ola mundo Ola mundo
Mais usualmente, o autor do script precisa ler um parâmetro em particular passado pelo script. É possível ler cada um dos parâmetros separadamente através das variáveis $1, $2, etc. A variável $0 expande para o comando usado para iniciar o programa. Veja o exemplo:
#!/bin/bash echo $0 $2
Executando esse script com o nome exemplo3, temos:
$ ./exemplo3 primeiro segundo ./exemplo3 segundo
Finalmente, algumas vezes a quantidade de parâmetros que um script recebe é variável. A quantidade de parâmetros passada ao script é guardada na variável $#.
4.6. O comando de decisão if
if é um comando built-in que serve para fazer uma decisão. O funcionamento dele é assim:
if expressão; then comando1 comando2 ... fi
“Expressão” é qualquer coisa que possa retornar um valor verdadeiro ou falso para o shell. Tipicamente, é um comando – seja um comando externo ou built-in.
Vamos criar um pequeno script que lê o arquivo /etc/passwd e procura um usuário no mesmo. O script vai retornar 0 caso o usuário exista, e 1 caso o usuário não exista.
O formato do arquivo /etc/passwd é o seguinte:
login:senha:UID:GID:Nome Completo:Diretório HOME:shell
Nós vamos usar o comando grep com uma pequena expressão regular para procurar o usuário.Em uma expressão regular, ^ indica o começo da linha. Logo, o que queremos encontrar no arquivo é aseqüência ^primeiroparâmetro:, ou ^$1:. Se existir uma linha que combine com essa expressão, o usuário existe no sistema. Ainda, o grep por padrão mostra a linha encontrada na tela. Nós não queremos a linha, queremos apenas saber se ela existe. Para suprimir a saída, o grep tem a opção -q. Fica assim nosso script checa-usuário:
#!/bin/bash if grep -q ^$1: /etc/passwd; then exit 0 fi exit 1
O if checa o código de retorno do grep. Quando o grep encontra a string no arquivo, ele retorna 0, o que causa a execução do exit 0. Caso negativo, a execução do script segue normalmente, acabando na expressão exit 1.
Opcionalmente nesse caso, pode-se usar a parte else do if. Funciona assim:
if expressão; then comando-se-verdadeiro else comando-se-falso fi
Nosso script revisitado usando else ficaria assim:
#!/bin/bash if grep -q ^$1: /etc/passwd; then exit 0 else exit 1 fi
Funcionalmente é idêntico, mas nem sempre tem-se a opção de não usar else.
É possível, ainda, negar o resultado de qualquer expressão, botando ! na frente. Por exemplo:
if ! grep -q ...
4.7. while
O laço while executa determinado código enquanto uma certa condição for satisfeita. Por exemplo, o seguinte script imprime “Esperando” até que alguém crie o arquivo /tmp/continue.
#!/bin/bash while ! test -f /tmp/continue; do echo Esperando sleep 10 # Espera dez segundos done
4.8. for
O for executa um comando (ou uma série de comandos) com uma variável definida cada vez para um valor em uma lista. Por exemplo, o seguinte exemplo imprime linhas com 1, 2 e 3 na tela.
#!/bin/bash for i in 1 2 3; do echo $i done
4.9. case
Às vezes é necessário checar o valor de uma variável ou a saída de um comando e, de acordo com o conteúdo, executar determinada ação. É possível fazer isso com uma série de if s, mas é mais prático fazer usando o comando case.
Por exemplo, vamos supor que uma variável $STATUS possa ter o conteúdo “OK”, “SUCCESS”, “FAILED”e “NOT RUNNING”(digamos que essa variável venha de um campo de um arquivo de log, por exemplo). Vamos criar uma pequena rotina que imprime na tela se a variável indica sucesso ou erro.
case "$STATUS" in OK | SUCCESS) echo Programa funcionando com sucesso ;; # Indica o fim do bloco de código FAILED) echo Programa falhou ;; "NOT RUNNING") echo Programa não está rodando ;; esac
Esse é um exemplo bastante completo do uso de case. Note como é possível usar o pipe como o valor de OR para que dois valores executem o mesmo trecho de código. Note também que o valor NOT RUNNING deve vir entre aspas por causa do espaço.
4.10. Expressões matemáticas
Expressões matemáticas são colocadas da seguinte forma:
$ VALOR=$((2+2)) $ echo $VALOR 4 $ echo $(($VALOR+2)) 6
4.11. Colocando a saída de um comando em uma variável
É possível colocar a saída de um comando em uam variável (ou de forma genérica setar o valor de uma expressão para a saída de um comando) usando $().
$ SAIDA=$(echo oi) $ echo $SAIDA oi
Uma maneira alternativa de fazer a mesma coisa é usando aspas invertidas:
$ SAIDA='echo oi'
Além de colocar a saída de um comando em uma variável, esse tipo de construção pode ser usada em muitas outras partes de um shell script, como em uma comparação, substituindo expressões de outro tipo, etc.
4.12. Fazendo comparações
O comando quase universal de comparações é o [. Na verdade um link para o comando test, às vezes presente como um comando built-in do shell, o [ permite comparar conteúdos de expressões como strings e numericamente, verificar permissões e comparar datas de arquivos. Um exemplo típico é comparar se o conteúdo de uma variável é igual à saída de um comando:
if [ "$MES" = $(date +%m) ]; then ... fi
Note que todo comando [ termina também com ], por motivos principalmente estéticos. Note também que o [ não é muito tolerante a falta de parâmetros. Se a variável MES estivesse vazia e não estivéssemos usando aspas, o comando executado após expansão do shell seria:
if [ = $(date +%m) ]; then
Isso causaria falha no programa, então proteja adequadamente as variáveis com aspas, especialmente dentro do comando [. Alguns outros exemplos de uso do [:
# Checa se o arquivo /tmp/foo não existe if [ ! -f /tmp/foo ]; then ... fi # Checa se $MES é menor ou igual ao mês atual if [ "$MES" -le $(date +%m) ]; then ... fi # Checa se o arquivo /etc/passwd é mais novo que /etc/nis.map if [ /etc/passwd -nt /etc/nis.map ]; then ... fi # Checa se o mês é menor ou igual ao mês atual OU se o arquivo /etc/nis.map # existe if [ "$MES" -le $(date +%m) -o -f /etc/nis.map ]; then ... fi
Uma lista completa de opções que podem ser usadas com o [ está na man page do comando test. Abaixo está a lista das mais comuns:
- -f – Checa se o arquivo existe
- = – Checa se strings são iguais
- != – Checa se strings são diferentes
- -eq – Checa se strings são algebricamente iguais
- -le – Menor ou igual
- -ge – Maior ou igual
- -lt – Menor que
- -gt – Maior que
- -ne – Diferente
- ! – Negação
- -a – E (AND)
- -o – Ou (OR)
4.13. As operações mais comuns em shell scripts
4.13.1. Contando linhas, palavras, etc.
O comando wc conta linhas, palavras e caracteres em um arquivo.
- wc -l Conta linhas
- wc Conta palavras
- wc -c Conta caracteres
4.13.2. Separando campos
O comando cut é a forma mais prática de separar campos. Rode da seguitne forma:
cut -d: -f 1,3
O caractere depois do -d é o separados dos campos, e depois do -f vem os campos que deseja pegar.É possível usar “ranges“, como 1-5 para indicar os campos de 1 até 5.
Note que usar o awk para fazer essa separação simples de campos é um exagero, que torna o script mais complexo sem necessidade.
4.13.3. Substituir caracteres
Para substituir caracteres, use o comando tr. A forma de usar é:
tr abc 123
Esse comando substitui as ocorrências de “a”por “1”, “b”por “2”, etc. Também é possível usar ranges:
tr [A-Z] [a-z]
Esse comando transforma todas as maiúsculas de A a Z em minúsculas.
4.13.4. Substituindo strings
O comando para substituir strings é o sed. A funcionalidade básica é:
sed s/isso/aquilo/
Porém, o sed é mais complexo que isso, pois ele aceita o uso de expressões regulares, que serão explicadas mais tarde.
4.13.5. Procurando texto em um arquivo
Para buscar texto em um arquivo ou na entrada padrão, use o comando grep, que vai retornar as linhas em que ocorre o texto:
$ grep root /etc/passwd root:x:0:0:root:/root:/bin/csh
Mais importante ainda que isso para quem vai fazer scripts é o código que o grep retorna: 0 para string encotnrada, 1 para string não encontrada. Isso, em conjunto com a opção -q, que suprime a saída, é muito útil em um script:
if [ grep -q root /etc/passwd ]; then echo Usuário root existe! fi
4.13.6. Enviando a saída padrão para a saída padrão e para um arquivo
Às vezes é desejável gravar a saída padrão de um programa e ao mesmo tempo enviá-la para um outro programa. Para isso, use o comando tee:
ps -ef | tee processos | grep named
O comando acima guarda a lista completa de processos (a saída do comando ps) no arquivo processos e imprime aquelas linhas que casam com a string “named”na tela.
4.13.7. Economizando tempo setando variáveis
Uma coisa que não foi dita antes é que a forma $VAR de uma variável é apenas uma maneira mais curta de usar a notação completa ${VAR}. A forma completa permite acrescentar alguns manipuladores especiais que economizam código e tempo. Os operadores mais comuns são:
- ${VAR:=valor} – Seta $VAR como “valor” se a variável estiver vazia ou não existir.
Funciona como referência normal a $VAR caso não-vazia. - ${VAR:-valor} – Se $VAR existe e tem um valor não nulo, age como $VAR, caso contrário $VAR passa a valer “valor“.
4.13.8. Interceptando sinais
Para manter o ambiente de seu script sempre limpo, use a função trap para interceptar sinais. Por exemplo, duas maneiras comuns de matar scripts são enviar os sinais HUP e TERM. Você deve criar um trap para interceptar esses sinais e fazer operações de limpeza (como limpar arquivos temporários).
AVISO: Como avisamos anteriormente, esta parte está incompleta!