Como analiso argumentos de linha de comando no Bash?

1922

Digamos, eu tenho um script que é chamado com esta linha:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile

ou este:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile 

Qual é a maneira aceita de analisar esta de tal forma que em cada caso (ou alguma combinação dos dois) $v, $fe $dtudo será definido para truee $outFileserá igual a /fizz/someOtherFile?

Lawrence Johnston
fonte
1
Para os usuários do zsh, existe um ótimo componente chamado zparseopts que pode fazer: zparseopts -D -E -M -- d=debug -debug=dE ter ambos -de --debugna $debugmatriz echo $+debug[1]retornará 0 ou 1 se um deles for usado. Ref: zsh.org/mla/users/2011/msg00350.html
Dezza
1
Realmente bom tutorial: linuxcommand.org/lc3_wss0120.php . Eu gosto especialmente do exemplo "Opções de linha de comando".
Gabriel Staples

Respostas:

2677

Método # 1: Usando o bash sem getopt [s]

Duas maneiras comuns de passar argumentos de pares de valores-chave são:

Bash Separado por Espaço (por exemplo, --option argument ) (sem getopt [s])

Uso demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -e|--extension)
    EXTENSION="$2"
    shift # past argument
    shift # past value
    ;;
    -s|--searchpath)
    SEARCHPATH="$2"
    shift # past argument
    shift # past value
    ;;
    -l|--lib)
    LIBPATH="$2"
    shift # past argument
    shift # past value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument
    ;;
    *)    # unknown option
    POSITIONAL+=("$1") # save it in an array for later
    shift # past argument
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 "$1"
fi
EOF

chmod +x /tmp/demo-space-separated.sh

/tmp/demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

saída de copiar e colar o bloco acima:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Bash é igual a separado (por exemplo, --option=argument) (sem getopt [s])

Uso demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

cat >/tmp/demo-equals-separated.sh <<'EOF'
#!/bin/bash

for i in "$@"
do
case $i in
    -e=*|--extension=*)
    EXTENSION="${i#*=}"
    shift # past argument=value
    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    shift # past argument=value
    ;;
    -l=*|--lib=*)
    LIBPATH="${i#*=}"
    shift # past argument=value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument with no value
    ;;
    *)
          # unknown option
    ;;
esac
done
echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 $1
fi
EOF

chmod +x /tmp/demo-equals-separated.sh

/tmp/demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

saída de copiar e colar o bloco acima:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Para entender melhor a ${i#*=}pesquisa por "Remoção de Substring" neste guia . É funcionalmente equivalente ao `sed 's/[^=]*=//' <<< "$i"`que chama um subprocesso desnecessário ou ao `echo "$i" | sed 's/[^=]*=//'`qual chama dois subprocessos desnecessários.

Método 2: Usando o bash com getopt [s]

from: http://mywiki.wooledge.org/BashFAQ/035#getopts

Limitações do getopt (1) ( getoptversões mais antigas e relativamente recentes ):

  • não pode lidar com argumentos que são cadeias vazias
  • não pode lidar com argumentos com espaço em branco incorporado

getoptVersões mais recentes não têm essas limitações.

Além disso, o shell POSIX (e outros) oferece getoptsque não possui essas limitações. Eu incluí um simplistagetopts exemplo .

Uso demo-getopts.sh -vf /etc/hosts foo bar

cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  verbose=1
        ;;
    f)  output_file=$OPTARG
        ;;
    esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
EOF

chmod +x /tmp/demo-getopts.sh

/tmp/demo-getopts.sh -vf /etc/hosts foo bar

saída de copiar e colar o bloco acima:

verbose=1, output_file='/etc/hosts', Leftovers: foo bar

As vantagens de getoptssão:

  1. É mais portátil e funcionará em outros tipos de conchas dash.
  2. Ele pode lidar com várias opções únicas, como -vf filenamena maneira típica do Unix, automaticamente.

A desvantagem getoptsé que ele só pode lidar com opções curtas ( -h, não --help) sem código adicional.

Há um tutorial getopts que explica o que todas as sintaxes e variáveis ​​significam. No bash, há também help getopts, o que pode ser informativo.

Bruno Bronosky
fonte
44
Isso é mesmo verdade? De acordo com a Wikipedia, há uma nova versão aprimorada do GNU, getoptque inclui todas as funcionalidades getoptse mais algumas. man getoptno Ubuntu 13.04 gera getopt - parse command options (enhanced)o nome, então presumo que esta versão aprimorada seja padrão agora.
Livven
47
Que algo é uma maneira certa no seu sistema é uma premissa muito fraca para basear as suposições de "ser padrão".
Szablica 17/07/2013
13
@Livven, isso getoptnão é um utilitário GNU, é parte dele util-linux.
Stephane Chazelas
4
Se você usar -gt 0, remova seu shiftapós o esac, aumente todos os shiftpor 1 e inclua este caso: *) break;;você pode manipular argumentos não opcionais. Ex: pastebin.com/6DJ57HTc
Nicolas Lacombe
2
Você não ecoa –default. No primeiro exemplo, eu aviso que, se –defaulté o último argumento, não é processado (consideradas não-opt), a menos que while [[ $# -gt 1 ]]é definido como while [[ $# -gt 0 ]]
kolydart
562

Nenhuma resposta menciona getopt aprimorado . E a resposta mais votada é enganosa: ela ignora -⁠vfdopções curtas de estilo (solicitadas pelo OP) ou opções após argumentos posicionais (também solicitadas pelo OP); e ignora erros de análise. Em vez de:

  • Use aprimorado getoptno util-linux ou anteriormente GNU glibc . 1
  • Trabalha com getopt_long() a função C do GNU glibc.
  • Tem tudo recursos distintivos úteis (os outros não os têm):
    • lida com espaços, citando caracteres e até binário nos argumentos 2 (sem aprimoramentogetopt não pode fazer isso)
    • ele pode lidar com opções no final: script.sh -o outFile file1 file2 -v(getopts não faz isso)
    • permite =opções longas ao estilo:script.sh --outfile=fileOut --infile fileIn (permitir que ambos sejam longos se forem analisados ​​automaticamente)
    • permite opções curtas combinadas, por exemplo -vfd(trabalho real se auto-análise)
    • permite tocar argumentos de opção, por exemplo, -oOutfileou-vfdoOutfile
  • Já é tão antigo 3 que nenhum sistema GNU está ausente (por exemplo, qualquer Linux o possui).
  • Você pode testar sua existência com: getopt --test→ valor de retorno 4.
  • Outros getoptou embutidos no shell getoptssão de uso limitado.

As seguintes chamadas

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

todos retornam

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

com o seguinte myscript

#!/bin/bash
# saner programming env: these switches turn some bugs into errors
set -o errexit -o pipefail -o noclobber -o nounset

# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo 'I’m sorry, `getopt --test` failed in this environment.'
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 getopt aprimorado está disponível na maioria dos "sistemas bash", incluindo Cygwin; no OS X, tente brew install gnu-getopt ousudo port install getopt
2, asexec()convençõesPOSIXnão têm uma maneira confiável de passar NULL binário nos argumentos da linha de comando; esses bytes prematuramente encerram aprimeira versãodo argumento
3 lançada em 1997 ou antes (eu só a rastreei até 1997)

Robert Siemer
fonte
4
Obrigado por isso. Acabado de confirmar na tabela de recursos em en.wikipedia.org/wiki/Getopts , se você precisar de suporte para opções longas e não estiver no Solaris, getopté o caminho a seguir.
johncip
4
Acredito que a única ressalva getopté que ele não pode ser usado convenientemente em scripts de wrapper, onde é possível ter poucas opções específicas para o script de wrapper e, em seguida, passar as opções de script que não são de wrapper para o executável empacotado, intacto. Digamos que eu tenho um grepinvólucro chamado mygrepe tenho uma opção --fooespecífica para mygrep, então não posso fazer mygrep --foo -A 2, e -A 2passei automaticamente para grep; Eu preciso fazer mygrep --foo -- -A 2. Aqui está minha implementação sobre sua solução.
27417 Kaushal Modi
2
@bobpaul Sua declaração sobre o util-linux também é errada e enganosa: o pacote está marcado como “essencial” no Ubuntu / Debian. Como tal, é sempre instalado. - De quais distros você está falando (onde você diz que precisa ser instalado de propósito)?
Robert Siemer
3
Observe que isso não funciona no Mac, pelo menos até a versão 10.14.3 atual. O getopt que os navios é BSD getopt a partir de 1999 ...
jjj
2
@transang Negação booleana do valor de retorno. E seu efeito colateral: permitir que um comando falhe (caso contrário, o errexit abortaria o programa por erro). - Os comentários no script informam mais. Caso contrário:man bash
Robert Siemer
144

Maneira mais sucinta

script.sh

#!/bin/bash

while [[ "$#" -gt 0 ]]; do
    case $1 in
        -d|--deploy) deploy="$2"; shift ;;
        -u|--uglify) uglify=1 ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done

echo "Should deploy? $deploy"
echo "Should uglify? $uglify"

Uso:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify
Inanc Gumus
fonte
3
Isto é o que estou fazendo. É necessário while [[ "$#" > 1 ]]se eu quiser apoiar o final da linha com um sinalizador booleano ./script.sh --debug dev --uglify fast --verbose. Exemplo: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
12
Uau! Simples e limpo! Isto é como eu estou usando isso: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
2
É muito melhor colar em cada script em vez de lidar com a fonte ou fazer com que as pessoas se perguntem onde sua funcionalidade realmente começa.
RealHandy 31/01/19
Aviso: isso tolera argumentos duplicados, o argumento mais recente prevalece. por exemplo ./script.sh -d dev -d prod, resultaria em deploy == 'prod'. Eu usei assim mesmo: P :): +1:
yair
Estou usando isso (obrigado!), Mas observe que ele permite o valor do argumento vazio, por exemplo ./script.sh -d, não geraria um erro, mas apenas definido $deploycomo uma string vazia.
EM0 12/01
137

de: digitalpeer.com com pequenas modificações

Uso myscript.sh -p=my_prefix -s=dirname -l=libname

#!/bin/bash
for i in "$@"
do
case $i in
    -p=*|--prefix=*)
    PREFIX="${i#*=}"

    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    ;;
    -l=*|--lib=*)
    DIR="${i#*=}"
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
            # unknown option
    ;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}

Para entender melhor a ${i#*=}pesquisa por "Remoção de Substring" neste guia . É funcionalmente equivalente a`sed 's/[^=]*=//' <<< "$i"` que chama um subprocesso desnecessário ou ao `echo "$i" | sed 's/[^=]*=//'`qual chama dois subprocessos desnecessários.

guneysus
fonte
4
Arrumado! Embora isso não funcione para argumentos separados por espaço à la mount -t tempfs .... Pode-se provavelmente corrigir isso através de algo como while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;etc
Tobias KIENZLER
3
Isso não pode lidar com -vfdopções curtas combinadas de estilo.
Robert Siemer
105

getopt()/ getopts()é uma boa opção. Roubado daqui :

O simples uso de "getopt" é mostrado neste mini-script:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

O que dissemos é que qualquer um de -a, -b, -c ou -d será permitido, mas que -c é seguido por um argumento (o "c:" diz isso).

Se chamarmos isso de "g" e tentarmos:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

Começamos com dois argumentos, e "getopt" separa as opções e coloca cada uma em seu próprio argumento. Ele também adicionou "-".

Matt J
fonte
4
O uso $*está quebrado de getopt. (Ele organiza argumentos com espaços.) Veja minha resposta para um uso adequado.
Robert Siemer
Por que você gostaria de torná-lo mais complicado?
precisa saber é o seguinte
@ Matt J, a primeira parte do script (para i) seria capaz de lidar com argumentos com espaços neles se você usar "$ i" em vez de $ i. As getopts não parecem capazes de lidar com argumentos com espaços. Qual seria a vantagem de usar getopt sobre o loop for i?
thebunnyrules
99

Correndo o risco de adicionar outro exemplo para ignorar, aqui está o meu esquema.

  • alças -n arge--name=arg
  • permite argumentos no final
  • mostra erros sãos se algo estiver incorreto
  • compatível, não usa basismos
  • legível, não requer manutenção de estado em um loop

Espero que seja útil para alguém.

while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done
Bronson
fonte
4
Desculpe o atraso. No meu script, a função handle_argument recebe todos os argumentos que não são de opção. Você pode substituir essa linha pelo que desejar, talvez, *) die "unrecognized argument: $1"ou coletar os argumentos em uma variável *) args+="$1"; shift 1;;.
Bronson
Surpreendente! Eu testei um par de respostas, mas este é o único que trabalhou para todos os casos, incluindo muitos parâmetros de posição (tanto antes como depois bandeiras)
Guilherme Garnier
2
bom código sucinto, mas usar -n e nenhum outro argumento causa loop infinito devido a erro shift 2, emitindo shiftduas vezes em vez de shift 2. Sugeriu a edição.
Lauksas
42

Estou com cerca de 4 anos de atraso para esta pergunta, mas quero retribuir. Usei as respostas anteriores como ponto de partida para arrumar minha antiga análise ad-hoc de parâmetros. Em seguida, refatorei o código de modelo a seguir. Ele lida com parâmetros longos e curtos, usando = ou argumentos separados por espaço, bem como vários parâmetros curtos agrupados. Finalmente, ele reinsere todos os argumentos não param para as variáveis ​​$ 1, $ 2 .. Espero que seja útil.

#!/usr/bin/env bash

# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi

echo "Before"
for i ; do echo - $i ; done


# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.

while [ -n "$1" ]; do
        # Copy so we can modify it (can't modify $1)
        OPT="$1"
        # Detect argument termination
        if [ x"$OPT" = x"--" ]; then
                shift
                for OPT ; do
                        REMAINS="$REMAINS \"$OPT\""
                done
                break
        fi
        # Parse current opt
        while [ x"$OPT" != x"-" ] ; do
                case "$OPT" in
                        # Handle --flag=value opts like this
                        -c=* | --config=* )
                                CONFIGFILE="${OPT#*=}"
                                shift
                                ;;
                        # and --flag value opts like this
                        -c* | --config )
                                CONFIGFILE="$2"
                                shift
                                ;;
                        -f* | --force )
                                FORCE=true
                                ;;
                        -r* | --retry )
                                RETRY=true
                                ;;
                        # Anything unknown is recorded for later
                        * )
                                REMAINS="$REMAINS \"$OPT\""
                                break
                                ;;
                esac
                # Check for multiple short options
                # NOTICE: be sure to update this pattern to match valid options
                NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
                if [ x"$OPT" != x"$NEXTOPT" ] ; then
                        OPT="-$NEXTOPT"  # multiple short opts, keep going
                else
                        break  # long form, exit inner loop
                fi
        done
        # Done with that param. move to next
        shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done
Shane Day
fonte
Este código não pode lidar com opções com argumentos como este: -c1. E o uso de =separar opções curtas de seus argumentos é incomum ...
Robert Siemer
2
Eu encontrei dois problemas com esse pedaço de código útil: 1) o "shift" no caso de "-c = foo" acaba consumindo o próximo parâmetro; e 2) 'c' não deve ser incluído no padrão "[cfr]" para opções curtas combináveis.
Sfnd 06/06
36

Eu achei o assunto de escrever uma análise portátil em scripts tão frustrante que escrevi Argbash - um gerador de código FOSS que pode gerar o código de análise de argumentos para o seu script, além de alguns recursos interessantes:

https://argbash.io

bubla
fonte
Obrigado por escrever argbash, eu apenas o usei e achei que funciona bem. Eu preferi o argbash porque é um gerador de código que suporta o bash 3.x mais antigo encontrado no OS X 10.11 El Capitan. A única desvantagem é que a abordagem de gerador de código significa bastante código em seu script principal, em comparação com a chamada de um módulo.
RichVel 18/08/16
Na verdade, você pode usar o Argbash de uma maneira que produza uma biblioteca de análise personalizada apenas para você, que você pode ter incluído no seu script ou em um arquivo separado e apenas a fonte. Adicionei um exemplo para demonstrar isso e também o tornei mais explícito na documentação.
bubla
Bom saber. Esse exemplo é interessante, mas ainda não está claro - talvez você possa alterar o nome do script gerado para 'parse_lib.sh' ou similar e mostrar para onde o script principal o chama (como na seção de quebra automática de scripts, que é um caso de uso mais complexo).
RichVel 24/08/16
Os problemas foram resolvidos na versão recente do argbash: A documentação foi aprimorada, um script de início rápido do argbash-init foi introduzido e você pode até usar o argbash online em argbash.io/generate
bubla
29

Minha resposta é amplamente baseada na resposta de Bruno Bronosky , mas eu meio que misturei suas duas implementações puras do bash em uma que eu uso com bastante frequência.

# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

Isso permite que você tenha opções / valores separados por espaço, bem como valores definidos iguais.

Então você pode executar seu script usando:

./myscript --foo -b -o /fizz/file.txt

assim como:

./myscript -f --bar -o=/fizz/file.txt

e ambos devem ter o mesmo resultado final.

PROS:

  • Permite tanto -arg = value quanto -arg value

  • Funciona com qualquer nome de argumento que você possa usar no bash

    • Significado -a ou -arg ou --arg ou -arg ou qualquer outra coisa
  • Festa pura. Não há necessidade de aprender / usar getopt ou getopts

CONTRAS:

  • Não é possível combinar args

    • Significado não -abc. Você deve fazer -a -b -c

Estes são os únicos prós / contras que consigo pensar em cima da minha cabeça

Ponyboy47
fonte
15

Eu acho que este é simples o suficiente para usar:

#!/bin/bash
#

readopt='getopts $opts opt;rc=$?;[ $rc$opt == 0? ]&&exit 1;[ $rc == 0 ]||{ shift $[OPTIND-1];false; }'

opts=vfdo:

# Enumerating options
while eval $readopt
do
    echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done

# Enumerating arguments
for arg
do
    echo ARG:$arg
done

Exemplo de chamada:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile
Alek
fonte
1
Eu li tudo e este é o meu preferido. Eu não gosto de usar -a=1como estilo argc. Prefiro colocar primeiro as principais opções - e depois as especiais com espaçamento simples -o option. Estou procurando a maneira mais simples vs melhor de ler argvs.
M3nda 20/05
Está funcionando muito bem, mas se você passar um argumento para uma opção não a: todas as opções a seguir serão consideradas argumentos. Você pode verificar esta linha ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFilecom seu próprio script. opção -d não está definido como d:
m3nda
15

Expandindo a excelente resposta de @ guneysus, aqui está um ajuste que permite ao usuário usar a sintaxe que preferir, por exemplo

command -x=myfilename.ext --another_switch 

vs

command -x myfilename.ext --another_switch

Ou seja, os iguais podem ser substituídos por espaços em branco.

Essa "interpretação difusa" pode não ser do seu agrado, mas se você estiver criando scripts intercambiáveis ​​com outros utilitários (como é o caso do meu, que deve funcionar com o ffmpeg), a flexibilidade é útil.

STD_IN=0

prefix=""
key=""
value=""
for keyValue in "$@"
do
  case "${prefix}${keyValue}" in
    -i=*|--input_filename=*)  key="-i";     value="${keyValue#*=}";; 
    -ss=*|--seek_from=*)      key="-ss";    value="${keyValue#*=}";;
    -t=*|--play_seconds=*)    key="-t";     value="${keyValue#*=}";;
    -|--stdin)                key="-";      value=1;;
    *)                                      value=$keyValue;;
  esac
  case $key in
    -i) MOVIE=$(resolveMovie "${value}");  prefix=""; key="";;
    -ss) SEEK_FROM="${value}";          prefix=""; key="";;
    -t)  PLAY_SECONDS="${value}";           prefix=""; key="";;
    -)   STD_IN=${value};                   prefix=""; key="";; 
    *)   prefix="${keyValue}=";;
  esac
done
não sincronizado
fonte
13

Este exemplo mostra como usar getopte evale HEREDOCe shiftpara lidar com parâmetros de curto e longo com e sem um valor necessário que se segue. Além disso, a declaração switch / case é concisa e fácil de seguir.

#!/usr/bin/env bash

# usage function
function usage()
{
   cat << HEREDOC

   Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]

   optional arguments:
     -h, --help           show this help message and exit
     -n, --num NUM        pass in a number
     -t, --time TIME_STR  pass in a time string
     -v, --verbose        increase the verbosity of the bash script
     --dry-run            do a dry run, dont change any files

HEREDOC
}  

# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=

# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  # uncomment the next line to see how shift is working
  # echo "\$1:\"$1\" \$2:\"$2\""
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

if (( $verbose > 0 )); then

   # print out all the parameters we read in
   cat <<-EOM
   num=$num_str
   time=$time_str
   verbose=$verbose
   dryrun=$dryrun
EOM
fi

# The rest of your script below

As linhas mais significativas do script acima são as seguintes:

OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Curto, direto ao ponto, legível e lida com quase tudo (IMHO).

Espero que ajude alguém.

phyatt
fonte
1
Esta é uma das melhores respostas.
Sr. Polywhirl
11

Dou-lhe a função parse_paramsque irá analisar parâmetros a partir da linha de comando.

  1. É uma solução Bash pura, sem utilitários adicionais.
  2. Não polui o escopo global.
  3. Sem esforço, você retorna variáveis ​​simples de usar, nas quais você pode construir mais lógica.
  4. A quantidade de traços antes dos parâmetros não importa ( --alligual a -alligual all=all)

O script abaixo é uma demonstração de trabalho de copiar e colar. Consulte a show_usefunção para entender como usarparse_params .

Limitações:

  1. Não suporta params delimitados por espaço ( -d 1)
  2. Os nomes de parâmetros perderão traços --any-parame -anyparamsão equivalentes
  3. eval $(parse_params "$@")deve ser usado dentro da função bash (não funcionará no escopo global)

#!/bin/bash

# Universal Bash parameter parsing
# Parse equal sign separated params into named local variables
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Puts un-named params as-is into ${ARGV[*]} array
# Additionally puts all named params as-is into ${ARGN[*]} array
# Additionally puts all standalone "option" params as-is into ${ARGO[*]} array
# @author Oleksii Chekulaiev
# @version v1.4.1 (Jul-27-2018)
parse_params ()
{
    local existing_named
    local ARGV=() # un-named params
    local ARGN=() # named params
    local ARGO=() # options (--params)
    echo "local ARGV=(); local ARGN=(); local ARGO=();"
    while [[ "$1" != "" ]]; do
        # Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage
        _escaped=${1/\*/\'\"*\"\'}
        _escaped=${_escaped//\'/\\\'}
        _escaped=${_escaped//\"/\\\"}
        # If equals delimited named parameter
        nonspace="[^[:space:]]"
        if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then
            # Add to named parameters array
            echo "ARGN+=('$_escaped');"
            # key is part before first =
            local _key=$(echo "$1" | cut -d = -f 1)
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # val is everything after key and = (protect from param==value error)
            local _val="${1/$_key=}"
            # remove dashes from key name
            _key=${_key//\-}
            # skip when key is empty
            # search for existing parameter name
            if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
                # if name already exists then it's a multi-value named parameter
                # re-declare it as an array if needed
                if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
                    echo "$_key=(\"\$$_key\");"
                fi
                # append new value
                echo "$_key+=('$_val');"
            else
                # single-value named parameter
                echo "local $_key='$_val';"
                existing_named=" $_key"
            fi
        # If standalone named parameter
        elif [[ "$1" =~ ^\-${nonspace}+ ]]; then
            # remove dashes
            local _key=${1//\-}
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # Add to options array
            echo "ARGO+=('$_escaped');"
            echo "local $_key=\"$_key\";"
        # non-named parameter
        else
            # Escape asterisk to prevent bash asterisk expansion
            _escaped=${1/\*/\'\"*\"\'}
            echo "ARGV+=('$_escaped');"
        fi
        shift
    done
}

#--------------------------- DEMO OF THE USAGE -------------------------------

show_use ()
{
    eval $(parse_params "$@")
    # --
    echo "${ARGV[0]}" # print first unnamed param
    echo "${ARGV[1]}" # print second unnamed param
    echo "${ARGN[0]}" # print first named param
    echo "${ARG0[0]}" # print first option param (--force)
    echo "$anyparam"  # print --anyparam value
    echo "$k"         # print k=5 value
    echo "${multivalue[0]}" # print first value of multi-value
    echo "${multivalue[1]}" # print second value of multi-value
    [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
Oleksii Chekulaiev
fonte
Para usar a demonstração para analisar os parâmetros que entram no seu script bash, basta fazer o seguinteshow_use "$@"
Oleksii Chekulaiev
Basicamente, descobri que o github.com/renatosilva/easyoptions faz o mesmo da mesma maneira, mas é um pouco mais massivo que essa função.
Oleksii Chekulaiev 28/09
10

O EasyOptions não requer nenhuma análise:

## Options:
##   --verbose, -v  Verbose mode
##   --output=FILE  Output filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "output file is ${output}"
    echo "${arguments[@]}"
fi
Renato Silva
fonte
Levei um minuto para perceber que os comentários na parte superior do seu script de exemplo estão sendo analisados ​​para fornecer uma sequência de ajuda de uso padrão, bem como especificações de argumentos. Esta é uma solução brilhante e lamento que ela tenha recebido apenas 6 votos em 2 anos. Talvez essa pergunta esteja muito cheia para as pessoas perceberem.
Metamórfico
Em certo sentido, sua solução é de longe a melhor (além da @ OleksiiChekulaiev, que não suporta a sintaxe da opção "padrão"). Isso ocorre porque sua solução exige apenas que o gravador de script especifique o nome de cada opção uma vez . O fato de outras soluções exigirem que ela seja especificada três vezes - no uso, no padrão de 'caso' e na configuração da variável - sempre me incomodou. Até a getopt tem esse problema. No entanto, seu código é lento na minha máquina - 0,11s para a implementação do Bash, 0,28s para o Ruby. Versus 0.02s para análise explícita de "caso a caso".
Metamórfico
Eu quero uma versão mais rápida, talvez escrita em C. Além disso, uma versão compatível com o zsh. Talvez isso mereça uma pergunta separada ("Existe uma maneira de analisar argumentos de linha de comando em shells semelhantes ao Bash que aceitam sintaxe de opção longa padrão e não exigem que os nomes das opções sejam digitados mais de uma vez?").
Metamórfico
10

O getopts funciona muito bem se o número 1 estiver instalado e o número 2 você pretende executá-lo na mesma plataforma. OSX e Linux (por exemplo) se comportam de maneira diferente a esse respeito.

Aqui está uma solução (não getopts) que suporta sinalizadores iguais, não iguais e booleanos. Por exemplo, você pode executar seu script desta maneira:

./script --arg1=value1 --arg2 value2 --shouldClean

# parse the arguments.
COUNTER=0
ARGS=("$@")
while [ $COUNTER -lt $# ]
do
    arg=${ARGS[$COUNTER]}
    let COUNTER=COUNTER+1
    nextArg=${ARGS[$COUNTER]}

    if [[ $skipNext -eq 1 ]]; then
        echo "Skipping"
        skipNext=0
        continue
    fi

    argKey=""
    argVal=""
    if [[ "$arg" =~ ^\- ]]; then
        # if the format is: -key=value
        if [[ "$arg" =~ \= ]]; then
            argVal=$(echo "$arg" | cut -d'=' -f2)
            argKey=$(echo "$arg" | cut -d'=' -f1)
            skipNext=0

        # if the format is: -key value
        elif [[ ! "$nextArg" =~ ^\- ]]; then
            argKey="$arg"
            argVal="$nextArg"
            skipNext=1

        # if the format is: -key (a boolean flag)
        elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
            argKey="$arg"
            argVal=""
            skipNext=0
        fi
    # if the format has not flag, just a value.
    else
        argKey=""
        argVal="$arg"
        skipNext=0
    fi

    case "$argKey" in 
        --source-scmurl)
            SOURCE_URL="$argVal"
        ;;
        --dest-scmurl)
            DEST_URL="$argVal"
        ;;
        --version-num)
            VERSION_NUM="$argVal"
        ;;
        -c|--clean)
            CLEAN_BEFORE_START="1"
        ;;
        -h|--help|-help|--h)
            showUsage
            exit
        ;;
    esac
done
vangorra
fonte
8

É assim que eu faço em uma função para evitar que os getopts sejam executados ao mesmo tempo em algum lugar mais alto da pilha:

function waitForWeb () {
   local OPTIND=1 OPTARG OPTION
   local host=localhost port=8080 proto=http
   while getopts "h:p:r:" OPTION; do
      case "$OPTION" in
      h)
         host="$OPTARG"
         ;;
      p)
         port="$OPTARG"
         ;;
      r)
         proto="$OPTARG"
         ;;
      esac
   done
...
}
akostadinov
fonte
8

Expandindo a resposta de @ bruno-bronosky, adicionei um "pré-processador" para lidar com algumas formatações comuns:

  • Expande --longopt=valpara--longopt val
  • Expande -xyzpara-x -y -z
  • Apoia -- para indicar o fim das bandeiras
  • Mostra um erro para opções inesperadas
  • Comutador de opções compacto e de fácil leitura
#!/bin/bash

# Report usage
usage() {
  echo "Usage:"
  echo "$(basename $0) [options] [--] [file1, ...]"

  # Optionally exit with a status code
  if [ -n "$1" ]; then
    exit "$1"
  fi
}

invalid() {
  echo "ERROR: Unrecognized argument: $1" >&2
  usage 1
}

# Pre-process options to:
# - expand -xyz into -x -y -z
# - expand --longopt=arg into --longopt arg
ARGV=()
END_OF_OPT=
while [[ $# -gt 0 ]]; do
  arg="$1"; shift
  case "${END_OF_OPT}${arg}" in
    --) ARGV+=("$arg"); END_OF_OPT=1 ;;
    --*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;;
    --*) ARGV+=("$arg"); END_OF_OPT=1 ;;
    -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;;
    *) ARGV+=("$arg") ;;
  esac
done

# Apply pre-processed options
set -- "${ARGV[@]}"

# Parse options
END_OF_OPT=
POSITIONAL=()
while [[ $# -gt 0 ]]; do
  case "${END_OF_OPT}${1}" in
    -h|--help)      usage 0 ;;
    -p|--password)  shift; PASSWORD="$1" ;;
    -u|--username)  shift; USERNAME="$1" ;;
    -n|--name)      shift; names+=("$1") ;;
    -q|--quiet)     QUIET=1 ;;
    -C|--copy)      COPY=1 ;;
    -N|--notify)    NOTIFY=1 ;;
    --stdin)        READ_STDIN=1 ;;
    --)             END_OF_OPT=1 ;;
    -*)             invalid "$1" ;;
    *)              POSITIONAL+=("$1") ;;
  esac
  shift
done

# Restore positional parameters
set -- "${POSITIONAL[@]}"
jchook
fonte
6

Existem várias maneiras de analisar args do cmdline (por exemplo, GNU getopt (não portátil) vs BSD (OSX) getopt vs getopts) - todos problemáticos. Esta solução é

  • portátil!
  • tem zero dependências, depende apenas de bash built-ins
  • permite opções curtas e longas
  • lida com espaço em branco entre opção e argumento, mas também pode usar =separador
  • suporta estilo de opção curta concatenado -vxf
  • lida com a opção com argumentos opcionais (veja o exemplo) e
  • não requer inchaço do código em comparação com alternativas para o mesmo conjunto de recursos. Ou seja, sucinta e, portanto, mais fácil de manter

Exemplos: Qualquer um

# flag
-f
--foo

# option with required argument
-b"Hello World"
-b "Hello World"
--bar "Hello World"
--bar="Hello World"

# option with optional argument
--baz
--baz="Optional Hello"

#!/usr/bin/env bash

usage() {
  cat - >&2 <<EOF
NAME
    program-name.sh - Brief description

SYNOPSIS
    program-name.sh [-h|--help]
    program-name.sh [-f|--foo]
                    [-b|--bar <arg>]
                    [--baz[=<arg>]]
                    [--]
                    FILE ...

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option

  -b, --bar <arg>
          Option requiring an argument <arg>

  --baz[=<arg>]
          Option that has an optional argument <arg>. If <arg>
          is not specified, defaults to 'DEFAULT'
  --     
          Specify end of options; useful if the first non option
          argument starts with a hyphen

EOF
}

fatal() {
    for i; do
        echo -e "${i}" >&2
    done
    exit 1
}

# For long option processing
next_arg() {
    if [[ $OPTARG == *=* ]]; then
        # for cases like '--opt=arg'
        OPTARG="${OPTARG#*=}"
    else
        # for cases like '--opt arg'
        OPTARG="${args[$OPTIND]}"
        OPTIND=$((OPTIND + 1))
    fi
}

# ':' means preceding option character expects one argument, except
# first ':' which make getopts run in silent mode. We handle errors with
# wildcard case catch. Long options are considered as the '-' character
optspec=":hfb:-:"
args=("" "$@")  # dummy first element so $1 and $args[1] are aligned
while getopts "$optspec" optchar; do
    case "$optchar" in
        h) usage; exit 0 ;;
        f) foo=1 ;;
        b) bar="$OPTARG" ;;
        -) # long option processing
            case "$OPTARG" in
                help)
                    usage; exit 0 ;;
                foo)
                    foo=1 ;;
                bar|bar=*) next_arg
                    bar="$OPTARG" ;;
                baz)
                    baz=DEFAULT ;;
                baz=*) next_arg
                    baz="$OPTARG" ;;
                -) break ;;
                *) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;;
            esac
            ;;
        *) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;;
    esac
done

shift $((OPTIND-1))

if [ "$#" -eq 0 ]; then
    fatal "Expected at least one required argument FILE" \
    "See '${0} --help' for usage"
fi

echo "foo=$foo, bar=$bar, baz=$baz, files=${@}"
tmoschou
fonte
5

Gostaria de oferecer minha versão da análise de opções, que permite o seguinte:

-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello

Também permite isso (pode ser indesejado):

-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder

Você precisa decidir antes de usar se = deve ser usado em uma opção ou não. Isso é para manter o código limpo (ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done
galmok
fonte
1
qual é o significado para "+ x" em $ {key + x}?
Luca Davanzo 14/11
1
É um teste para ver se 'chave' está presente ou não. Mais abaixo, desmarco a tecla e isso quebra o loop while interno.
galmok
5

Solução que preserva argumentos não tratados. Demonstrações incluídas.

Aqui está a minha solução. É MUITO flexível e diferente de outros, não deve exigir pacotes externos e manipula os argumentos restantes de maneira limpa.

O uso é: ./myscript -flag flagvariable -otherflag flagvar2

Tudo o que você precisa fazer é editar a linha validflags. Anexa um hífen e pesquisa todos os argumentos. Em seguida, define o próximo argumento como o nome da bandeira, por exemplo

./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2

O código principal (versão curta, detalhada com exemplos mais abaixo, também uma versão com erro):

#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
    for flag in $validflags
    do
        sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers

A versão detalhada com demos de eco integrados:

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
$@"
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
#   argval=$(echo $@ | cut -d ' ' -f$count)
    for flag in $validflags
    do
            sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done

#Cleanup then restore the leftovers
echo "pre final clear args:
$@"
shift $#
echo "post final clear args:
$@"
set -- $leftovers
echo "all post set args:
$@"
echo arg1: $1 arg2: $2

echo leftovers: $leftovers
echo rate $rate time $time number $number

Final, este erro ocorre se um argumento inválido for passado.

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in $@
do
    argval=$1
    match=0
        if [ "${argval:0:1}" == "-" ]
    then
        for flag in $validflags
        do
                sflag="-"$flag
            if [ "$argval" == "$sflag" ]
            then
                declare $flag=$2
                match=1
            fi
        done
        if [ "$match" == "0" ]
        then
            echo "Bad argument: $argval"
            exit 1
        fi
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers

Prós: O que faz, lida muito bem. Ele preserva argumentos não utilizados que muitas das outras soluções aqui não. Também permite que variáveis ​​sejam chamadas sem serem definidas manualmente no script. Também permite a pré-população de variáveis ​​se nenhum argumento correspondente for fornecido. (Veja exemplo detalhado).

Contras: Não é possível analisar uma única cadeia complexa de arg, por exemplo, -xcvf seria processada como um único argumento. Você poderia escrever com facilidade um código adicional no meu que adiciona essa funcionalidade.


fonte
3

Observe que getopt(1) foi um erro de curta duração da AT&T.

O getopt foi criado em 1984, mas já foi enterrado em 1986 porque não era realmente utilizável.

Uma prova do fato de getoptestar muito desatualizado é que a getopt(1)página de manual ainda menciona, em "$*"vez de "$@", que foi adicionada ao Bourne Shell em 1986 junto com ogetopts(1) shell interno para lidar com argumentos com espaços internos.

BTW: se você estiver interessado em analisar opções longas em scripts de shell, pode ser interessante saber que a getopt(3)implementação da libc (Solaris) e ksh93ambas adicionaram uma implementação uniforme de opções longas que suporta opções longas como aliases para opções curtas. Isso faz com que ksh93ea Bourne Shellimplementar uma interface uniforme para opções longas via getopts.

Um exemplo de opções longas retiradas da página do manual Bourne Shell:

getopts "f:(file)(input-file)o:(output-file)" OPTX "$@"

mostra por quanto tempo os aliases de opções podem ser usados ​​no Bourne Shell e no ksh93.

Veja a página de manual de um Bourne Shell recente:

http://schillix.sourceforge.net/man/man1/bosh.1.html

e a página de manual para getopt (3) do OpenSolaris:

http://schillix.sourceforge.net/man/man3c/getopt.3c.html

e, por último, a página do manual getopt (1) para verificar o $ * desatualizado:

http://schillix.sourceforge.net/man/man1/getopt.1.html

esperto
fonte
3

Eu escrevi um auxiliar de bash para escrever uma boa ferramenta de bash

página inicial do projeto: https://gitlab.mbedsys.org/mbedsys/bashopts

exemplo:

#!/bin/bash -ei

# load the library
. bashopts.sh

# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR

# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"

# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args

vai dar ajuda:

NAME:
    ./example.sh - This is myapp tool description displayed on help message

USAGE:
    [options and commands] [-- [extra args]]

OPTIONS:
    -h,--help                          Display this help
    -n,--non-interactive true          Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -f,--first "John"                  First name - [$first_name] (type:string, default:"")
    -l,--last "Smith"                  Last name - [$last_name] (type:string, default:"")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"$first_name $last_name")
    --number 0                         Age - [$age] (type:number, default:0)
    --email                            Email adress - [$email_list] (type:string, default:"")

aproveitar :)

Emeric Verschuur
fonte
Eu recebo isso no Mac OS X: `` `lib / bashopts.sh: linha 138: declare: -A: opção inválida declare: use: declare [-afFirtx] [-p] [name [= value] ...] Erro no lib / bashopts.sh: 138. 'declare -x -A bashopts_optprop_name' encerrado com status 2 Árvore de chamadas: 1: lib / controller.sh: 4 source (...) Saindo com status 1 `` ``
Josh Wulf
Você precisa do Bash versão 4 para usar isso. No Mac, a versão padrão é 3. Você pode usar cerveja em casa para instalar o bash 4.
Josh Wulf
3

Aqui está a minha abordagem - usando o regexp.

  • sem getopts
  • ele lida com bloco de parâmetros curtos -qwerty
  • ele lida com parâmetros curtos -q -w -e
  • ele lida com opções longas --qwerty
  • você pode passar o atributo para a opção curta ou longa (se estiver usando um bloco de opções curtas, o atributo será anexado à última opção)
  • você pode usar espaços ou =fornecer atributos, mas as correspondências de atributos até encontrar o hífen + espaço "delimitador", portanto, em--q=qwe ty qwe ty existe um atributo
  • ele lida com a mistura de tudo acima, então -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributeé válido

roteiro:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

  ${0##*/} [-h][-l FILENAME][-d]

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

parse_options() {
  case $opt in
    h|help)
      help_menu
      exit
     ;;
    l|logfile)
      logfile=${attr}
      ;;
    d|debug)
      debug=true
      ;;
    *)
      echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
      exit 1
  esac
}
options=$@

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done
a_z
fonte
Como este. Talvez apenas adicione -e param para ecoar com a nova linha.
precisa saber é
3

Suponha que criamos um script de shell chamado a test_args.shseguir

#!/bin/sh
until [ $# -eq 0 ]
do
  name=${1:1}; shift;
  if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi  
done
echo "year=$year month=$month day=$day flag=$flag"

Depois de executarmos o seguinte comando:

sh test_args.sh  -year 2017 -flag  -month 12 -day 22 

A saída seria:

year=2017 month=12 day=22 flag=true
John
fonte
5
Isso segue a mesma abordagem da resposta de Noah , mas tem menos verificações / salvaguardas de segurança. Isso nos permite escrever argumentos arbitrários no ambiente do script e tenho certeza de que o uso de eval aqui pode permitir a injeção de comandos.
quer
2

Use o módulo "argumentos" do bash-modules

Exemplo:

#!/bin/bash
. import.sh log arguments

NAME="world"

parse_arguments "-n|--name)NAME;S" -- "$@" || {
  error "Cannot parse command line."
  exit 1
}

info "Hello, $NAME!"
Volodymyr M. Lisivka
fonte
2

Misturando argumentos posicionais e baseados em sinalizadores

--param = arg (igual a delimitado)

Misturando livremente sinalizadores entre argumentos posicionais:

./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d

pode ser realizado com uma abordagem bastante concisa:

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   param=${!pointer}
   if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      case $param in
         # paramter-flags with arguments
         -e=*|--environment=*) environment="${param#*=}";;
                  --another=*) another="${param#*=}";;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \
         || set -- ${@:((pointer + 1)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

--param arg (delimitado por espaço)

É geralmente mais claro não misturar --flag=valuee --flag valueestilos.

./script.sh dumbo 127.0.0.1 --environment production -q -d

É um pouco arriscado de ler, mas ainda é válido

./script.sh dumbo --environment production 127.0.0.1 --quiet -d

Fonte

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      param=${!pointer}
      ((pointer_plus = pointer + 1))
      slice_len=1

      case $param in
         # paramter-flags with arguments
         -e|--environment) environment=${!pointer_plus}; ((slice_len++));;
                --another) another=${!pointer_plus}; ((slice_len++));;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \
         || set -- ${@:((pointer + $slice_len)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2
Mark Fox
fonte
2

Aqui está um getopts que obtém a análise com código mínimo e permite definir o que você deseja extrair em um caso usando eval com substring.

Basicamente eval "local key='val'"

function myrsync() {

        local backup=("${@}") args=(); while [[ $# -gt 0 ]]; do k="$1";
                case "$k" in
                    ---sourceuser|---sourceurl|---targetuser|---targeturl|---file|---exclude|---include)
                        eval "local ${k:3}='${2}'"; shift; shift    # Past two arguments
                    ;;
                    *)  # Unknown option  
                        args+=("$1"); shift;                        # Past argument only
                    ;;                                              
                esac                                                
        done; set -- "${backup[@]}"                                 # Restore $@


        echo "${sourceurl}"
}

Declara as variáveis ​​como locais em vez de globais como a maioria das respostas aqui.

Chamado como:

myrsync ---sourceurl http://abc.def.g ---sourceuser myuser ... 

O $ {k: 3} é basicamente uma substring para remover o primeiro ---da chave.

mmm
fonte
1

Também pode ser útil saber, você pode definir um valor e, se alguém fornecer entrada, substitua o padrão por esse valor.

myscript.sh -f ./serverlist.txt ou apenas ./myscript.sh (e assume os padrões)

    #!/bin/bash
    # --- set the value, if there is inputs, override the defaults.

    HOME_FOLDER="${HOME}/owned_id_checker"
    SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt"

    while [[ $# > 1 ]]
    do
    key="$1"
    shift

    case $key in
        -i|--inputlist)
        SERVER_FILE_LIST="$1"
        shift
        ;;
    esac
    done


    echo "SERVER LIST   = ${SERVER_FILE_LIST}"
Mike Q
fonte
1

Outra solução sem getopt [s], POSIX, antigo estilo Unix

Semelhante à solução que Bruno Bronosky publicou aqui é uma sem o uso de getopt(s).

O principal diferencial da minha solução é que ela permite concatenar opções da mesma forma que tar -xzf foo.tar.gzé igual a tar -x -z -f foo.tar.gz. E, assim como em etc. tar, pso hífen principal é opcional para um bloco de opções curtas (mas isso pode ser alterado facilmente). Opções longas também são suportadas (mas quando um bloco começa com um, são necessários dois hífens principais).

Código com opções de exemplo

#!/bin/sh

echo
echo "POSIX-compliant getopt(s)-free old-style-supporting option parser from phk@[se.unix]"
echo

print_usage() {
  echo "Usage:

  $0 {a|b|c} [ARG...]

Options:

  --aaa-0-args
  -a
    Option without arguments.

  --bbb-1-args ARG
  -b ARG
    Option with one argument.

  --ccc-2-args ARG1 ARG2
  -c ARG1 ARG2
    Option with two arguments.

" >&2
}

if [ $# -le 0 ]; then
  print_usage
  exit 1
fi

opt=
while :; do

  if [ $# -le 0 ]; then

    # no parameters remaining -> end option parsing
    break

  elif [ ! "$opt" ]; then

    # we are at the beginning of a fresh block
    # remove optional leading hyphen and strip trailing whitespaces
    opt=$(echo "$1" | sed 's/^-\?\([a-zA-Z0-9\?-]*\)/\1/')

  fi

  # get the first character -> check whether long option
  first_chr=$(echo "$opt" | awk '{print substr($1, 1, 1)}')
  [ "$first_chr" = - ] && long_option=T || long_option=F

  # note to write the options here with a leading hyphen less
  # also do not forget to end short options with a star
  case $opt in

    -)

      # end of options
      shift
      break
      ;;

    a*|-aaa-0-args)

      echo "Option AAA activated!"
      ;;

    b*|-bbb-1-args)

      if [ "$2" ]; then
        echo "Option BBB with argument '$2' activated!"
        shift
      else
        echo "BBB parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    c*|-ccc-2-args)

      if [ "$2" ] && [ "$3" ]; then
        echo "Option CCC with arguments '$2' and '$3' activated!"
        shift 2
      else
        echo "CCC parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    h*|\?*|-help)

      print_usage
      exit 0
      ;;

    *)

      if [ "$long_option" = T ]; then
        opt=$(echo "$opt" | awk '{print substr($1, 2)}')
      else
        opt=$first_chr
      fi
      printf 'Error: Unknown option: "%s"\n' "$opt" >&2
      print_usage
      exit 1
      ;;

  esac

  if [ "$long_option" = T ]; then

    # if we had a long option then we are going to get a new block next
    shift
    opt=

  else

    # if we had a short option then just move to the next character
    opt=$(echo "$opt" | awk '{print substr($1, 2)}')

    # if block is now empty then shift to the next one
    [ "$opt" ] || shift

  fi

done

echo "Doing something..."

exit 0

Para o exemplo de uso, consulte os exemplos abaixo.

Posição das opções com argumentos

Pelo que vale a pena, as opções com argumentos não são as últimas (somente as opções longas precisam ser). Portanto, enquanto que, por exemplo, em tar(pelo menos em algumas implementações) as fopções precisam ser as últimas, porque o nome do arquivo segue ( tar xzf bar.tar.gzfunciona, mas tar xfz bar.tar.gznão funciona ), esse não é o caso aqui (veja os exemplos posteriores).

Várias opções com argumentos

Como outro bônus, os parâmetros das opções são consumidos na ordem das opções pelos parâmetros com as opções necessárias. Basta olhar para a saída do meu script aqui com a linha de comando abc X Y Z(ou -abc X Y Z):

Option AAA activated!
Option BBB with argument 'X' activated!
Option CCC with arguments 'Y' and 'Z' activated!

Opções longas concatenadas também

Também é possível ter opções longas no bloco de opções, uma vez que elas ocorrem pela última vez no bloco. Portanto, as seguintes linhas de comando são todas equivalentes (incluindo a ordem em que as opções e seus argumentos estão sendo processados):

  • -cba Z Y X
  • cba Z Y X
  • -cb-aaa-0-args Z Y X
  • -c-bbb-1-args Z Y X -a
  • --ccc-2-args Z Y -ba X
  • c Z Y b X a
  • -c Z Y -b X -a
  • --ccc-2-args Z Y --bbb-1-args X --aaa-0-args

Tudo isso leva a:

Option CCC with arguments 'Z' and 'Y' activated!
Option BBB with argument 'X' activated!
Option AAA activated!
Doing something...

Não nesta solução

Argumentos opcionais

Opções com argumentos opcionais devem ser possíveis com um pouco de trabalho, por exemplo, observando se existe um bloco sem hífen; o usuário precisaria colocar um hífen na frente de cada bloco após um bloco com um parâmetro com um parâmetro opcional. Talvez isso seja muito complicado para se comunicar com o usuário; portanto, é necessário apenas um hífen principal neste caso.

As coisas ficam ainda mais complicadas com vários parâmetros possíveis. Eu desaconselharia a fazer as opções tentarem ser inteligentes, determinando se o argumento pode ser a favor ou não (por exemplo, com uma opção aceita apenas um número como argumento opcional), pois isso pode ser interrompido no futuro.

Pessoalmente, sou a favor de opções adicionais em vez de argumentos opcionais.

Argumentos de opção introduzidos com um sinal de igual

Assim como nos argumentos opcionais, eu não sou fã disso (BTW, existe um tópico para discutir os prós / contras de diferentes estilos de parâmetros?), Mas se você quiser isso, provavelmente poderá implementá-lo por conta própria, como feito em http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop com um--long-with-arg=?* declaração de caso e, em seguida, retira o sinal de igual (este é o site que diz que fazer concatenação de parâmetros é possível com algum esforço, mas "o deixou como um exercício para o leitor "o que me fez acreditar na palavra deles, mas comecei do zero).

Outras notas

Compatível com POSIX, funciona mesmo em configurações antigas do Busybox com as quais tive que lidar (com cut, por exemplo , heade getoptsausente).

phk
fonte