getopts: criando scripts Bash com parâmetros e argumentos personalizáveis

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.

[ Hits: 5.363 ]

Por: Bruno Rafael Santos em 17/11/2022 | Blog: https://cutt.ly/4H7vrPh


Introdução



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.

   

Páginas do artigo
   1. Introdução
Outros artigos deste autor

GNU Parallel: criando atividades em paralelo com shell script

Campos no LibreOffice: usos e abusos

Guia Rápido do Miniconda para Aplicações Científicas - Instalação e Configuração

Cronogramas e gestão do tempo com o LibreOffice Calc

Assinatura de documentos PDF em lote via Bash

Leitura recomendada

Script GitPratico para criar repositórios remotos sem logar no GitHub

Script de firewall completíssimo

Backup automatizado com HD externo

Shell script com PHP

Entendendo, criando e editando pacotes Debian (.deb)

  
Comentários
[1] Comentário enviado por maurixnovatrento em 20/11/2022 - 09:46h


Eu uso. É muito bom.

___________________________________________________________
Conhecimento não se Leva para o Túmulo.
https://github.com/mxnt10

[2] Comentário enviado por willium532 em 24/11/2022 - 05:56h

have the same issue. I even tried installing the toolbox first, but it didn’t work. Did you happen to resolve this? https://www.arise-portal.com/

[3] Comentário enviado por fabionavarro em 27/07/2023 - 15:46h

Sensacional! Obrigado por compartilhar. Cada dia mais eu fico impressionado com o poder do bash e também outras ferramentas como awk e sed.

[4] Comentário enviado por fabiolimace em 02/11/2023 - 01:43h

Excelente artigo! Ajudou-me a entender a documentação do Bash referente ao comando `getopts`.

A única coisa que senti falta no `getopts` foi falta do suporte às opções longas, isto é, aquelas neste formato: "--opcao-longa". Utilizo-as muito quando preciso ser mais explícito nos scripts que faço.

Percebi que é possível contornar essa limitação utilizando o recurso de substituição de strings do próprio Bash. Acredito que seja uma forma menos "gambiosa" de simular o suporte às opções longas.

Para simular as opções longas, as strings que começam com dois hifens são substituídas por suas opções curtas correspondentes; por exemplo, uma opção longa hipotética chamada `--help` é substituída por `-h`.

Resumindo, estas linhas são acrescentadas ao início do "universal argument parser":

```
# [MODIFICATION 1]
args=$@ # use builtin string substitution to simulate long options
args=${args//--long-option-a/-a} # replace `--long-option-a` with `-a`
args=${args//--long-option-b/-b} # replace `--long-option-b` with `-b`
args=${args//--long-option-c/-c} # replace `--long-option-c` with `-c`

# [MODIFICATION 2]
# replace unknown long options as
# they can cause parsing issues
shopt -s extglob
args=${args//--+([a-zA-Z0-9-])/-?} # replace `--unknown-long-option` with '-?'
```

Com isso, espero que o script passe a atender 91% dos casos de uso. Esse 1% de acréscimo é a parte que me cabe deste latifúndio :)

Cadastrei um script aqui no VOL, que ainda está esperando moderação. Caso seja aprovado, postarei o link dele aqui.

Enquanto isso não acontece, posto o link do script no Github: https://gist.github.com/fabiolimace/b124f47ca5f1f429ec3e8045b5aff653

P.S.: perdoem eu não saber usar markdown aqui no VOL.

---
Atualização 11/11/2023: o script foi publicado nesta URL: https://www.vivaolinux.com.br/script/The-Universal-Argument-Parser-with-long-options

[5] Comentário enviado por maurixnovatrento em 09/11/2023 - 21:56h


[4] Comentário enviado por fabiolimace em 02/11/2023 - 01:43h

Excelente artigo! Ajudou-me a entender a documentação do Bash referente ao comando `getopts`.

A única coisa que senti falta no `getopts` foi falta do suporte às opções longas, isto é, aquelas neste formato: "--opcao-longa". Utilizo-as muito quando preciso ser mais explícito nos scripts que faço.

Percebi que é possível contornar essa limitação utilizando o recurso de substituição de strings do próprio Bash. Acredito que seja uma forma menos "gambiosa" de simular o suporte às opções longas.

Para simular as opções longas, as strings que começam com dois hifens são substituídas por suas opções curtas correspondentes; por exemplo, uma opção longa hipotética chamada `--help` é substituída por `-h`.

Resumindo, estas linhas são acrescentadas ao início do "universal argument parser":

```
# [MODIFICATION 1]
args=$@ # use builtin string substitution to simulate long options
args=${args//--long-option-a/-a} # replace `--long-option-a` with `-a`
args=${args//--long-option-b/-b} # replace `--long-option-b` with `-b`
args=${args//--long-option-c/-c} # replace `--long-option-c` with `-c`

# [MODIFICATION 2]
# replace unknown long options as
# they can cause parsing issues
shopt -s extglob
args=${args//--+([a-zA-Z0-9-])/-?} # replace `--unknown-long-option` with '-?'
```

Com isso, espero que o script passe a atender 91% dos casos de uso. Esse 1% de acréscimo é a parte que me cabe deste latifúndio :)

Cadastrei um script aqui no VOL, que ainda está esperando moderação. Caso seja aprovado, postarei o link dele aqui.

Enquanto isso não acontece, posto o link do script no Github: https://gist.github.com/fabiolimace/b124f47ca5f1f429ec3e8045b5aff653

P.S.: perdoem eu não saber usar markdown aqui no VOL.


É uma boa ideia.


Contribuir com comentário




Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts