我希望使用shell脚本调用长形式和短形式的命令行选项。

我知道可以使用getopts,但是像在Perl中一样,我不能用shell做同样的事情。

关于如何做到这一点的任何想法,这样我就可以使用如下选项:

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

在上面,这两个命令对我的shell意味着同样的事情,但使用getopts,我还不能实现这些?


当前回答

发明了另一个版本的轮子……

这个函数(希望)是一个posix兼容的普通bourne shell替换GNU getopt。它支持短/长选项,可以接受强制/可选/无参数,并且指定选项的方式几乎与GNU getopt相同,因此转换是微不足道的。

当然,要放入脚本中,这仍然是相当大的代码块,但它大约是众所周知的getopt_long shell函数的一半行数,并且在您只想替换现有的GNU getopt使用的情况下可能更可取。

这是相当新的代码,所以是YMMV(如果因为某种原因它实际上与POSIX不兼容,请务必告诉我——可移植性是一开始的意图,但我没有一个有用的POSIX测试环境)。

代码和示例用法如下:

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

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

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

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

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

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

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

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

使用示例:

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

其他回答

看一下shFlags,它是一个可移植的shell库(意思是:sh, bash, dash, ksh, zsh在Linux, Solaris等上)。

它使得添加新标志就像在脚本中添加一行一样简单,并且它提供了一个自动生成的用法函数。

这是一个简单的Hello, world!使用shFlag:

#!/bin/sh

# source shflags from current directory
. ./shflags

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

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

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

对于具有支持长选项的增强getopt的操作系统(例如Linux),您可以这样做:

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

对于其他情况,您必须使用短选项:

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

添加一个新标志就像添加一个新的DEFINE_调用一样简单。

改进的解决方案:

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

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

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

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

下面是一个实际使用长选项的getopt的示例:

aflag=no
bflag=no
cargument=none

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

set -- $options

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

我研究那个课题已经很长时间了。并制作了我自己的库,你将需要在你的主脚本的来源。 有关示例,请参见libopt4shell和cd2mpc。 希望能有所帮助!

为了保持跨平台兼容性,避免依赖外部可执行文件,我从另一种语言移植了一些代码。

我发现它很容易使用,这里有一个例子:

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

ArgParser::parse "$@"

ArgParser::isset help && ArgParser::showArgs

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

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

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

所需的BASH有点长,但我希望避免依赖BASH 4的关联数组。你也可以直接从http://nt4.com/bash/argparser.inc.sh下载

#!/usr/bin/env bash

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

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

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

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

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

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

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


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

function decho
{
    :
}

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

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

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

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

function ArgParser::parse
{

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

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

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

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

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

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

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

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

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

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

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

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

        description="${EXPLODED[4]}"

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

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

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

    ArgParser::parse "$@"

    ArgParser::isset help && ArgParser::showArgs

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

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

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

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

}

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