Como obter argumentos com sinalizadores no Bash

283

Eu sei que posso facilmente obter parâmetros posicionados como este no bash:

$0 ou $1

Eu quero poder usar opções de sinalizador como esta para especificar para que cada parâmetro é usado:

mysql -u user -h host

Qual é a melhor maneira de obter -u paramvalor e -h paramvalor por sinalizador em vez de por posição?

Stann
fonte
2
Pode ser uma boa idéia de pedir / verificar sobre a unix.stackexchange.com bem
MRR0GERS
8
google para "bash getopts" - muitos tutoriais.
Glenn Jackman
89
@ Glenn-Jackman: Definitivamente vou pesquisá-lo no Google agora que sei o nome. A coisa sobre o google é - para fazer uma pergunta - você já deve saber 50% da resposta.
Stann

Respostas:

291

Este é o idioma que eu costumo usar:

while test $# -gt 0; do
  case "$1" in
    -h|--help)
      echo "$package - attempt to capture frames"
      echo " "
      echo "$package [options] application [arguments]"
      echo " "
      echo "options:"
      echo "-h, --help                show brief help"
      echo "-a, --action=ACTION       specify an action to use"
      echo "-o, --output-dir=DIR      specify a directory to store output in"
      exit 0
      ;;
    -a)
      shift
      if test $# -gt 0; then
        export PROCESS=$1
      else
        echo "no process specified"
        exit 1
      fi
      shift
      ;;
    --action*)
      export PROCESS=`echo $1 | sed -e 's/^[^=]*=//g'`
      shift
      ;;
    -o)
      shift
      if test $# -gt 0; then
        export OUTPUT=$1
      else
        echo "no output dir specified"
        exit 1
      fi
      shift
      ;;
    --output-dir*)
      export OUTPUT=`echo $1 | sed -e 's/^[^=]*=//g'`
      shift
      ;;
    *)
      break
      ;;
  esac
done

Os pontos principais são:

  • $# é o número de argumentos
  • O loop while examina todos os argumentos fornecidos, correspondendo aos seus valores dentro de uma instrução case
  • turno leva o primeiro embora. Você pode mudar várias vezes dentro de uma instrução de caso para obter vários valores.
Flexo
fonte
3
O que os casos --action*e --output-dir*fazem?
Lucio
1
Eles apenas salvam os valores que obtêm no ambiente.
Flexo
22
@Lucio Super comentário antigo, mas adicioná-lo no caso de alguém visitar esta página. O asterisco (*) é para o caso em que alguém tipos --action=[ACTION], bem como o caso em que alguém tipos--action [ACTION]
cooper
2
Por *)que você quebra aí, não deveria sair ou ignorar a má opção? Em outras palavras, -bad -o dira -o dirpeça nunca é processada.
newguy
@newguy boa pergunta. Eu acho que estava tentando deixá-los cair em outra coisa
Flexo
427

Este exemplo usa o getoptscomando interno do Bash e é do Guia de estilos do Google Shell :

a_flag=''
b_flag=''
files=''
verbose='false'

print_usage() {
  printf "Usage: ..."
}

while getopts 'abf:v' flag; do
  case "${flag}" in
    a) a_flag='true' ;;
    b) b_flag='true' ;;
    f) files="${OPTARG}" ;;
    v) verbose='true' ;;
    *) print_usage
       exit 1 ;;
  esac
done

Nota: Se um caractere for seguido por dois pontos (por exemplo f:), essa opção deverá ter um argumento.

Exemplo de uso: ./script -v -a -b -f filename

O uso de getopts tem várias vantagens sobre a resposta aceita:

  • a condição while é muito mais legível e mostra quais são as opções aceitas
  • código mais limpo; sem contar o número de parâmetros e mudar
  • você pode juntar opções (por exemplo, -a -b -c-abc )

No entanto, uma grande desvantagem é que ele não suporta opções longas, apenas opções de caractere único.

Dennis
fonte
48
Uma pergunta por que esta resposta, usando um builtin bash, não é a de cima
Will Barnwell
13
Para a posteridade: os dois pontos depois em 'abf: v' denotam que -f recebe um argumento adicional (o nome do arquivo nesse caso).
Zahbaz 7/09/16
1
Eu tive que mudar a linha de erro para isto: #?) printf '\nUsage: %s: [-a] aflag [-b] bflag\n' $0; exit 2 ;;
Andy
7
Você poderia adicionar uma observação sobre os dois pontos? Em que, após cada letra, nenhum ponto e vírgula significa arg, um ponto e vírgula significa um argumento e dois pontos significam um argumento opcional?
precisa saber é o seguinte
3
O @WillBarnwell deve observar que foi adicionado três anos após a pergunta, enquanto a resposta principal foi adicionada no mesmo dia.
Rbennell
47

getopt é seu amigo .. um exemplo simples:

function f () {
TEMP=`getopt --long -o "u:h:" "$@"`
eval set -- "$TEMP"
while true ; do
    case "$1" in
        -u )
            user=$2
            shift 2
        ;;
        -h )
            host=$2
            shift 2
        ;;
        *)
            break
        ;;
    esac 
done;

echo "user = $user, host = $host"
}

f -u myself -h some_host

Deve haver vários exemplos no seu diretório / usr / bin.

Shizzmo
fonte
3
Um exemplo mais extenso pode ser encontrado no diretório /usr/share/doc/util-linux/examples, pelo menos nas máquinas Ubuntu.
Serge Stroobandt
10

Eu acho que isso serviria como um exemplo mais simples do que você deseja alcançar. Não há necessidade de usar ferramentas externas. As ferramentas integradas do Bash podem fazer o trabalho por você.

function DOSOMETHING {

   while test $# -gt 0; do
           case "$1" in
                -first)
                    shift
                    first_argument=$1
                    shift
                    ;;
                -last)
                    shift
                    last_argument=$1
                    shift
                    ;;
                *)
                   echo "$1 is not a recognized flag!"
                   return 1;
                   ;;
          esac
  done  

  echo "First argument : $first_argument";
  echo "Last argument : $last_argument";
 }

Isso permitirá que você use sinalizadores; não importa em que ordem você esteja passando os parâmetros, obterá o comportamento adequado.

Exemplo:

 DOSOMETHING -last "Adios" -first "Hola"

Resultado :

 First argument : Hola
 Last argument : Adios

Você pode adicionar essa função ao seu perfil ou colocá-la dentro de um script.

Obrigado!

Editar: salve-o como um arquivo aa e execute-o como yourfile.sh -last "Adios" -first "Hola"

#!/bin/bash
while test $# -gt 0; do
           case "$1" in
                -first)
                    shift
                    first_argument=$1
                    shift
                    ;;
                -last)
                    shift
                    last_argument=$1
                    shift
                    ;;
                *)
                   echo "$1 is not a recognized flag!"
                   return 1;
                   ;;
          esac
  done  

  echo "First argument : $first_argument";
  echo "Last argument : $last_argument";
Matias Barrios
fonte
Eu uso o código acima e, quando executado, não está imprimindo nada. ./hello.sh DOSOMETHING -last "Adios" -first "Hola"
dinu0101
@ dinu0101 Esta é uma função. Não é um script. Você deve usá-lo como DOSOMETHING -último "Adios" - primeiro "Hola"
Matias Barrios
Obrigado @Matias. Entendido. como executar dentro do script.
dinu0101
1
Muito obrigado @Matias #
dinu0101
2
Usando return 1;com o último exemplo de saídas can only 'return' from a function or sourced scriptno macOS. A mudança para exit 1;funciona como esperado.
Mattias
5

Outra alternativa seria usar algo como o exemplo abaixo, que permitiria o uso de tags --image ou -i longas e também permitiria -i = "example.jpg" ou métodos -i example.jpg compilados para passar argumentos .

# declaring a couple of associative arrays
declare -A arguments=();  
declare -A variables=();

# declaring an index integer
declare -i index=1;

# any variables you want to use here
# on the left left side is argument label or key (entered at the command line along with it's value) 
# on the right side is the variable name the value of these arguments should be mapped to.
# (the examples above show how these are being passed into this script)
variables["-gu"]="git_user";  
variables["--git-user"]="git_user";  
variables["-gb"]="git_branch";  
variables["--git-branch"]="git_branch";  
variables["-dbr"]="db_fqdn";  
variables["--db-redirect"]="db_fqdn";  
variables["-e"]="environment";  
variables["--environment"]="environment";

# $@ here represents all arguments passed in
for i in "$@"  
do  
  arguments[$index]=$i;
  prev_index="$(expr $index - 1)";

  # this if block does something akin to "where $i contains ="
  # "%=*" here strips out everything from the = to the end of the argument leaving only the label
  if [[ $i == *"="* ]]
    then argument_label=${i%=*} 
    else argument_label=${arguments[$prev_index]}
  fi

  # this if block only evaluates to true if the argument label exists in the variables array
  if [[ -n ${variables[$argument_label]} ]]
    then
        # dynamically creating variables names using declare
        # "#$argument_label=" here strips out the label leaving only the value
        if [[ $i == *"="* ]]
            then declare ${variables[$argument_label]}=${i#$argument_label=} 
            else declare ${variables[$argument_label]}=${arguments[$index]}
        fi
  fi

  index=index+1;
done;

# then you could simply use the variables like so:
echo "$git_user";
Robert McMahan
fonte
3

Gosto da resposta de Robert McMahan da melhor forma possível, pois parece ser mais fácil transformar em arquivos compartilháveis ​​compartilháveis ​​para qualquer um dos seus scripts usar. Mas parece ter uma falha com a linha que if [[ -n ${variables[$argument_label]} ]]lança a mensagem "variáveis: matriz incorreta subscrita". Não tenho o representante para comentar e duvido que essa seja a 'correção' adequada, mas envolva isso ifemif [[ -n $argument_label ]] ; then limpeza.

Aqui está o código com o qual eu acabei, se você souber uma maneira melhor, adicione um comentário à resposta de Robert.

Incluir arquivo "flags-declares.sh"

# declaring a couple of associative arrays
declare -A arguments=();
declare -A variables=();

# declaring an index integer
declare -i index=1;

Incluir arquivo "flags-arguments.sh"

# $@ here represents all arguments passed in
for i in "$@"
do
  arguments[$index]=$i;
  prev_index="$(expr $index - 1)";

  # this if block does something akin to "where $i contains ="
  # "%=*" here strips out everything from the = to the end of the argument leaving only the label
  if [[ $i == *"="* ]]
    then argument_label=${i%=*}
    else argument_label=${arguments[$prev_index]}
  fi

  if [[ -n $argument_label ]] ; then
    # this if block only evaluates to true if the argument label exists in the variables array
    if [[ -n ${variables[$argument_label]} ]] ; then
      # dynamically creating variables names using declare
      # "#$argument_label=" here strips out the label leaving only the value
      if [[ $i == *"="* ]]
        then declare ${variables[$argument_label]}=${i#$argument_label=} 
        else declare ${variables[$argument_label]}=${arguments[$index]}
      fi
    fi
  fi

  index=index+1;
done;

Seu "script.sh"

. bin/includes/flags-declares.sh

# any variables you want to use here
# on the left left side is argument label or key (entered at the command line along with it's value) 
# on the right side is the variable name the value of these arguments should be mapped to.
# (the examples above show how these are being passed into this script)
variables["-gu"]="git_user";
variables["--git-user"]="git_user";
variables["-gb"]="git_branch";
variables["--git-branch"]="git_branch";
variables["-dbr"]="db_fqdn";
variables["--db-redirect"]="db_fqdn";
variables["-e"]="environment";
variables["--environment"]="environment";

. bin/includes/flags-arguments.sh

# then you could simply use the variables like so:
echo "$git_user";
echo "$git_branch";
echo "$db_fqdn";
echo "$environment";
Michael
fonte
3

Se você está familiarizado com o Python argparse e não se importa de chamar o python para analisar argumentos do bash, há um pedaço de código que eu achei realmente útil e super fácil de usar chamado argparse-bash https://github.com/nhoffman/ argparse-bash

Exemplo de exemplo do script example.sh:

#!/bin/bash

source $(dirname $0)/argparse.bash || exit 1
argparse "$@" <<EOF || exit 1
parser.add_argument('infile')
parser.add_argument('outfile')
parser.add_argument('-a', '--the-answer', default=42, type=int,
                    help='Pick a number [default %(default)s]')
parser.add_argument('-d', '--do-the-thing', action='store_true',
                    default=False, help='store a boolean [default %(default)s]')
parser.add_argument('-m', '--multiple', nargs='+',
                    help='multiple values allowed')
EOF

echo required infile: "$INFILE"
echo required outfile: "$OUTFILE"
echo the answer: "$THE_ANSWER"
echo -n do the thing?
if [[ $DO_THE_THING ]]; then
    echo " yes, do it"
else
    echo " no, do not do it"
fi
echo -n "arg with multiple values: "
for a in "${MULTIPLE[@]}"; do
    echo -n "[$a] "
done
echo
Linh
fonte
2

Eu proponho um TLDR simples :; exemplo para os não iniciados.

Crie um script bash chamado helloworld.sh

#!/bin/bash

while getopts "n:" arg; do
  case $arg in
    n) Name=$OPTARG;;
  esac
done

echo "Hello $Name!"

Você pode passar um parâmetro opcional -nao executar o script.

Execute o script da seguinte maneira:

$ bash helloworld.sh -n 'World'

Resultado

$ Hello World!

Notas

Se você deseja usar vários parâmetros:

  1. estender while getops "n:" arg: docom mais paramaters como while getops "n:o:p:" arg: do
  2. estenda o comutador de caso com atribuições extras de variáveis. Tais como o) Option=$OPTARGep) Parameter=$OPTARG
pijemcolu
fonte
1
#!/bin/bash

if getopts "n:" arg; then
  echo "Welcome $OPTARG"
fi

Salve-o como sample.sh e tente executar

sh sample.sh -n John

no seu terminal.

Nishant Ingle
fonte
1

Como tive problemas com getopts com vários sinalizadores, escrevi esse código. Ele usa uma variável modal para detectar sinalizadores e usá-los para atribuir argumentos a variáveis.

Observe que, se um sinalizador não tiver um argumento, algo que não seja a configuração de CURRENTFLAG pode ser feito.

    for MYFIELD in "$@"; do

        CHECKFIRST=`echo $MYFIELD | cut -c1`

        if [ "$CHECKFIRST" == "-" ]; then
            mode="flag"
        else
            mode="arg"
        fi

        if [ "$mode" == "flag" ]; then
            case $MYFIELD in
                -a)
                    CURRENTFLAG="VARIABLE_A"
                    ;;
                -b)
                    CURRENTFLAG="VARIABLE_B"
                    ;;
                -c)
                    CURRENTFLAG="VARIABLE_C"
                    ;;
            esac
        elif [ "$mode" == "arg" ]; then
            case $CURRENTFLAG in
                VARIABLE_A)
                    VARIABLE_A="$MYFIELD"
                    ;;
                VARIABLE_B)
                    VARIABLE_B="$MYFIELD"
                    ;;
                VARIABLE_C)
                    VARIABLE_C="$MYFIELD"
                    ;;
            esac
        fi
    done
Jessica Richards
fonte
0

Então aqui está a minha solução. Eu queria poder manipular sinalizadores booleanos sem hífen, com um hífen e com dois hífens, bem como com a atribuição de parâmetro / valor com um e dois hífens.

# Handle multiple types of arguments and prints some variables
#
# Boolean flags
# 1) No hyphen
#    create   Assigns `true` to the variable `CREATE`.
#             Default is `CREATE_DEFAULT`.
#    delete   Assigns true to the variable `DELETE`.
#             Default is `DELETE_DEFAULT`.
# 2) One hyphen
#      a      Assigns `true` to a. Default is `false`.
#      b      Assigns `true` to b. Default is `false`.
# 3) Two hyphens
#    cats     Assigns `true` to `cats`. By default is not set.
#    dogs     Assigns `true` to `cats`. By default is not set.
#
# Parameter - Value
# 1) One hyphen
#      c      Assign any value you want
#      d      Assign any value you want
#
# 2) Two hyphens
#   ... Anything really, whatever two-hyphen argument is given that is not
#       defined as flag, will be defined with the next argument after it.
#
# Example:
# ./parser_example.sh delete -a -c VA_1 --cats --dir /path/to/dir
parser() {
    # Define arguments with one hyphen that are boolean flags
    HYPHEN_FLAGS="a b"
    # Define arguments with two hyphens that are boolean flags
    DHYPHEN_FLAGS="cats dogs"

    # Iterate over all the arguments
    while [ $# -gt 0 ]; do
        # Handle the arguments with no hyphen
        if [[ $1 != "-"* ]]; then
            echo "Argument with no hyphen!"
            echo $1
            # Assign true to argument $1
            declare $1=true
            # Shift arguments by one to the left
            shift
        # Handle the arguments with one hyphen
        elif [[ $1 == "-"[A-Za-z0-9]* ]]; then
            # Handle the flags
            if [[ $HYPHEN_FLAGS == *"${1/-/}"* ]]; then
                echo "Argument with one hyphen flag!"
                echo $1
                # Remove the hyphen from $1
                local param="${1/-/}"
                # Assign true to $param
                declare $param=true
                # Shift by one
                shift
            # Handle the parameter-value cases
            else
                echo "Argument with one hyphen value!"
                echo $1 $2
                # Remove the hyphen from $1
                local param="${1/-/}"
                # Assign argument $2 to $param
                declare $param="$2"
                # Shift by two
                shift 2
            fi
        # Handle the arguments with two hyphens
        elif [[ $1 == "--"[A-Za-z0-9]* ]]; then
            # NOTE: For double hyphen I am using `declare -g $param`.
            #   This is the case because I am assuming that's going to be
            #   the final name of the variable
            echo "Argument with two hypens!"
            # Handle the flags
            if [[ $DHYPHEN_FLAGS == *"${1/--/}"* ]]; then
                echo $1 true
                # Remove the hyphens from $1
                local param="${1/--/}"
                # Assign argument $2 to $param
                declare -g $param=true
                # Shift by two
                shift
            # Handle the parameter-value cases
            else
                echo $1 $2
                # Remove the hyphens from $1
                local param="${1/--/}"
                # Assign argument $2 to $param
                declare -g $param="$2"
                # Shift by two
                shift 2
            fi
        fi

    done
    # Default value for arguments with no hypheb
    CREATE=${create:-'CREATE_DEFAULT'}
    DELETE=${delete:-'DELETE_DEFAULT'}
    # Default value for arguments with one hypen flag
    VAR1=${a:-false}
    VAR2=${b:-false}
    # Default value for arguments with value
    # NOTE1: This is just for illustration in one line. We can well create
    #   another function to handle this. Here I am handling the cases where
    #   we have a full named argument and a contraction of it.
    #   For example `--arg1` can be also set with `-c`.
    # NOTE2: What we are doing here is to check if $arg is defined. If not,
    #   check if $c was defined. If not, assign the default value "VD_"
    VAR3=$(if [[ $arg1 ]]; then echo $arg1; else echo ${c:-"VD_1"}; fi)
    VAR4=$(if [[ $arg2 ]]; then echo $arg2; else echo ${d:-"VD_2"}; fi)
}


# Pass all the arguments given to the script to the parser function
parser "$@"


echo $CREATE $DELETE $VAR1 $VAR2 $VAR3 $VAR4 $cats $dir

Algumas referências

  • O procedimento principal foi encontrado aqui .
  • Mais sobre como passar todos os argumentos para uma função aqui .
  • Mais informações sobre valores padrão aqui .
  • Mais informações sobre declaredo$ bash -c "help declare".
  • Mais informações sobre shiftdo $ bash -c "help shift".
H. Sánchez
fonte