Em sistemas Unix/Linux e mesmo em outros ambientes que possuam a capacidade de executar arquivos com sequências de comandos do sistema operacional, chamados scripts shell, este recurso é muito útil para automatizar e padronizar operações complexas ou repetitivas, facilitar a execução de tarefas por operadores e agendar a execução automática de tarefas periódicas.
O ambiente de script shell Unix conta com recursos variados e poderosos que tornam a criação de scripts um efetivo desenvolvimento de programas. Apesar de representarem um ambiente de programação, os scripts shell nem sempre são tratados com boas técnicas de programação como deveriam. Além da preocupação com a programação das ações devidas, dois outros aspectos são essenciais para robustez e controle da execução:
stdout
) e de erro (stderr
)
gerada pelos comandos executados em arquivos de log (histórico de execução);Este artigo apresenta uma proposta para se construir um script shell bem estruturado e com recursos robustos para tratamento de log e exceções. São listados trechos de um script do início ao fim, descrevendo informações importantes, recomendações e técnicas úteis. O texto pressupõe um conhecimento básico do leitor sobre os comandos e o ambiente shell do Unix.
Um modelo de script completo, com todos os trechos reunidos, está disponível para download: modelo_script.sh
Todo script deve começar com a primeira linha que identifica a shell a ser
utilizada, com #!
seguido do executável da shell desejada.
A shell padrão encontrada em todo Unix é a Bourne Shell (sh), mas muitas vezes
os scripts usam em substituição a Korn Shell (ksh) ou a Bourne Again Shell
(bash), que são baseadas na mesma sintaxe básica da Bourne, porém mais
evoluídas, sem algumas limitações da sh e com diversas melhorias.
A ksh é encontrada em praticamente todas as variantes Unix, oferecendo
portanto ótima portabilidade. A bash surgiu no Linux (onde a sh na verdade
é a bash, ou seja, sh é um mero link para bash), mas já foi
incorporada à distribuição padrão de muitos Unix, como o Sun Solaris 9 por exemplo.
Existe também a C Shell (csh), com sintaxe similar à linguagem C, entre outras.
Em seguida, é importante adicionar um cabeçalho de linhas de comentário com uma descrição, instruções e outras informações importantes sobre o script.
#!/bin/sh # # meuscript.sh # Comentario descritivo do programa # incluindo revisor/data de alteracao # Se for um script agendado, incluir sintaxe do cron #
Para tornar o script configurável, flexível e adaptável, é importante parametrizar o máximo de suas opções e propriedades. Além disso, as variáveis que definem estes parâmetros devem ser definidas logo no início do script, de modo que fique fácil localizá-las para um eventual ajuste.
Procure também seguir uma nomenclatura consistente para as variáveis, facilitando a compreensão e leitura. Eis algumas sugestões: utilizar apenas letras maiúsculas para facilitar sua identificação no código; utilizar prefixos padronizados como DIR_ para nome de diretório ou caminho, TMP_ para nome de arquivo temporário e assim por diante.
#=====! Opcoes e atributos configuraveis do script !===== MAX_RETENCAO=500 MAIL_TO=operador@dominio
Existem algumas informações básicas sobre o script que devem ser inicialmente
obtidas e referenciadas sempre que necessário, ao longo do script: o nome do
arquivo do programa script (por exemplo, meuscript.sh
), o caminho
de diretórios (path) em que se localiza o arquivo, o nome do script sem
a extensão de arquivo usual .sh (por exemplo, meuscript
), e a
identificação (username) do usuário que o está executando.
PROG=`basename $0` DIR_PROG=`dirname $0`; DIR_PROG=`cd $DIR_PROG; pwd` SCRIPT=`echo $PROG | sed 's/\.sh$//'` USERNAME=`id | sed 's/^uid=[0-9]*(//; s/).*//'`
Nomes de diretórios, arquivos e dispositivos utilizados pelo script devem ser sempre parametrizados como variáveis, não só para personalização, mas principalmente para facilitar a identificação das dependências do script.
Na criação de arquivos temporários, procure sempre incluir o número do
processo atual do script em execução, dado por $$
, no nome de cada
arquivo. Esta técnica evita conflito com outros arquivos e ainda facilita
identificar a qual processo em execução pertence o arquivo. Utilize
preferencialmente a área de arquivos temporários padrão do sistema operacional,
normalmente /var/tmp/
nos sistemas Unix.
DIR_LOG=$DIR_PROG/log LOG=$DIR_LOG/$SCRIPT.log TMP_PID=/var/tmp/$SCRIPT.pid TMP_LOG=/var/tmp/${SCRIPT}_$$.log TMP_TRUNC=/var/tmp/${SCRIPT}_$$.trunc TMP_MAIL=/var/tmp/cab_mail_$$.txt
Modularize seu script com funções. É fácil criar uma função em shell script:
sua declaração consiste no nome seguido de abre- e fecha-parênteses, seguido de
um bloco de comandos delimitado por chaves. Podem ser passados parâmetros para
uma função, que são referenciados dentro dela por $1
, $2
etc.
As funções racionalizam o código permitindo eliminar trechos repetidos em mais de um ponto do programa, além de tornar o fluxo principal do programa mais claro e legível. Existem três funções que costumam ser sempre muito oportunas em scripts:
rm
, use sempre o parâmetro -f
, que tem dois efeitos
úteis para execução em script: força a remoção mesmo quando a permissão de
leitura do arquivo não está consistente com a permissão do diretório; e não gera
saída de erro quando o arquivo não existe.
No sistema operacional Solaris, é recomendável
acrescentar o parâmetro -t
no comando mail
, para que
o destinatário do e-mail fique visível no campo To/Para do cabeçalho.
fn_fim_script
e gerado um código de saída diferente de 0 (sugestão de padronização: 1), para
sinalizar a finalização anormal do script. A mensagem de erro deve ser enviada
para a saída de erro (stderr
, descritor &2
). Além
disso, optamos por enviar a mensagem também para o log, com o seguinte critério:
avisos vão sempre para o log e erros vão apenas quando já existe algum log.fn_fim_script
para limpeza e gerar um código de saída distinto (sugestão: 2).##### fn_fim_script ##### fn_fim_script() { # Retencao de log: Preserva no maximo MAX_RETENCAO linhas anteriores no log if [ `cat $LOG 2> /dev/null | wc -l` -gt $MAX_RETENCAO ]; then echo "##### $PROG: $MAX_RETENCAO ultimas linhas preservadas\n" > $TMP_TRUNC tail -$MAX_RETENCAO $LOG >> $TMP_TRUNC mv $TMP_TRUNC $LOG fi # Envia o arquivo de LOG corrente por email e o anexa ao LOG cumulativo if [ -f $TMP_LOG ]; then cat > $TMP_MAIL <<EOT Subject: Resultado do script $PROG EOT cat $TMP_LOG >> $LOG cat $TMP_MAIL $TMP_LOG | mail $MAIL_TO fi # Remove arquivos temporarios rm -f $TMP_PID $TMP_LOG $TMP_TRUNC $TMP_MAIL } ##### fn_erro ##### fn_erro() { SAIR=$1 shift case $SAIR in [sSyY]*|1) echo "$PROG ERRO : $*" >&2 [ -f $TMP_LOG ] && echo "##### $PROG ERRO : $*" >> $TMP_LOG fn_fim_script exit 1 ;; *) echo "$PROG Aviso: $*" >&2 echo "##### $PROG Aviso: $*" >> $TMP_LOG ;; esac } # fn_erro ##### fn_trap ##### fn_trap() { fn_erro N "Script interrompido em `date`" fn_fim_script exit 2 } # fn_trap
Variáveis de configuração e definições de função em arquivos externos podem
ser "importados" no script, com o comando ponto (.
).
. $DIR_PROG/setenv
Terminadas as seções de variáveis e funções, inicie o programa principal, identificando-o para fácil localização no texto do script.
########## Programa Principal ##########
A primeira medida tomada pelo programa, aconselhável principalmente para
scripts de execução periódica automática (no cron
), é o uso de um
esquema de "lock" que garanta que haja apenas uma instância em execução do script.
Em algumas situações, isto pode ser obrigatório para o correto funcionamento
do script, ou recomendado para evitar sobrecarga em scripts que envolvem
processamento ou E/S intensos. Se este não for o seu caso, você pode suprimir
este trecho. O mecanismo de trava consiste em três passos simples e eficazes:
fn_fim_script
deve, na finalização do programa,
remover o arquivo de trava.Se o script em execução for interrompido com o sinal 9 (SIGKILL:
kill -9
), o arquivo de trava será deixado para trás, existente.
O sinal SIGKILL não é mascarável com trap, de forma que o processo é
abruptamente interrompido sem chance de executar qualquer tratamento de
finalização. (Veja mais informações
neste link.)
Mesmo neste caso, como o mecanismo de trava testa se o processo indicado no
arquivo realmente está em execução, isto não deve representar problema.
# Garante a execucao de apenas uma instancia do script if [ -s $TMP_PID ]; then PID=`cat $TMP_PID` if ps -p $PID 2> /dev/null >&2; then echo "$PROG: Outra instancia em execucao PID=$PID em `date`" >&2 exit 3 fi fi echo $$ > $TMP_PID
O próximo passo é fazer testes de pré-condições, consistências necessárias
para o funcionamento correto e seguro do script. Por exemplo, pode ser testado
se o usuário que está executando é o esperado ou requerido, se os diretórios
necessários existem etc. A ação em caso negativo pode ser reportar um aviso ou
erro de interrupção do programa (para estes casos, temos a função
fn_erro
), ou ainda a tomada de medidas corretivas (exemplo:
criar um diretório necessário).
# Pre-condicoes [ "$USERNAME" = "admin" ] || fn_erro S "Execute este script como admin." [ -d $DIR_LOG ] || mkdir $DIR_LOG
Vencidas as ações preliminares, o programa irá começar sua real atividade. É hora então de capturar e tratar os sinais de interrupção mascaráveis do sistema operacional, para lidar apropriadamente com as tentativas de interrupção do processo. Os principais sinais de interrupção mascaráveis são:
kill
para informar
que o processo deve terminar.# Impede a interrupcao por HANGUP (1), INTERRUPT (2) e TERMINATE (15) trap "fn_trap" 1 2 15
Até agora foi apresentado um bocado de código, mas nada da efetiva atividade para a qual o script tenha sido criado, seja ela qual for. Você deve estar se perguntando: "Afinal onde e quando eu vou colocar o código que realmente interessa?" Pois bem, essa hora finalmente chegou. Coloque neste ponto os comandos para executar a tarefa para a qual o script se destina.
Salve log de tudo, capturando a saída padrão e de erro de
todos os comandos pertinentes, com redirecionamentos para arquivo. Se a saída de
um comando não for necessária, descarte-a explicitamente redirecionando para
/dev/null
. Desta forma, será possível rastrear e conferir as ações
ocorridas no script, mesmo quando este for executado de forma não interativa
(com cron
ou at
). Só devem restar exibidas na tela ou
saída padrão as mensagens geradas intencionalmente pelo programa, mais as
exceções e erros não previstos.
( cat <<EOT ====================================================================== $PROG INICIO: `date` ---------------------------------------------------------------------- EOT ... aqui_entram_seus_comandos ... cat <<EOT ---------------------------------------------------------------------- $PROG TERMINO: `date` ====================================================================== EOT ) >> $TMP_LOG 2>&1
Por último, mas não por menos, não esqueça de chamar fn_fim_script
ao término do programa. Se quiser garantir que o script finalizado normalmente
retorne código de saída 0 (sucesso), ao invés do código de retorno do último
comando executado (em fn_fim_script), adicione um exit 0
.
fn_fim_script exit 0 #fim
A abordagem de geração de log que foi aqui apresentada é adequada para scripts de execução repetida ou periódica, consistindo no tratamento de log em duas etapas:
TMP_LOG
), que
vai sendo acumulado ao longo desta execução;LOG
), ao qual o log
individual é concatenado ao final e então descartado.Isto possibilita, por exemplo, enviar para o e-mail de um operador apenas o resultado de uma execução, evitando ter que enviar todo o log cumulativo.
Outra abordagem também muito usada é manter logs separados por execução,
incluindo a data/hora de execução no nome do arquivo de log. Neste caso, o
próprio arquivo referenciado por $LOG
já seria usado na saída dos
comandos. Para limpeza automática de logs acumulados, pode ser definido um
perído máximo de retenção (em dias) e executado um comando find
que
elimine os arquivos de logs antigos por este critério. O trecho a seguir resume
as modificações no script para esta alternativa.
DATA=`date "+%Y%m%d_%H%M"` MAX_RETENCAO=60 # Dias (ao inves de linhas) LOG=$DIR_LOG/${SCRIPT}_${DATA}.log ... fn_fim_script() { # Retencao de log: Exclui logs criados/modificados ha mais de MAX_RETENCAO dias echo "\n##### $PROG: Remocao de arquivos com mais de $MAX_RETENCAO dias" >> $LOG find $DIR_LOG -mtime +$MAX_RETENCAO -print -exec rm -f {} \; >> $LOG 2>&1 ... cat $TMP_MAIL $LOG | mail $MAIL_TO ... } ... comandos >> $LOG 2>&1
Vimos que a criação de um script shell bem estruturado e robusto envolve uma preocupação em tratar muito mais que apenas a finalidade pretendida para o script, abordando também diversos aspectos de organização, controle, segurança e gerenciamento. Este artigo porém não esgota as características e recursos do ambiente shell e a programação de scripts, que provê um vasto universo de possibilidades para a automação de tarefas.
Existem também outros bons ambientes para programação de scripts e automação de tarefas em Unix/Linux e outras plataformas, muito populares e cada vez mais encontrados em distribuições padrão de sistema operacional. Em especial, destacam-se:
© 2003-2025, Márcio d'Ávila, mhavila.com.br, direitos reservados. O texto e código-fonte apresentados podem ser referenciados, distribuídos e utilizados, desde que expressamente citada esta fonte e o crédito do(s) autor(es). A informação aqui apresentada, apesar de todo o esforço para garantir sua precisão e correção, é oferecida "como está", sem quaisquer garantias explícitas ou implícitas decorrentes de sua utilização ou suas conseqüências diretas e indiretas.