Como analiso argumentos opcionais em um script bash se nenhuma ordem é dada?

13

Estou confuso sobre como incluir argumentos / sinalizadores opcionais ao escrever um script bash para o seguinte programa:

O programa requer dois argumentos:

run_program --flag1 <value> --flag2 <value>

No entanto, existem vários sinalizadores opcionais:

run_program --flag1 <value> --flag2 <value> --optflag1 <value> --optflag2 <value> --optflag3 <value> --optflag4 <value> --optflag5 <value> 

Eu gostaria de executar o script bash de forma que ele receba argumentos do usuário. Se os usuários digitarem apenas dois argumentos em ordem, seria:

#!/bin/sh

run_program --flag1 $1 --flag2 $2

Mas e se algum dos argumentos opcionais for incluído? Eu pensaria que seria

if [ --optflag1 "$3" ]; then
    run_program --flag1 $1 --flag2 $2 --optflag1 $3
fi

Mas e se $ 4 forem dados, mas não $ 3?

ShanZhengYang
fonte
getoptsé o que você quer. Sem isso, você pode usar um loop com uma instrução switch para detectar cada sinalizador, opcional ou não.
Orion
@orion Com getopts, eu precisaria especificar cada combinação de argumentos? 3 e 4, 3 e 5, 3 e 4 e 5, etc.?
ShanZhengYang
Não, basta defini-los se obtê-los, caso contrário, ele informa que não foi encontrado. Então, basicamente, você "obtém" cada opção, em qualquer ordem e especifica-as em qualquer ordem, se for o caso. Mas basta ler a página do manual do bash, está tudo lá.
orion
@ orion Me desculpe, mas ainda não entendo direito getopts. Digamos que eu force os usuários a executar o script com todos os argumentos: run_program.sh VAL VAL FALSE FALSE FALSE FALSE FALSEque executa o programa como program --flag1 VAL --flag2 VAL. Se você executou run_program.sh VAL VAL FALSE 10 FALSE FALSE FALSE, o programa seria executado como program --flag1 VAL --flag2 VAL --optflag2 10. Como você pode obter esse comportamento getopts?
ShanZhengYang

Respostas:

25

Este artigo mostra duas maneiras diferentes - shifte getopts(e discute as vantagens e desvantagens das duas abordagens).

Com o shiftseu script $1, ele decide qual ação executar e, em seguida shift, executa , movendo $2para $1, $3para $2etc.

Por exemplo:

while :; do
    case $1 in
        -a|--flag1) flag1="SET"            
        ;;
        -b|--flag2) flag2="SET"            
        ;;
        -c|--optflag1) optflag1="SET"            
        ;;
        -d|--optflag2) optflag2="SET"            
        ;;
        -e|--optflag3) optflag3="SET"            
        ;;
        *) break
    esac
    shift
done

Com getoptsvocê, defina as opções (curtas) na whileexpressão:

while getopts abcde opt; do
    case $opt in
        a) flag1="SET"
        ;;
        b) flag2="SET"
        ;;
        c) optflag1="SET"
        ;;
        d) optflag2="SET"
        ;;
        e) optflag3="SET"
        ;;
    esac
done

Obviamente, esses são apenas trechos de código e deixei de lado a validação - verificando se os argumentos obrigatórios flag1 e flag2 estão definidos, etc.

Qual abordagem você usa é, até certo ponto, uma questão de gosto - quão portátil você deseja que seu script seja, se você pode viver apenas com opções curtas (POSIX) ou se deseja opções longas (GNU) etc.

John N
fonte
Eu costumo fazer em while (( $# ))vez de, while :;e muitas vezes desisto, com um erro no *caso
Jasen
isso responde ao título, mas não à pergunta.
Jasen
@ Jasen Eu olhei para isso ontem à noite e não pude ver minha vida por que. Hoje de manhã está muito mais claro (e já vi a resposta de orion agora). Excluirei esta resposta ainda hoje (queria agradecer seu comentário primeiro, e essa parecia a maneira mais fácil de fazê-lo).
John N
sem problemas, ainda é uma boa resposta, apenas a questão foi esquecida.
Jasen
1
NB: No caso da set -o nounsetprimeira solução, ocorrerá um erro se nenhum parâmetro for fornecido. Correção:case ${1:-} in
Raphael
3

use uma matriz.

#!/bin/bash

args=( --flag1 "$1" --flag2 "$2" )
[  "x$3" = xFALSE ] ||  args+=( --optflag1 "$3" )
[  "x$4" = xFALSE ] ||  args+=( --optflag2 "$4" )
[  "x$5" = xFALSE ] ||  args+=( --optflag3 "$5" )
[  "x$6" = xFALSE ] ||  args+=( --optflag4 "$6" )
[  "x$7" = xFALSE ] ||  args+=( --optflag5 "$7" )

program_name "${args[@]}"

isso manipulará argumentos com espaços neles corretamente.

[edit] Eu estava usando a sintaxe aproximadamente equivalente, args=( "${args[@]}" --optflag1 "$3" )mas o G-Man sugeriu uma maneira melhor.

Jasen
fonte
1
Você pode simplificar isso um pouco dizendo args+=( --optflag1 "$3" ). Você pode querer ver minha resposta ao nosso trabalho de referência, Implicações de segurança de não citar uma variável nos shells bash / POSIX , onde discuto essa técnica (de construção de uma linha de comando em uma matriz anexando condicionalmente argumentos opcionais).
G-Man diz 'Reinstate Monica'
@ G-Man Obrigado, eu não tinha visto isso na documentação, entendendo [@]que tinha uma ferramenta suficiente para resolver meu problema.
Jasen
1

Em um script de shell, os argumentos são "$ 1", "$ 2", "$ 3" etc. O número de argumentos é $ #.
Se o seu script não reconhecer opções, você pode deixar de fora a detecção de opções e tratar todos os argumentos como operandos.
Para reconhecer opções, use as getopts internas

Michael Durrant
fonte
0

Se suas opções de entrada são posicionais (você sabe em que lugares eles estão) e não são especificadas com sinalizadores, o que você deseja é apenas construir a linha de comando. Basta preparar os argumentos de comando para todos eles:

FLAG1="--flag1 $1"
FLAG2="--flag2 $2"
OPTFLAG1=""
OPTFLAG2=""
OPTFLAG3=""
if [ xFALSE != x"$3" ]; then
   OPTFLAG1="--optflag1 $3"
fi
if [ xFALSE != x"$4" ]; then
   OPTFLAG2="--optflag2 $4"
fi
#the same for other flags

#now just run the program
runprogram $FLAG1 $FLAG2 $OPTFLAG1 $OPTFLAG2 $OPTFLAG3

Se os parâmetros não forem especificados, as seqüências correspondentes estarão vazias e se expandirão em nada. Observe que na última linha, não há aspas. Isso porque você deseja que o shell divida os parâmetros em palavras (para fornecer --flag1e $1como argumentos separados para o seu programa). Obviamente, isso dará errado se seus parâmetros originais contiverem espaços. Se você é quem está executando isso, pode deixá-lo, mas se for um script geral, ele poderá ter um comportamento inesperado se o usuário inserir algo com espaços. Para lidar com isso, você precisará tornar o código um pouco mais feio.

O xprefixo no []teste está lá $3ou $4está vazio. Nesse caso, o bash se expandirá [ FALSE != $3 ]para o [ FALSE != ]que é um erro de sintaxe, então outro caractere arbitrário existe para se proteger contra isso. Essa é uma maneira muito comum, você a verá em muitos scripts.

Eu configurei OPTFLAG1e o restante deles ""no início apenas para ter certeza (no caso de eles terem sido configurados para algo antes), mas se eles não foram realmente declarados no ambiente, você não precisa fazer isso estritamente.

Algumas observações adicionais:

  • Você pode realmente receber os parâmetros da mesma maneira que recebe runprogram: com sinalizadores. É disso que John Nse trata. É aí que getoptsse torna útil.
  • Usar FALSE para esse objetivo é um pouco incomum e bastante longo. Normalmente, usaria-se um único caractere (possivelmente -) para sinalizar um valor vazio, ou apenas passasse uma string vazia, como "", se isso não for um valor válido do parâmetro. String vazia também torna o teste mais curto, basta usar if [ -n "$3" ].
  • Se houver apenas um sinalizador opcional, ou se eles dependem, para que você nunca possa ter o OPTFLAG2, mas não o OPTFLAG1, poderá simplesmente pular os que não deseja definir. Se você usar ""parâmetros vazios, como sugerido acima, poderá pular todos os vazios finais de qualquer maneira.

Esse método é basicamente o modo como as opções são passadas para os compiladores no Makefiles.

E novamente - se as entradas podem conter espaços, fica feio.

orion
fonte
Não, você não deseja que o shell divida os parâmetros em palavras. Você deve sempre citar todas as referências a variáveis ​​de shell, a menos que tenha um bom motivo para não fazê-lo e tenha certeza de que sabe o que está fazendo. Consulte Implicações de segurança de não citar uma variável nos shells bash / POSIX , caso você não esteja familiarizado com ela, e role para baixo até a minha resposta , onde eu resolvo exatamente esse problema (com a mesma técnica que Jasen usa ).
G-Man diz 'Reinstate Monica'