Scripts Shell sob Controle

Márcio d'Ávila, 25 de fevereiro de 2004. Revisão 1, 5 de outubro de 2005.
Categoria: Unix / Linux

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:

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:

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:


Creative Commons License

© 2003-2007, 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.