Utilizzo di getopts nello script della shell bash per ottenere opzioni della riga di comando lunghe e brevi

Desidero avere forms lunghe e brevi di opzioni della riga di comando invocate usando il mio script di shell.

So che i getopts possono essere usati, ma come in Perl, non sono stato in grado di fare lo stesso con shell.

Qualche idea su come questo può essere fatto, in modo che io possa usare opzioni come:

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

In quanto sopra, entrambi i comandi significano la stessa cosa per la mia shell, ma usando getopts , non sono stato in grado di implementarli?

Il bash getopts incorporato non supporta i nomi di opzioni lunghe con il prefisso double-dash. Supporta solo opzioni a singolo carattere.

C’è uno strumento di shell getopt che è un altro programma, non un builtin bash. L’implementazione GNU di getopt(3) (usata dalla riga di comando getopt(1) su Linux) supporta l’analisi di opzioni lunghe.

Ma l’implementazione BSD di getopt (ad esempio su Mac OS X) non supporta le opzioni lunghe.


Alcune altre risposte mostrano una soluzione per l’utilizzo dei getop incorporati in bash per simulare le opzioni lunghe. Quella soluzione fa in realtà una breve opzione il cui carattere è “-“. Quindi ottieni “-” come bandiera. Quindi tutto ciò che segue diventa OPTARG e testate OPTARG con un case annidato.

Questo è intelligente, ma viene fornito con avvertimenti:

  • getopts non può far rispettare le specifiche opt. Non può restituire errori se l’utente fornisce un’opzione non valida. Devi eseguire il tuo controllo degli errori mentre analizzi OPTARG.
  • OPTARG è usato per il nome dell’opzione lunga, che complica l’utilizzo quando l’opzione lunga stessa ha un argomento. Finisci per dover codificare te stesso come caso aggiuntivo.

Quindi, mentre è ansible scrivere più codice per aggirare la mancanza di supporto per le opzioni lunghe, questo è molto più lavoro e in parte vanifica lo scopo di usare un parser getopt per semplificare il codice.

getopt e getopts sono animali diversi, e la gente sembra avere un po ‘di incomprensione su quello che fanno. getopts è un comando incorporato per bash per elaborare le opzioni della riga di comando in un ciclo e assegnare a ciascuna opzione e valore trovati a sua volta variabili incorporate, in modo da poterle elaborare ulteriormente. getopt , tuttavia, è un programma di utilità esterno e in effetti non elabora le opzioni per te come fanno, ad esempio, i Getopt bash, il modulo Perl Getopt oi moduli optparse / argparse Python. Tutto ciò che getopt fa è canonicalizzare le opzioni che vengono passate – cioè convertirle in una forma più standard, in modo che sia più facile per uno script di shell elaborarle. Ad esempio, un’applicazione di getopt potrebbe convertire quanto segue:

 myscript -ab infile.txt -ooutfile.txt 

in questo:

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

Devi fare l’elaborazione vera e propria. Non è necessario utilizzare getopt tutto se si applicano varie restrizioni sul modo in cui è ansible specificare le opzioni:

  • metti solo un’opzione per argomento;
  • tutte le opzioni precedono qualsiasi parametro posizionale (cioè argomenti non opzionali);
  • per le opzioni con valori (ad esempio -o sopra), il valore deve andare come argomento separato (dopo uno spazio).

Perché usare getopt invece di getopts ? La ragione di base è che solo GNU getopt offre supporto per le opzioni della riga di comando con nome lungo. 1 (GNU getopt è l’impostazione predefinita su Linux: Mac OS X e FreeBSD hanno un getopt semplice e non molto utile, ma la versione GNU può essere installata, vedi sotto).

Ad esempio, ecco un esempio di utilizzo di GNU getopt , da una mia javawrap chiamata 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' -- "[email protected]"` 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 

Questo ti permette di specificare opzioni come --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt" o simili. L’effetto della chiamata a getopt è di canonicalizzare le opzioni per --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt" modo da poter elaborare più facilmente loro. Il quoting intorno a "$1" e "$2" è importante in quanto garantisce che gli argomenti con spazi in essi vengano gestiti correttamente.

Se elimini le prime 9 linee (tutto attraverso la linea di eval set ), il codice funzionerà ancora ! Tuttavia, il tuo codice sarà molto più selettivo nel tipo di opzioni che accetta: in particolare, dovrai specificare tutte le opzioni nel modulo “canonico” sopra descritto. Con l’uso di getopt , tuttavia, puoi raggruppare le opzioni a lettera singola, usare forms più corte non ambigue di opzioni lunghe, usare lo stile --file foo.txt o --file=foo.txt , usa il -m 4096 o -m4096 , mix opzioni e non-opzioni in qualsiasi ordine, ecc. getopt emette anche un messaggio di errore se vengono trovate opzioni non riconosciute o ambigue.

NOTA : Esistono in realtà due versioni completamente diverse di getopt , getopt base e getopt GNU, con caratteristiche diverse e diverse convenzioni di chiamata. 2 getopt base è abbastanza rotto: non solo non gestisce le opzioni lunghe, ma non può nemmeno gestire gli spazi incorporati all’interno di argomenti o argomenti vuoti, mentre getopts fa correttamente. Il codice sopra non funzionerà in getopt base. GNU getopt è installato di default su Linux, ma su Mac OS X e FreeBSD deve essere installato separatamente. Su Mac OS X, installa MacPorts ( http://www.macports.org ) e poi sudo port install getopt per installare GNU getopt (di solito in /opt/local/bin ) e assicurati che /opt/local/bin è nel percorso della shell davanti a /usr/bin . Su FreeBSD, installa misc/getopt .

Una guida rapida per modificare il codice di esempio per il tuo programma: tra le prime righe, tutto è “boilerplate” che dovrebbe rimanere lo stesso, tranne la linea che chiama getopt . Dovresti cambiare il nome del programma dopo -n , specificare le opzioni brevi dopo -o e le opzioni lunghe dopo --long . Metti i due punti dopo le opzioni che prendono un valore.

Infine, se vedi il codice appena set invece di eval set , è stato scritto per getopt BSD. Dovresti cambiarlo per usare lo stile eval set , che funziona bene con entrambe le versioni di getopt , mentre il set semplice non funziona correttamente con GNU getopt .

1 In realtà, getopts in ksh93 supporta le opzioni con ksh93 lunghi, ma questa shell non viene utilizzata tutte le volte che bash . In zsh , usa zparseopts per ottenere questa funzionalità.

2 Tecnicamente, “GNU getopt ” è un termine improprio; questa versione è stata scritta per Linux piuttosto che per il progetto GNU. Tuttavia, segue tutte le convenzioni GNU, e il termine “GNU getopt ” è comunemente usato (ad esempio su FreeBSD).

La funzione getopts di Bash built può essere utilizzata per analizzare le opzioni lunghe inserendo un carattere tratteggiato seguito da due punti nell’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[=]]" >&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 

Dopo aver copiato il nome del file eseguibile = getopts_test.sh nella directory di lavoro corrente , si può produrre un output simile

 $ ./getopts_test.sh $ ./getopts_test.sh -f Non-option argument: '-f' $ ./getopts_test.sh -h usage: code/getopts_test.sh [-v] [--loglevel[=]] $ ./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' 

Ovviamente getopts non esegue né il controllo OPTERR né l’analisi degli argomenti delle opzioni per le opzioni lunghe. Il frammento di script sopra mostra come questo può essere fatto manualmente. Il principio di base funziona anche nella shell di Debian Almquist (“trattino”). Nota il caso speciale:

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

Nota che, come indica GreyCat da over a http://mywiki.wooledge.org/BashFAQ , questo trucco sfrutta un comportamento non standard della shell che consente l’argomento dell’opzione (cioè il nome del file in “-f filename”) da concatenare all’opzione (come in “-ffilename”). Lo standard POSIX dice che deve esserci uno spazio tra loro, che nel caso di “- longoption” terminerebbe l’opzione-parsing e trasformsrebbe tutte le longoptions in argomenti non-option.

Il comando getopts integrato è ancora, AFAIK, limitato alle sole opzioni a carattere singolo.

C’è (o in passato) un programma esterno getopt che riorganizza un insieme di opzioni in modo tale da renderlo più semplice da analizzare. È ansible adattare il design per gestire anche le opzioni lunghe. Esempio di utilizzo:

 aflag=no bflag=no flist="" set -- $(getopt abf: "[email protected]") 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 ... 

Potresti usare uno schema simile con un comando getoptlong .

Si noti che la debolezza fondamentale con il programma getopt esterno è la difficoltà di gestire argomenti con spazi in essi, e nel preservare questi spazi in modo accurato. Questo è il motivo per cui i getopts integrati sono superiori, anche se limitati dal fatto che gestisce solo opzioni a lettera singola.

Ecco un esempio che utilizza effettivamente getopt con opzioni lunghe:

 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: -- "[email protected]") 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 

Le opzioni lunghe possono essere analizzate dai getopts standard come “argomenti” nella “opzione”

Questa è una shell POSIX portatile e nativa – non sono necessari programmi o bashismi esterni.

Questa guida implementa lunghe opzioni come argomenti all’opzione - , quindi --alpha è visto da getopts come - con l’argomento alpha e --bravo=foo è visto come - con argomento bravo=foo . Il vero argomento può essere raccolto con una semplice sostituzione: ${OPTARG#*=} .

In questo esempio, -b (e la sua forma lunga, --bravo ) ha un’opzione obbligatoria (si noti la ricostruzione manuale dell’applicazione del modulo lungo). Le opzioni non booleane per gli argomenti lunghi vengono dopo i segni di uguale, ad esempio --bravo=foo (i delimitatori di spazio per le opzioni lunghe sarebbero difficili da implementare).

Poiché questo utilizza getopts , questa soluzione supporta l’utilizzo come cmd -ac --bravo=foo -d FILE (che ha combinato le opzioni -a e – c e interleava le opzioni lunghe con le opzioni standard) mentre la maggior parte delle altre risposte qui combatte o non riesce quella.

 while getopts ab:c-: arg; do case $arg in a ) ARG_A=true ;; b ) ARG_B="$OPTARG" ;; c ) ARG_C=true ;; - ) LONG_OPTARG="${OPTARG#*=}" case $OPTARG in alpha ) ARG_A=true ;; bravo=?* ) ARG_B="$LONG_OPTARG" ;; bravo* ) echo "No arg for --$OPTARG option" >&2; exit 2 ;; charlie ) ARG_C=true ;; alpha* | charlie* ) echo "No arg allowed for --$OPTARG option" >&2; exit 2 ;; '' ) break ;; # "--" terminates argument processing * ) echo "Illegal option --$OPTARG" >&2; exit 2 ;; esac ;; \? ) exit 2 ;; # getopts already reported the illegal option esac done shift $((OPTIND-1)) # remove parsed options and args from [email protected] list 

Quando l’argomento è un trattino ( - ), ha altri due componenti: il nome del flag e (facoltativamente) il suo argomento. Delimino questi il ​​modo standard con cui ogni comando sarebbe, con il primo segno di uguale ( = ). $LONG_OPTARG è quindi semplicemente il contenuto di $OPTARG senza il nome della bandiera o il segno di uguale.

La case interna implementa manualmente le opzioni lunghe, quindi necessita di alcune pulizie:

  • bravo=? corrisponde a --bravo=foo ma non --bravo= (nota: il case ferma dopo la prima corrispondenza)
  • bravo* segue, --bravo nota dell’argomento richiesto mancante in --bravo e --bravo=
  • alpha* | charlie* alpha* | charlie* prende gli argomenti dati alle opzioni che non li supportano
  • '' è presente per supportare le opzioni che iniziano con dei trattini
  • * cattura tutte le altre opzioni lunghe e ricrea l’errore generato da getopts per un’opzione non valida

Non hai necessariamente bisogno di tutti quegli articoli di pulizia. Per esempio, forse vuoi --bravo avere un argomento opzionale (che -b non può supportare a causa di una limitazione in getopts ). Rimuovere semplicemente il =? e il relativo caso di errore, quindi chiama ${ARG_B:=$DEFAULT_ARG_B} la prima volta che utilizzi $ARG_B .

Dai un’occhiata a shFlags che è una libreria di shell portatile (che significa: sh, bash, dash, ksh, zsh su Linux, Solaris, ecc.).

Rende l’aggiunta di nuovi flag semplici come l’aggiunta di una riga allo script e fornisce una funzione di utilizzo generata automaticamente.

Ecco un semplice 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 "[email protected]" || exit 1 eval set -- "${FLAGS_ARGV}" # say hello echo "Hello, ${FLAGS_name}!" 

Per i sistemi operativi che hanno il getopt avanzato che supporta le opzioni lunghe (ad es. Linux), puoi fare:

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

Per il resto, è necessario utilizzare l’opzione breve:

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

Aggiungere un nuovo flag è semplice come aggiungere una nuova DEFINE_ call .

Usando getopts con opzioni e argomenti short / long


Funziona con tutte le combinazioni, eG:

  • foobar -f –bar
  • foobar –foo -b
  • foobar -bf –bar –foobar
  • foobar -fbFBAshorty –bar -FB –arguments = longhorn
  • foobar -fA “text shorty” -B –arguments = “testo longhorn”
  • bash foobar -F –barfoo
  • sh foobar -B –foobar – …
  • bash ./foobar -F –bar

Alcune dichiarazioni per questi esempi

 [email protected] 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 

Come apparirà la funzione Usage

 function _usage() { ###### USAGE : Help and ERROR ###### cat < 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 con getops long / short e argomenti lunghi

 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 

Produzione

 ################################################################## 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 quanto sopra in una sceneggiatura coesa

 #!/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 these example ###### [email protected] 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() { ###### USAGE : Help and ERROR ###### cat < 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 

Ho risolto questo modo:

 # A string with command options [email protected] # 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; 

Sono stupido o qualcosa del genere? getopt e getopts sono così confusi.

Un altro modo…

 # 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 

Nel caso in cui non si desideri la dipendenza getopt , è ansible farlo:

 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') "[email protected]" continue ;; # Done with options *) break ;; esac # for testing purposes: echo "$1" shift done 

Of course, then you can’t use long style options with one dash. And if you want to add shortened versions (eg –verbos instead of –verbose), then you need to add those manually.

But if you are looking to get getopts functionality along with long options, this is a simple way to do it.

I also put this snippet in a gist .

The built-in getopts can’t do this. There is an external getopt (1) program that can do this, but you only get it on Linux from the util-linux package. It comes with an example script getopt-parse.bash .

There is also a getopts_long written as a shell function.

 #!/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]==> [email protected]" 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 

In ksh93 , getopts does support long names…

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

Or so the tutorials I have found have said. Try it and see.

Inventing yet another version of the wheel…

This function is a (hopefully) POSIX-compatible plain bourne shell replacement for GNU getopt. It supports short/long options which can accept mandatory/optional/no arguments, and the way in which options are specified is almost identical to GNU getopt, so conversion is trivial.

Of course this is still a sizeable chunk of code to drop into a script, but it’s about half the lines of the well-known getopt_long shell function, and might be preferable in cases where you just want to replace existing GNU getopt uses.

This is pretty new code, so YMMV (and definitely please let me know if this isn’t actually POSIX-compatible for any reason — portability was the intention from the outset, but I don’t have a useful POSIX test environment).

Code and example usage follows:

 #!/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 "[email protected]") # 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" -- "[email protected]") # eval set -- ${opts} # # We instead use: # opts=$(posix_getopt "$shortopts" "$longopts" "[email protected]") # eval "set -- ${opts}" posix_getopt () { # args: "$shortopts" "$longopts" "[email protected]" 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 "[email protected]")" 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 "[email protected]")" 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 } 

Esempio di utilizzo:

 # 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)" -- "[email protected]") opts=$(posix_getopt "$shortopts" "$longopts" "[email protected]") 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 

Here you can find a few different approaches for complex option parsing in bash: http://mywiki.wooledge.org/ComplexOptionParsing

I did create the following one, and I think it’s a good one, because it’s minimal code and both long and short options work. A long option can also have multiple arguments with this approach.

 #!/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 in 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[=]]" >&2 exit 2 ;; esac break; done done # End of file 

I only write shell scripts now and then and fall out of practice, so any feedback is appreciated.

Using the strategy proposed by @Arvid Requate, we noticed some user errors. A user who forgets to include a value will accidentally have the next option’s name treated as a value:

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

will cause the value of “loglevel” to be seen as “–toc=TRUE”. This can be avoided.

I adapted some ideas about checking user error for CLI from http://mwiki.wooledge.org/BashFAQ/035 discussion of manual parsing. I inserted error checking into handling both “-” and “–” arguments.

Then I started fiddling around with the syntax, so any errors in here are strictly my fault, not the original authors.

My approach helps users who prefer to enter long with or without the equal sign. That is, it should have same response to “–loglevel 9” as “–loglevel=9”. In the –/space method, it is not possible to know for sure if the user forgets an argument, so some guessing is needed.

  1. if the user has the long/equal sign format (–opt=), then a space after = triggers an error because an argument was not supplied.
  2. if user has long/space arguments (–opt ), this script causes a fail if no argument follows (end of command) or if argument begins with dash)

In case you are starting out on this, there is an interesting difference between “–opt=value” and “–opt value” formats. With the equal sign, the command line argument is seen as “opt=value” and the work to handle that is string parsing, to separate at the “=”. In contrast, with “–opt value”, the name of the argument is “opt” and we have the challenge of getting the next value supplied in the command line. That’s where @Arvid Requate used ${!OPTIND}, the indirect reference. I still don’t understand that, well, at all, and comments in BashFAQ seem to warn against that style ( http://mywiki.wooledge.org/BashFAQ/006 ). BTW, I don’t think previous poster’s comments about importance of OPTIND=$(( $OPTIND + 1 )) are correct. I mean to say, I see no harm from omitting it.

In newest version of this script, flag -v means VERBOSE printout.

Save it in a file called “cli-5.sh”, make executable, and any of these will work, or fail in the desired way

 ./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 

Here is example output of the error-checking on user 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 

You should consider turning on -v, because it prints out internals of OPTIND and OPTARG

 #/usr/bin/env bash ## Paul Johnson ## 20171016 ## ## Combines ideas from ## https://stackoverflow.com/questions/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[=]] [--toc[=]]" >&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[=]] [--toc[=]]" >&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" 

I have been working on that subject for quite a long time… and made my own library which you will need to source in your main script. See libopt4shell and cd2mpc for an example. Hope it helps !

An improved solution:

 # 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 

Maybe it’s simpler to use ksh, just for the getopts part, if need long command line options, as it can be easier done there.

 # 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 

I wanted something without external dependencies, with strict bash support (-u), and I needed it to work on even the older bash versions. This handles various types of params:

  • short bools (-h)
  • short options (-i “image.jpg”)
  • long bools (–help)
  • equals options (–file=”filename.ext”)
  • space options (–file “filename.ext”)
  • concatinated bools (-hvm)

Just insert the following at the top of your 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&2 fi shift $shift_count done } 

And use it like so:

 # 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 $* 

I don’t have enough rep yet to comment or vote his solution up, but sme’s answer worked extremely well for me. The only issue I ran into was that the arguments end up wrapped in single-quotes (so I have an strip them out).

I also added some example usages and HELP text. I’ll included my slightly extended version here:

 #!/bin/bash # getopt example # from: https://stackoverflow.com/questions/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: -- "[email protected]") 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 

In order to stay cross-platform compatible, and avoid the reliance on external executables, I ported some code from another language.

I find it very easy to use, here is an example:

 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 "[email protected]" 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 )" 

The required BASH is a little longer than it could be, but I wanted to avoid reliance on BASH 4’s associative arrays. You can also download this directly from 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: "[email protected]" 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  into  # 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 "[email protected]" 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 "[email protected]" fi 

If all your long options have unique, and matching, first characters as the short options, so for example

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

Equivale a

 ./slamm -c 23 -p test -q 

You can use this before getopts to rewrite $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 ..... 

Thanks for mtvee for the inspiration 😉

Builtin getopts only parse short options (except in ksh93), but you can still add few lines of scripting to make getopts handles long options.

Here is a part of code found in 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)) 

Here is a test:

 # 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 

Otherwise in recent Korn Shell ksh93, getopts can naturally parse long options and even display a man page alike. (See http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options )

Th built-in OS X (BSD) getopt does not support long options, but the GNU version does: brew install gnu-getopt . Then, something similar to: cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt .

EasyOptions handles short and long options:

 ## 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 

getopts “could be used” for parsing long options as long as you don’t expect them to have arguments…

Here’s how to:

 $ 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 

If you try to use OPTIND for getting a parameter for the long option, getopts will treat it as the first no optional positional parameter and will stop parsing any other parameters. In such a case you’ll be better off handling it manually with a simple case statement.

This will “always” work:

 $ 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 

Albeit is not as flexible as getopts and you have to do much of the error checking code yourself within the case instances…

But it is an option.

hm.

not really satisfied with the pure bash options. why not use perl to get what you want. Directly parse the $* array, and auto-name your options.

simple helper script:

 #!/usr/bin/perl use Getopt::Long; my $optstring = shift; my @opts = split(m#,#, $optstring); my %opt; GetOptions(\%opt, @opts); print "set -- " . join(' ', map("'$_'", @ARGV)) . ";"; my $xx; my $key; foreach $key (keys(%opt)) { print "export $key='$opt{$key}'; "; } 

then you can use in your script as a one liner, for example:

 #!/bin/bash eval `getopts.pl reuse:s,long_opt:s,hello $*`; echo "HELLO: $hello" echo "LONG_OPT: $long_opt" echo "REUSE: $reuse" echo $* 

/tmp/script.sh hello –reuse me –long_opt whatever_you_want_except_spaces –hello 1 2 3

HELLO: 1 LONG_OPT: whatever_you_want_except spaces REUSE: me

1 2 3

Only caveat here is spaces don’t work. But it avoids bash’s rather complicated looping syntax, works with long args, auto-names them as variables and automatically resizes $*, so will work 99% of the time.