Jak zdobyć argumenty z flagami w Bash

283

Wiem, że w bashu mogę łatwo uzyskać takie parametry pozycjonowania:

$0 lub $1

Chcę mieć możliwość korzystania z takich opcji flagi, aby określić, do czego służy każdy parametr:

mysql -u user -h host

Jaki jest najlepszy sposób na uzyskanie -u paramwartości i -h paramwartości według flagi zamiast pozycji?

Stann
źródło
2
To może być dobry pomysł, aby zapytać / sprawdzić na co unix.stackexchange.com także
MRR0GERS
8
google dla „bash getopts” - wiele samouczków.
glenn jackman
89
@ glenn-jackman: Zdecydowanie google go teraz, kiedy znam nazwę. Chodzi o to, że w Google - aby zadać pytanie - powinieneś już znać 50% odpowiedzi.
Stann,

Odpowiedzi:

292

To jest idiom, którego zwykle używam:

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

Kluczowe punkty to:

  • $# to liczba argumentów
  • pętla while sprawdza wszystkie dostarczone argumenty, dopasowując ich wartości do instrukcji case
  • shift odbiera pierwszy. Możesz wielokrotnie przesuwać się w instrukcji case, aby przyjmować wiele wartości.
Flexo
źródło
3
Co robią --action*i --output-dir*przypadki?
Lucio
1
Po prostu zapisują wartości, które dostają do środowiska.
Flexo
22
@Lucio Super stary komentarz, ale dodający go na wypadek, gdyby ktoś odwiedził tę stronę. * (Symbol wieloznaczny) dotyczy przypadku, w którym ktoś pisze, --action=[ACTION]a także w przypadku, gdy ktoś --action [ACTION]
pisze
2
Po *)co się tam włamujesz, nie powinieneś wyjść lub zignorować złej opcji? Innymi słowy część nigdy nie jest przetwarzane. -bad -o dir-o dir
newguy
@newguy dobre pytanie. Myślę, że próbowałem pozwolić im upaść na coś innego
Flexo
427

W tym przykładzie użyto wbudowanego getoptspolecenia Bash i pochodzi ono z Przewodnika po stylu 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

Uwaga: jeśli po znaku następuje dwukropek (np. f:), Oczekuje się, że ta opcja będzie miała argument.

Przykładowe użycie: ./script -v -a -b -f filename

Korzystanie z getopts ma kilka zalet w stosunku do przyjętej odpowiedzi:

  • warunek while jest o wiele bardziej czytelny i pokazuje, jakie są akceptowane opcje
  • czystszy kod; nie licząc liczby parametrów i przesunięć
  • możesz dołączyć opcje (np. -a -b -c-abc)

Jednak dużą wadą jest to, że nie obsługuje długich opcji, tylko opcje jednoznakowe.

Dennis
źródło
48
Można się zastanawiać, dlaczego ta odpowiedź, wykorzystująca wbudowane bash, nie jest najlepsza
Will Barnwell
13
Dla potomności: dwukropek po „abf: v” oznacza, że ​​-f przyjmuje dodatkowy argument (w tym przypadku nazwę pliku).
zahbaz
1
Musiałem zmienić linię błędu na następującą:?) printf '\nUsage: %s: [-a] aflag [-b] bflag\n' $0; exit 2 ;;
Andy
7
Czy możesz dodać notatkę na temat okrężnicy? W tym, że po każdej literze dwukropek oznacza brak arg, jeden dwukropek oznacza arg, a dwa dwukropki oznaczają opcjonalny arg?
limasxgoesto0
3
@WillBarnwell należy zauważyć, że został on dodany 3 lata po zadaniu pytania, a najlepsza odpowiedź została dodana tego samego dnia.
rbennell,
47

getopt jest twoim przyjacielem .. prosty przykład:

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

W katalogu / usr / bin powinny znajdować się różne przykłady.

Shizzmo
źródło
3
Bardziej obszerny przykład można znaleźć w katalogu /usr/share/doc/util-linux/examples, przynajmniej na maszynach Ubuntu.
Serge Stroobandt
10

Myślę, że byłby to prostszy przykład tego, co chcesz osiągnąć. Nie ma potrzeby korzystania z zewnętrznych narzędzi. Wbudowane narzędzia Bash mogą wykonać zadanie za Ciebie.

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";
 }

Umożliwi to użycie flag, więc bez względu na kolejność przekazywania parametrów uzyskasz właściwe zachowanie.

Przykład:

 DOSOMETHING -last "Adios" -first "Hola"

Wynik :

 First argument : Hola
 Last argument : Adios

Możesz dodać tę funkcję do swojego profilu lub umieścić ją w skrypcie.

Dzięki!

Edycja: Zapisz to jako plik, a następnie uruchom jako 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
źródło
Używam powyższego kodu i po uruchomieniu nic nie drukuje. ./hello.sh DOSOMETHING -last "Adios" -first "Hola"
dinu0101
@ dinu0101 To jest funkcja. Nie skrypt. Powinieneś użyć go jako DOSOMETHING -ostatni „Adios” -pierwszy „Hola”
Matias Barrios
Dzięki @Matias. Zrozumiany. jak uruchomić skrypt.
dinu0101
1
Dziękuję bardzo @Matias
dinu0101
2
Korzystanie return 1;z ostatniego przykładu danych wyjściowych can only 'return' from a function or sourced scriptw systemie macOS. Przejście do exit 1;działa zgodnie z oczekiwaniami.
Mattias
5

Inną alternatywą byłoby użycie czegoś takiego jak w poniższym przykładzie, który pozwoliłby na użycie długich tagów --image lub short -i , a także pozwoliłby na skompilowanie -i = "example.jpg" lub oddzielne metody przekazywania argumentów -i example.jpg .

# 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
źródło
3

Najbardziej podoba mi się odpowiedź Roberta McMahana, ponieważ wydaje się, że najłatwiej jest zrobić z niej pliki do współużytkowania dla każdego ze skryptów. Ale wydaje się, że ma wadę, gdy wiersz if [[ -n ${variables[$argument_label]} ]]rzuca komunikat „zmienne: zły indeks tablicy”. Nie mam przedstawiciela, który mógłby komentować, i wątpię, aby to była poprawna poprawka, ale zawinięcie jej ifw if [[ -n $argument_label ]] ; thento oczyszcza.

Oto kod, na którym skończyłem. Jeśli znasz lepszy sposób, dodaj komentarz do odpowiedzi Roberta.

Dołącz plik „flags-declares.sh”

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

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

Dołącz plik „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;

Twój „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
źródło
3

Jeśli jesteś zaznajomiony z Pythonem Argparse i nie masz nic przeciwko wywoływaniu Pythona w celu przeanalizowania argumentów bash, istnieje kawałek kodu, który uważam za bardzo pomocny i bardzo łatwy w użyciu, zwany argparse-bash https://github.com/nhoffman/ argparse-bash

Przykład pobierz ze skryptu 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
źródło
2

Proponuję prosty TLDR :; przykład dla niezainicjowanych.

Utwórz skrypt bash o nazwie helloworld.sh

#!/bin/bash

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

echo "Hello $Name!"

Następnie możesz przekazać opcjonalny parametr -npodczas wykonywania skryptu.

Wykonaj skrypt jako taki:

$ bash helloworld.sh -n 'World'

Wynik

$ Hello World!

Notatki

Jeśli chcesz użyć wielu parametrów:

  1. rozszerzyć while getops "n:" arg: doo więcej parametrów, takich jak while getops "n:o:p:" arg: do
  2. rozszerzyć przełącznik wielkości liter o dodatkowe zmienne przypisania. Takich jak o) Option=$OPTARGip) Parameter=$OPTARG
pijemcolu
źródło
1
#!/bin/bash

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

Zapisz go jako sample.sh i spróbuj uruchomić

sh sample.sh -n John

w twoim terminalu.

Nishant Ingle
źródło
1

Miałem problem z użyciem getopts z wieloma flagami, więc napisałem ten kod. Wykorzystuje zmienną modalną do wykrywania flag i do używania tych flag do przypisywania argumentów do zmiennych.

Zauważ, że jeśli flaga nie powinna mieć argumentu, można zrobić coś innego niż ustawienie CURRENTFLAG.

    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
źródło
0

Oto moje rozwiązanie. Chciałem móc obsługiwać flagi logiczne bez łącznika, z jednym łącznikiem i dwoma łącznikami, a także przypisywanie parametrów / wartości za pomocą jednego i dwóch łączników.

# 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

Niektóre referencje

  • Główna procedura została znaleziona tutaj .
  • Więcej informacji o przekazywaniu wszystkich argumentów do funkcji tutaj .
  • Więcej informacji na temat wartości domyślnych tutaj .
  • Więcej informacji na temat declaredo $ bash -c "help declare".
  • Więcej informacji na temat shiftdo $ bash -c "help shift".
H. Sánchez
źródło