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: 6.073 ]

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

Defesa pessoal com o GPG, Nautilus Scripts, partições encriptadas e leves doses de paranoia

Recuperação de arquivos do LibreOffice

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

Python para pesquisadores: material didático

Leitura recomendada

pam_mount e CiD - Gerenciamento centralizado dos mapeamentos de unidades de rede no Ubuntu

Extracttext - como extrair texto de uma área selecionada da tela

Entendendo, criando e editando pacotes Debian (.deb)

Desligar servidores quando o nobreak entra na bateria e envio de aviso por e-mail

Liberar navegação para Speedy Home

  
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