bash getopts, apenas opções curtas, todos exigem valores, validação própria

8

Estou tentando criar um script de shell que aceita várias opções e getoptsparece ser uma boa solução, pois pode lidar com a ordem variável das opções e argumentos (eu acho!).

Usarei apenas opções curtas e cada opção curta exigirá um valor correspondente, por exemplo: ./command.sh -a arga -g argg -b argbmas gostaria de permitir que as opções fossem inseridas em uma ordem não específica, como é o modo como a maioria das pessoas está acostumada a trabalhar com comandos do shell .

O outro ponto é que eu gostaria de fazer minha própria verificação dos valores dos argumentos das opções, idealmente dentro das caseinstruções. A razão para isso é que meus testes :)em minha casedeclaração produziram resultados inconsistentes (provavelmente por falta de entendimento da minha parte).
Por exemplo:

#!/bin/bash
OPTIND=1 # Reset if getopts used previously
if (($# == 0)); then
        echo "Usage"
        exit 2
fi
while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                :)
                       echo "Option -$OPTARG requires an argument" >&2
                       exit 2;;
        esac
done
shift $((OPTIND-1))
echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

Estava falhando em ocorrências como esta ... ./command.sh -d -h
Quando eu quero sinalizar -d como exigindo um argumento, mas recebo o valor de -d=-hque não é o que eu preciso.

Portanto, imaginei que seria mais fácil executar minha própria validação nas instruções de caso para garantir que cada opção seja definida e definida apenas uma vez.

Estou tentando fazer o seguinte, mas meus if [ ! "$MYSQL_HOST" ]; thenbloqueios não são acionados.

OPTIND=1 # Reset if getopts used previously

if (($# == 0)); then
        echo "Usage"
        exit 2
fi

while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        if [ ! "$MYSQL_HOST" ]; then
                                echo "host not set"
                                exit 2
                        fi
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        if [ ! "$MYSQL_USER" ]; then
                                echo "username not set"
                                exit 2
                        fi
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        if [ ! "$MYSQL_PASS" ]; then
                                echo "password not set"
                                exit 2
                        fi
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        if [ ! "$BACKUP_DIR" ]; then
                                echo "backup dir not set"
                                exit 2
                        fi
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                #:)
                #       echo "Option -$opt requires an argument" >&2
                #       exit 2;;
        esac
done
shift $((OPTIND-1))

echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

Existe uma razão pela qual não consigo verificar se uma OPTARGtem comprimento zero a partir de dentro getopts ... while ... case?

Qual é a melhor maneira de executar minha própria validação de argumento getoptsem um caso em que não quero depender da :). Executar minha validação de argumento fora do while ... case ... esac?
Então eu poderia acabar com valores de argumento de -detc e não pegar uma opção que faltava.

batfastad
fonte

Respostas:

4

Quando você chama seu segundo script (eu salvei como getoptit) com:

getoptit -d -h

Isso imprimirá:

MYSQL_HOST=''  MYSQL_USER=''  MYSQL_PASS=''  BACKUP_DIR='-h' Additionals: 

Então BACKUP_DIR está definido e você está testando com if [ ! "$BACKUP_DIR" ]; then se não estiver definido; portanto, é normal que o código dentro dele não seja acionado.

Se você deseja testar se cada opção está definida uma vez, é necessário fazer isso antes de fazer a atribuição a partir do valor $ OPTARG. E você provavelmente também deve verificar se o $ OPTARG começa com um '-'(para o -d -herro) antes de atribuir:

...
            d)
                    if [ ! -z "$BACKUP_DIR" ]; then
                            echo "backup dir already set"
                            exit 2
                    fi
                    if [ z"${OPTARG:0:1}" == "z-" ]; then
                            echo "backup dir starts with option string"
                            exit 2
                    fi
                    BACKUP_DIR=$OPTARG
                    ;;
...
Anthon
fonte
Excelente explicação. Faz todo o sentido, uma vez que pensei sobre o fato de que, na verdade, existe um whileloop sobre as opções. Esse é o método que eu uso, embora seja detalhado em comparação com outros, é muito claro o que está acontecendo no script. E a manutenção por outros é importante neste caso.
batfastad
3

Como as outras respostas não respondem muito à sua pergunta: este é um comportamento esperado e com base em como o POSIX está sendo interpretado:

Quando a opção requer um argumento de opção, o utilitário getopts deve colocá-lo na variável de shell OPTARG. Se nenhuma opção foi encontrada ou se a opção encontrada não tiver um argumento de opção, OPTARG será desativado.)

Isso é tudo o que está sendo afirmado, e para resumir uma história (não muito longa): getoptsnão sabe magicamente que o argumento perfeitamente válido que ele vê para a opção que você precisa para ter um argumento na verdade não é um argumento de opção, mas outra opção . Nada impede que você tenha -hum argumento válido para a -dopção, o que, na prática, significa que getoptsapenas gerará um erro se sua opção que requer um argumento vier por último na linha de comando, por exemplo:

test.sh:

#!/bin/sh

while getopts ":xy:" o; do
    case "${o}" in
        :) echo "${OPTARG} requires an argument"; exit 1;
    esac
done

Exemplo:

$ ./test.sh -y -x
$

$ ./test.sh -x -y
y requires an argument

A razão pela qual sua segunda abordagem não funciona é porque a análise getoptsainda é a mesma; portanto, uma vez dentro do loop, o "dano" já está feito.

Se você absolutamente deseja proibir isso, então, como Benubird apontou, você terá que verificar seus argumentos de opção e gerar um erro se eles equivalerem a uma opção válida.

Adrian Frühwirth
fonte
2

Eu continuaria a usar, getoptsmas faria uma verificação extra depois de: (não testado)

errs=0
declare -A option=(
    [MYSQL_HOST]="-h"
    [MYSQL_USER]="-u"
    [MYSQL_PASS]="-p"
    [BACKUP_DIR]="-d" 
)
for var in "${!option[@]}"; do
    if [[ -z "${!var}" ]]; then
        echo "error: specify a value for $var with ${option[var]}"
        ((errs++))
    fi
done
((errs > 0)) && exit 1

requer bash versão 4

Glenn Jackman
fonte
1

O getopt interno não shell do Gnu funciona menos para você, mas permite maior flexibilidade para determinar o que acontece depois que uma flag é encontrada, incluindo a desativação dos argumentos.

Encontrei um artigo comparando getopts e getopt, o que provavelmente será útil, pois o manual é meio difícil de entender (pelo menos antes do café).

msw
fonte
1

Primeiro, você deve estar usando if [ -z "$MYSQL_USER" ].

Em segundo lugar, não há necessidade da tarefa - if [ -z "$OPTARG" ]funcionará bem.

Em terceiro lugar, eu suspeito que você realmente quer if [ ${#OPTARG} = 0 ]. $ {# x} é uma coisa do bash que retorna o comprimento da string $ x (veja mais aqui ).

Em quarto lugar, se você estiver fazendo sua própria validação, recomendo o getopt em vez de getopts, pois ele oferece muito mais flexibilidade.

Por fim, para responder sua primeira pergunta, você pode detectar quando uma bandeira é passada como argumento colocando uma lista de bandeiras no topo, da seguinte maneira:

args=( -h -u -p -d )

E, em seguida, marque a opção em que deseja verificar se o argumento fornecido é uma opção, algo como isto:

d)
    BACKUP_DIR=$OPTARG
    if [ "$(echo ${args[@]/$OPTARG/})" = "$(echo ${args[@]})" ]
    then
        echo "Argument is an option! Error!"
    fi

Não é uma resposta perfeita, mas funciona! Seu problema é que "-h" é um argumento perfeitamente válido, e você não pode dar ao shell saber que está apenas procurando parâmetros que também não são sinalizadores válidos.

Benubird
fonte