كيف يمكنني تحليل حجج سطر الأوامر في Bash؟



Answers

لا توجد إجابة تذكر تحسين getopt . والإجابة الأعلى تصويتًا مضللة: إنها تتجاهل الخيارات القصيرة للنمط -⁠vfd (التي يطلبها OP) ، والخيارات بعد الحجج الموضعية (تطلب أيضًا من البروتوكول الاختياري) وتتجاهل أخطاء التحليل. في حين أن:

  • استخدام getopt المحسنة من استخدام لينكس أو سابقا GNU glibc . 1
  • إنه يعمل مع getopt_long() الدالة C من GNU glibc.
  • يحتوي على جميع الميزات المميزة المفيدة (لا يمتلكها الآخرون):
    • يعالج المسافات ، نقلا عن الأحرف وحتى ثنائي في الحجج 2
    • يمكن معالجة الخيارات في النهاية: script.sh -o outFile file1 file2 -v
    • يسمح خيارات طويلة على غرار = : script.sh --outfile=fileOut --infile fileIn
  • قديم جدا 3 لا يفقد نظام غنو هذا (على سبيل المثال أي لينكس).
  • يمكنك اختبار وجودها مع: getopt --test return value 4.
  • غيرها من getopt أو getopt shell- getopts هي ذات الاستخدام المحدود.

المكالمات التالية

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

كل العودة

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

مع myscript التالي

#!/bin/bash

getopt --test > /dev/null
if [[ $? -ne 4 ]]; then
    echo "I’m sorry, `getopt --test` failed in this environment."
    exit 1
fi

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

# -temporarily store output to be able to check for errors
# -e.g. use “--options” parameter by name to activate quoting/enhanced mode
# -pass arguments only via   -- "$@"   to separate them correctly
PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTIONS --name "$0" -- "$@")
if [[ $? -ne 0 ]]; then
    # e.g. $? == 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"

# 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 المحسنة متوفرة في معظم "أنظمة bash" ، بما في ذلك Cygwin ؛ على OS X محاولة الشراب تركيب gnu-getopt
2 لا تحتوي اصطلاحات POSIX exec() على طريقة موثوقة لتمرير BULL NULL في وسائط سطر الأوامر؛ تلك البايت إنهاء الأسبق الخلاف
3 الإصدار الأول الذي صدر في عام 1997 أو قبل ذلك (أنا فقط تتبع ذلك مرة أخرى إلى عام 1997)

Question

قل ، لدي برنامج نصي يتم استدعاؤه باستخدام هذا الخط:

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

ما سر جديدة هذا:

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

ما هي الطريقة المقبولة لتحليل هذا بحيث في كل حالة (أو مزيج من الاثنين) $v ، $f ، و $d سيتم تعيينها كلها إلى true و $outFile سيكون مساويا لـ /fizz/someOtherFile ؟




أرغب في تقديم الإصدار الخاص بي من تحليل الخيار ، والذي يسمح بما يلي:

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

كما يسمح لهذا (يمكن أن يكون غير مرغوب فيه):

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

يجب عليك أن تقرر قبل الاستخدام إذا = هو أن تستخدم في خيار أم لا. هذا هو الحفاظ على رمز نظيفة (العش).

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



هذه هي الطريقة التي أقوم بها في وظيفة لتفادي كسر getopts في نفس الوقت في مكان أعلى في المكدس:

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



أعتقد أن هذا بسيط بما يكفي للاستخدام:

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

مثال على الاستحضار:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile



لا يتطلب EasyOptions أي تحليل:

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



Here is my improved solution of Bruno Bronosky's answer using variable arrays.

it lets you mix parameters position and give you a parameter array preserving the order without the options

#!/bin/bash

echo $@

PARAMS=()
SOFT=0
SKIP=()
for i in "$@"
do
case $i in
    -n=*|--skip=*)
    SKIP+=("${i#*=}")
    ;;
    -s|--soft)
    SOFT=1
    ;;
    *)
        # unknown option
        PARAMS+=("$i")
    ;;
esac
done
echo "SKIP            = ${SKIP[@]}"
echo "SOFT            = $SOFT"
    echo "Parameters:"
    echo ${PARAMS[@]}

Will output for example:

$ ./test.sh parameter -s somefile --skip=.c --skip=.obj
parameter -s somefile --skip=.c --skip=.obj
SKIP            = .c .obj
SOFT            = 1
Parameters:
parameter somefile



أنا في وقت متأخر من 4 سنوات لهذا السؤال ، ولكن أريد أن أعود. لقد استخدمت الإجابات السابقة كنقطة بداية لترتيب إعجابي القديم الأساسي. أنا ثم refactored من رمز القالب التالي. يتعامل مع كل من params طويلة وقصيرة ، باستخدام الحجج = أو الفضاء فصل ، فضلا عن عدة معلمات قصيرة مجمعة معا. وأخيرًا ، يعيد إدراج أي حجج غير معلمة مرة أخرى في المتغيرات $ 1 ، $ 2 ... آمل أن تكون مفيدة.

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



لاحظ أن getopt(1) كان خطأ حيًا قصيرًا من AT & T.

تم إنشاء getopt في عام 1984 ولكن دفنت بالفعل في عام 1986 لأنه لم يكن قابلا للاستخدام حقا.

والدليل على حقيقة أن getopt قديم جدا هو أن صفحة الرجل getopt(1) لا تزال تذكر "$*" بدلا من "$@" ، التي أضيفت إلى Bourne Shell في عام 1986 جنبا إلى جنب مع getopts(1) shell getopts(1) من أجل التعامل مع الحجج بمسافات بداخله.

راجع للشغل: إذا كنت مهتمًا بتحليل خيارات طويلة في نصوص القشرة ، قد يكون من المثير معرفة أن تطبيق getopt(3) من libc (Solaris) و ksh93 أضاف كلاهما خيارًا طويلًا للتنفيذ يدعم خيارات طويلة ksh93 مستعارة قصيرة خيارات. هذا يسبب ksh93 و Bourne Shell لتنفيذ واجهة موحدة لخيارات طويلة عن طريق getopts .

مثال على خيارات طويلة مأخوذة من صفحة رجل بورن شل:

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

يوضح كيف يمكن استخدام خيار الأسماء المستعارة للخيار في كل من Bourne Shell و ksh93.

انظر صفحة رجل من Bourne Shell الأخيرة:

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

والصفحة الرجل للحصول على getopt (3) من OpenSolaris:

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

وأخيرًا ، صفحة getopt (1) man للتحقق من $ * قديم:

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




من: digitalpeer.com مع تعديلات طفيفة

Usage 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}

لفهم أفضل ${i#*=} ابحث عن "Substring Removal" في هذا الدليل . وهي مكافئة وظيفيا ل `sed 's/[^=]*=//' <<< "$i"` الذي يستدعي عملية فرعية لا داعي لها أو `echo "$i" | sed 's/[^=]*=//'` `echo "$i" | sed 's/[^=]*=//'` الذي يستدعي معالجة ثانوية غير ضرورية.




Solution that preserves unhandled arguments. Demos Included.

Here is my solution. It is VERY flexible and unlike others, shouldn't require external packages and handles leftover arguments cleanly.

Usage is: ./myscript -flag flagvariable -otherflag flagvar2

All you have to do is edit the validflags line. It prepends a hyphen and searches all arguments. It then defines the next argument as the flag name eg

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

The main code (short version, verbose with examples further down, also a version with erroring out):

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

The verbose version with built in echo demos:

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

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

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

Final one, this one errors out if an invalid -argument is passed through.

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

Pros: What it does, it handles very well. It preserves unused arguments which a lot of the other solutions here don't. It also allows for variables to be called without being defined by hand in the script. It also allows prepopulation of variables if no corresponding argument is given. (See verbose example).

Cons: Can't parse a single complex arg string eg -xcvf would process as a single argument. You could somewhat easily write additional code into mine that adds this functionality though.




لقد وجدت هذه المسألة لكتابة تحليل محمول في البرامج النصية المحبطة جدًا لدرجة أنني كتبت Argbash - وهو مولد شفرة FOSS يمكنه إنشاء شفرة تحليل الحجج للنص البرمجي بالإضافة إلى أنه يحتوي على بعض الميزات Argbash :

https://argbash.io




قد يكون هذا مفيدًا أيضًا في معرفة ما إذا كان بإمكانك تعيين قيمة وما إذا كان هناك شخص ما يقدم مدخلات أم لا ، فعليك تجاوز القيمة الافتراضية بتلك القيمة.

myscript.sh -f ./serverlist.txt أو فقط ./myscript.sh (ويستغرق الإعدادات الافتراضية)

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



Here is my approach - using regexp.

  • no getopts
  • it handles block of short parameters -qwerty
  • it handles short parameters -q -w -e
  • it handles long options --qwerty
  • you can pass attribute to short or long option (if you are using block of short options, attribute is attached to the last option)
  • you can use spaces or = to provide attributes, but attribute matches until encountering hyphen+space "delimiter", so in --q=qwe ty qwe ty is one attribute
  • it handles mix of all above so -oa -op attr ibute --option=att ribu te --op-tion attribute --option att-ribute is valid

script:

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



This example shows how to use getopt and eval and HEREDOC and shift to handle short and long parameters with and without a required value that follows. Also the switch/case statement is concise and easy to follow.

#!/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, don't 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

The most significant lines of the script above are these:

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

Short, to the point, readable, and handles just about everything (IMHO).

Hope that helps someone.






Related