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

8

Estou tentando construir um script de shell que aceite várias opções e o getopts parece uma boa solução, já que pode manipular a ordenação de variáveis das opções e argumentos (acho!).

Só usarei opções curtas e cada opção curta exigirá um valor correspondente, por exemplo: ./command.sh -a arga -g argg -b argb , mas gostaria de permitir que as opções sejam inseridas em uma ordem não específica, assim como a maioria das pessoas é acostumados a trabalhar com comandos de shell.

O outro ponto é que eu gostaria de fazer meus próprios valores de argumento de opção, idealmente dentro das declarações case . A razão para isso é que o teste de :) na instrução case gerou resultados inconsistentes (provavelmente devido à falta de compreensão 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: $@"

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

Então, imaginei que seria mais fácil executar minha própria validação dentro das instruções case para garantir que cada opção fosse definida e definida apenas uma vez.

Estou tentando fazer o seguinte, mas meus blocos if [ ! "$MYSQL_HOST" ]; then 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 um motivo pelo qual não consigo verificar se um OPTARG tem comprimento zero dentro de getopts ... while ... case ?

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

    
por batfastad 09.05.2013 / 13:03

5 respostas

4

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

getoptit -d -h

Isso imprimirá:

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

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

Se você quiser testar se cada opção é configurada uma vez, é necessário fazer isso antes de executar o cálculo a partir do valor $ OPTARG. E você provavelmente também deve verificar o $ OPTARG para começar com '-' (para o erro -d -h ) 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
                    ;;
...
    
por 09.05.2013 / 13:58
3

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

When the option requires an option-argument, the getopts utility shall place it in the shell variable OPTARG . If no option was found, or if the option that was found does not have an option-argument, OPTARG shall be unset.)

Isto é tudo o que está sendo dito, e colocar uma história (não tão longa) curta: getopts não sabe magicamente que o argumento perfeitamente válido que vê para a opção que você requer ter um argumento não é, na verdade, um argumento de opção, mas uma outra opção. Nada impede que você tenha -h como um argumento válido para a opção -d , o que na prática significa que getopts só lançará 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 que getopts faz ainda é a mesma, então, quando você estiver dentro do loop, o "dano" já estará feito.

Se você realmente quer proibir isso então, como Benubird apontou, você terá que checar seus argumentos de opção e lançar um erro se eles forem iguais a uma opção válida.

    
por 10.05.2013 / 22:25
2

Eu continuo usando getopts , mas faço algumas verificações extras depois: (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 a versão 4 do bash

    
por 09.05.2013 / 15:23
1

O getopt embutido no Gnu não-shell faz menos trabalho para você, mas permite maior flexibilidade para determinar o que acontece depois que um sinalizador é encontrado, incluindo a alteração dos argumentos.

Eu encontrei um artigo comparando getopts e getopt , que provavelmente será útil como o manual é meio difícil de fazer sentido (pelo menos antes do café).

    
por 09.05.2013 / 13:47
1

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

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

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

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

Finalmente, para responder à sua primeira pergunta, você pode detectar quando um sinalizador é passado como um argumento colocando uma lista de sinalizadores no topo, assim:

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

E, em seguida, ter uma seleção na opção onde você deseja verificar se o argumento fornecido é uma opção, algo assim:

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 dá nenhum meio para o shell saber que ele está apenas procurando parâmetros que também não são válidos.

    
por 10.05.2013 / 15:45

Tags