Usando getopts para processar opções de linha de comando longas e curtas

410

Desejo que formas longas e curtas de opções de linha de comando sejam chamadas usando meu script de shell.

Eu sei que isso getoptspode ser usado, mas, como no Perl, não fui capaz de fazer o mesmo com o shell.

Alguma idéia de como isso pode ser feito, para que eu possa usar opções como:

./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/

Acima, os dois comandos significam a mesma coisa para o meu shell, mas getopts, usando , não fui capaz de implementá-los?

gagneet
fonte
2
IMHO, a resposta aceita não é a melhor. Ele não mostra como usar getopts para lidar com os argumentos "-" e "-", o que pode ser feito, como o @Arvid Requate demonstrou. Estou inserindo outra resposta usando conceito semelhante, mas também lida com o erro do usuário de "esquecer" para inserir valores para argumentos necessários. Ponto-chave: getopts podem ser feitos para funcionar. O usuário deve evitar o uso de "getopt" se for necessário portabilidade entre plataformas. Além disso, o getopts faz parte do padrão POSIX para shells, portanto é provável que seja portátil.
pauljohn32

Respostas:

304

Há três implementações que podem ser consideradas:

  • Bash builtin getopts. Isso não suporta nomes longos de opções com o prefixo de traço duplo. Ele suporta apenas opções de um caractere.

  • Implementação do BSD UNIX do getoptcomando independente (que é o que o MacOS usa). Isso também não oferece opções longas.

  • Implementação GNU de autônomo getopt. O GNU getopt(3)(usado pela linha de comando getopt(1)no Linux) suporta a análise de opções longas.


Algumas outras respostas mostram uma solução para usar o bash embutido getoptspara imitar opções longas. Essa solução, na verdade, faz uma pequena opção cujo caractere é "-". Então você recebe "-" como a bandeira. Em seguida, qualquer coisa a seguir que se torne OPTARG e você teste o OPTARG com um aninhadocase .

Isso é inteligente, mas vem com advertências:

  • getoptsnão pode aplicar a especificação de opção. Não pode retornar erros se o usuário fornecer uma opção inválida. Você precisa fazer sua própria verificação de erro ao analisar o OPTARG.
  • OPTARG é usado para o nome da opção longa, o que complica o uso quando sua própria opção longa possui um argumento. Você acaba tendo que codificar isso como um caso adicional.

Portanto, embora seja possível escrever mais código para solucionar a falta de suporte para opções longas, isso é muito mais trabalhoso e derrota parcialmente o propósito de usar um analisador getopt para simplificar seu código.

Bill Karwin
fonte
18
Assim. Qual é a solução portátil multiplataforma?
troelskn
6
O GNU Getopt parece ser a única opção. No Mac, instale o GNU getopt a partir de macports. No Windows, eu instalaria o GNU getopt com Cygwin.
Bill Karwin 18/10/09
2
Aparentemente , o ksh getopts pode lidar com opções longas.
Tgr 23/07/10
1
@ Bill +1, embora também seja bastante simples criar getopt a partir da fonte ( software.frodo.looijaard.name/getopt ) no Mac. Você também pode verificar a versão do getopt instalado no seu sistema a partir de scripts com "getopt -T; echo $?".
Chinasaur 30/08/11
8
@ Bill Karwin: "O bash getopts embutido não suporta nomes de opções longos com o prefixo de traço duplo." Mas é possível obter getopts para oferecer suporte a opções longas: consulte stackoverflow.com/a/7680682/915044 abaixo.
TomRoche
307

getopte getoptssão bestas diferentes, e as pessoas parecem ter um pouco de mal-entendido do que fazem. getoptsé um comando interno para bashprocessar opções de linha de comando em um loop e atribuir cada opção e valor encontrado, por sua vez, a variáveis ​​internas, para que você possa processá-las ainda mais. getopt, no entanto, é um programa utilitário externo e, na verdade, não processa suas opções para você da maneira que, por exemplo getopts, o bash , o Getoptmódulo Perl ou o Python optparse/ argparsemodules. Tudo o que getoptfaz é canonizar as opções passadas - ou seja, convertê-las para um formato mais padrão, para que seja mais fácil para um script de shell processá-las. Por exemplo, um aplicativo de getoptpode converter o seguinte:

myscript -ab infile.txt -ooutfile.txt

nisso:

myscript -a -b -o outfile.txt infile.txt

Você precisa fazer o processamento real sozinho. Você não precisa usargetopt -lo se fizer várias restrições na maneira de especificar opções:

  • coloque apenas uma opção por argumento;
  • todas as opções vão antes de qualquer parâmetro posicional (isto é, argumentos sem opção);
  • para opções com valores (por exemplo, -oacima), o valor deve ser apresentado como um argumento separado (após um espaço).

Por que usar em getoptvez de getopts? O motivo básico é que apenas o GNU getoptoferece suporte para opções de linha de comando com nomes longos. 1 (GNU getopté o padrão no Linux. O Mac OS X e o FreeBSD vêm com uma funcionalidade básica e não muito útilgetopt , mas a versão do GNU pode ser instalada; veja abaixo.)

Por exemplo, aqui está um exemplo de uso do GNU getopt, de um script meu chamado javawrap:

# NOTE: This requires GNU getopt.  On Mac OS X and FreeBSD, you have to install this
# separately; see below.
TEMP=`getopt -o vdm: --long verbose,debug,memory:,debugfile:,minheap:,maxheap: \
             -n 'javawrap' -- "$@"`

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

# Note the quotes around `$TEMP': they are essential!
eval set -- "$TEMP"

VERBOSE=false
DEBUG=false
MEMORY=
DEBUGFILE=
JAVA_MISC_OPT=
while true; do
  case "$1" in
    -v | --verbose ) VERBOSE=true; shift ;;
    -d | --debug ) DEBUG=true; shift ;;
    -m | --memory ) MEMORY="$2"; shift 2 ;;
    --debugfile ) DEBUGFILE="$2"; shift 2 ;;
    --minheap )
      JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MinHeapFreeRatio=$2"; shift 2 ;;
    --maxheap )
      JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MaxHeapFreeRatio=$2"; shift 2 ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Isso permite especificar opções como --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt"ou semelhante. O efeito da chamada para getopté canonizar as opções para --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt"que você possa processá-las mais facilmente. A citação ao redor"$1" e "$2"é importante, pois garante que os argumentos com espaços sejam tratados adequadamente.

Se você excluir as 9 primeiras linhas (tudo na eval setlinha), o código ainda funcionará ! No entanto, seu código será muito mais exigente em quais tipos de opções ele aceita: Em particular, você precisará especificar todas as opções no formulário "canônico" descrito acima. Com o uso de getopt, no entanto, você pode opções único grupo letras, usar mais curtos formas não ambíguas de longas-opções, utilize o --file foo.txtou --file=foo.txtestilo, utilize o -m 4096ou -m4096estilo, misturar opções e não-opções em qualquer ordem, etc. getopt também gera uma mensagem de erro se forem encontradas opções não reconhecidas ou ambíguas.

NOTA : Na verdade, existem duas versões totalmente diferentesgetopt , básica getopte GNU getopt, com recursos diferentes e convenções de chamada diferentes. 2 O Basic getoptestá bastante quebrado: ele não apenas lida com opções longas, como também não consegue lidar com espaços incorporados dentro de argumentos ou argumentos vazios, enquanto getoptsfaz isso corretamente. O código acima não funcionará no básico getopt. O GNU getopté instalado por padrão no Linux, mas no Mac OS X e FreeBSD ele precisa ser instalado separadamente. No Mac OS X, instale o MacPorts ( http://www.macports.org ) e, em seguida, sudo port install getoptinstale o GNU getopt(geralmente no . No FreeBSD, instale ./opt/local/bin ), e verifique se /opt/local/binestá no caminho do shell antes de/usr/binmisc/getopt

Um guia rápido para modificar o código de exemplo para o seu próprio programa: Das primeiras linhas, tudo é "padrão" que deve permanecer o mesmo, exceto a linha que chama getopt. Você deve alterar o nome do programa depois -n, especificar opções curtas depois -oe opções longas depois --long. Coloque dois pontos depois das opções que levam um valor.

Finalmente, se você vir um código que acabou de ser setsubstituído eval set, ele foi escrito para o BSD getopt. Você deve alterá-lo para usar o eval setestilo, que funciona bem com as duas versões getopt, enquanto o plain setnão funciona corretamente com o GNU getopt.

1 Na verdade, getoptsin ksh93suporta opções de nomes longos, mas esse shell não é usado com tanta frequência quanto bash. Em zsh, use zparseoptspara obter essa funcionalidade.

2 Tecnicamente, "GNU getopt" é um nome impróprio; esta versão foi realmente escrita para Linux e não para o projeto GNU. No entanto, segue todas as convenções do GNU, e o termo "GNU getopt" é comumente usado (por exemplo, no FreeBSD).

Vagabundo Urbano
fonte
3
Isso foi muito útil, a idéia de usar o getopt para verificar as opções e depois processá-las em um loop muito simples funcionou muito bem quando eu quis adicionar opções de estilo longas a um script bash. Obrigado.
Ianmjones
2
getoptno Linux não é um utilitário GNU e o tradicional getoptnão vem inicialmente do BSD, mas do AT&T Unix. O ksh93 getopts(também da AT&T) suporta opções longas no estilo GNU.
Stephane Chazelas
@StephaneChazelas - editado para refletir seus comentários. Eu ainda prefiro o termo "GNU getopt", mesmo que seja um nome impróprio, porque esta versão segue as convenções GNU e geralmente age como um programa GNU (por exemplo, fazendo uso de POSIXLY_CORRECT), enquanto "getopt aprimorado pelo Linux" sugere erroneamente que esta versão existe apenas em Linux.
Vagabond Urban
1
Ele vem do pacote util-linux, portanto, é apenas Linux, pois esse pacote de software é destinado apenas ao Linux (que getoptpode ser facilmente transportado para outros Unices, mas muitos outros softwares util-linuxsão específicos do Linux). Todos os programas não-GNU que usam o GNU getopt (3) entendem $POSIX_CORRECT. Por exemplo, você não diria que aplayé o GNU apenas por esses motivos. Eu suspeito que quando o FreeBSD menciona o GNU getopt, eles significam a API C do GNU getopt (3).
Stephane Chazelas
@StephaneChazelas - O FreeBSD tem uma mensagem de erro "Construir dependência: instale o GNU getopt" que se refere inequivocamente ao getoptutilitário, não getopt (3).
amigos estão dizendo sobre urban vagabond
202

A função getopts interna do Bash pode ser usada para analisar opções longas, colocando um caractere de traço seguido de dois pontos no optspec:

#!/usr/bin/env bash 
optspec=":hv-:"
while getopts "$optspec" optchar; do
    case "${optchar}" in
        -)
            case "${OPTARG}" in
                loglevel)
                    val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2;
                    ;;
                loglevel=*)
                    val=${OPTARG#*=}
                    opt=${OPTARG%=$val}
                    echo "Parsing option: '--${opt}', value: '${val}'" >&2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h)
            echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2
            exit 2
            ;;
        v)
            echo "Parsing option: '-${optchar}'" >&2
            ;;
        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done

Após copiar para o nome do arquivo executável = getopts_test.shno diretório de trabalho atual , é possível produzir uma saída como

$ ./getopts_test.sh
$ ./getopts_test.sh -f
Non-option argument: '-f'
$ ./getopts_test.sh -h
usage: code/getopts_test.sh [-v] [--loglevel[=]<value>]
$ ./getopts_test.sh --help
$ ./getopts_test.sh -v
Parsing option: '-v'
$ ./getopts_test.sh --very-bad
$ ./getopts_test.sh --loglevel
Parsing option: '--loglevel', value: ''
$ ./getopts_test.sh --loglevel 11
Parsing option: '--loglevel', value: '11'
$ ./getopts_test.sh --loglevel=11
Parsing option: '--loglevel', value: '11'

Obviamente, getopts não realiza OPTERRverificação nem análise de argumento de opção para as opções longas. O fragmento de script acima mostra como isso pode ser feito manualmente. O princípio básico também funciona no shell Debian Almquist ("traço"). Observe o caso especial:

getopts -- "-:"  ## without the option terminator "-- " bash complains about "-:"
getopts "-:"     ## this works in the Debian Almquist shell ("dash")

Observe que, como aponta o GreyCat em http://mywiki.wooledge.org/BashFAQ , esse truque explora um comportamento não-padrão do shell que permite o argumento da opção (ou seja, o nome do arquivo em "-f filename") para ser concatenado com a opção (como em "-ffilename"). O padrão POSIX diz que deve haver um espaço entre eles, que no caso de "- longoption" encerraria a análise de opções e transformaria todas as longoptions em argumentos que não são de opções.

Arvid Requate
fonte
2
Uma pergunta: qual é a semântica de !in val="${!OPTIND}?
TomRoche
2
@TomRoche é Substituição Indireta: unix.stackexchange.com/a/41293/84316
ecbrodie
2
@ ecbrodie: É porque dois argumentos foram realmente processados, em oposição a apenas um. O primeiro argumento é a palavra "loglevel" e o próximo é o argumento desse argumento. Enquanto isso, getoptsautomaticamente apenas incrementa OPTINDcom 1, mas, no nosso caso, precisamos incrementar em 2; portanto, incrementamos manualmente em 1 e, em seguida, getoptsaumentamos em 1 novamente para nós automaticamente.
Victor Zamanian
3
Como estamos no equilíbrio da festa aqui: nomes de variáveis ​​nuas são permitidos dentro de expressões aritméticas, não $necessárias. OPTIND=$(( $OPTIND + 1 ))pode ser justo OPTIND=$(( OPTIND + 1 )). Ainda mais interessante, você pode até atribuir e aumentar variáveis ​​dentro de uma expressão aritmética, para que seja possível abreviá-la ainda mais : $(( ++OPTIND )), ou mesmo (( ++OPTIND ))levar em conta que ++OPTINDsempre será positivo, para que não desarme um shell executado com o-e opção :-) gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html
clacke
3
Por que não --very-bad dá um aviso?
Tom Hale
148

O getoptscomando interno ainda é, AFAIK, limitado apenas às opções de caractere único.

Existe (ou costumava ser) um programa externo getoptque reorganizava um conjunto de opções para facilitar a análise. Você pode adaptar esse design para lidar com opções longas também. Exemplo de uso:

aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$@")
while [ $# -gt 0 ]
do
    case "$1" in
    (-a) aflag=yes;;
    (-b) bflag=yes;;
    (-f) flist="$flist $2"; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*)  break;;
    esac
    shift
done

# Process remaining non-option arguments
...

Você pode usar um esquema semelhante com um getoptlongcomando.

Observe que a fraqueza fundamental do getoptprograma externo é a dificuldade de lidar com argumentos com espaços neles e em preservar esses espaços com precisão. É por isso que o built-in getoptsé superior, embora limitado pelo fato de lidar apenas com opções de letra única.

Jonathan Leffler
fonte
11
O getopt, exceto a versão GNU (que possui uma convenção de chamada diferente), é fundamentalmente quebrado. Não use isso. Em vez disso, use ** getopts bash-hackers.org/wiki/doku.php/howto/getopts_tutorial
hendry
9
@hendry - a partir do seu próprio link: "Observe que o getopts não é capaz de analisar opções longas no estilo GNU (--myoption) ou opções longas no estilo XF86 (-myoption)!"
Tom Auger
1
Jonathan - você deve reescrever o exemplo para usar eval setentre aspas (veja minha resposta abaixo) para que ele também funcione corretamente com o GNU getopt (o padrão no Linux) e lide com os espaços corretamente.
Urban Vagabond
@UrbanVagabond: Não sei por que devo fazer isso. A pergunta está marcada com o Unix, não o Linux. Estou mostrando o mecanismo tradicional, deliberadamente, e há problemas com espaços em branco nos argumentos, etc. Você pode demonstrar a versão moderna específica do Linux, se desejar, e sua resposta faz isso. (Observo, passim, que seu uso ${1+"$@"}é singular e está em desacordo com o que é necessário nos shells modernos e especificamente com qualquer shell que você encontraria no Linux. Consulte Usando $ 1: + "$ @"} em / bin / sh para obter uma discussão dessa notação.)
Jonathan Leffler
Você deve fazê-lo porque eval setfaz a coisa certa com o GNU e o BSD getopt, enquanto o simples setapenas faz a coisa certa com o BSD getopt. Portanto, você também pode eval setincentivar as pessoas a adquirir o hábito de fazer isso. Entre, obrigado, eu não sabia que ${1+"$@"}não era mais necessário. Preciso escrever coisas que funcionem tanto no Mac OS X quanto no Linux - entre as duas, elas forçam muita portabilidade. Acabei de verificar e "$@", de fato, fazer a coisa certa em todos sh, bash, ksh, e zshno Mac OS X; certamente no Linux também.
Urban Vagabond
78

Aqui está um exemplo que realmente usa getopt com opções longas:

aflag=no
bflag=no
cargument=none

# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc: -l along,blong,clong: -- "$@")
then
    # something went wrong, getopt will put out an error message for us
    exit 1
fi

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag="yes" ;;
    -b|--blong) bflag="yes" ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done
sme
fonte
1
Você deve reescrever o exemplo para usar eval setentre aspas (veja minha resposta abaixo) para que ele também funcione corretamente com o GNU getopt (o padrão no Linux) e lide com os espaços corretamente.
Urban Vagabond
2
Isso está sendo usado getoptenquanto a pergunta é sobre o assunto getopts.
Niklas Berglund
1
São (--, (-*e (*padrões válidos? Como eles são diferentes de --, -*e *?
Marëlan
1
@ Maëlan - O parêntese aberto principal é opcional, portanto (--)é idêntico a --)uma caseestrofe. É estranho ver a indentação desigual e o uso inconsistente desse parênteses principal opcional, mas o código atual da resposta parece válido para mim.
23819 Adam
59

Opções longas podem ser analisadas pelo padrão getoptsincorporado como "argumentos" para a -"opção"

Este é um shell POSIX portátil e nativo - nenhum programa ou bashisms externo é necessário.

Este guia implementa opções longas como argumentos para a -opção, portanto, --alphaé visto getoptscomo -com argumento alphae --bravo=foocomo -argumento bravo=foo. O verdadeiro argumento pode ser colhida com uma substituição simples: ${OPTARG#*=}.

Neste exemplo, -be -c(e suas formas longas, --bravoe --charlie) têm argumentos obrigatórios. Os argumentos para opções longas vêm após sinais de igual, por exemplo --bravo=foo(delimitadores de espaço para opções longas seria difícil de implementar, veja abaixo).

Como isso usa o getoptsbuilt-in , essa solução suporta o uso de como cmd --bravo=foo -ac FILE(que combinou opções -ae -cintercala opções longas com opções padrão), enquanto a maioria das outras respostas aqui se esforçam ou deixam de fazer isso.

die() { echo "$*" >&2; exit 2; }  # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }

while getopts ab:c:-: OPT; do
  # support long options: https://stackoverflow.com/a/28466267/519360
  if [ "$OPT" = "-" ]; then   # long option: reformulate OPT and OPTARG
    OPT="${OPTARG%%=*}"       # extract long option name
    OPTARG="${OPTARG#$OPT}"   # extract long option argument (may be empty)
    OPTARG="${OPTARG#=}"      # if long option argument, remove assigning `=`
  fi
  case "$OPT" in
    a | alpha )    alpha=true ;;
    b | bravo )    needs_arg; bravo="$OPTARG" ;;
    c | charlie )  needs_arg; charlie="$OPTARG" ;;
    ??* )          die "Illegal option --$OPT" ;;  # bad long option
    \? )           exit 2 ;;  # bad short option (error reported via getopts)
  esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list

Quando a opção é um hífen ( -), é uma opção longa. getoptsterá analisado a opção longa real em $OPTARG, por exemplo, --bravo=foooriginalmente define OPT='-'e OPTARG='bravo=foo'. A ifestrofe define $OPTo conteúdo de $OPTARGantes do primeiro sinal de igual ( bravono nosso exemplo) e depois remove o mesmo do início de $OPTARG(produzindo =foonesta etapa, ou uma sequência vazia, se não houver =). Finalmente, retiramos a liderança do argumento =. Neste ponto, $OPTexiste uma opção curta (um caractere) ou uma opção longa (2 ou mais caracteres).

O caseentão corresponde às opções curtas ou longas. Para opções curtas, getoptsreclama automaticamente sobre opções e argumentos ausentes, portanto, temos que replicá-los manualmente usando a needs_argfunção, que sai fatalmente quando $OPTARGestá vazia. A ??*condição corresponderá a qualquer opção longa restante ( ?corresponde a um único caractere e *corresponde a zero ou mais, portanto, ??*corresponde a 2 ou mais caracteres), permitindo emitir o erro "Opção ilegal" antes de sair.

(Uma observação sobre nomes de variáveis ​​com letras maiúsculas: geralmente, o conselho é reservar variáveis ​​com letras maiúsculas para uso do sistema. Estou mantendo $OPTcomo letras maiúsculas para mantê-las alinhadas $OPTARG, mas isso quebra essa convenção. se encaixa porque isso é algo que o sistema deveria ter feito e deve ser seguro porque não há padrões (afaik) que usem essa variável.)


Para reclamar de argumentos inesperados para opções longas, imite o que fizemos para argumentos obrigatórios: use uma função auxiliar. Basta virar o teste para reclamar de um argumento quando não se espera:

no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }

Uma versão mais antiga desta resposta tentou aceitar opções longas com argumentos delimitados por espaço, mas não era confiável; getoptspoderia terminar prematuramente com a suposição de que o argumento estava além de seu escopo e o incremento manual $OPTINDnão funciona em todos os shells.

Isso seria realizado usando uma destas técnicas:

e depois concluiu com algo como [ $# -gt $OPTIND ] && OPTIND=$((OPTIND+1))

Adam Katz
fonte
Solução independente muito agradável. Uma pergunta: como letter-cnão precisa de argumento, não seria suficiente usá-lo letter-c)? o *parece redundante.
Philip Kearns
1
@Arne Os argumentos posicionais são ruins no UX; eles são difíceis de entender e argumentos opcionais são bastante confusos. getoptspara no primeiro argumento posicional, pois não foi projetado para lidar com eles. Isso permite subcomandos com seus próprios argumentos, por exemplo git diff --color, então eu interpretaria command --foo=moo bar --baz wazcomo tendo --foocomo argumento para commande --baz wazcomo argumento (com opção) para o barsubcomando. Isso pode ser feito com o código acima. Rejeito --bravo -blahporque --bravorequer um argumento e não está claro que -blahnão seja outra opção.
Adam Katz
1
Não concordo com o UX: argumentos posicionais são úteis e fáceis, desde que você limite o número (no máximo 2 ou 1 mais N do mesmo tipo). Deve ser possível intercalá-los com argumentos de palavras-chave, porque os usuários podem criar um comando passo a passo (ou seja, ls abc -la).
Arne Babenhauserheide
1
@ AdamKatz: escrevi um pequeno artigo com isso: draketo.de/english/free-software/shell-argument-parsing - inclui leitura repetida dos argumentos restantes para pegar as opções à direita.
Arne Babenhauserheide
1
@ArneBabenhauserheide: Atualizei esta resposta para dar suporte a argumentos delimitados por espaço. Por exigir evalno shell POSIX, está listado abaixo do restante da resposta.
23419 Adam
33

Dê uma olhada no shFlags, que é uma biblioteca shell portátil (ou seja: sh, bash, dash, ksh, zsh no Linux, Solaris, etc.).

Isso torna a adição de novos sinalizadores tão simples quanto adicionar uma linha ao seu script e fornece uma função de uso gerada automaticamente.

Aqui está um simples Hello, world!usando shFlag :

#!/bin/sh

# source shflags from current directory
. ./shflags

# define a 'name' command-line string flag
DEFINE_string 'name' 'world' 'name to say hello to' 'n'

# parse the command-line
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"

# say hello
echo "Hello, ${FLAGS_name}!"

Para sistemas operacionais que possuem o getopt aprimorado que suporta opções longas (por exemplo, Linux), você pode:

$ ./hello_world.sh --name Kate
Hello, Kate!

De resto, você deve usar a opção curta:

$ ./hello_world.sh -n Kate
Hello, Kate!

Adicionar um novo sinalizador é tão simples quanto adicionar um novo DEFINE_ call.

k0pernikus
fonte
2
Isso é fantástico, mas infelizmente o meu getopt (OS X) não suporta espaços nos argumentos: / pergunto se existe uma alternativa.
Alastair Stuart
@AlastairStuart - existe de fato uma alternativa no OS X. Use o MacPorts para instalar o GNU getopt (ele geralmente será instalado em / opt / local / bin / getopt).
Urban Vagabond
3
@UrbanVagabond - a instalação de ferramentas padrão que não são do sistema infelizmente não é um requisito aceitável para uma ferramenta suficientemente portátil.
Alastair Stuart
@AlastairStuart - veja minha resposta para uma solução portátil que usa os getopts embutidos em vez do GNU getopt. É o mesmo que o uso básico de getopts, mas com uma iteração extra para opções longas.
Adam Katz
31

Usando getoptscom opções curtas / longas e argumentos


Funciona com todas as combinações, por exemplo:

  • foobar -f --bar
  • foobar --foo -b
  • foobar -bf --bar --foobar
  • foobar -fbFBAshorty --bar -FB --arguments = longhorn
  • foobar -fA "texto baixinho" -B --arguments = "texto longhorn"
  • basobar foobar -F --barfoo
  • sh foobar -B --foobar - ...
  • bash ./foobar -F --bar

Algumas declarações para este exemplo

Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty

Como seria a função de uso

function _usage() 
{
  ###### U S A G E : Help and ERROR ######
  cat <<EOF
   foobar $Options
  $*
          Usage: foobar <[options]>
          Options:
                  -b   --bar            Set bar to yes    ($foo)
                  -f   --foo            Set foo to yes    ($bart)
                  -h   --help           Show this message
                  -A   --arguments=...  Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
                  -B   --barfoo         Set barfoo to yes ($barfoo)
                  -F   --foobar         Set foobar to yes ($foobar)
EOF
}

[ $# = 0 ] && _usage "  >>>>>>>> no options given "

getops com sinalizadores longos / curtos, bem como argumentos longos

while getopts ':bfh-A:BF' OPTION ; do
  case "$OPTION" in
    b  ) sbar=yes                       ;;
    f  ) sfoo=yes                       ;;
    h  ) _usage                         ;;   
    A  ) sarguments=yes;sARG="$OPTARG"  ;;
    B  ) sbarfoo=yes                    ;;
    F  ) sfoobar=yes                    ;;
    -  ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
         eval OPTION="\$$optind"
         OPTARG=$(echo $OPTION | cut -d'=' -f2)
         OPTION=$(echo $OPTION | cut -d'=' -f1)
         case $OPTION in
             --foo       ) lfoo=yes                       ;;
             --bar       ) lbar=yes                       ;;
             --foobar    ) lfoobar=yes                    ;;
             --barfoo    ) lbarfoo=yes                    ;;
             --help      ) _usage                         ;;
             --arguments ) larguments=yes;lARG="$OPTARG"  ;; 
             * )  _usage " Long: >>>>>>>> invalid options (long) " ;;
         esac
       OPTIND=1
       shift
      ;;
    ? )  _usage "Short: >>>>>>>> invalid options (short) "  ;;
  esac
done

Resultado

##################################################################
echo "----------------------------------------------------------"
echo "RESULT short-foo      : $sfoo                                    long-foo      : $lfoo"
echo "RESULT short-bar      : $sbar                                    long-bar      : $lbar"
echo "RESULT short-foobar   : $sfoobar                                 long-foobar   : $lfoobar"
echo "RESULT short-barfoo   : $sbarfoo                                 long-barfoo   : $lbarfoo"
echo "RESULT short-arguments: $sarguments  with Argument = \"$sARG\"   long-arguments: $larguments and $lARG"

Combinando o acima em um script coeso

#!/bin/bash
# foobar: getopts with short and long options AND arguments

function _cleanup ()
{
  unset -f _usage _cleanup ; return 0
}

## Clear out nested functions on exit
trap _cleanup INT EXIT RETURN

###### some declarations for this example ######
Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty

function _usage() 
{
  ###### U S A G E : Help and ERROR ######
  cat <<EOF
   foobar $Options
   $*
          Usage: foobar <[options]>
          Options:
                  -b   --bar            Set bar to yes    ($foo)
                    -f   --foo            Set foo to yes    ($bart)
                      -h   --help           Show this message
                  -A   --arguments=...  Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
                  -B   --barfoo         Set barfoo to yes ($barfoo)
                  -F   --foobar         Set foobar to yes ($foobar)
EOF
}

[ $# = 0 ] && _usage "  >>>>>>>> no options given "

##################################################################    
#######  "getopts" with: short options  AND  long options  #######
#######            AND  short/long arguments               #######
while getopts ':bfh-A:BF' OPTION ; do
  case "$OPTION" in
    b  ) sbar=yes                       ;;
    f  ) sfoo=yes                       ;;
    h  ) _usage                         ;;   
    A  ) sarguments=yes;sARG="$OPTARG"  ;;
    B  ) sbarfoo=yes                    ;;
    F  ) sfoobar=yes                    ;;
    -  ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
         eval OPTION="\$$optind"
         OPTARG=$(echo $OPTION | cut -d'=' -f2)
         OPTION=$(echo $OPTION | cut -d'=' -f1)
         case $OPTION in
             --foo       ) lfoo=yes                       ;;
             --bar       ) lbar=yes                       ;;
             --foobar    ) lfoobar=yes                    ;;
             --barfoo    ) lbarfoo=yes                    ;;
             --help      ) _usage                         ;;
               --arguments ) larguments=yes;lARG="$OPTARG"  ;; 
             * )  _usage " Long: >>>>>>>> invalid options (long) " ;;
         esac
       OPTIND=1
       shift
      ;;
    ? )  _usage "Short: >>>>>>>> invalid options (short) "  ;;
  esac
done
Rapa nui
fonte
Isso não funciona com mais de um argumento longo (-). Parece apenas ler o primeiro para mim.
Sinaesthetic
@ Sinestésico - Sim, eu estava brincando com a evalabordagem de argumentos espaçados em opções longas e achei que não era confiável com certas conchas (embora eu espere que funcione com o bash, caso em que você não precisa usar eval). Veja minha resposta para saber como aceitar argumentos de opção longos =e minhas tentativas notadas de usar o espaço. Minha solução não faz chamadas externas, enquanto esta é utilizada cutalgumas vezes.
Adam Katz
24

Outra maneira ...

# translate long options to short
for arg
do
    delim=""
    case "$arg" in
       --help) args="${args}-h ";;
       --verbose) args="${args}-v ";;
       --config) args="${args}-c ";;
       # pass through anything else
       *) [[ "${arg:0:1}" == "-" ]] || delim="\""
           args="${args}${delim}${arg}${delim} ";;
    esac
done
# reset the translated args
eval set -- $args
# now we can process with getopt
while getopts ":hvc:" opt; do
    case $opt in
        h)  usage ;;
        v)  VERBOSE=true ;;
        c)  source $OPTARG ;;
        \?) usage ;;
        :)
        echo "option -$OPTARG requires an argument"
        usage
        ;;
    esac
done
mtvee
fonte
1
Isso não precisa de espaço em cada $argsreatribuição? Isso pode até ser feito sem basismos, mas esse código perderá espaço em opções e argumentos (não acho que o $delimtruque funcione). Você pode executar set dentro do forloop se tiver cuidado o suficiente para esvaziá-lo apenas na primeira iteração. Aqui está uma versão mais segura sem bashisms.
Adam Katz
18

Eu meio que resolvi assim:

# A string with command options
options=$@

# An array with all the arguments
arguments=($options)

# Loop index
index=0

for argument in $options
  do
    # Incrementing index
    index=`expr $index + 1`

    # The conditions
    case $argument in
      -a) echo "key $argument value ${arguments[index]}" ;;
      -abc) echo "key $argument value ${arguments[index]}" ;;
    esac
  done

exit;

Estou sendo burro ou algo assim? getopte getoptssão tão confusos.

Benjamin
fonte
1
Isso parece funcionar para mim, não sei qual é o problema com esse método, mas parece simples, portanto deve haver uma razão pela qual todos os outros não o estão usando.
Billy Moon
1
@ Billy Sim, isso é simples, porque eu não uso nenhum script para gerenciar meus parâmetros e etc. Basicamente, converto a sequência de argumentos ($ @) em uma matriz e faço um loop através dela. No loop, o valor atual será a chave e o próximo será o valor. Simples assim.
1
@ Theodore, estou feliz que isso tenha sido útil para você! Foi uma dor para mim também. Se você estiver interessado, poderá ver um exemplo disso em ação aqui: raw.github.com/rafaelrinaldi/swf-to-html/master/swf-to-html.sh
2
Definitivamente, a maneira mais fácil que eu já vi. Eu mudei um pouco, como usar i = $ (($ i + 1)) em vez de expr, mas o conceito é hermético.
Thomas Dignan
6
Você não é burro, mas pode estar faltando um recurso: getopt (s) pode reconhecer opções que são misturadas (por exemplo: -ltrou -lt -rtambém -l -t -r). E também fornece algum tratamento de erro e uma maneira fácil de mudar os parâmetros tratados quando a opção de tratamento é finalizada.
precisa
14

Caso você não queira a getoptdependência, faça o seguinte:

while test $# -gt 0
do
  case $1 in

  # Normal option processing
    -h | --help)
      # usage and help
      ;;
    -v | --version)
      # version info
      ;;
  # ...

  # Special cases
    --)
      break
      ;;
    --*)
      # error unknown (long) option $1
      ;;
    -?)
      # error unknown (short) option $1
      ;;

  # FUN STUFF HERE:
  # Split apart combined short options
    -*)
      split=$1
      shift
      set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$@"
      continue
      ;;

  # Done with options
    *)
      break
      ;;
  esac

  # for testing purposes:
  echo "$1"

  shift
done

Obviamente, você não pode usar opções de estilo longas com apenas um traço. E se você quiser adicionar versões reduzidas (por exemplo, --verbos em vez de --verbose), precisará adicioná-las manualmente.

Mas se você deseja obter getoptsfuncionalidade juntamente com opções longas, esta é uma maneira simples de fazê-lo.

Eu também coloquei esse trecho em uma essência .

jakesandlund
fonte
Isso parece funcionar apenas com uma opção longa por vez, mas atendeu à minha necessidade. Obrigado!
kingjeffrey
No caso especial --), parece estar shift ;faltando. No momento, --ele permanecerá como o primeiro argumento não opcional.
DGW
Eu acho que essa é realmente a melhor resposta, embora, como aponta o dgw, a --opção precise de uma shift. Digo isso é melhor porque as alternativas são ou plataforma versões dependentes de getoptou getopts_longou você tem que forçar-curtas opções para ser usado apenas no início do comando (ou seja - você usa getopts, em seguida, processar opções muito tempo depois), enquanto esta dá qualquer ordem e controle completo.
Haravikk
Essa resposta me faz pensar por que temos uma lista de dezenas de respostas para fazer o trabalho que pode ser feito com nada mais do que essa solução absolutamente clara e direta e se há alguma razão para o bilhão de getopt (s) casos de uso que não sejam provar a si mesmo.
Florian Heigl
11

O built-in getoptsnão pode fazer isso. Existe um programa externo getopt (1) que pode fazer isso, mas você só o obtém no Linux a partir do pacote util-linux . Ele vem com um script de exemplo getopt-parse.bash .

Há também um getopts_longescrito como uma função shell.

Nietzche-jou
fonte
3
Ele getoptfoi incluído no FreeBSD versão 1.0 em 1993 e faz parte do FreeBSD desde então. Como tal, foi adotado no FreeBSD 4.x para inclusão no projeto Darwin da Apple. A partir do OS X 10.6.8, a página de manual incluída pela Apple continua sendo uma duplicata exata da página de manual do FreeBSD. Então, sim, ele está incluído no OS X e em outros sistemas operacionais, além do Linux. -1 nesta resposta para informações incorretas.
ghoti
8
#!/bin/bash
while getopts "abc:d:" flag
do
  case $flag in
    a) echo "[getopts:$OPTIND]==> -$flag";;
    b) echo "[getopts:$OPTIND]==> -$flag";;
    c) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
    d) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
  esac
done

shift $((OPTIND-1))
echo "[otheropts]==> $@"

exit

.

#!/bin/bash
until [ -z "$1" ]; do
  case $1 in
    "--dlong")
      shift
      if [ "${1:1:0}" != "-" ]
      then
        echo "==> dlong $1"
        shift
      fi;;
    *) echo "==> other $1"; shift;;
  esac
done
exit
3ED
fonte
2
Uma explicação seria legal. O primeiro script aceita opções curtas apenas enquanto o segundo script possui um erro em seu argumento de opção longa analisando; sua variável deve ser "${1:0:1}"para o argumento nº 1, substring no índice 0, comprimento 1. Isso não permite misturar opções curtas e longas.
Adam Katz
7

Em ksh93, getoptssuporta nomes longos ...

while getopts "f(file):s(server):" flag
do
    echo "$flag" $OPTIND $OPTARG
done

Ou então os tutoriais que eu encontrei disseram. Experimente e veja.

Richard Lynch
fonte
4
Este é o getsh do ksh93 embutido. Além dessa sintaxe, ela também possui uma sintaxe mais complicada que também permite opções longas sem um equivalente curto e muito mais.
Jilles
2
Uma resposta razoável. O OP não especificou qual shell.
ghoti
6

Eu só escrevo shell scripts de vez em quando e saio da prática, para que qualquer feedback seja apreciado.

Usando a estratégia proposta pelo @Arvid Requate, notamos alguns erros do usuário. Um usuário que se esquece de incluir um valor acidentalmente terá o nome da próxima opção tratado como um valor:

./getopts_test.sh --loglevel= --toc=TRUE

fará com que o valor de "loglevel" seja visto como "--toc = TRUE". Isso pode ser evitado.

Adaptei algumas idéias sobre a verificação de erro do usuário para a CLI na http://mwiki.wooledge.org/BashFAQ/035 discussão sobre análise manual. Eu inseri a verificação de erros no tratamento dos argumentos "-" e "-".

Então comecei a brincar com a sintaxe, para que qualquer erro aqui seja estritamente minha culpa, não dos autores originais.

Minha abordagem ajuda os usuários que preferem entrar muito tempo com ou sem o sinal de igual. Ou seja, ele deve ter a mesma resposta para "--loglevel 9" que "--loglevel = 9". No método - / space, não é possível saber com certeza se o usuário esquece um argumento; portanto, é necessário adivinhar.

  1. se o usuário tiver o formato de sinal longo / igual (--opt =), um espaço após = acionará um erro porque um argumento não foi fornecido.
  2. se o usuário tiver argumentos longos / de espaço (--opt), esse script causará uma falha se nenhum argumento for seguido (final do comando) ou se o argumento começar com traço)

Caso você esteja começando com isso, há uma diferença interessante entre os formatos "--opt = value" e "--opt value". Com o sinal de igual, o argumento da linha de comando é visto como "opt = value" e o trabalho para manipular a análise de cadeia de caracteres, para separar no "=". Por outro lado, com "--opt value", o nome do argumento é "opt" e temos o desafio de obter o próximo valor fornecido na linha de comando. É aí que o @Arvid Requate usou $ {! OPTIND}, a referência indireta. Ainda não entendo isso, bem, e os comentários no BashFAQ parecem alertar contra esse estilo ( http://mywiki.wooledge.org/BashFAQ/006 ). BTW, eu não acho que os comentários do pôster anterior sobre a importância de OPTIND = $ (($ OPTIND + 1)) estejam corretos. Eu quis dizer,

Na versão mais recente desse script, o sinalizador -v significa impressão VERBOSE.

Salve-o em um arquivo chamado "cli-5.sh", torne o executável e qualquer um deles funcionará ou falhará da maneira desejada

./cli-5.sh  -v --loglevel=44 --toc  TRUE
./cli-5.sh  -v --loglevel=44 --toc=TRUE
./cli-5.sh --loglevel 7
./cli-5.sh --loglevel=8
./cli-5.sh -l9

./cli-5.sh  --toc FALSE --loglevel=77
./cli-5.sh  --toc=FALSE --loglevel=77

./cli-5.sh   -l99 -t yyy
./cli-5.sh   -l 99 -t yyy

Aqui está um exemplo de saída da verificação de erro no usuário intpu

$ ./cli-5.sh  --toc --loglevel=77
ERROR: toc value must not have dash at beginning
$ ./cli-5.sh  --toc= --loglevel=77
ERROR: value for toc undefined

Você deve ativar a opção -v, pois ela imprime as partes internas do OPTIND e OPTARG

#/usr/bin/env bash

## Paul Johnson
## 20171016
##

## Combines ideas from
## /programming/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
## by @Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035

# What I don't understand yet: 
# In @Arvid REquate's answer, we have 
# val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
# this works, but I don't understand it!


die() {
    printf '%s\n' "$1" >&2
    exit 1
}

printparse(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
    fi
}

showme(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'VERBOSE: %s\n' "$1" >&2;
    fi
}


VERBOSE=0
loglevel=0
toc="TRUE"

optspec=":vhl:t:-:"
while getopts "$optspec" OPTCHAR; do

    showme "OPTARG:  ${OPTARG[*]}"
    showme "OPTIND:  ${OPTIND[*]}"
    case "${OPTCHAR}" in
        -)
            case "${OPTARG}" in
                loglevel) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect?
                    printparse "--${OPTARG}" "  " "${val}"
                    loglevel="${val}"
                    shift
                    ;;
                loglevel=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        printparse "--${opt}" "=" "${val}"
                        loglevel="${val}"
                        ## shift CAUTION don't shift this, fails othewise
                    else
                        die "ERROR: $opt value must be supplied"
                    fi
                    ;;
                toc) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) #??
                    printparse "--${opt}" " " "${val}"
                    toc="${val}"
                    shift
                    ;;
                toc=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        toc=${val}
                        printparse "--$opt" " -> " "$toc"
                        ##shift ## NO! dont shift this
                    else
                        die "ERROR: value for $opt undefined"
                    fi
                    ;;

                help)
                    echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
                    exit 2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h|-\?|--help)
            ## must rewrite this for all of the arguments
            echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
            exit 2
            ;;
        l)
            loglevel=${OPTARG}
            printparse "-l" " "  "${loglevel}"
            ;;
        t)
            toc=${OPTARG}
            ;;
        v)
            VERBOSE=1
            ;;

        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done



echo "
After Parsing values
"
echo "loglevel  $loglevel" 
echo "toc  $toc"
pauljohn32
fonte
OPTIND=$(( $OPTIND + 1 )): é necessário sempre que você "devora" o parâmetro do OPTIND (por exemplo: quando um usado --toc value : value está no número do parâmetro $ OPTIND. Depois de recuperá-lo para o valor de toc, você deve informar ao getopts que o próximo parâmetro a analisar não é valor, mas o que está depois dele (daí o:. OPTIND=$(( $OPTIND + 1 )) e seu script (assim como o script a que você se refere) está ausente, depois do feito: shift $(( $OPTIND -1 ))(como getopts saiu após analisar os parâmetrosrs 1 a OPTIND-1, é necessário alterá-los para que $@está agora qualquer restantes "não-opções" parâmetros
Olivier Dulac
ah, quando você se muda, você "muda" os parâmetros abaixo das getopts, então o OPTIND está sempre apontando a coisa certa ... mas acho isso muito confuso. Eu acredito (não posso testar seu script agora) que você ainda precisa do shift $ (($ OPTIND - 1)) após as getopts while loop, para que $ 1 agora não aponte para o $ 1 original (uma opção), mas ao primeiro dos argumentos restantes (os que vêm depois de todas as opções e seus valores). ex: myrm -foo -bar = baz thisArg thenthisone thenanother
Olivier Dulac
5

Inventando mais uma versão do volante ...

Esta função é (esperançosamente) uma substituição simples do shell bourne compatível com POSIX para o GNU getopt. Ele suporta opções curtas / longas que podem aceitar argumentos obrigatórios / opcionais / sem argumentos, e a maneira como as opções são especificadas é quase idêntica à GNU getopt, portanto a conversão é trivial.

É claro que este ainda é um pedaço considerável de código a ser inserido em um script, mas é cerca de metade das linhas da conhecida função shell getopt_long, e pode ser preferível nos casos em que você deseja substituir apenas os usos existentes do GNU getopt.

Este é um código bastante novo, então YMMV (e definitivamente por favor me avise se isso não é realmente compatível com POSIX por qualquer motivo - portabilidade era a intenção desde o início, mas eu não tenho um ambiente de teste POSIX útil).

O código e o exemplo de uso a seguir:

#!/bin/sh
# posix_getopt shell function
# Author: Phil S.
# Version: 1.0
# Created: 2016-07-05
# URL: http://stackoverflow.com/a/37087374/324105

# POSIX-compatible argument quoting and parameter save/restore
# http://www.etalabs.net/sh_tricks.html
# Usage:
# parameters=$(save "$@") # save the original parameters.
# eval "set -- ${parameters}" # restore the saved parameters.
save () {
    local param
    for param; do
        printf %s\\n "$param" \
            | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
    done
    printf %s\\n " "
}

# Exit with status $1 after displaying error message $2.
exiterr () {
    printf %s\\n "$2" >&2
    exit $1
}

# POSIX-compatible command line option parsing.
# This function supports long options and optional arguments, and is
# a (largely-compatible) drop-in replacement for GNU getopt.
#
# Instead of:
# opts=$(getopt -o "$shortopts" -l "$longopts" -- "$@")
# eval set -- ${opts}
#
# We instead use:
# opts=$(posix_getopt "$shortopts" "$longopts" "$@")
# eval "set -- ${opts}"
posix_getopt () { # args: "$shortopts" "$longopts" "$@"
    local shortopts longopts \
          arg argtype getopt nonopt opt optchar optword suffix

    shortopts="$1"
    longopts="$2"
    shift 2

    getopt=
    nonopt=
    while [ $# -gt 0 ]; do
        opt=
        arg=
        argtype=
        case "$1" in
            # '--' means don't parse the remaining options
            ( -- ) {
                getopt="${getopt}$(save "$@")"
                shift $#
                break
            };;
            # process short option
            ( -[!-]* ) {         # -x[foo]
                suffix=${1#-?}   # foo
                opt=${1%$suffix} # -x
                optchar=${opt#-} # x
                case "${shortopts}" in
                    ( *${optchar}::* ) { # optional argument
                        argtype=optional
                        arg="${suffix}"
                        shift
                    };;
                    ( *${optchar}:* ) { # required argument
                        argtype=required
                        if [ -n "${suffix}" ]; then
                            arg="${suffix}"
                            shift
                        else
                            case "$2" in
                                ( -* ) exiterr 1 "$1 requires an argument";;
                                ( ?* ) arg="$2"; shift 2;;
                                (  * ) exiterr 1 "$1 requires an argument";;
                            esac
                        fi
                    };;
                    ( *${optchar}* ) { # no argument
                        argtype=none
                        arg=
                        shift
                        # Handle multiple no-argument parameters combined as
                        # -xyz instead of -x -y -z. If we have just shifted
                        # parameter -xyz, we now replace it with -yz (which
                        # will be processed in the next iteration).
                        if [ -n "${suffix}" ]; then
                            eval "set -- $(save "-${suffix}")$(save "$@")"
                        fi
                    };;
                    ( * ) exiterr 1 "Unknown option $1";;
                esac
            };;
            # process long option
            ( --?* ) {            # --xarg[=foo]
                suffix=${1#*=}    # foo (unless there was no =)
                if [ "${suffix}" = "$1" ]; then
                    suffix=
                fi
                opt=${1%=$suffix} # --xarg
                optword=${opt#--} # xarg
                case ",${longopts}," in
                    ( *,${optword}::,* ) { # optional argument
                        argtype=optional
                        arg="${suffix}"
                        shift
                    };;
                    ( *,${optword}:,* ) { # required argument
                        argtype=required
                        if [ -n "${suffix}" ]; then
                            arg="${suffix}"
                            shift
                        else
                            case "$2" in
                                ( -* ) exiterr 1 \
                                       "--${optword} requires an argument";;
                                ( ?* ) arg="$2"; shift 2;;
                                (  * ) exiterr 1 \
                                       "--${optword} requires an argument";;
                            esac
                        fi
                    };;
                    ( *,${optword},* ) { # no argument
                        if [ -n "${suffix}" ]; then
                            exiterr 1 "--${optword} does not take an argument"
                        fi
                        argtype=none
                        arg=
                        shift
                    };;
                    ( * ) exiterr 1 "Unknown option $1";;
                esac
            };;
            # any other parameters starting with -
            ( -* ) exiterr 1 "Unknown option $1";;
            # remember non-option parameters
            ( * ) nonopt="${nonopt}$(save "$1")"; shift;;
        esac

        if [ -n "${opt}" ]; then
            getopt="${getopt}$(save "$opt")"
            case "${argtype}" in
                ( optional|required ) {
                    getopt="${getopt}$(save "$arg")"
                };;
            esac
        fi
    done

    # Generate function output, suitable for:
    # eval "set -- $(posix_getopt ...)"
    printf %s "${getopt}"
    if [ -n "${nonopt}" ]; then
        printf %s "$(save "--")${nonopt}"
    fi
}

Exemplo de uso:

# Process command line options
shortopts="hvd:c::s::L:D"
longopts="help,version,directory:,client::,server::,load:,delete"
#opts=$(getopt -o "$shortopts" -l "$longopts" -n "$(basename $0)" -- "$@")
opts=$(posix_getopt "$shortopts" "$longopts" "$@")
if [ $? -eq 0 ]; then
    #eval set -- ${opts}
    eval "set -- ${opts}"
    while [ $# -gt 0 ]; do
        case "$1" in
            ( --                ) shift; break;;
            ( -h|--help         ) help=1; shift; break;;
            ( -v|--version      ) version_help=1; shift; break;;
            ( -d|--directory    ) dir=$2; shift 2;;
            ( -c|--client       ) useclient=1; client=$2; shift 2;;
            ( -s|--server       ) startserver=1; server_name=$2; shift 2;;
            ( -L|--load         ) load=$2; shift 2;;
            ( -D|--delete       ) delete=1; shift;;
        esac
    done
else
    shorthelp=1 # getopt returned (and reported) an error.
fi
phils
fonte
4

A resposta aceita faz um trabalho muito bom de apontar todas as deficiências do bash embutido getopts. A resposta termina com:

Portanto, embora seja possível escrever mais código para solucionar a falta de suporte para opções longas, isso é muito mais trabalhoso e derrota parcialmente o propósito de usar um analisador getopt para simplificar seu código.

E, apesar de concordar em princípio com essa afirmação, sinto que o número de vezes que todos implementamos esse recurso em vários scripts justifica um pouco de esforço na criação de uma solução "padronizada" e bem testada.

Como tal, eu "atualizei" o bash embutido getoptsimplementando getopts_longo bash puro, sem dependências externas. O uso da função é 100% compatível com o built-in getopts.

Ao incluir getopts_long( hospedado no GitHub ) em um script, a resposta para a pergunta original pode ser implementada da seguinte maneira:

source "${PATH_TO}/getopts_long.bash"

while getopts_long ':c: copyfile:' OPTKEY; do
    case ${OPTKEY} in
        'c'|'copyfile')
            echo 'file supplied -- ${OPTARG}'
            ;;
        '?')
            echo "INVALID OPTION -- ${OPTARG}" >&2
            exit 1
            ;;
        ':')
            echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
            exit 1
            ;;
        *)
            echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
            exit 1
            ;;
    esac
done

shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
UrsaDK
fonte
3

Ainda não tenho representante suficiente para comentar ou votar em sua solução, mas a resposta da sme funcionou extremamente bem para mim. O único problema que encontrei foi que os argumentos acabam em aspas simples (então, eu os retiro).

Também adicionei alguns exemplos de uso e texto de AJUDA. Vou incluir minha versão um pouco estendida aqui:

#!/bin/bash

# getopt example
# from: /programming/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
HELP_TEXT=\
"   USAGE:\n
    Accepts - and -- flags, can specify options that require a value, and can be in any order. A double-hyphen (--) will stop processing options.\n\n

    Accepts the following forms:\n\n

    getopt-example.sh -a -b -c value-for-c some-arg\n
    getopt-example.sh -c value-for-c -a -b some-arg\n
    getopt-example.sh -abc some-arg\n
    getopt-example.sh --along --blong --clong value-for-c -a -b -c some-arg\n
    getopt-example.sh some-arg --clong value-for-c\n
    getopt-example.sh
"

aflag=false
bflag=false
cargument=""

# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc:h\? -l along,blong,help,clong: -- "$@")
then
    # something went wrong, getopt will put out an error message for us
    exit 1
fi

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag=true ;;
    -b|--blong) bflag=true ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    -h|--help|-\?) echo -e $HELP_TEXT; exit;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done

# to remove the single quotes around arguments, pipe the output into:
# | sed -e "s/^'\\|'$//g"  (just leading/trailing) or | sed -e "s/'//g"  (all)

echo aflag=${aflag}
echo bflag=${bflag}
echo cargument=${cargument}

while [ $# -gt 0 ]
do
    echo arg=$1
    shift

    if [[ $aflag == true ]]; then
        echo a is true
    fi

done
kghastie
fonte
3

Aqui você pode encontrar algumas abordagens diferentes para a análise de opções complexas no bash: http://mywiki.wooledge.org/ComplexOptionParsing

Eu criei o seguinte e acho que é bom, porque é um código mínimo e as opções longas e curtas funcionam. Uma opção longa também pode ter vários argumentos com essa abordagem.

#!/bin/bash
# Uses bash extensions.  Not portable as written.

declare -A longoptspec
longoptspec=( [loglevel]=1 ) #use associative array to declare how many arguments a long option expects, in this case we declare that loglevel expects/has one argument, long options that aren't listed i n this way will have zero arguments by default
optspec=":h-:"
while getopts "$optspec" opt; do
while true; do
    case "${opt}" in
        -) #OPTARG is name-of-long-option or name-of-long-option=value
            if [[ "${OPTARG}" =~ .*=.* ]] #with this --key=value format only one argument is possible
            then
                opt=${OPTARG/=*/}
                OPTARG=${OPTARG#*=}
                ((OPTIND--))    
            else #with this --key value1 value2 format multiple arguments are possible
                opt="$OPTARG"
                OPTARG=(${@:OPTIND:$((longoptspec[$opt]))})
            fi
            ((OPTIND+=longoptspec[$opt]))
            continue #now that opt/OPTARG are set we can process them as if getopts would've given us long options
            ;;
        loglevel)
          loglevel=$OPTARG
            ;;
        h|help)
            echo "usage: $0 [--loglevel[=]<value>]" >&2
            exit 2
            ;;
    esac
break; done
done

# End of file
user3573558
fonte
2

Estou trabalhando nesse assunto há muito tempo ... e criei minha própria biblioteca, que você precisará obter no seu script principal. Veja libopt4shell e cd2mpc para um exemplo. Espero que ajude !

liealgebra
fonte
2

Uma solução aprimorada:

# translate long options to short
# Note: This enable long options but disable "--?*" in $OPTARG, or disable long options after  "--" in option fields.
for ((i=1;$#;i++)) ; do
    case "$1" in
        --)
            # [ ${args[$((i-1))]} == ... ] || EndOpt=1 ;;& # DIRTY: we still can handle some execptions...
            EndOpt=1 ;;&
        --version) ((EndOpt)) && args[$i]="$1"  || args[$i]="-V";;
        # default case : short option use the first char of the long option:
        --?*) ((EndOpt)) && args[$i]="$1"  || args[$i]="-${1:2:1}";;
        # pass through anything else:
        *) args[$i]="$1" ;;
    esac
    shift
done
# reset the translated args
set -- "${args[@]}"

function usage {
echo "Usage: $0 [options] files" >&2
    exit $1
}

# now we can process with getopt
while getopts ":hvVc:" opt; do
    case $opt in
        h)  usage ;;
        v)  VERBOSE=true ;;
        V)  echo $Version ; exit ;;
        c)  source $OPTARG ;;
        \?) echo "unrecognized option: -$opt" ; usage -1 ;;
        :)
        echo "option -$OPTARG requires an argument"
        usage -1
        ;;
    esac
done

shift $((OPTIND-1))
[[ "$1" == "--" ]] && shift
jbar
fonte
2

Talvez seja mais simples usar o ksh, apenas para a parte getopts, se precisar de longas opções de linha de comando, pois pode ser mais fácil fazer isso lá.

# Working Getopts Long => KSH

#! /bin/ksh
# Getopts Long
USAGE="s(showconfig)"
USAGE+="c:(createdb)"
USAGE+="l:(createlistener)"
USAGE+="g:(generatescripts)"
USAGE+="r:(removedb)"
USAGE+="x:(removelistener)"
USAGE+="t:(createtemplate)"
USAGE+="h(help)"

while getopts "$USAGE" optchar ; do
    case $optchar in
    s)  echo "Displaying Configuration" ;;
        c)  echo "Creating Database $OPTARG" ;;
    l)  echo "Creating Listener LISTENER_$OPTARG" ;;
    g)  echo "Generating Scripts for Database $OPTARG" ;;
    r)  echo "Removing Database $OPTARG" ;;
    x)  echo "Removing Listener LISTENER_$OPTARG" ;;
    t)  echo "Creating Database Template" ;;
    h)  echo "Help" ;;
    esac
done
efstathiou_e
fonte
+1 - Observe que isso é limitado a ksh93 - do projeto AST de código aberto (AT&T Research).
Henk Langeveld
2

Eu queria algo sem dependências externas, com suporte estrito ao bash (-u), e precisava que ele funcionasse mesmo nas versões mais antigas do bash. Isso lida com vários tipos de parâmetros:

  • bools curtos (-h)
  • opções curtas (-i "image.jpg")
  • bools longos (--help)
  • igual a opções (--file = "filename.ext")
  • opções de espaço (--arquivo "filename.ext")
  • bools concatinados (-hvm)

Basta inserir o seguinte na parte superior do seu script:

# Check if a list of params contains a specific param
# usage: if _param_variant "h|?|help p|path f|file long-thing t|test-thing" "file" ; then ...
# the global variable $key is updated to the long notation (last entry in the pipe delineated list, if applicable)
_param_variant() {
  for param in $1 ; do
    local variants=${param//\|/ }
    for variant in $variants ; do
      if [[ "$variant" = "$2" ]] ; then
        # Update the key to match the long version
        local arr=(${param//\|/ })
        let last=${#arr[@]}-1
        key="${arr[$last]}"
        return 0
      fi
    done
  done
  return 1
}

# Get input parameters in short or long notation, with no dependencies beyond bash
# usage:
#     # First, set your defaults
#     param_help=false
#     param_path="."
#     param_file=false
#     param_image=false
#     param_image_lossy=true
#     # Define allowed parameters
#     allowed_params="h|?|help p|path f|file i|image image-lossy"
#     # Get parameters from the arguments provided
#     _get_params $*
#
# Parameters will be converted into safe variable names like:
#     param_help,
#     param_path,
#     param_file,
#     param_image,
#     param_image_lossy
#
# Parameters without a value like "-h" or "--help" will be treated as
# boolean, and will be set as param_help=true
#
# Parameters can accept values in the various typical ways:
#     -i "path/goes/here"
#     --image "path/goes/here"
#     --image="path/goes/here"
#     --image=path/goes/here
# These would all result in effectively the same thing:
#     param_image="path/goes/here"
#
# Concatinated short parameters (boolean) are also supported
#     -vhm is the same as -v -h -m
_get_params(){

  local param_pair
  local key
  local value
  local shift_count

  while : ; do
    # Ensure we have a valid param. Allows this to work even in -u mode.
    if [[ $# == 0 || -z $1 ]] ; then
      break
    fi

    # Split the argument if it contains "="
    param_pair=(${1//=/ })
    # Remove preceeding dashes
    key="${param_pair[0]#--}"

    # Check for concatinated boolean short parameters.
    local nodash="${key#-}"
    local breakout=false
    if [[ "$nodash" != "$key" && ${#nodash} -gt 1 ]]; then
      # Extrapolate multiple boolean keys in single dash notation. ie. "-vmh" should translate to: "-v -m -h"
      local short_param_count=${#nodash}
      let new_arg_count=$#+$short_param_count-1
      local new_args=""
      # $str_pos is the current position in the short param string $nodash
      for (( str_pos=0; str_pos<new_arg_count; str_pos++ )); do
        # The first character becomes the current key
        if [ $str_pos -eq 0 ] ; then
          key="${nodash:$str_pos:1}"
          breakout=true
        fi
        # $arg_pos is the current position in the constructed arguments list
        let arg_pos=$str_pos+1
        if [ $arg_pos -gt $short_param_count ] ; then
          # handle other arguments
          let orignal_arg_number=$arg_pos-$short_param_count+1
          local new_arg="${!orignal_arg_number}"
        else
          # break out our one argument into new ones
          local new_arg="-${nodash:$str_pos:1}"
        fi
        new_args="$new_args \"$new_arg\""
      done
      # remove the preceding space and set the new arguments
      eval set -- "${new_args# }"
    fi
    if ! $breakout ; then
      key="$nodash"
    fi

    # By default we expect to shift one argument at a time
    shift_count=1
    if [ "${#param_pair[@]}" -gt "1" ] ; then
      # This is a param with equals notation
      value="${param_pair[1]}"
    else
      # This is either a boolean param and there is no value,
      # or the value is the next command line argument
      # Assume the value is a boolean true, unless the next argument is found to be a value.
      value=true
      if [[ $# -gt 1 && -n "$2" ]]; then
        local nodash="${2#-}"
        if [ "$nodash" = "$2" ]; then
          # The next argument has NO preceding dash so it is a value
          value="$2"
          shift_count=2
        fi
      fi
    fi

    # Check that the param being passed is one of the allowed params
    if _param_variant "$allowed_params" "$key" ; then
      # --key-name will now become param_key_name
      eval param_${key//-/_}="$value"
    else
      printf 'WARNING: Unknown option (ignored): %s\n' "$1" >&2
    fi
    shift $shift_count
  done
}

E use-o assim:

# Assign defaults for parameters
param_help=false
param_path=$(pwd)
param_file=false
param_image=true
param_image_lossy=true
param_image_lossy_quality=85

# Define the params we will allow
allowed_params="h|?|help p|path f|file i|image image-lossy image-lossy-quality"

# Get the params from arguments provided
_get_params $*
Heath Dutton
fonte
1

Para manter a compatibilidade entre plataformas e evitar a dependência de executáveis ​​externos, eu portava algum código de outro idioma.

Eu acho muito fácil de usar, aqui está um exemplo:

ArgParser::addArg "[h]elp"    false    "This list"
ArgParser::addArg "[q]uiet"   false    "Supress output"
ArgParser::addArg "[s]leep"   1        "Seconds to sleep"
ArgParser::addArg "v"         1        "Verbose mode"

ArgParser::parse "$@"

ArgParser::isset help && ArgParser::showArgs

ArgParser::isset "quiet" \
   && echo "Quiet!" \
   || echo "Noisy!"

local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
   && echo "Sleep for $__sleep seconds" \
   || echo "No value passed for sleep"

# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"

O BASH necessário é um pouco mais longo do que poderia ser, mas eu queria evitar a dependência das matrizes associativas do BASH 4. Você também pode fazer o download diretamente em http://nt4.com/bash/argparser.inc.sh

#!/usr/bin/env bash

# Updates to this script may be found at
# http://nt4.com/bash/argparser.inc.sh

# Example of runtime usage:
# mnc.sh --nc -q Caprica.S0*mkv *.avi *.mp3 --more-options here --host centos8.host.com

# Example of use in script (see bottom)
# Just include this file in yours, or use
# source argparser.inc.sh

unset EXPLODED
declare -a EXPLODED
function explode 
{
    local c=$# 
    (( c < 2 )) && 
    {
        echo function "$0" is missing parameters 
        return 1
    }

    local delimiter="$1"
    local string="$2"
    local limit=${3-99}

    local tmp_delim=$'\x07'
    local delin=${string//$delimiter/$tmp_delim}
    local oldifs="$IFS"

    IFS="$tmp_delim"
    EXPLODED=($delin)
    IFS="$oldifs"
}


# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
# Usage: local "$1" && upvar $1 "value(s)"
upvar() {
    if unset -v "$1"; then           # Unset & validate varname
        if (( $# == 2 )); then
            eval $1=\"\$2\"          # Return single value
        else
            eval $1=\(\"\${@:2}\"\)  # Return array
        fi
    fi
}

function decho
{
    :
}

function ArgParser::check
{
    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        matched=0
        explode "|" "${__argparser__arglist[$i]}"
        if [ "${#1}" -eq 1 ]
        then
            if [ "${1}" == "${EXPLODED[0]}" ]
            then
                decho "Matched $1 with ${EXPLODED[0]}"
                matched=1

                break
            fi
        else
            if [ "${1}" == "${EXPLODED[1]}" ]
            then
                decho "Matched $1 with ${EXPLODED[1]}"
                matched=1

                break
            fi
        fi
    done
    (( matched == 0 )) && return 2
    # decho "Key $key has default argument of ${EXPLODED[3]}"
    if [ "${EXPLODED[3]}" == "false" ]
    then
        return 0
    else
        return 1
    fi
}

function ArgParser::set
{
    key=$3
    value="${1:-true}"
    declare -g __argpassed__$key="$value"
}

function ArgParser::parse
{

    unset __argparser__argv
    __argparser__argv=()
    # echo parsing: "$@"

    while [ -n "$1" ]
    do
        # echo "Processing $1"
        if [ "${1:0:2}" == '--' ]
        then
            key=${1:2}
            value=$2
        elif [ "${1:0:1}" == '-' ]
        then
            key=${1:1}               # Strip off leading -
            value=$2
        else
            decho "Not argument or option: '$1'" >& 2
            __argparser__argv+=( "$1" )
            shift
            continue
        fi
        # parameter=${tmp%%=*}     # Extract name.
        # value=${tmp##*=}         # Extract value.
        decho "Key: '$key', value: '$value'"
        # eval $parameter=$value
        ArgParser::check $key
        el=$?
        # echo "Check returned $el for $key"
        [ $el -eq  2 ] && decho "No match for option '$1'" >&2 # && __argparser__argv+=( "$1" )
        [ $el -eq  0 ] && decho "Matched option '${EXPLODED[2]}' with no arguments"        >&2 && ArgParser::set true "${EXPLODED[@]}"
        [ $el -eq  1 ] && decho "Matched option '${EXPLODED[2]}' with an argument of '$2'"   >&2 && ArgParser::set "$2" "${EXPLODED[@]}" && shift
        shift
    done
}

function ArgParser::isset
{
    declare -p "__argpassed__$1" > /dev/null 2>&1 && return 0
    return 1
}

function ArgParser::getArg
{
    # This one would be a bit silly, since we can only return non-integer arguments ineffeciently
    varname="__argpassed__$1"
    echo "${!varname}"
}

##
# usage: tryAndGetArg <argname> into <varname>
# returns: 0 on success, 1 on failure
function ArgParser::tryAndGetArg
{
    local __varname="__argpassed__$1"
    local __value="${!__varname}"
    test -z "$__value" && return 1
    local "$3" && upvar $3 "$__value"
    return 0
}

function ArgParser::__construct
{
    unset __argparser__arglist
    # declare -a __argparser__arglist
}

##
# @brief add command line argument
# @param 1 short and/or long, eg: [s]hort
# @param 2 default value
# @param 3 description
##
function ArgParser::addArg
{
    # check for short arg within long arg
    if [[ "$1" =~ \[(.)\] ]]
    then
        short=${BASH_REMATCH[1]}
        long=${1/\[$short\]/$short}
    else
        long=$1
    fi
    if [ "${#long}" -eq 1 ]
    then
        short=$long
        long=''
    fi
    decho short: "$short"
    decho long: "$long"
    __argparser__arglist+=("$short|$long|$1|$2|$3")
}

## 
# @brief show available command line arguments
##
function ArgParser::showArgs
{
    # declare -p | grep argparser
    printf "Usage: %s [OPTION...]\n\n" "$( basename "${BASH_SOURCE[0]}" )"
    printf "Defaults for the options are specified in brackets.\n\n";

    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        local shortname=
        local fullname=
        local default=
        local description=
        local comma=

        explode "|" "${__argparser__arglist[$i]}"

        shortname="${EXPLODED[0]:+-${EXPLODED[0]}}" # String Substitution Guide: 
        fullname="${EXPLODED[1]:+--${EXPLODED[1]}}" # http://tldp.org/LDP/abs/html/parameter-substitution.html
        test -n "$shortname" \
            && test -n "$fullname" \
            && comma=","

        default="${EXPLODED[3]}"
        case $default in
            false )
                default=
                ;;
            "" )
                default=
                ;;
            * )
                default="[$default]"
        esac

        description="${EXPLODED[4]}"

        printf "  %2s%1s %-19s %s %s\n" "$shortname" "$comma" "$fullname" "$description" "$default"
    done
}

function ArgParser::test
{
    # Arguments with a default of 'false' do not take paramaters (note: default
    # values are not applied in this release)

    ArgParser::addArg "[h]elp"      false       "This list"
    ArgParser::addArg "[q]uiet" false       "Supress output"
    ArgParser::addArg "[s]leep" 1           "Seconds to sleep"
    ArgParser::addArg "v"           1           "Verbose mode"

    ArgParser::parse "$@"

    ArgParser::isset help && ArgParser::showArgs

    ArgParser::isset "quiet" \
        && echo "Quiet!" \
        || echo "Noisy!"

    local __sleep
    ArgParser::tryAndGetArg sleep into __sleep \
        && echo "Sleep for $__sleep seconds" \
        || echo "No value passed for sleep"

    # This way is often more convienient, but is a little slower
    echo "Sleep set to: $( ArgParser::getArg sleep )"

    echo "Remaining command line: ${__argparser__argv[@]}"

}

if [ "$( basename "$0" )" == "argparser.inc.sh" ]
then
    ArgParser::test "$@"
fi
Orwellophile
fonte
1

Se todas as suas opções longas tiverem primeiros caracteres únicos e correspondentes como as opções curtas, por exemplo, por exemplo

./slamm --chaos 23 --plenty test -quiet

É o mesmo que

./slamm -c 23 -p test -q

Você pode usar isso antes de getopts para reescrever $ args:

# change long options to short options

for arg; do 
    [[ "${arg:0:1}" == "-" ]] && delim="" || delim="\""
    if [ "${arg:0:2}" == "--" ]; 
       then args="${args} -${arg:2:1}" 
       else args="${args} ${delim}${arg}${delim}"
    fi
done

# reset the incoming args
eval set -- $args

# proceed as usual
while getopts ":b:la:h" OPTION; do
    .....

Obrigado por mtvee pela inspiração ;-)

commonpike
fonte
Eu não entendo o significado de eval aqui
user.friendly
1

se simplesmente é assim que você deseja chamar o script

myscript.sh --input1 "ABC" --input2 "PQR" --input2 "XYZ"

então você pode seguir esta maneira mais simples de alcançá-lo com a ajuda de getopt e --longoptions

tente isso, espero que seja útil

# Read command line options
ARGUMENT_LIST=(
    "input1"
    "input2"
    "input3"
)



# read arguments
opts=$(getopt \
    --longoptions "$(printf "%s:," "${ARGUMENT_LIST[@]}")" \
    --name "$(basename "$0")" \
    --options "" \
    -- "$@"
)


echo $opts

eval set --$opts

while true; do
    case "$1" in
    --input1)  
        shift
        empId=$1
        ;;
    --input2)  
        shift
        fromDate=$1
        ;;
    --input3)  
        shift
        toDate=$1
        ;;
      --)
        shift
        break
        ;;
    esac
    shift
done
Ashish Shetkar
fonte
0

getopts "poderia ser usado" para analisar opções longas, desde que você não espere que elas tenham argumentos ...

Veja como:

$ cat > longopt
while getopts 'e:-:' OPT; do
  case $OPT in
    e) echo echo: $OPTARG;;
    -) #long option
       case $OPTARG in
         long-option) echo long option;;
         *) echo long option: $OPTARG;;
       esac;;
  esac
done

$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test

Se você tentar usar OPTIND para obter um parâmetro para a opção long, getopts o tratará como o primeiro parâmetro posicional não opcional e interromperá a análise de outros parâmetros. Nesse caso, será melhor manipulá-lo manualmente com uma simples declaração de caso.

Isso "sempre" funcionará:

$ cat >longopt2
while (($#)); do
    OPT=$1
    shift
    case $OPT in
        --*) case ${OPT:2} in
            long1) echo long1 option;;
            complex) echo comples with argument $1; shift;;
        esac;;

        -*) case ${OPT:1} in
            a) echo short option a;;
            b) echo short option b with parameter $1; shift;;
        esac;;
    esac
done


$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test

Embora não seja tão flexível quanto getopts, você deve executar grande parte do código de verificação de erro nas instâncias do caso ...

Mas é uma opção.

estani
fonte
Mas opções longas geralmente esperam argumentos. E você pode fazer mais com o - para fazê-lo funcionar, mesmo que seja um hack. No final, pode-se argumentar que, se não o suporta de forma nativa, todas as formas de implementá-lo são uma espécie de hack, mas mesmo assim você pode estender o - também. E yes shift é muito útil, mas é claro que, se ele espera um argumento, pode acabar que o próximo argumento (se não especificado pelo usuário) fizer parte do argumento esperado.
Pryftan
sim, este é um argumento para nomes longos sem argumentos, para diferenciar entre os dois você precisa de algum tipo de configuração, como getops. E em relação à mudança, você sempre pode "colocá-la de volta" com o conjunto. De qualquer forma, deve ser configurável se um parâmetro é esperado ou não. Você pode até usar um pouco de mágica para isso, mas depois forçará os usuários a usar - para sinalizar que a mágica para e os parâmetros posicionais são iniciados, o que é pior ainda.
estani 14/08/19
Justo. Isso é mais do que razoável. Nem me lembro do que estava acontecendo e havia me esquecido totalmente dessa questão. Só me lembro vagamente como eu o encontrei. Felicidades. Ah, e marque um +1 com a ideia. Você passou pelo esforço e também esclareceu o que estava fazendo. Eu respeito as pessoas que passam pelo esforço para dar aos outros ideias etc.
Pryftan
0

O built-ingetopts analisa apenas opções curtas (exceto no ksh93), mas você ainda pode adicionar poucas linhas de script para fazer com que o getopts lide com opções longas.

Aqui está uma parte do código encontrado em http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts

  #== set short options ==#
SCRIPT_OPTS=':fbF:B:-:h'
  #== set long options associated with short one ==#
typeset -A ARRAY_OPTS
ARRAY_OPTS=(
    [foo]=f
    [bar]=b
    [foobar]=F
    [barfoo]=B
    [help]=h
    [man]=h
)

  #== parse options ==#
while getopts ${SCRIPT_OPTS} OPTION ; do
    #== translate long options to short ==#
    if [[ "x$OPTION" == "x-" ]]; then
        LONG_OPTION=$OPTARG
        LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2)
        LONG_OPTIND=-1
        [[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1)
        [[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND"
        OPTION=${ARRAY_OPTS[$LONG_OPTION]}
        [[ "x$OPTION" = "x" ]] &&  OPTION="?" OPTARG="-$LONG_OPTION"

        if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then
            if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then 
                OPTION=":" OPTARG="-$LONG_OPTION"
            else
                OPTARG="$LONG_OPTARG";
                if [[ $LONG_OPTIND -ne -1 ]]; then
                    [[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 ))
                    shift $OPTIND
                    OPTIND=1
                fi
            fi
        fi
    fi

    #== options follow by another option instead of argument ==#
    if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then 
        OPTARG="$OPTION" OPTION=":"
    fi

    #== manage options ==#
    case "$OPTION" in
        f  ) foo=1 bar=0                    ;;
        b  ) foo=0 bar=1                    ;;
        B  ) barfoo=${OPTARG}               ;;
        F  ) foobar=1 && foobar_name=${OPTARG} ;;
        h ) usagefull && exit 0 ;;
        : ) echo "${SCRIPT_NAME}: -$OPTARG: option requires an argument" >&2 && usage >&2 && exit 99 ;;
        ? ) echo "${SCRIPT_NAME}: -$OPTARG: unknown option" >&2 && usage >&2 && exit 99 ;;
    esac
done
shift $((${OPTIND} - 1))

Aqui está um teste:

# Short options test
$ ./foobar_any_getopts.sh -bF "Hello world" -B 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello world
files=file1 file2

# Long and short options test
$ ./foobar_any_getopts.sh --bar -F Hello --barfoo 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello
files=file1 file2

Caso contrário, no recente Korn Shell ksh93, é getoptspossível analisar naturalmente opções longas e até exibir uma página de manual. (Consulte http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options )

Michel VONGVILAY uxora.com
fonte
0

Th built-in OS X (BSD) getopt não suporta opções longas, mas a versão GNU faz: brew install gnu-getopt. Então, algo semelhante a: cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt.

wprl
fonte
0

O EasyOptions lida com opções curtas e longas:

## Options:
##   --verbose, -v   Verbose mode
##   --logfile=NAME  Log filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "log file: ${logfile}"
    echo "arguments: ${arguments[@]}"
fi
Renato Silva
fonte