Olá, recentemente criei uma série de scripts para automatizar tarefas em meu sistema. Eu estava utilizando o
Python para fazer isso, mas ele se mostrou muito complicado de utilizar em alguns casos, pois a maioria dos meus scripts envolvia chamadas de sistema (rsync, ffmpeg etc) e o Python acaba sendo pouco sucinto para lidar com comandos simples.
Seguindo um dos princípios do texto seminal de
Eric Raymond How to become a Hacker, optei por aprender escrever em
Shell Script decentemente. Fiquei surpreso com o quanto o Bash é capaz de lidar com coisas complexas, principalmente quando se tem que lidar com comandos em texto.
Neste tutorial simples, mostrarei como utilizar o comando
getopts, que coleta parâmetros da linha de comando para tornar o script mais interativo. A documentação do getopts é bastante obscura e explica pouca coisa; além disto, a maioria dos tutoriais que encontrei usa exemplos complicados demais. Um dos desafios do getopts é ter a criatividade para explorar seu potencial.
Operação básica
O comando getopts tem uma estrutura muito simples:
getopts opstring name args;
Tradução:
- getopts: o comando em si;
- optstring: uma string com as opções que serão filtradas;
- name: nome da variável que captura o resultado do getopts;
- args: argumentos passados dentro do script, não é obrigatório, serve para testes;
Vejamos os detalhes utilizando um exemplo prático. Comecemos com um comando de referência:
do -a x -b y -c 1 2 3;
Então aqui temos o comando "do", que faz nada, e para ele passei três parâmetros (a, b, c), onde a e b tem inputs e mais uma lista de parâmetros arbitrários (1, 2, 3).
O comando getopts para este script seria:
getopts "a:b:c" opt;
Agora vejamos como funciona em detalhes:
1. O getopts lê por padrão a variável $@. Isso equivale, no exemplo anterior a:
getopts "a:b:c" opt "$@";
Mas é desnecessário identificar a variável de entrada, exceto se você quiser forçar os parâmetros para testar os recursos.
2. O getopts lê o $@ um passo de cada vez:
Em vez de ler o $@ de uma vez e retornar um array ou algo do tipo, cada chamada do getopts lê um parâmetro do $@ a cada chamada. Por isso, é comum utilizar o getopts dentro de um while:
while getopts "a:b:c" opt; do
: #comando que nada faz
done;
A cada chamada o getopts coloca o parâmetro em questão na variável $opt. Quando o getopts parar de encontrar parâmetros ele retorna vazio e o while finaliza.
Então a cada rodada, o $opt será:
a
b
c
3. O getopts cria duas variáveis de ambiente: OPTIND e OPTARGS
$OPTIND contém a próxima posição a ser lida pelo getops. Equivale a tratar o $@ como se fosse um array. Esta variável pode ser utilizada com o comando shift para separar as opções dos demais argumentos:
shift $(( ${OPTIND} - 1 ));
Só tomem cuidado com o comando acima, pois ele assume que os argumentos que estão no começo são nominais e os no final do comando são posicionais (como em uma função de verdade). Se misturarmos a ordem dos parâmetros o raciocínio muda. Na prática, o getopts assume que o ultimo parâmetro com um traço (-c) é de fato o último parâmetro nominal, logo qualquer coisa depois disto será ignorada de um modo ou de outro:
do -a x -b y -c 1 2 3 -w;
Neste caso o -w é sumariamente ignorado pelo getopts. Ele será tratado como parte dos argumentos posicionais.
$OPTARGS contém os valores que foram passados para o opção em questão. No nosso caso seriam os valores x e y dos parâmetros. Quando a opção for sem argumentos, então ela ficará vazia (unset).
Considerando o comando que demos como exemplo, os valores armazenados seriam:
x
y
" " # nada
4. O getopts só aceita argumentos curtos
Uma limitação do getopts é que ele só aceita parâmetros simples (-h) quem podem ser agrupados sem problema (-abc), mas não podem ser longos (--help).
Existem muitas gambiarras para forçá-lo a funcionar com opções longas, mas não me pareceu útil. Se você faz muita questão de opções longas, passe as opções como valores e as processe internamente. Essa é a minha sugestão:
do -o help -o me;
Mas fiquemos atentos para como capturar os valores passados depois.
5. O getopts distingue opções com valores e sem valores
Esta é a ultima dica. Notem que na opstring algumas opções possuem um dois pontos depois delas. Isso significa que aquela opção recebe um valor. Se ela for dada sem valor o getopts assumirá que o próximo valor é o valor que pode criar confusão em uma sequência de valores:
OPTSTRING="a:b:c:";
do -a -b -c x;
O que acontecerá aqui é:
- o -b será o valor do -a;
- o x será o valor do -c;
Virou confusão. Uma vez que um parâmetro seja definido como portador de valor, ele sempre deve ser passado com os valores.
A sintaxe da opstring é o mais simples possível:
"a:bc" #a recebe valores, b e c não
"a:b:c:" #a, b e c recebem valores
Existe um detalhe importante. Se passarmos uma opção não prevista na opstring o getopts retornará erro e travará o script. Isso pode ser evitado colocando um dois-pontos no início da optrstring, isso torna o getopts mais tolerante:
":abc" # aceita qualquer coisa, mas prefere a, b e c
Neste caso o getopts muda de comportamento. Se passarmos uma variável desconhecida para o getopts com esta optstring acima, ele armazenará a letra em si em $OPTARG e o nome da variável será "?". Neste caso você terá que lidar com as exceções internamente no script.
Aplicações
Na prática, as aplicações do getopts dependem muito do problema em questão e das preferências do programador. Aqui mostrarei uma aplicação bastante genérica que atente 90% dos casos de utilização. Abaixo mostrarei o código e comentarei linha por linha.
# universal argument parser
## arrays
declare -A options;
## collect arguments
while getopts "$OPTSTRING" name; do
# if argument parameter was given
if [[ ${OPTARG} ]]; then
options[${name}]=${OPTARG};
# if argument is just a flag
else
options[${name}]=${name};
fi
done;
## shift the $@
shift $(( ${OPTIND} - 1 ));
## OPTSTRING="";
Aqui defino a opstring para todo o script. Deixar a opstring vazia como acima literalmente desliga o getopts.
## declare -A options;
Este comando cria um array associativo que utilizarei para armazenar os resultados do processamento. Optei por um array associativo por ser mais versátil que um indexado. Adicionalmente, o getopts é pouco confiável em termos ordem de processamento dos parâmetros.
## while getopts "$OPTSTRING" name; do
Aqui está a chamada do getops utilizando a OPTSTRING que defini acima e o nome da variável (name) que será utilizado dentro do loop.
## if [[ ${OPTARG} ]]; then options[${name}]=${OPTARG};
Aqui nos testamos se o parâmetro da vez tem um valor associado a ele. Notem que como OPTARG fica vazio quando nenhum valor é passado, podemos assumir que o teste retornará falso.
## else options[${name}]=${name}; fi; done;
Caso nenhum valor tenha sido passado, assumir o nome da variável como o valor da mesma. Assim nó podemos depois processar as opções utilizando os nomes delas. O "fi" finaliza o if e o "done" finaliza o while.
## shift $(( ${OPTIND} - 1 ));
Este último comando move o índice o $@ para frente o mesmo número de parâmetros que foi processado pelo getopts. Isso isola os parâmetros posicionais (como arquivos), que podem ter um número arbitrário de posições.
Uma curiosidade do Bash. É possível escrever o bloco if-fi comando acima de forma mais compacta utilizando um tipo de expansão de parâmetros bem específica do Shell Script. Eu particularmente acho ilegível, por isso não utilizei no exemplo. A saber:
# se optarg estiver definido, retorne, caso contrário use name
options[${name}]=${OPTARG:-name};
Utilização
Este script é bastante genérico e se adequará ao OPTSTRING dado sem muita complicação. Assuma que estamos utilizando a OPTSTRING que dei no começo do artigo:
OPTSTRING="a:b:c";
Para acessar um parâmetro que tenha valores basta invocá-lo diretamente onde ele for necessário:
variavel=${options['a']};
O mesmo se aplica às opções que não tenham valores, que agora funcionam como switches:
if [[ ${options['c']} ]]
Utilizar as expansões do Shell (
Shell Parameter Expansion) aqui pode ser bastante útil, embora torne o código indigesto:
variavel=${options['a']:-"x"}; # se a não foi dado, assuma x
${options['a']:="x"}; # se a não foi dado, defina como x, bom para os defaults
${options['a']:?"Erro!"}; # se a não foi dado, declare erro e encerre
${options['c']:+"x"}; # se c foi dado, retorne x (mini switch)
Conclusão
O getopts é um comando encontrado nativamente no
Linux e faz parte no conjunto de comandos do Posix. Ele abre novas possibilidades para escrever scripts interativos em Bash de uma forma relativamente simples. Mesmo com as suas limitações, o getopts é relativamente simples de utilizar e pode ser generalizado com alguma facilidade para vários scripts.
Adicionalmente, o getopts pode ser utilizado tanto no script principal quando nas chamadas de função do Bash, as quais, possuem uma estrutura de parâmetros similar ao de um script tradicional. O resultado final lembra uma sub-rotina em Perl.
Outros tutoriais aqui no VOL sobre o assunto utilizam uma estrutura muito comum associada ao getops que são os case do Bash (
Criando programas com opções), funciona bem com um número limitado de opções simultâneas. E tem bastante coisa no fórum também.