sábado, 2 de março de 2019

Shell Script: tratamento de argumentos e opções

0 comentários
Na grande maioria das vezes, a linguagem shell script é usada para criar uma sequência de comandos que automatizam uma tarefa. Nisso, ela é extremamente eficiente e rápida. Combinar comandos é uma grande vantagem que o jeito UNIX de ser nos trouxe: várias ferramentas que fazem tarefas básicas, especializadas, e que quando se juntam realizam grandes feitos trabalhando juntas. Mas isso nunca impediu que se criasse também programas completos em shell script.
Uma característica para deixar um shell-script mais robusto e menos “sequencial/batch-mode” é o tratamento de argumentos. No meu clássico tutorial Programando em Shell-Script, o tópico Variáveis Especiais nos traz os primeiros itens que devemos aprender para o tratamento de argumentos. Existem variáveis especiais que tratam os argumentos passados para um programa ou uma função. Estes são:
  • $0 – Retorna o nome do script que foi executado
  • $N – Onde N é um número, corresponde ao argumento passado (1 = primeiro argumento, 2 = segundo argumento, 3 = terceiro argumento, etc)
  • $* – Retorna todos os argumentos de uma vez.
  • $# – Retorna a quantidade de argumentos passado para o script. (argc)
Vejamos agora um shell-script exempo que faz uso de todos esses argumentos:


#!/bin/bash
if [ $# -lt 1 ]; then
   echo "Faltou utilizar pelo menos um argumento!"
   exit 1
fi
echo "Numero de argumentos: $#"
COUNT=0
for ARG in $*; do
   COUNT=`expr $COUNT + 1`
   echo "Argumento $COUNT: $ARG"
done
As linhas 3 a 6 verificam se a quantidade de argumentos ($#) é menor (-lt – less than) que 1. Ou seja, se o usuário não chamou o programa com nenhum argumento, ele imprime um erro e sai do programa com status 1.
A linha 8 mostra quantos argumentos foram utilizados, usando novamente o $#.
O resto das linhas, 10 a 14, usam o $* com um laço for e um contador para mostrar quais foram os argumentos.
Executando agora este script sem argumentos:


./tmp.sh
Faltou utilizar pelo menos um argumento!
Agora executando com dois argumentos:
./tmp.sh naosei testando
Numero de argumentos: 2
Argumento 1: naosei
Argumento 2: testando
E agora com 4 argumentos:
./tmp.sh a b c d
Numero de argumentos: 4
Argumento 1: a
Argumento 2: b
Argumento 3: c
Argumento 4: d
Bem simples né?

Argumentos como opções e seus valores

Algo comum que vemos nos programas são opções. Opções não deixam de ser argumentos para um programa, mas eles tem um significado especial. Do tipo: Se a opção -d existir, ativar durante o programa o modo de depuração. Se houver um -h, então mostre uma ajuda e não faça mais nada. Se houver um -v mostre a versão, e por aí vai.
Exemplo:

#!/bin/bash
case $1 in
   "-h") echo "Isto seria uma ajuda... Mas fiquei com preguiça de escrevê-la."
         ;;
   "-v") echo "Versão 666."
         ;;
   *) echo "Opção inválida!"
      exit 1
      ;;
esac
Exemplos do uso do script:
$ ./tmp.sh -h
Isto seria uma ajuda... Mas fiquei com preguiça de escrevela.
$ ./tmp.sh -v
Versão 666.
$ ./tmp.sh -O
Opção inválida!
$ ./tmp.sh
Opcao invalida!
Com isso a gente resolve um problema e cria mais outros dois…
  • E se o usuário colocar as duas opções? Só uma funcionaria.
  • E se uma das opções precisasse de um valor? Estilo “-f arquivo.log” gravaria um arquivo de log com as operações.
Poderíamos escrever vários algoritmos que verificassem cada um de todos os argumentos, testasse se fosse um ou outro, utilizasse as opções… Mas felizmente não precisamos fazer nada disso! O bash conta com uma função interna que trata os argumentos: o famoso getopts.

Utilizando o getopts para tratar tratar argumentos e opções

Seguindo a mesma linha de raciocínio, vamos logo para um exemplo de programa. Supondo que queiramos um shell-script que faça isso:
  • Caso a opção -h seja usada, mostra a ajuda e sai do programa.
  • Caso a opção -v seja usada, mostra a versão e sai do programa.
  • Caso a opção -o seja usada, grava um arquivo de log com as operações efetuadas e resultados.
  • Caso a opção -u seja usada, mostra o resultado do comando “uname -a”
  • Caso a opção -m seja usada, mostra o resultado do comando “free -m”
  • Caso a opção -s seja usada, mostra o resultado do comando “swap -s”
Note que apenas as opções -h e -v saem do programa após a execução. Agora vamos ao código:


#!/bin/bash
function PrintUsage() {
   echo "Uso: `basename $0` <-umsf> [-ohv]"
   exit 1
}
while getopts "hvo:umsf" OPTION
do
   case $OPTION in
      h) PrintUsage
         ;;
      v) echo "`basename $0` versao 666."
         exit
         ;;
      o) ARQUIVO_LOG=$OPTARG
         ;;
      u) DO_UNAME=1
         ;;
      m) DO_FREE=1
         ;;
      s) DO_SWAPON=1
         ;;
      ?) PrintUsage
         ;;
   esac
done
shift $((OPTIND-1))
if [ -z "$DO_UNAME" ] && [ -z "$DO_FREE" ] && [ -z "$DO_SWAPON" ] && [ -z "$DO_FDISK" ]; then
   PrintUsage
fi
if [ "$ARQUIVO_LOG" ]; then   echo "Execucao iniciada em `date`." >> $ARQUIVO_LOG
   if [ "$DO_UNAME" == 1 ]; then
      uname -a >> $ARQUIVO_LOG
   fi
   if [ "$DO_FREE" == 1 ]; then
      free -m >> $ARQUIVO_LOG
   fi
   if [ "$DO_SWAPON" == 1 ]; then
      swapon -s >> $ARQUIVO_LOG
   fi
else
   echo "Execucao iniciada em `date`."
   if [ "$DO_UNAME" == 1 ]; then
      uname -a
   fi
   if [ "$DO_FREE" == 1 ]; then
      free -m
   fi
   if [ "$DO_SWAPON" == 1 ]; then
      swapon -s
   fi
fi
O interessante para nós são as linhas 8 a 28. O laço while getopts começa a tratar todos os argumentos. A cada iteração do laço, ele coloca a letra da opção na variável $OPTION.
Note que para cada opção que precisamos, colocamos uma letra no primeiro argumento do getopts:

while getopts "hvo:umsf" OPTION
Note também que depois da letra o temos um dois pontos (:). Esse dois pontos significa que logo após a opção -o, o usuário precisa fornecer um valor. Este valor é automaticamente armazenado na variável $OPTARG.
Dessa maneira, podemos executar esse programa de diversas formas:

./tmp.sh -o arquivo.log -u
(executa o "uname -a" e grava no arquivo arquivo.log)
./tmp.sh -um
(executa os comandos "uname -a" e "free -m")
./tmp.sh -m -s -u
(executa os comandos "free -m", "swapon -s" e "uname -a")
Ou seja, não importa a ordem, o getopts vai reconhecer e executar as ações de acordo com a opção especificada.
E se você colocar uma opção que não está contemplatada… O “?” do case irá ser executado, por exemplo:

./tmp.sh -a
./tmp.sh: illegal option -- a
Uso: tmp.sh <-umsf> [-ohv]
E dessa forma fica bem fácil de entender e usar o getopts :) Depois que o laço é todo feito e executado em todos os argumentos (no meu caso, preferi apenas configurar variáveis para cada opção e tratá-las depois), ele executa o comando que está na linha 28:
shift $((OPTIND-1))
Este comando faz com que os argumentos de opções sejam “comidos“, até que não sobre nenhuma opção. Em outras palavras, os argumentos representados pelas variáveis $N só serão aqueles que não pertençam a nenhuma opção. Exemplo:
./tmp.sh -u -o arquivo.log -m argumento1 argumento2
Nesse caso, o $1 seria o argumento1 e o $2 seria o argumento2, quando na verdade, sem o shift, eles seriam respectivamente o $5 e $6.
Como nem tudo é perfeito, a função getopts do bash não aceita opções longas (–nome-da-opcao), ou seja, voce só pode utilizar uma letra como opção. Represente bem suas opções com as letras! :)

Argumentos dentro de funções

Se dentro de um shell-script temos uma função, essa função é enxergada pela shell como se fosse um comando. Nesse sentido, dentro de uma função as variáveis $N definidas pelo programa não funcionarão. Exemplo:

#!/bin/bash
function Dummy() {
   echo "Numero de argumentos: $#"
   COUNT=0
   for ARG in $*; do
      COUNT=`expr $COUNT + 1`
      echo "Argumento $COUNT: $ARG"
   done
}
Dummy
Não importa o que você executar com o script acima, a saída será sempre a mesma: 0 números de argumentos, como mostrado a seguir.
$ ./tmp.sh
Numero de argumentos: 0
$ ./tmp.sh naosei temporario
Numero de argumentos: 0
$ ./tmp.sh a b c d e f g
Numero de argumentos: 0
Para a função Dummy, as variáveis especiais dos argumentos funcionam apenas para a função e não para o programa inteiro. É como se as variáveis fossem locais, e não globais. Vamos então substituir a linha da chamada da função Dummy (linha 13) por:
Dummy a b c d
E tentar executar novamente:
$ ./tmp.sh
Numero de argumentos: 4
Argumento 1: a
Argumento 2: b
Argumento 3: c
Argumento 4: d
Sabendo disso, não se percam na hora de usar os argumentos dentro das funções e lembrem-se que isto pode ser útil na hora de implementar diversas funções dentro de um script. Um bom exemplo disso é implementar a função PrintUsage que usamos anteriormente para, além de mostrar uma mensagem de uso, mostrar também uma mensagem de erro personalizada:
function PrintUsage() {
   [ "$1" ] && echo -ne "Erro: $1\n"
   echo "Uso: $(basename $0)  <-umsf> [-ohv]"
   exit 1
}
Agora é so chamar a função como…

PrintUsage "Faltando parâmetros."
PrintUsage "Opção inválida."
PrintUsage "No donut for you."
Use a criatividade de um programador (afinal, programação é arte) e comece a aprimorar suas ferramentas bash! :)

Leave a Reply