Scripts : Shell sob Controle

Em sistemas Unix/Linux e mesmo em outros ambientes que possuam a capacidade de executar arquivos com seqüê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:
  • captura da saída padrão (stdout) e de erro (stderr) gerada pelos comandos executados em arquivos de log (histórico de execução);
  • tratamento das situações de erro previsíveis e de interrupções.
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: Download modelo_script.sh

Um script trecho-a-trecho

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
#

Parametrização

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

Modularização

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:
fn_fim_script( )
Concentre aqui as operações de "limpeza" e fechamento que devem sempre ser feitas na finalização do programa, seja no final da execução normal, seja quando o programa é interrompido (por erro fatal identificado ou interrupção). A utilização desta função garante a finalização consistente do programa nas diversas situações. Neste exemplo tratamos: finalização de log — limpar logs antigos, enviar log corrente por email e concatená-lo ao log cumulativo — e remoção dos arquivos temporários. No comando 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_erro( )
Tratamento padronizado de erros fatais (que impedem o prosseguimento da execução) e de avisos (erros "leves", não-fatais) no script. No caso de encerramento do programa, deve ser chamada a função 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_trap( )
Tratamento dos sinais mascaráveis de sistema operacional capturados pelo script, normalmente aqueles que causam interrupção do programa. Similar ao tratamento dos erros fatais, deve exibir um aviso, chamar 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 ##########

Preparativos

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:
  1. Verificar se o arquivo de trava deste script existe e aponta para um processo ainda em execução. Em caso positivo, encerrar a execução atual (sugestão de código de saída: 3).
  2. Quando não há outra execução do script em andamento, esta instância deve então criar o arquivo de trava contendo o seu número de processo.
  3. A função 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:
1 = SIGHUP / HANGUP
Sinal enviado ao processo pelo sistema operacional quando a shell ou sessão a partir da qual o script foi executado é finalizada.
2 = SIGINT / INTERRUPT
Sinal enviado pelo sistema operacional ao processo em execução em uma sessão interativa, quando o usuário pressiona CTRL+C.
15 = SIGTERM / TERMINATE
Sinal padrão enviado pelo comando 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

O principal

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

Geração de Log

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:
  • um log temporário apenas da instância corrente (TMP_LOG), que vai sendo acumulado ao longo desta execução;
  • e outro log permanente e cumulativo (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

Conclusão

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:
  • A linguagem de script Perl, que é inspirada no próprio ambiente shell Unix e na linguagem C. Perl é uma linguagem interpretada multi-plataforma com suporte à orientação a objetos e que dipõe de uma quantidade enorme de recursos nativos, bibliotecas com APIs e funcionalidades prontas para as mais variadas finalidades, além do grande poder de expressão e manipulação em texto com o uso extensivo de expressões regulares.
  • A linguagem Python, interpretada, orientada a objetos e portável, que apesar de menos popular que Perl, combina enorme poder com uma sintaxe clara e estruturada.
  • A ferramenta de automação Make, mecanismo padrão do Unix para compilação, montagem e distribuição de programas e pacotes de software.
  • A ferramenta de automação Apache Ant, com finalidade similar ao Make, porém mais poderosa e estruturada. Ant é baseada em Java e sintaxe XML, portável e extensível.
fonte: http://www.mhavila.com.br/topicos/unix/shscript.html
Close Menu