Curso: interfaces para scripts em shell – Aula 3

Até então, parece que as interfaces CLI nos limitam à ordem de ocorrência dos argumentos na linha do comando, mas isso não é verdade e, nesta aula, nós veremos como trabalhar com argumentos não-ordenados.


Para aprender mais sobre o shell

Formas de apoio


Aula 3 - Argumentos não-ordenados

Com o que sabemos até o momento, toda a passagem de argumentos para o nosso script está, pelo menos aparentemente, limitada a ser processada segundo a ordem em que as palavras aparecem na linha do comando:

:~$ ./script arg1 arg2 arg3 ... <-- Na linha de comandos.
               ↑    ↑    ↑
              $1   $2   $3  ... <-- No script.

Só que isso não é necessariamente verdade. Sim, é fato que a ordem dos argumentos afetará os dados associados aos parâmetros posicionais, como no exemplo abaixo (script aula3ex1.sh):

#!/usr/bin/env bash

echo "Seu nome é $1 $2."

O resultado da execução do script está completamente amarrada à ordem de passagem dos argumentos:

:~$ ./aula3ex1.sh José Maria
Seu nome é José Maria
:~$ ./aula3ex1.sh Maria José
Seu nome é Maria José

Todavia, esta limitação tem mais relação com a forma como o código foi escrito do que com os parâmetros posicionais ou a ordem dos argumentos.

3.1 - Associação de argumentos a padrões de texto

Repare neste segundo exemplo, um pouco mais complexo (aula3ex2.sh):

#!/usr/bin/env bash

uso='Uso: aula3ex2.sh [n=NOME] [s=SOBRENOME]'

(($#)) || { echo "$uso"; exit 1; }

for arg in "$@"; do
    case ${arg%%=*} in
        n) n="${arg#*=}" ;;
        s) s="${arg#*=}" ;;
        *) echo "$uso"; exit 1
    esac
done

[[ "$s" ]] && s="$s." || n="$n."
[[ "$n" ]] && n="$n " || n=''

echo "Seu nome é $n$s"

Veja que, desta vez, nós estamos percorrendo todos os parâmetros recebidos (a expansão de "$@" na definição do loop for) para verificar a ocorrência dos dois padrões esperados (n e/ou s), o que é feito aparando tudo que vem depois do =, inclusive, com a expansão ${arg%%=*}.

Nas duas primeiras cláusulas do case, caso n ou s encontrem correspondência, as variáveis correspondentes serão definidas aparando da expansão de arg tudo que vier antes da primeira ocorrência de =, inclusive: ${arg#*=}.

Caso qualquer outra string diferente de n ou s seja encontrada, o script imprimirá a informação de uso (echo $uso) e será encerrado com erro (exit 1).

Como o case está em um loop, a ordem de passagem dos argumentos não faz a menor diferença: seja qual for o parâmetro recebido, ele é que será comparado com os padrões nas cláusulas do case. O único inconveniente, porém, é que, se não houver argumentos, o for não executará nenhum loop, o que exige que nós façamos o tratamento deste tipo de erro logo no início do script:

(($#)) || { echo "$uso"; exit 1; }

Aqui, se a expansão $# resultar em 0 argumentos, a expressão aritmética terminará com erro, o script imprimirá a informação de uso (echo $uso) e será encerrado com erro (exit 1).

Ainda em função das possibilidades de uso do nosso script, nós precisaremos tratar os valores recebidos antes da impressão da saída que queremos produzir:

[[ "$s" ]] && s="$s." || n="$n."  # Resolve o problema do ponto final.
[[ "$n" ]] && n="$n " || n=''     # Resolve o problema do espaçamento.

echo "Seu nome é $n$s"            # Imprime a saída.

Agora podemos testar os resultados:

:~$ ./aula3ex2.sh                         # Sem argumentos
Uso: aula3ex2.sh [n=NOME] [s=SOBRENOME]
:~$ ./aula3ex2.sh n=Blau                  # Apenas com o nome
Seu nome é Blau.
:~$ ./aula3ex2.sh s=Araujo                # Apenas com o sobrenome
Seu nome é Araujo.
:~$ ./aula3ex2.sh s=Araujo n=Blau         # Sobrenome antes do nome
Seu nome é Blau Araujo.
:~$ ./aula3ex2.sh n=Blau s=Araujo         # Nome antes do sobrenome
Seu nome é Blau Araujo.
:~$ ./aula3ex2.sh n=Blau s=Araujo banana  # Argumentos inválidos
Uso: aula3ex2.sh [n=NOME] [s=SOBRENOME]

Como podemos ver, a associação a padrões de texto é uma forma razoavelmente simples de tornar a passagem de argumentos independente da sua ordem na linha do comando. Na prática, o que nós fizemos com os padrões de texto foi identificar os argumentos com nomes, ou seja: a nossa interface CLI passou a receber argumentos nomeados.

3.2 - Argumentos nomeados

De modo geral, do ponto de vista de como o utilizador entrará com os dados, nós podemos classificar os argumentos de uma linha de comando em dois grandes tipos: "ordenados" ou "não-ordenados".

Os argumentos ordenados, agora do ponto de vista do processamento, são aqueles cuja captura no script é feita diretamente pela expansão de parâmetros posicionais de acordo com a sua ordem de aparição na linha do comando. Os argumentos não-ordenados, por sua vez, sempre serão identificados pela comparação com algum tipo de padrão.

Portanto, toda interface de linha de comando que ofereça a possibilidade de uma entrada não-ordenada de dados terá que implementar alguma forma de processamento de argumentos nomeados.

Tipos de argumentos nomeados

Nós ainda podemos classificar os argumentos nomeados de acordo com as funções que os dados que eles representam terão no script.

Identificação de valores

É qualquer argumento definido como o identificador de um dado qualquer, o que pode acontecer na mesma palavra, na forma de um prefixo, ou como um argumento anterior ao dado que se quer identificar:

script.sh name=Fulano score=10

script.sh --name=Fulano --score=10

script.sh -name Fulano -score 10

script.sh -n Fulano -s 10

Convencionou-se que, quando os identificadores forem precedidos por dois traços (--nome), também chamados de "argumentos longos" ou "nomes longos", os dados que eles identificam devem estar na mesma palavra separados pelo caractere =. Já os "argumentos curtos", aqueles que só possuem um traço (-n ou -nome), identificam a palavra que estiver no argumento seguinte. Se este tipo de argumento não aparecer prefixando ou precedendo um dado, ele geralmente será um comando ou uma opção.

Comandos

São os argumentos nomeados que denotam ações que o script deve executar:

meu_script.sh edit

meu_script.sh --edit

meu_script.sh -e

Ainda segundo as convenções, quando o argumento nomeado for um comando ou uma opção, a sua forma por extenso (argumento longo) aparecerá prefixada por dois traços, enquanto a forma abreviada (argumento curto) receberá apenas um traço.

Em termos práticos, não faz diferença como nós implementamos os argumentos nomeados. Contudo, existe uma certa regularidade na forma como os programas costumam receber seus argumentos em sistemas unix-like e os utilizadores acabam se acostumando.

Por exemplo, para exibir a ajuda de um programa ainda desconhecido, todo usuário medianamente experimentado tentará -h ou --help, mas dificilmente cogitará tentar -help ou --h.

Opções (ou 'flags')

As opções também costumam ser implementadas como uma palavra única e seguem basicamente a mesma notação dos comandos -- a diferença fica por conta apenas da finalidade do argumento no script:

outro_script.sh --matches-only --columns

outro_script.sh -m -c

outro_script.sh matches-only columns

Uma reflexão sobre o termo 'flag'

As opções da linha do comando são, frequentemente, chamadas de "flags", o que eu compreendo, mas é uma nomenclatura que, conceitualmente, me incomoda um pouco.

Em computação, as flags são valores utilizados para sinalizar estados que serão avaliados condicionalmente. Em tese, a monitoração dos valores que representarão esses estados acontecerá ao longo da execução do programa, não antes, determinando uma condição inicial de execução ou, mais precisamente, como um parâmetro de execução.

Além disso, é claro que a presença ou não de um argumento opcional pode ser utilizada para sinalizar algo e alterar condicionalmente o comportamento do programa. Porém, o espaço da linha do comando pertence ao utilizador, é domínio dele, e é da perspectiva dele que eu acredito que devemos avaliar os elementos da interface. Vendo por este lado, ao digitar a linha de um comando, o utilizador estará, entre outras coisas, manifestando suas opções através de argumentos.

3.3 - Iteração de parâmetros

Se observarmos novamente o script aula3ex2.sh, nós veremos outro elemento chave do trabalho com argumentos não-ordenados: a iteração pelos parâmetros recebidos que, no caso, foi feita pela estrutura de repetição for.

for arg in "$@"; do
    case ${arg%%=*} in
        n) n="${arg#*=}" ;;
        s) s="${arg#*=}" ;;
        *) echo "$uso"; exit 1
    esac
done

Na aula 2, nós passamos uma boa parte do tempo conhecendo as estruturas de repetição nativas do Bash (o loops for, while e until): e isso não foi sem motivos.

Com argumentos ordenados ou não, não será rara a necessidade de percorrer os parâmetros recebidos pelo script. No caso de argumentos não-ordenados, porém, especialmente se o nosso script esperar por mais de um argumento, isso será praticamente obrigatório! O ponto é que, com argumentos nomeados, nós não temos como fugir da comparação, parâmetro a parâmetro, dos padrões de texto que nos dirão a que se referem cada um dos dados recebidos.

Toda regra tem uma exceção

Observe, abaixo, o script aula3ex3.sh:

#!/usr/bin/env bash

ajuda() {
    echo 'Isso é uma ajuda. Bye!'
    exit
}

versao() {
    echo 'Esta é a versão. Fui!'
    exit
}

comando() {
    echo 'Pronto! Sempre às ordens!'
    exit
}

case $1 in
    -h|--help)    ajuda;;
    -v|--version) versao;;
    -c|--command) comando;;
esac

echo 'Já que você não quer nada... T+!'

Nele, nós estamos comparando a expansão do parâmetro $1 com os padrões nas cláusulas do case. Além disso, os argumentos parecem ser nomeados, mas onde está o loop?

A resposta é muito simples: nós estamos comparando apenas a expansão do parâmetro $1, o primeiro argumento digitado na linha do comando. Portanto, não se trata de argumentos ordenados ou não-ordenados, trata-se do único argumento esperado pelo programa: logo, não há pelo que iterar.

Aqui, o fato dos argumentos serem nomeados é apenas um recurso de comunicação com o utilizador, parte essencial do estudo de interfaces, e por isso mesmo foi relevante defini-los dessa forma.

Deslocamento de parâmetros ('shift')

Muitas vezes, o processamento dos argumentos pode ser mais eficiente e simples se, em vez de circularmos pelos parâmetros segundo a sua quantidade e posição, nós pudermos tratá-los como uma "fila" de dados: é quando entra em cena o bultin shift.

O comando interno shift (deslocar, em inglês) remove N parâmetros do início da lista de parâmetros posicionais:

shift [N]

Por padrão, quando N não é informado, apenas um parâmetro posicional será removido.

Por exemplo, digamos que os parâmetros sejam registrados da seguinte forma:

:~$ set -- -a -b banana laranja
:~$ echo $@
-a -b banana laranja

Cada vez que executarmos o comando shift sem argumentos, um parâmetro será removido do início da fila:

:~$ shift
:~$ echo $@
-b banana laranja

:~$ shift
:~$ echo $@
banana laranja

:~$ shift
:~$ echo $@
laranja

:~$ shift
:~$ echo $@

:~$

Agora, imagine que -a e -b sejam identificadores dos dados que vierem após cada um deles:

:~$ set -- -a banana -b laranja
:~$ echo $@
-a banana -b laranja

Esta situação é equivalente àquela onde, ao invocar o nosso script, o utilizador poderia escrever, livremente, na linha de comando:

:~$ ./meu_script -a banana -b laranja

Ou...

:~$ ./meu_script -b laranja -a banana

Sendo assim, a iteração pelos parâmetros precisa passar por duas etapas:

  • Localizar o argumento nomeado segundo seu padrão de texto;
  • Capturar o argumento seguinte, pois este é o valor que realmente nos interessa.

Uma abordagem possível, e bastante comum, é técnica abaixo (leia os comentários):

# Percorrer todos os parâmetros recebidos...
for arg in "$@"; do

    # Somente as expansões de 'arg' que casarem com os padrões
    # nas cláusulas do 'case' resultarão em alguma ação...
    case $arg in

        -a) # Se o padrão casar com '-a', o valor em questão
            # será o segundo parâmetro em '@'...
            var1="${@:2:1}"
            ;;

        -b) # Se o padrão casar com '-b', o valor em questão
            # também será o segundo parâmetro '@'...
            var2="${@:2:1}"
            ;;
    esac

    # O primeiro parâmetro em '@' é removido da fila...
    shift
done

Note que os parâmetros expandidos no for não são utilizados para a atribuição de var1 e var2. Todos os comandos nas cláusulas utilizam apenas expansões dos valores em @, que mudam toda vez que o comando shift é executado.

Vamos aplicar isso ao script aula3ex4.sh:

#!/usr/bin/env bash

echo "\$@ antes do processamento: $@"

# Percorrer todos os parâmetros recebidos...
for arg in "$@"; do

    # Somente as expansões de 'arg' que casarem com os padrões
    # nas cláusulas do 'case' resultarão em alguma ação...
    case $arg in

        -a) # Se o padrão casar com '-a', o valor em questão
            # será o segundo parâmetro em '@'...
            var1="${@:2:1}"
            ;;

        -b) # Se o padrão casar com '-b', o valor em questão
            # também será o segundo parâmetro '@'...
            var2="${@:2:1}"
            ;;
    esac

    # O primeiro parâmetro em '@' é removido...
    shift
done

echo "var1=$var1"
echo "var2=$var2"
echo "\$@ depois do processamento: ${@:-nada}"

Executando:

:~$ ./aula3ex4.sh -a banana -b laranja
$@ antes do processamento: -a banana -b laranja
var1=banana
var2=laranja
$@ depois do processamento: nada

Invertendo a ordem dos argumentos:

:~$ ./aula3ex4.sh -b laranja -a banana
$@ antes do processamento: -b laranja -a banana
var1=banana    <--- 'var1' continua associado a '-a'
var2=laranja   <--- 'var2' continua associado a '-b'
$@ depois do processamento: nada

Entretanto, nós só expandimos ${@:2:1} porque era o que vínhamos utilizando quando não sabíamos qual seria o parâmetro a ser processado nos loops. Com o shift removendo o primeiro parâmetro posicional da sessão, o parâmetro expandido no case será sempre equivalente a $1 e o parâmetro seguinte será sempre $2, o que nos permite reescrever o script aula3ex4-final.sh da seguinte forma:

#!/usr/bin/env bash

echo "\$@ antes do processamento: $@"

# A expansão "$@" servirá apenas para termos a quantidade
# necessária de iterações pelos parâmetros...
for arg in "$@"; do

    # A cada iteração, o 'shift' será executado fazendo
    # com que a expansão '$1' seja sempre igual à
    # expansão de 'arg'...
    case $arg in

        -a) # Se o padrão casar com '-a', o valor em questão
            # será o segundo parâmetro posicional disponível...
            var1="$2"
            ;;

        -b) # Se o padrão casar com '-b', o valor em questão
            # será o segundo parâmetro posicional disponível...
            var2="$2"
            ;;
    esac

    # O primeiro parâmetro em '@' é removido...
    shift
done

echo "var1=$var1"
echo "var2=$var2"
echo "\$@ depois do processamento: ${@:-nada}"

Ou ainda, visto que a variável arg tornou-se desnecessária, nós podemos trocar o loop for por um loop while (aula3ex4-while.sh):

#!/usr/bin/env bash

echo "\$@ antes do processamento: $@"

# A expansão "$@" servirá de referência para o fim
# do loop 'while' quando não expandir mais nada...

while [[ "$@" ]]; do

    # Agora, só precisamos da expansão '$1'...

    case "$1" in
        -a) # Se o padrão casar com '-a', o valor em questão
            # será o segundo parâmetro posicional disponível...
            var1="$2"
            ;;
        -b) # Se o padrão casar com '-b', o valor em questão
            # será o segundo parâmetro posicional disponível...
            var2="$2"
            ;;
    esac

    # O primeiro parâmetro posicional é removido...

    shift
done

echo "var1=$var1"
echo "var2=$var2"
echo "\$@ depois do processamento: ${@:-nada}"

Padronização e tratamento de erros

Repare num ponto muito importante: tornar a digitação de argumentos independente da ordem de entrada não exclui a necessidade de estabelecer algum tipo de padrão de entrada de argumentos.

No exemplo anterior, nós possibilitamos que o utilizador digitasse tanto...

:~$ ./aula3ex4.sh -a banana -b laranja

Quanto...

:~$ ./aula3ex4.sh -b laranja -a banana

Mas seria inaceitável que ele digitasse, por exemplo:

:~$ ./aula3ex4.sh -b -a laranja banana

O que resultaria em:

:~$ ./aula3ex4.sh -b -a laranja banana
$@ antes do processamento: -b -a laranja banana
var1=laranja    <--- 'laranja' foi associado a '-a'
var2=-a         <--- '-a' foi associado a '-b'
$@ depois do processamento: nada

Daí a necessidade imperativa de identificar possíveis erros de entrada de argumentos e tratá-los de algum modo: geralmente, emitindo alguma mensagem de de ajuda e interrompendo a execução do script.

Convenções são definições de padrões

Até para facilitar a identificação de erros, convenciona-se utilizar um ou dois traços prefixando os argumentos nomeados. Assim, basta um teste da expansão dos primeiros caracteres do parâmetro recebido para determinar se ele corresponde ou não ao que se espera.

Aliás, além do aspecto formal dos argumentos nomeados (se eles têm traços ou não), é preciso especificar se eles obrigarão ou não a existência de um argumento adicional na sequência. Isso é chamado de argumento obrigatório e está diretamente relacionado com os argumentos nomeados que identificam valores.

É por isso que não podemos incorrer em ambiguidades, como na sintaxe:

  Colchetes significam "opcional"
             ↓     ↓
script.sh -a [VALOR]

Se o valor depois de -a é um argumento opcional, é muito mais seguro e interessante definir um comportamento padrão para o script ou designar outro argumento nomeado para o caso de VALOR não ser necessário:

      A barra significa "ou".
                 ↓
script [-a VALOR | -b]

Assim, nós poderemos esperar -a seguido de um argumento obrigatório ou apenas -b: um ou outro, nunca os dois.

Note que esse tipo de preocupação não tem nenhuma relação com "antecipar os erros do utilizador". A ambiguidade na definição dos argumentos é um caso típico de erro do projeto da interface CLI.

3.4 - O comando interno 'getopts'

Boa parte do processamento de argumentos nomeados curtos (um caractere prefixado com apenas um traço), com ou sem argumentos obrigatórios, pode ser simplificado com o uso do comando interno getopts.

Observe a sua sintaxe geral:

getopts [:]STRING_DAS_OPÇÕES VAR LISTA_DE_ARGUMENTOS

Onde:

Argumento Descrição
: Silencia os erros do shell e utiliza os caracteres especiais do getopts para tratamento de erros.
STRING_DAS_OPÇÕES A lista das letras que serão processadas como opções.
VAR Uma variável que armazenará a opção que estiver sendo processada no momento.
LISTA_DE_ARGUMENTOS Quando não informada, o getopts lerá todos os argumentos passados na linha do comando que invocou o script ($@), mas nós podemos expandir qualquer outra lista de palavras para esta finalidade.

Cada vez que for invocado, getopts lerá uma palavra da LISTA_DE_ARGUMENTOS. Se a palavra iniciar com traço (-) e corresponder a uma das letras definidas na STRING_DAS_OPÇÕES, ela será passada para VAR (sem o traço) e getopts emitirá um estado de saída de sucesso (0).

Nota: se você quiser testar os exemplos a seguir, será preciso reiniciar a sessão do shell antes de cada um deles com o comando: exec bash ou reiniciar a variável do shell OPTIND com o valor 1: OPTIND=1.

No exemplo a seguir, nós definiremos uma LISTA_DE_ARGUMENTOS para entender o funcionamento do getopts:

  string das opções
          |
          |  opção corrente
          |    |
          |    |
getopts 'abc' var -a -b -c
                  ----+---
                      |
               lista de argumentos

A ideia é capturar o estado de saída de getopts e imprimir a expansão de var:

:~$ getopts 'abc' var -a -b -c; echo $? $var
0 a

Na primeira execução, getopts encontrou o argumento -a e saiu com sucesso. Do mesmo modo como nós utilizamos o shift anteriormente, getopts também elimina o primeiro argumento da fila e verifica se o argumento seguinte está entre as opções esperadas:

:~$ getopts 'abc' var -a -b -c; echo $? $var
0 b

O mesmo acontecerá quando executarmos o comando uma terceira vez:

:~$ getopts 'abc' var -a -b -c; echo $? $var
0 c

Tentando uma quarta vez, quando já não há mais argumentos na lista, getopts terminará com erro e atribuirá o caractere ? a var:

:~$ getopts 'abc' var -a -b -c; echo $? $var
1 ?

Argumentos inesperados

Observe o que acontecerá se a nossa lista contiver algum argumento imprevisto na string de opções:

:~$ getopts 'abc' var -a -b -c -d; echo $? $var
0 a
:~$ getopts 'abc' var -a -b -c -d; echo $? $var
0 b
:~$ getopts 'abc' var -a -b -c -d; echo $? $var
0 c
:~$ getopts 'abc' var -a -b -c -d; echo $? $var
bash: opção ilegal -- d
0 ?
:~$ getopts 'abc' var -a -b -c -d; echo $? $var
1 ?

Aqui, -d não estava na string de opções, o que causou a emissão de uma mensagem de erro do Bash, mas o estado de saída de sucesso foi mantido e o valor associado a var foi, novamente, o caractere ?.

Observe novamente:

:~$ getopts 'abc' var -a -b -c -d; echo $? $var
bash: opção ilegal -- d
0 ?

Sobre isso, nós temos dois pontos a destacar:

  • O comando getopts só termina com erro se não houver mais argumentos a analisar;
  • A mensagem de erro do bash pode ser omitida se o primeiro caractere da string de opções for :, como nos testes abaixo:
:~$ getopts ':abc' var -a -b -c -d; echo $? $var
0 a
:~$ getopts ':abc' var -a -b -c -d; echo $? $var
0 b
:~$ getopts ':abc' var -a -b -c -d; echo $? $var
0 c
:~$ getopts ':abc' var -a -b -c -d; echo $? $var
0 ?
:~$ getopts ':abc' var -a -b -c -d; echo $? $var
1 ?

Nota importante: diferente da técnica anterior de iteração pelos parâmetros com o comando shift, o comando getopts não remove os parâmetros posicionais da lista. Em vez disso, ele utiliza uma variável do shell para registrar o índice do próximo parâmetro a ser avaliado, como veremos a seguir.

Variáveis 'OPTIND', 'OPTARG' e 'OPTERR'

Além dos estados de saída e do valor atribuído à variável (var, nos testes anteriores), o comando getopts trabalha com três variáveis:

  • OPTIND - recebe o valor 1 no momento em que a sessão do shell é iniciada e receberá o índice do próximo parâmetro a ser avaliado a cada execução do comando getopts.
  • OPTARG - recebe o dado no argumento obrigatório associado a um dos caracteres da string de opções ou um argumento inesperado.
  • OPTERR - iniciada com 1 (habilitado), indica se o Bash deve ou não exibir mensagens de erro. Atribuir 0 a OPTERR desabilita as mensagens de erro mesmo que o primeiro caractere da string de opções não seja o dois pontos (:).

Argumentos obrigatórios

Para definir que um argumento da string de opções deve ser sucedido por um argumento obrigatório, nós incluímos o dois pontos (:) após o caractere da opção, por exemplo:

        não exibir mensagens de erro
         ↓
getopts ':a:b' var
           ↑
          'a' espera um argumento obrigatório

Se o argumento a for encontrado, o argumento seguinte será considerado o dado associado a ele e será registrado na variável OPTARG.

Observe:

:~$ OPTIND=1  <-- Reiniciando 'OPTIND'
:~$ getopts ':a:b' var -a banana -b; echo $?:$var:$OPTARG
0:a:banana    <-- O valor 'banana' foi atribuído a 'OPTARG'
:~$ getopts ':a:b' var -a banana -b; echo $?:$var:$OPTARG
0:b:          <-- A avaliação passou diretamente para 'b'

Caso o argumento obrigatório não fosse informado, o próprio -b seria processado como o valor associado a -a:

:~$ OPTIND=1
:~$ getopts ':a:b' var -a -b; echo $?:$var:$OPTARG
0:a:-b   <-- O argumento '-b' foi atribuído a 'OPTARG'
:~$ getopts ':a:b' var -a -b; echo $?:$var:$OPTARG
1:?:     <-- Nada mais a processar.

Na verdade, este é um comportamento que facilita muito a ocorrência de erros de uso e (ao menos no meu ponto de vista) trata-se de uma falha grave, tanto na implementação do comando getopts em si, quanto no projeto de interfaces CLI que o utilizem. Obviamente, é possível tratar o erro causado pelo dado indevidamente recebido e até fazer com que ele seja um indicador da presença da opção -b, mas estes são os tipos de complexidade e vulnerabilidade que, geralmente, não queremos nos nossos scripts.

Em função disso, pessoalmente, eu evito utilizar o getopts se meus scripts precisarem de argumentos obrigatórios. No script de exemplo aula3ex4.sh, isso também pode acontecer:

:~$ ./aula3ex4.sh -a -b laranja
$@ antes do processamento: -a -b laranja
var1=-b      <-- Veja que 'var1' recebeu '-b'.
var2=laranja <-- Mas '-b' não deixou de ser processado.
$@ depois do processamento: nada

Contudo, ainda é bem mais simples tratar erros e tomar decisões quando nós estamos no controle da iteração pelos parâmetros. Além disso, do ponto de vista do projeto de interfaces, faz muito mais sentido que argumentos obrigatórios sejam implementados na forma de argumentos longos:

--nome=valor

Isso reduz as vulnerabilidades e restringe a complexidade de erros que precisarão ser tratados. Mas, como sempre, toda facilidade oferecida para o utilizador implicará em alguma complexidade de implementação, o que terá que será que ser avaliado no projeto da interface.

No caso do getopts, aqui estão alguns pontos que precisam ser levados em conta a respeito de seu uso:

  • Se a ideia for receber apenas opções (uma letra antecedida por um traço) pela linha de comando, não há no que pensar: podemos contar com o getopts.

  • Se quisermos receber apenas opções com argumentos obrigatórios, o getopts também é uma boa escolha, pois ainda será fácil tratar eventuais erros de uso.

  • Se precisarmos receber opções com e sem argumentos obrigatórios, o getopts ainda será uma opção, mas o script estará mais vulnerável a usos incorretos e demandará uma complexidade maior no tratamento de erros.

  • Mas, se tivermos que receber argumentos mistos (opções com e sem argumentos obrigatórios e argumentos sem identificadores), o trabalho de pré-processar todos os parâmetros posicionais pode tornar o getopts de pouca ou nenhuma serventia no nosso script.

3.5 - Argumentos mistos

Outra situação muito comum na linha de comandos é a possibilidade de recebermos dados como argumentos nomeados e não nomeados. Por exemplo:

:~$ meu_script.sh -s arquivo1.txt arquivo2.txt

Com esta linha de comando imaginária, estamos supondo que -s seja o identificador de um arquivo fonte de dados (arquivo1.txt) e que arquivo2.txt seja um arquivo que receberá os dados processados. No caso, ainda é possível implementar um código que permita a inversão dos argumentos e ainda contar com as informações esperadas:

:~$ meu_script.sh arquivo2.txt -s arquivo2.txt

A complexidade da lógica aumentou, mas ainda estamos lidando com argumentos não-ordenados. Com o getopts, porém, isso pode ser bastante problemático, mas não é algo insolúvel.

:~$ OPTIND=1
:~$ getopts ':ab' var -a -b -1; echo $?:$var:$OPTARG
0:a:
:~$ getopts ':ab' var -a -b -1; echo $?:$var:$OPTARG
0:b:
:~$ getopts ':ab' var -a -b -1; echo $?:$var:$OPTARG
0:?:1  <-- o dado '-1' foi visto como uma opção inesperada!

Separador de argumentos

Se, em vez de aumentarmos a complexidade do processamento, nós decidirmos aumentar um pouco a complexidade de uso, nós podemos utilizar dois traços (--) para separar os argumentos nomeados dos não nomeados:

:~$ OPTIND=1
:~$ getopts ':ab' var -a -b -- -1; echo $?:$var:$OPTARG
0:a:
:~$ getopts ':ab' var -a -b -- -1; echo $?:$var:$OPTARG
0:b:
:~$ getopts ':ab' var -a -b -- -1; echo $?:$var:$OPTARG
1:?:  <-- O estado de erro indica que o 'getopts' não
          processará mais opções.

Entretanto, como os argumentos depois do -- serão capturados?

A resposta é: lembra do shift?

Observe o mesmo exemplo novamente, desta vez, verificando o último valor em OPTIND:

:~$ OPTIND=1
:~$ getopts ':ab' var -a -b -- -1; echo $?:$var:$OPTARG
0:a:
:~$ echo $OPTIND
2
:~$ getopts ':ab' var -a -b -- -1; echo $?:$var:$OPTARG
0:b:
:~$ echo $OPTIND
3
:~$ getopts ':ab' var -a -b -- -1; echo $?:$var:$OPTARG
1:?:  <-- Fim da iteração do 'getopts'
:~$ echo $OPTIND
4     <-- Próximo índice a ser processado.

Capturando parâmetros depois do separador

Antes de falarmos de uma solução, vamos utilizar o script aula3ex5.sh para analisarmos melhor o problema tendo uma lista de parâmetros à disposição:

#!/usr/bin/env bash

echo "Parâmetros em '@': $@"
echo "Valor inicial em 'OPTIND': $OPTIND"

while getopts ':a:bc:' arg; do
    echo ---
    echo "Valor em 'arg': $arg"
    echo "Valor em 'OPTIND': $OPTIND"
    echo "Valor em 'OPTARG': $OPTARG"
done

echo ---

echo "Parâmetros em '@': $@"

Executando sem o separador de argumentos:

:~$ ./aula3ex5.sh -a -b -1
Parâmetros em '@': -a -b -1
Valor inicial em 'OPTIND': 1
---
Valor em 'arg': a
Valor em 'OPTIND': 2
Valor em 'OPTARG':
---
Valor em 'arg': b
Valor em 'OPTIND': 3
Valor em 'OPTARG':
---
Valor em 'arg': ?    <-- Opção inesperada!
Valor em 'OPTIND': 4 <-- Índice do próximo argumento.
Valor em 'OPTARG': 1
---
Parâmetros em '@': -a -b -1

Agora, vejamos o que acontece com o separador:

:~$ ./aula3ex5.sh -a -b -- -1
Parâmetros em '@': -a -b -- -1
Valor inicial em 'OPTIND': 1
---
Valor em 'arg': a
Valor em 'OPTIND': 2
Valor em 'OPTARG':
---
Valor em 'arg': b    <-- Última iteração válida do 'getopts'
Valor em 'OPTIND': 3
Valor em 'OPTARG':
---
Parâmetros em '@': -a -b -- -1

A iteração seguinte, não vista nesta saída, foi quando o getopts encontrou o separador -- e interrompeu o loop. Neste ponto, OPTIND registrava o valor 4, mas o quarto parâmetro da lista (-1) não chegou a ser processado: é aí que entra o shift!

Quando falamos do comando shift, nós vimos que é possível remover N parâmetros da lista de parâmetros. Se OPTIND registra o valor 4, mas nós precisamos remover os 3 primeiros parâmetros da fila, tudo que temos que fazer é calcular N subtraindo 1 do valor em OPTIND, o que demonstraremos com o script aula3ex6.sh:

#!/usr/bin/env bash

echo "Parâmetros em '@': $@"
echo "Valor inicial em 'OPTIND': $OPTIND"

while getopts ':ab' arg; do
    echo ---
    echo "Valor em 'arg': $arg"
    echo "Valor em 'OPTIND': $OPTIND"
    echo "Valor em 'OPTARG': $OPTARG"
done

echo ---

echo "Valor em 'OPTIND': $OPTIND"

echo ---

echo "Parâmetros em '@' antes do shift: $@"

shift $((OPTIND-1))

echo "Valor de 'N' (OPTIND-1): $((OPTIND-1))"
echo "Parâmetros em '@' depois do shift: $@"

Executando:

:~$ ./aula3ex6.sh -a -b -- -1
Parâmetros em '@': -a -b -- -1
Valor inicial em 'OPTIND': 1
---
Valor em 'arg': a
Valor em 'OPTIND': 2
Valor em 'OPTARG':
---
Valor em 'arg': b
Valor em 'OPTIND': 3
Valor em 'OPTARG':
---
Valor em 'OPTIND': 4  <-- 'OPTIND' depois do loop
---
Parâmetros em '@' antes do shift: -a -b -- -1
Valor de 'N' (OPTIND-1): 3
Parâmetros em '@' depois do shift: -1

Deste ponto em diante, independente da quantidade de argumentos depois do separador, basta processar da forma que for necessária todos os parâmetros restantes na sessão do shell.


Para aprender mais sobre o shell

Formas de apoio


Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment