Pertanyaan Bagaimana cara menguraikan argumen baris perintah di Bash?


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 untuk menguraikan ini sedemikian rupa sehingga dalam setiap kasus (atau beberapa kombinasi dari keduanya) $v, $f, dan $d semua akan diatur untuk true dan $outFile akan sama dengan /fizz/someOtherFile ?


1340
2017-10-10 16:57


asal


Jawaban:


Metode # 1: Menggunakan bash tanpa getopt [s]

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

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

Pemakaian ./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts

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

Bash Setara-Terpisah (misalnya, --option=argument) (tanpa getopt [s])

Pemakaian ./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

#!/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 "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

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

Menggunakan getopt [s]

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

batasan getopt (1) (lebih tua, relatif baru getopt versi):

  • tidak bisa menangani argumen yang string kosong
  • tidak dapat menangani argumen dengan spasi yang disematkan

Lebih baru getopt versi tidak memiliki batasan ini.

Selain itu, penawaran POSIX shell (dan lainnya) getopts yang tidak memiliki batasan ini. Di sini adalah sederhana getopts contoh:

#!/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: $@"

# End of file

Keuntungan dari getopts adalah:

  1. Ini lebih portabel, dan akan bekerja di shell lain seperti dash.
  2. Dapat menangani beberapa opsi tunggal seperti -vf filename dengan cara Unix yang khas, secara otomatis.

Kerugian dari getopts adalah hanya dapat menangani opsi singkat (-htidak --help) tanpa kode tambahan.

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


1936
2018-01-07 20:01



Tidak ada jawaban yang menyebutkan ditingkatkan getopt. Dan itu jawaban teratas yang terpilih menyesatkan: Itu mengabaikan -⁠vfd gaya opsi pendek (diminta oleh OP), opsi setelah argumen posisi (juga diminta oleh OP) dan mengabaikan kesalahan parsing. Sebagai gantinya:

  • Gunakan yang ditingkatkan getopt dari util-linux atau sebelumnya GNU glibc.1
  • Ia bekerja dengan getopt_long() fungsi C dari GNU glibc.
  • Has semua fitur pembeda yang bermanfaat (yang lain tidak memilikinya):
    • menangani spasi, mengutip karakter dan bahkan biner dalam argumen2
    • ia dapat menangani opsi di akhir: script.sh -o outFile file1 file2 -v
    • memungkinkan =-pilihan panjang gaya: script.sh --outfile=fileOut --infile fileIn
  • Sudah sangat tua3 bahwa tidak ada sistem GNU yang hilang ini (misalnya Linux memilikinya).
  • Anda dapat menguji keberadaannya dengan: getopt --test → mengembalikan nilai 4.
  • Lain getopt atau shell-builtin getopts adalah 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 yang berikut ini myscript

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

! 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

# -use ! and PIPESTATUS to get exit code with errexit set
# -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 pada sebagian besar "bash-sistem", termasuk Cygwin; pada OS X coba buat instalasi gnu-getopt
2 POSIX exec() konvensi tidak memiliki cara yang dapat diandalkan untuk melewatkan NULL biner dalam argumen baris perintah; mereka byte secara prematur mengakhiri argumen
3 versi pertama dirilis pada tahun 1997 atau sebelumnya (saya hanya melacaknya kembali ke 1997)


328
2018-04-20 17:47



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#*=} cari "Penghapusan Substring" di panduan ini. Secara fungsional setara dengan `sed 's/[^=]*=//' <<< "$i"` yang memanggil subproses yang tidak perlu atau `echo "$i" | sed 's/[^=]*=//'` panggilan mana dua subproses yang tidak perlu.


103
2017-11-13 10:31



getopt()/getopts() adalah pilihan yang bagus. 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 kami katakan adalah bahwa apa pun -a,   -b, -c atau -d akan diizinkan, tetapi itu -c diikuti oleh argumen ("c:" mengatakan itu).

Jika kita menyebut ini "g" dan mencobanya:

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

Kami mulai dengan dua argumen, dan   "Getopt" memisahkan opsi dan   menempatkan masing-masing dalam argumennya sendiri. Juga   menambahkan "-".


101
2017-10-10 17:03



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

  • pegangan -n arg dan --name=arg
  • memungkinkan argumen di bagian akhir
  • menunjukkan kesalahan yang waras jika ada yang salah eja
  • kompatibel, tidak menggunakan bashisms
  • dapat dibaca, tidak perlu mempertahankan keadaan 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

59
2017-07-15 23:43



Cara lebih ringkas

script.sh

#!/bin/bash

while [[ "$#" > 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

39
2017-11-20 12:28



Saya sekitar 4 tahun terlambat untuk pertanyaan ini, tetapi ingin membalas. Saya menggunakan jawaban sebelumnya sebagai titik awal untuk merapikan parsing param adhoc lama saya. Saya kemudian refactor keluar kode template berikut. Ini menangani params panjang dan pendek, menggunakan argumen = atau ruang terpisah, serta beberapa params pendek dikelompokkan bersama. Akhirnya itu 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

36
2017-07-01 01:20



Jawaban saya sebagian besar didasarkan pada jawabannya oleh Bruno Bronosky, tapi saya semacam tumbuk dua bash implementasi murni menjadi salah 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 terpisah ruang, serta nilai yang sama.

Jadi Anda bisa 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 nilai ug-nilai dan nilai-balik

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

    • Arti -a atau -arg atau --arg atau -a-r-g atau apa pun
  • Pesta murni. Tidak perlu belajar / menggunakan getopt atau getopts

CONS:

  • Tidak bisa menggabungkan arg

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

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


21
2017-09-08 18:59