Bagaimana cara mem-parsing argumen baris perintah di Bash?

1922

Katakanlah, saya memiliki skrip yang dipanggil dengan baris ini:

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

atau yang ini:

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

Apa cara yang diterima parsing ini sehingga dalam setiap kasus (atau beberapa kombinasi dari dua) $v, $fdan $dsemua akan diatur truedan $outFileakan sama dengan /fizz/someOtherFile?

Lawrence Johnston
sumber
1
Untuk pengguna zsh ada builtin hebat yang disebut zparseopts yang bisa dilakukan: zparseopts -D -E -M -- d=debug -debug=dDan memiliki keduanya -ddan --debugdalam $debugarray echo $+debug[1]akan mengembalikan 0 atau 1 jika salah satu dari mereka digunakan. Ref: zsh.org/mla/users/2011/msg00350.html
dezza
1
Tutorial yang sangat bagus: linuxcommand.org/lc3_wss0120.php . Saya terutama menyukai contoh "Opsi Perintah Baris".
Gabriel Staples

Jawaban:

2677

Metode # 1: Menggunakan bash tanpa getopt [s]

Dua cara umum untuk menyampaikan argumen pasangan kunci-nilai adalah:

Bash Space-Separated (eg, --option argument) (tanpa getopt [s])

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

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

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

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

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

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

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

output dari copy-paste blok di atas:

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

Bash Equals-Separated (eg, --option=argument) (tanpa getopt [s])

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

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

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

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

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

output dari copy-paste blok di atas:

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

Untuk lebih memahami ${i#*=}pencarian "Penghapusan Substring" dalam panduan ini . Secara fungsional ini setara dengan `sed 's/[^=]*=//' <<< "$i"`panggilan subproses yang tidak perlu atau `echo "$i" | sed 's/[^=]*=//'`yang memanggil dua subproses yang tidak perlu.

Metode # 2: Menggunakan bash dengan getopt [s]

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

batasan getopt (1) ( getoptversi lama, relatif baru ):

  • tidak dapat menangani argumen yang merupakan string kosong
  • tidak dapat menangani argumen dengan spasi putih tertanam

getoptVersi yang lebih baru tidak memiliki batasan ini.

Selain itu, shell POSIX (dan lainnya) menawarkan getoptsyang tidak memiliki batasan ini. Saya telah memasukkan getoptscontoh sederhana .

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

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

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

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

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

shift $((OPTIND-1))

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

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

chmod +x /tmp/demo-getopts.sh

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

output dari copy-paste blok di atas:

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

Keuntungannya getoptsadalah:

  1. Ini lebih portabel, dan akan bekerja di cangkang lain seperti dash.
  2. Itu dapat menangani beberapa opsi tunggal seperti -vf filenamedalam cara Unix yang khas, secara otomatis.

Kerugiannya getoptsadalah ia hanya bisa menangani opsi pendek ( -h, tidak --help) tanpa kode tambahan.

Ada tutorial getopts yang menjelaskan apa arti semua sintaks dan variabel. Di bash, ada juga help getopts, yang mungkin informatif.

Bruno Bronosky
sumber
44
Apakah ini benar? Menurut Wikipedia ada versi yang ditingkatkan GNU baru getoptyang mencakup semua fungsi getoptsdan beberapa. man getoptpada Ubuntu 13,04 keluaran getopt - parse command options (enhanced)seperti namanya, jadi saya kira versi yang disempurnakan ini adalah standar sekarang.
Livven
47
Sesuatu yang merupakan cara tertentu pada sistem Anda adalah premis yang sangat lemah untuk mendasarkan asumsi "menjadi standar".
szablica
13
@Livven, itu getoptbukan utilitas GNU, itu bagian dari util-linux.
Stephane Chazelas
4
Jika Anda menggunakan -gt 0, hapus shiftsetelah Anda esac, shifttambahkan semua dengan 1 dan tambahkan case ini: *) break;;Anda dapat menangani argumen yang tidak opsional. Contoh: pastebin.com/6DJ57HTc
Nicolas Lacombe
2
Anda tidak menggema –default. Dalam contoh pertama, saya perhatikan bahwa jika –defaultargumen terakhir, itu tidak diproses (dianggap sebagai non-opt), kecuali while [[ $# -gt 1 ]]ditetapkan sebagai while [[ $# -gt 0 ]]
kolydart
562

Tidak ada jawaban yang menyebutkan getopt yang ditingkatkan . Dan jawaban yang terpilih adalah menyesatkan: ia mengabaikan -⁠vfdopsi gaya pendek (diminta oleh OP) atau opsi setelah argumen posisi (juga diminta oleh OP); dan mengabaikan kesalahan parsing. Sebagai gantinya:

  • Gunakan ditingkatkan getoptdari util-linux atau sebelumnya GNU glibc . 1
  • Ini bekerja dengan getopt_long()fungsi C GNU glibc.
  • Memiliki semua fitur pembeda yang berguna (yang lain tidak memilikinya):
    • menangani spasi, mengutip karakter, dan bahkan biner dalam argumen 2 (tidak ditingkatkan getopttidak dapat melakukan ini)
    • itu dapat menangani opsi di akhir: script.sh -o outFile file1 file2 -v( getoptstidak melakukan ini)
    • memungkinkan =opsi-gaya panjang: script.sh --outfile=fileOut --infile fileIn(memungkinkan keduanya panjang jika parsing sendiri)
    • memungkinkan opsi pendek gabungan, mis. -vfd(pekerjaan nyata jika parsing sendiri)
    • memungkinkan menyentuh argumen-opsi, misalnya -oOutfileatau-vfdoOutfile
  • Begitu lama sudah 3 bahwa tidak ada sistem GNU yang hilang ini (misalnya setiap Linux memiliki itu).
  • Anda dapat menguji keberadaannya dengan: getopt --test→ mengembalikan nilai 4.
  • Lain getoptatau built-in shell getoptsadalah penggunaan terbatas.

Panggilan berikut

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

semua kembali

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

dengan berikut ini myscript

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

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

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

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

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

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

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

1 getopt yang ditingkatkan tersedia di sebagian besar "sistem-bash", termasuk Cygwin; pada OS X coba buat instal gnu-getopt atausudo port install getopt
2exec() konvensiPOSIXtidak memiliki cara yang dapat diandalkan untuk meneruskan NULL biner dalam argumen baris perintah; byte tersebut sebelum waktunya mengakhiri argumen
3 versi pertama yang dirilis pada tahun 1997 atau sebelumnya (saya hanya melacaknya kembali ke 1997)

Robert Siemer
sumber
4
Terima kasih untuk ini. Baru saja dikonfirmasi dari tabel fitur di en.wikipedia.org/wiki/Getopts , jika Anda memerlukan dukungan untuk opsi yang panjang, dan Anda tidak menggunakan Solaris, getoptadalah jalan yang harus diambil.
johncip
4
Saya percaya bahwa satu-satunya peringatan dengan getoptadalah bahwa hal itu tidak dapat digunakan dengan mudah dalam skrip pembungkus di mana orang mungkin memiliki beberapa opsi khusus untuk skrip pembungkus, dan kemudian meneruskan opsi skrip non-pembungkus ke executable yang dibungkus, utuh. Katakanlah saya memiliki greppembungkus yang dipanggil mygrepdan saya memiliki opsi --fookhusus untuk mygrep, maka saya tidak dapat melakukannya mygrep --foo -A 2, dan -A 2meneruskannya secara otomatis ke grep; Saya perlu melakukannya mygrep --foo -- -A 2. Inilah implementasi saya di atas solusi Anda.
Kaushal Modi
2
@ bobpaul Pernyataan Anda tentang util-linux juga salah dan menyesatkan: paket ditandai "esensial" di Ubuntu / Debian. Karena itu, selalu diinstal. - Distro mana yang Anda bicarakan (di mana menurut Anda perlu diinstal dengan sengaja)?
Robert Siemer
3
Perhatikan ini tidak berfungsi pada Mac setidaknya hingga saat ini 10.14.3. Getopt yang dikirimkan adalah BSD getopt dari 1999 ...
jjj
2
@transang Boolean negasi dari nilai kembali. Dan efek sampingnya: biarkan perintah gagal (jika tidak errexit akan membatalkan program karena kesalahan). - Komentar dalam skrip memberi tahu Anda lebih banyak. Sebaliknya:man bash
Robert Siemer
144

Cara yang lebih ringkas

script.sh

#!/bin/bash

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

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

Pemakaian:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify
Inanc Gumus
sumber
3
Inilah yang sedang saya lakukan. Harus while [[ "$#" > 1 ]]jika saya ingin mendukung mengakhiri garis dengan bendera boolean ./script.sh --debug dev --uglify fast --verbose. Contoh: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
12
Wow! Sederhana dan bersih! Ini adalah bagaimana saya menggunakan ini: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli
2
Ini jauh lebih baik untuk ditempelkan ke setiap skrip daripada berurusan dengan sumber atau membuat orang bertanya-tanya di mana sebenarnya fungsi Anda dimulai.
RealHandy
Peringatan: ini mentolerir argumen yang digandakan, argumen terbaru berlaku. misalnya ./script.sh -d dev -d prodakan menghasilkan deploy == 'prod'. Saya tetap menggunakannya: P :): +1:
yair
Saya menggunakan ini (terima kasih!) Tetapi perhatikan bahwa ini memungkinkan nilai argumen kosong, misalnya ./script.sh -dtidak akan menghasilkan kesalahan tetapi hanya disetel $deployke string kosong.
EM0
137

dari: digitalpeer.com dengan sedikit modifikasi

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

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

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

Untuk lebih memahami ${i#*=}pencarian "Penghapusan Substring" dalam panduan ini . Secara fungsional ini setara dengan `sed 's/[^=]*=//' <<< "$i"`panggilan subproses yang tidak perlu atau `echo "$i" | sed 's/[^=]*=//'`yang memanggil dua subproses yang tidak perlu.

guneysus
sumber
4
Rapi! Meskipun ini tidak akan berfungsi untuk argumen yang dipisahkan ruang à la mount -t tempfs .... Seseorang mungkin dapat memperbaiki ini melalui sesuatu seperti while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;dll
Tobias Kienzler
3
Ini tidak dapat menangani -vfdkombinasi opsi pendek gaya.
Robert Siemer
105

getopt()Saya getopts()adalah pilihan yang baik. Dicuri dari sini :

Penggunaan sederhana "getopt" ditunjukkan dalam skrip mini ini:

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

Apa yang telah kami katakan adalah bahwa setiap -a, -b, -c atau -d akan diizinkan, tetapi -c diikuti oleh argumen ("c:" mengatakan itu).

Jika kami menyebutnya "g" dan mencobanya:

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

Kita mulai dengan dua argumen, dan "getopt" memecah opsi dan menempatkan masing-masing dalam argumennya sendiri. Itu juga menambahkan "-".

Matt J
sumber
4
Penggunaan $*rusak penggunaan getopt. (Ini menyembunyikan argumen dengan spasi.) Lihat jawaban saya untuk penggunaan yang tepat.
Robert Siemer
Mengapa Anda ingin membuatnya lebih rumit?
SDsolar
@ Mat J, bagian pertama dari skrip (untuk saya) akan dapat menangani argumen dengan spasi di dalamnya jika Anda menggunakan "$ i" bukannya $ i. Getopt tampaknya tidak mampu menangani argumen dengan spasi. Apa keuntungan menggunakan getopt daripada for i loop?
thebunnyrules
99

Dengan risiko menambahkan contoh lain untuk diabaikan, inilah skema saya.

  • menangani -n argdan--name=arg
  • memungkinkan argumen di akhir
  • menunjukkan kesalahan waras jika ada yang salah eja
  • kompatibel, tidak menggunakan bashism
  • dapat dibaca, tidak perlu mempertahankan status dalam satu lingkaran

Semoga bermanfaat bagi seseorang.

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

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

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done
bronson
sumber
4
Maaf atas keterlambatannya. Dalam skrip saya, fungsi handle_argument menerima semua argumen non-opsi. Anda bisa mengganti baris itu dengan apa pun yang Anda inginkan, mungkin *) die "unrecognized argument: $1"atau mengumpulkan args menjadi variabel *) args+="$1"; shift 1;;.
bronson
Luar biasa! Saya telah menguji beberapa jawaban, tetapi ini adalah satu-satunya yang bekerja untuk semua kasus, termasuk banyak parameter posisi (baik sebelum dan sesudah bendera)
Guilherme Garnier
2
kode ringkas yang bagus, tetapi menggunakan -n dan tidak ada argumen lain yang menyebabkan infinite loop karena kesalahan aktif shift 2, mengeluarkan shiftdua kali alih-alih shift 2. Menyarankan hasil edit.
lauksas
42

Saya terlambat sekitar 4 tahun untuk pertanyaan ini, tetapi ingin memberi kembali. Saya menggunakan jawaban sebelumnya sebagai titik awal untuk merapikan parameter parsing adhoc lama saya. Saya kemudian refactored keluar kode templat berikut. Ia menangani params panjang dan pendek, menggunakan argumen = atau spasi terpisah, serta beberapa params pendek dikelompokkan bersama. Akhirnya ia memasukkan kembali argumen non-param kembali ke variabel $ 1, $ 2 .. Semoga bermanfaat.

#!/usr/bin/env bash

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

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


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

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


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done
Shane Day
sumber
Kode ini tidak dapat menangani pilihan dengan argumen seperti ini: -c1. Dan penggunaan =untuk memisahkan opsi pendek dari argumen mereka tidak biasa ...
Robert Siemer
2
Saya mengalami dua masalah dengan potongan kode yang berguna ini: 1) "shift" dalam kasus "-c = foo" akhirnya memakan parameter berikutnya; dan 2) 'c' tidak boleh dimasukkan dalam pola "[cfr]" untuk opsi pendek yang dapat digabung.
sfnd
36

Saya telah menemukan masalah untuk menulis parsing portabel dalam skrip yang sangat membuat frustasi sehingga saya telah menulis Argbash - generator kode FOSS yang dapat menghasilkan kode parsing argumen untuk skrip Anda plus memiliki beberapa fitur yang bagus:

https://argbash.io

bubla
sumber
Terima kasih telah menulis argbash, saya hanya menggunakannya dan ternyata berfungsi dengan baik. Saya kebanyakan menggunakan argbash karena merupakan pembuat kode yang mendukung bash 3.x yang lebih lama ditemukan pada OS X 10.11 El Capitan. Satu-satunya downside adalah bahwa pendekatan generator kode berarti cukup banyak kode dalam skrip utama Anda, dibandingkan dengan memanggil modul.
RichVel
Anda benar-benar dapat menggunakan Argbash dengan cara yang menghasilkan parsing pustaka yang dibuat khusus hanya untuk Anda yang dapat Anda sertakan dalam skrip Anda atau Anda dapat memilikinya dalam file yang terpisah dan cukup sumber itu. Saya telah menambahkan contoh untuk menunjukkan itu dan saya juga membuatnya lebih eksplisit dalam dokumentasi.
bubla
Senang mendengarnya. Contoh itu menarik tetapi masih belum benar-benar jelas - mungkin Anda dapat mengubah nama skrip yang dibuat menjadi 'parse_lib.sh' atau serupa dan menunjukkan di mana skrip utama memanggilnya (seperti di bagian skrip pembungkus yang merupakan kasus penggunaan yang lebih kompleks).
RichVel
Masalah-masalah tersebut dibahas dalam versi terbaru dari argbash: Dokumentasi telah diperbaiki, skrip argbash-init quickstart telah diperkenalkan dan Anda bahkan dapat menggunakan argbash online di argbash.io/generate
bubla
29

Jawaban saya sebagian besar didasarkan pada jawaban oleh Bruno Bronosky , tapi saya semacam menumbuk dua implementasi bash murni menjadi satu yang saya gunakan cukup sering.

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

Ini memungkinkan Anda untuk memiliki kedua opsi / nilai yang dipisahkan dengan ruang, serta nilai yang didefinisikan sama.

Jadi Anda dapat menjalankan skrip Anda menggunakan:

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

sebaik:

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

dan keduanya harus memiliki hasil akhir yang sama.

PROS:

  • Memungkinkan untuk -arg = nilai dan nilai -arg

  • Bekerja dengan nama arg apa pun yang dapat Anda gunakan di bash

    • Berarti -a atau -arg atau --arg atau -arg atau apa pun
  • Bash murni. Tidak perlu belajar / menggunakan getopt atau getopt

CONS:

  • Tidak dapat menggabungkan args

    • Berarti tidak -abc. Anda harus melakukan -a -b -c

Ini adalah satu-satunya pro / kontra yang bisa saya pikirkan dari atas kepala saya

Ponyboy47
sumber
15

Saya pikir ini cukup sederhana untuk digunakan:

#!/bin/bash
#

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

opts=vfdo:

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

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

Contoh doa:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile
Alek
sumber
1
Saya membaca semua dan yang ini adalah yang saya sukai. Saya tidak suka menggunakan -a=1gaya argc. Saya lebih suka mengutamakan opsi-opsi utama dan kemudian yang khusus dengan spasi tunggal -o option. Saya sedang mencari cara paling sederhana-vs-lebih baik untuk membaca argvs.
m3nda
Ini bekerja sangat baik tetapi jika Anda meneruskan argumen ke opsi non a: semua opsi berikut akan dianggap sebagai argumen. Anda dapat memeriksa baris ini ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFiledengan skrip Anda sendiri. -d opsi tidak diset sebagai d:
m3nda
15

Memperluas jawaban yang sangat baik oleh @guneysus, berikut ini adalah tweak yang memungkinkan pengguna menggunakan sintaksis mana pun yang mereka sukai, misalnya

command -x=myfilename.ext --another_switch 

vs.

command -x myfilename.ext --another_switch

Artinya persamaan bisa diganti dengan spasi putih.

"Interpretasi kabur" ini mungkin tidak sesuai dengan keinginan Anda, tetapi jika Anda membuat skrip yang dapat dipertukarkan dengan utilitas lain (seperti halnya dengan milik saya, yang harus bekerja dengan ffmpeg), fleksibilitasnya berguna.

STD_IN=0

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

Contoh ini menunjukkan cara menggunakan getoptdan evaldan HEREDOCdan shiftmenangani parameter pendek dan panjang dengan dan tanpa nilai yang diperlukan berikut. Juga pernyataan switch / casenya ringkas dan mudah diikuti.

#!/usr/bin/env bash

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

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

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

HEREDOC
}  

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

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

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

if (( $verbose > 0 )); then

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

# The rest of your script below

Baris paling penting dari skrip di atas adalah ini:

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

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

Singkat, to the point, dapat dibaca, dan menangani hampir semua hal (IMHO).

Semoga itu bisa membantu seseorang.

phyatt
sumber
1
Ini adalah salah satu jawaban terbaik.
Tn. Polywhirl
11

Saya memberi Anda Fungsi parse_paramsyang akan mem-parsing params dari baris perintah.

  1. Ini adalah solusi Bash murni, tidak ada utilitas tambahan.
  2. Tidak mencemari ruang lingkup global.
  3. Mudah mengembalikan Anda sederhana untuk menggunakan variabel, bahwa Anda dapat membangun logika lebih lanjut.
  4. Jumlah tanda hubung sebelum param tidak masalah ( --allsama -alldengan sama all=all)

Script di bawah ini adalah demonstrasi kerja copy-paste. Lihat show_usefungsi untuk memahami cara menggunakanparse_params .

Keterbatasan:

  1. Tidak mendukung parem dibatasi ruang ( -d 1)
  2. Nama param akan kehilangan tanda hubung begitu --any-paramdan -anyparamsetara
  3. eval $(parse_params "$@")harus digunakan di dalam fungsi bash (tidak akan berfungsi dalam lingkup global)

#!/bin/bash

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

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

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

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
Oleksii Chekulaiev
sumber
Untuk menggunakan demo untuk mem-parsing params yang masuk ke skrip bash Anda, Anda cukup melakukannyashow_use "$@"
Oleksii Chekulaiev
Pada dasarnya saya mengetahui bahwa github.com/renatosilva/easyoptions melakukan hal yang sama dengan cara yang sama tetapi sedikit lebih masif dari fungsi ini.
Oleksii Chekulaiev
10

EasyOptions tidak memerlukan penguraian:

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

source easyoptions || exit

if test -n "${verbose}"; then
    echo "output file is ${output}"
    echo "${arguments[@]}"
fi
Renato Silva
sumber
Perlu beberapa saat untuk menyadari bahwa komentar di bagian atas skrip contoh Anda sedang diuraikan untuk memberikan string bantuan penggunaan default, serta spesifikasi argumen. Ini adalah solusi yang brilian dan saya minta maaf karena hanya mendapat 6 suara dalam 2 tahun. Mungkin pertanyaan ini terlalu dibanjiri orang untuk diperhatikan.
Metamorphic
Di satu sisi solusi Anda sejauh ini adalah yang terbaik (selain dari @ OleksiiChekulaiev yang tidak mendukung sintaks opsi "standar"). Ini karena solusi Anda hanya memerlukan penulis skrip untuk menentukan nama setiap opsi satu kali . Fakta bahwa solusi lain mengharuskannya untuk ditentukan 3 kali - dalam penggunaan, dalam pola 'kasus', dan dalam pengaturan variabel - terus menerus mengganggu saya. Bahkan getopt memiliki masalah ini. Namun, kode Anda lambat di mesin saya - 0,11 untuk implementasi Bash, 0,28 untuk Ruby. Versus 0,02s untuk parsing "sementara kasus" eksplisit.
Metamorphic
Saya ingin versi yang lebih cepat, mungkin ditulis dalam C. Juga, versi yang kompatibel dengan zsh. Mungkin ini pantas mendapatkan pertanyaan terpisah ("Apakah ada cara untuk mengurai argumen baris perintah di shell mirip Bash yang menerima sintaks opsi-panjang standar dan tidak memerlukan nama opsi untuk diketik lebih dari satu kali?").
Metamorphic
10

getopts berfungsi dengan baik jika # 1 Anda menginstalnya dan # 2 Anda bermaksud menjalankannya pada platform yang sama. OSX dan Linux (misalnya) berperilaku berbeda dalam hal ini.

Berikut adalah solusi (non getopts) yang mendukung flag sama dengan, tidak sama, dan boolean. Misalnya Anda dapat menjalankan skrip Anda dengan cara ini:

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

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

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

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

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

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

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

Inilah yang saya lakukan dalam suatu fungsi untuk menghindari kerusakan menjalankan getop pada saat yang sama di suatu tempat yang lebih tinggi di stack:

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

Memperluas jawaban @ bruno-bronosky, saya menambahkan "preprocessor" untuk menangani beberapa format umum:

  • Perluas --longopt=val menjadi--longopt val
  • Perluas -xyz menjadi-x -y -z
  • Mendukung -- untuk menunjukkan akhir bendera
  • Menunjukkan kesalahan untuk opsi yang tidak terduga
  • Beralih opsi yang ringkas dan mudah dibaca
#!/bin/bash

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

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

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

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

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

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

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

Ada beberapa cara untuk mengurai argumen cmdline (mis. GNU getopt (bukan portable) vs BSD (OSX) getopt vs getopts) - semuanya bermasalah. Solusi ini

  • portabel!
  • memiliki nol dependensi, hanya bergantung pada bash built-in
  • memungkinkan untuk opsi pendek dan panjang
  • menangani spasi putih antara opsi dan argumen tetapi juga dapat menggunakan =pemisah
  • mendukung gaya opsi pendek bersambung -vxf
  • menangani opsi dengan argumen opsional (lihat contoh), dan
  • tidak memerlukan kode mengasapi dibandingkan dengan alternatif untuk set fitur yang sama. Yaitu ringkas, dan karena itu lebih mudah dirawat

Contoh: Salah satu dari

# flag
-f
--foo

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

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

#!/usr/bin/env bash

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

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

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option

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

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

EOF
}

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

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

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

shift $((OPTIND-1))

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

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

Saya ingin menawarkan parsing opsi versi saya, yang memungkinkan untuk yang berikut:

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

Juga memungkinkan untuk ini (bisa tidak diinginkan):

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

Anda harus memutuskan sebelum menggunakan apakah = akan digunakan pada opsi atau tidak. Ini untuk menjaga kode tetap bersih (ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done
galmok
sumber
1
apa arti dari "+ x" di $ {key + x}?
Luca Davanzo
1
Ini adalah tes untuk melihat apakah 'kunci' ada atau tidak. Lebih jauh ke bawah saya membatalkan kunci dan ini merusak loop sementara batin.
galmok
5

Solusi yang mempertahankan argumen tidak tertangani. Demo Termasuk.

Ini solusinya. Ini SANGAT fleksibel dan tidak seperti yang lain, seharusnya tidak memerlukan paket eksternal dan menangani argumen sisa dengan bersih.

Penggunaannya adalah: ./myscript -flag flagvariable -otherflag flagvar2

Yang harus Anda lakukan adalah mengedit baris tanda valid. Ini menambahkan tanda hubung dan mencari semua argumen. Itu kemudian mendefinisikan argumen berikutnya sebagai nama bendera misalnya

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

Kode utama (versi pendek, verbose dengan contoh lebih jauh ke bawah, juga versi dengan kesalahan):

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

Versi verbose dengan demo gema bawaan:

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

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

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

Yang terakhir, kesalahan yang satu ini keluar jika -argumen yang tidak valid dilewatkan.

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

Pro: Apa yang dilakukannya, itu menangani dengan sangat baik. Ini mempertahankan argumen yang tidak digunakan yang banyak dari solusi lain di sini tidak. Hal ini juga memungkinkan variabel dipanggil tanpa harus didefinisikan dengan tangan dalam skrip. Ini juga memungkinkan pra-populasi variabel jika tidak ada argumen yang sesuai diberikan. (Lihat contoh verbose).

Cons: Tidak dapat menguraikan string arg kompleks tunggal misalnya -xcvf akan diproses sebagai argumen tunggal. Anda dapat dengan mudah menulis kode tambahan ke tambang yang menambahkan fungsi ini.


sumber
5

Saya ingin mengirimkan proyek saya: https://github.com/flyingangel/argparser

source argparser.sh
parse_args "$@"

Sederhana seperti itu. Lingkungan akan diisi dengan variabel dengan nama yang sama dengan argumen

Trung Thanh
sumber
3

Catat itu getopt(1) itu adalah kesalahan hidup singkat dari AT&T.

getopt dibuat pada tahun 1984 tetapi sudah dimakamkan pada tahun 1986 karena itu tidak benar-benar dapat digunakan.

Sebuah bukti untuk fakta yang getoptsangat ketinggalan jaman adalah bahwa getopt(1)halaman manual masih menyebutkan "$*"bukannya "$@", yang ditambahkan ke Bourne Shell pada tahun 1986 bersama dengangetopts(1) shell builtin untuk menangani argumen dengan ruang di dalamnya.

BTW: jika Anda tertarik untuk menguraikan opsi panjang dalam skrip shell, mungkin menarik untuk mengetahui bahwa getopt(3)implementasi dari libc (Solaris) dan ksh93keduanya menambahkan implementasi opsi panjang yang seragam yang mendukung opsi panjang sebagai alias untuk opsi pendek. Ini menyebabkan ksh93dan Bourne Shelluntuk mengimplementasikan antarmuka yang seragam untuk opsi panjang viagetopts .

Contoh untuk opsi panjang yang diambil dari halaman manual Bourne Shell:

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

menunjukkan berapa lama alias opsi dapat digunakan di Bourne Shell dan ksh93.

Lihat halaman manual dari Bourne Shell terbaru:

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

dan halaman manual untuk getopt (3) dari OpenSolaris:

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

dan terakhir, halaman manual getopt (1) untuk memverifikasi $ * yang sudah usang:

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

schily
sumber
3

Saya telah menulis bash helper untuk menulis alat bash yang bagus

rumah proyek: https://gitlab.mbedsys.org/mbedsys/bashopts

contoh:

#!/bin/bash -ei

# load the library
. bashopts.sh

# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR

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

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

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args

akan memberi bantuan:

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

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

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

Nikmati :)

Verschuur Emerik
sumber
Saya mendapatkan ini di Mac OS X: `` `lib / bashopts.sh: line 138: menyatakan: -A: opsi tidak valid menyatakan: penggunaan: menyatakan [-afFirtx] [-p] [nama [= nilai] ...] Kesalahan di lib / bashopts.sh: 138. 'mendeklarasikan -x -A bashopts_optprop_name' keluar dengan status 2 Pohon panggilan: 1: lib / controller.sh: 4 sumber (...) Keluar dengan status 1 `` `
Josh Wulf
Anda perlu Bash versi 4 untuk menggunakan ini. Di Mac, versi standarnya adalah 3. Anda dapat menggunakan brew home untuk menginstal bash 4.
Josh Wulf
3

Inilah pendekatan saya - menggunakan regexp.

  • tidak ada getopt
  • ini menangani blok parameter pendek -qwerty
  • menangani parameter pendek -q -w -e
  • ini menangani opsi panjang --qwerty
  • Anda bisa meneruskan atribut ke opsi pendek atau panjang (jika Anda menggunakan blok opsi pendek, atribut dilampirkan ke opsi terakhir)
  • Anda dapat menggunakan spasi atau =untuk menyediakan atribut, tetapi atribut cocok sampai bertemu dengan tanda hubung + spasi "pembatas", jadi dalam--q=qwe ty qwe ty adalah satu atribut
  • ini menangani semua campuran di atas sehingga -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributevalid

naskah:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

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

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

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

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done
a_z
sumber
Seperti yang ini. Mungkin cukup tambahkan -e param untuk digaungkan dengan baris baru.
mauron85
3

Asumsikan kita membuat skrip shell bernama test_args.shsebagai follow

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

Setelah kami menjalankan perintah berikut:

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

Outputnya adalah:

year=2017 month=12 day=22 flag=true
John
sumber
5
Ini mengambil pendekatan yang sama dengan jawaban Nuh , tetapi kurang memiliki pemeriksaan keamanan / perlindungan. Ini memungkinkan kami untuk menulis argumen arbitrer ke lingkungan skrip dan saya cukup yakin penggunaan eval Anda di sini memungkinkan injeksi perintah.
Will Barnwell
2

Gunakan modul "argumen" dari bash-modules

Contoh:

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

NAME="world"

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

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

Menggabungkan argumen berbasis posisi dan bendera

--param = arg (sama dengan dibatasi)

Mencampur bendera dengan bebas di antara argumen posisi:

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

dapat dicapai dengan pendekatan yang cukup ringkas:

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

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

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

# positional remain
node_name=$1
ip_address=$2

--param arg (dibatasi ruang)

Biasanya lebih jelas untuk tidak memadukan --flag=valuedan --flag valuegaya.

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

Ini agak tidak pasti untuk dibaca, tetapi masih valid

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

Sumber

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

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

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

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

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

Berikut adalah getopts yang mencapai parsing dengan kode minimal dan memungkinkan Anda untuk menentukan apa yang ingin Anda ekstrak dalam satu kasus menggunakan eval with substring.

Pada dasarnya eval "local key='val'"

function myrsync() {

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


        echo "${sourceurl}"
}

Deklarasikan variabel sebagai lokal bukan global karena sebagian besar jawaban di sini.

Disebut sebagai:

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

$ {K: 3} pada dasarnya adalah sebuah substring untuk menghapus yang pertama ---dari kunci.

mmm
sumber
1

Ini juga mungkin berguna untuk diketahui, Anda dapat menetapkan nilai dan jika seseorang memberikan input, ganti default dengan nilai itu.

myscript.sh -f ./serverlist.txt atau hanya ./myscript.sh (dan dibutuhkan default)

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

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

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

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


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

Solusi lain tanpa getopt [POS], POSIX, gaya Unix lama

Mirip dengan solusi yang diposting Bruno Bronosky ini di sini adalah satu tanpa penggunaan getopt(s).

Fitur pembeda utama dari solusi saya adalah memungkinkan untuk memiliki opsi yang disatukan bersama- tar -xzf foo.tar.gzsama sama dengan tar -x -z -f foo.tar.gz. Dan seperti di tar,ps dll. Tanda hubung utama adalah opsional untuk blok opsi pendek (tetapi ini dapat diubah dengan mudah). Opsi panjang juga didukung (tetapi ketika sebuah blok dimulai dengan satu maka dua tanda hubung utama diperlukan).

Kode dengan opsi contoh

#!/bin/sh

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

print_usage() {
  echo "Usage:

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

Options:

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

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

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

" >&2
}

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

opt=
while :; do

  if [ $# -le 0 ]; then

    # no parameters remaining -> end option parsing
    break

  elif [ ! "$opt" ]; then

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

  fi

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

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

    -)

      # end of options
      shift
      break
      ;;

    a*|-aaa-0-args)

      echo "Option AAA activated!"
      ;;

    b*|-bbb-1-args)

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

    c*|-ccc-2-args)

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

    h*|\?*|-help)

      print_usage
      exit 0
      ;;

    *)

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

  esac

  if [ "$long_option" = T ]; then

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

  else

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

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

  fi

done

echo "Doing something..."

exit 0

Untuk contoh penggunaan, silakan lihat contoh lebih lanjut di bawah ini.

Posisi opsi dengan argumen

Untuk apa nilainya di sana, opsi dengan argumen tidak menjadi yang terakhir (hanya opsi panjang yang diperlukan). Jadi sementara misalnya dalam tar(setidaknya dalam beberapa implementasi) fopsi harus yang terakhir karena nama file mengikuti ( tar xzf bar.tar.gzberfungsi tetapi tar xfz bar.tar.gztidak) ini tidak terjadi di sini (lihat contoh selanjutnya).

Opsi berganda dengan argumen

Sebagai bonus lain, parameter opsi dikonsumsi dalam urutan opsi oleh parameter dengan opsi yang diperlukan. Lihat saja hasil skrip saya di sini dengan baris perintah abc X Y Z(atau -abc X Y Z):

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

Opsi panjang juga digabungkan

Anda juga dapat memiliki opsi panjang di blok opsi mengingat bahwa mereka muncul terakhir di blok. Jadi baris perintah berikut semuanya setara (termasuk urutan di mana opsi dan argumennya sedang diproses):

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

Semua ini mengarah ke:

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

Tidak dalam solusi ini

Argumen opsional

Opsi dengan argumen opsional harus dimungkinkan dengan sedikit kerja, misalnya dengan melihat ke depan apakah ada blok tanpa tanda hubung; pengguna kemudian harus meletakkan tanda hubung di depan setiap blok mengikuti blok dengan parameter yang memiliki parameter opsional. Mungkin ini terlalu rumit untuk berkomunikasi dengan pengguna jadi lebih baik hanya memerlukan tanda hubung terkemuka sama sekali dalam kasus ini.

Banyak hal menjadi lebih rumit dengan beberapa parameter yang mungkin. Saya akan menyarankan untuk tidak membuat pilihan yang mencoba menjadi pintar dengan menentukan apakah suatu argumen mungkin untuk itu atau tidak (misalnya dengan opsi hanya mengambil angka sebagai argumen opsional) karena ini mungkin pecah di masa depan.

Saya pribadi menyukai opsi tambahan, bukan argumen opsional.

Argumen opsi diperkenalkan dengan tanda sama dengan

Sama seperti dengan argumen opsional, saya bukan penggemar ini (BTW, apakah ada utas untuk membahas pro / kontra dari berbagai gaya parameter?) Tetapi jika Anda menginginkan ini, Anda mungkin bisa menerapkannya sendiri seperti yang dilakukan di http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop dengan --long-with-arg=?*pernyataan kasus dan kemudian membuang tanda sama dengan (ini adalah BTW situs yang mengatakan bahwa membuat penggabungan parameter dimungkinkan dengan beberapa upaya tetapi "meninggalkannya sebagai latihan untuk pembaca "Yang membuat saya menerima kata-kata mereka tetapi saya mulai dari awal).

Catatan lain

Sesuai dengan POSIX, berfungsi bahkan pada pengaturan Busybox kuno yang harus saya tangani (misalnya cut, headdan getoptshilang).

phk
sumber