我希望使用shell脚本调用长形式和短形式的命令行选项。
我知道可以使用getopts,但是像在Perl中一样,我不能用shell做同样的事情。
关于如何做到这一点的任何想法,这样我就可以使用如下选项:
./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/
在上面,这两个命令对我的shell意味着同样的事情,但使用getopts,我还不能实现这些?
我希望使用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
其他回答
我是这样解决的:
# A string with command options
options=$@
# An array with all the arguments
arguments=($options)
# Loop index
index=0
for argument in $options
do
# Incrementing index
index=`expr $index + 1`
# The conditions
case $argument in
-a) echo "key $argument value ${arguments[index]}" ;;
-abc) echo "key $argument value ${arguments[index]}" ;;
esac
done
exit;
我是不是太笨了?Getopt和getopts太让人困惑了。
为了保持跨平台兼容性,避免依赖外部可执行文件,我从另一种语言移植了一些代码。
我发现它很容易使用,这里有一个例子:
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
内置的getopts命令仍然仅限于单字符选项。
现在有(或者曾经有)一个外部程序getopt,它可以重新组织一组选项,使其更容易解析。你也可以调整这种设计来处理长选项。使用示例:
aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$@")
while [ $# -gt 0 ]
do
case "$1" in
(-a) aflag=yes;;
(-b) bflag=yes;;
(-f) flist="$flist $2"; shift;;
(--) shift; break;;
(-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
# Process remaining non-option arguments
...
您可以对getoptlong命令使用类似的方案。
请注意,外部getopt程序的基本弱点是难以处理其中包含空格的参数,以及难以准确地保留这些空格。这就是内置getopts优越的原因,尽管它只能处理单字母选项。
长选项可以被内置的标准getopts作为- " option "的"参数"解析。
这是可移植的本地POSIX shell -不需要外部程序或bashisms。
本指南将长选项作为-选项的参数实现,因此——alpha被getopts视为参数alpha为-,而——bravo=foo被参数bravo=foo视为-。true实参通过shell参数展开获取,更新$OPT和$OPTARG。
在本例中,-b和-c(以及它们的长形式——bravo和——charlie)具有强制实参。长选项的参数出现在等号之后,例如——bravo=foo(长选项的空格分隔符很难实现,参见下文)。
因为它使用了内置的getopts,所以这个解决方案支持像cmd——bravo=foo -ac FILE这样的用法(它组合了选项-a和-c,并将长选项与标准选项交织在一起),而这里的大多数其他答案要么很难做到,要么无法做到这一点。
die() { echo "$*" >&2; exit 2; } # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }
while getopts ab:c:-: OPT; do
# support long options: https://stackoverflow.com/a/28466267/519360
if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG
OPT="${OPTARG%%=*}" # extract long option name
OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty)
OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=`
fi
case "$OPT" in
a | alpha ) alpha=true ;;
b | bravo ) needs_arg; bravo="$OPTARG" ;;
c | charlie ) needs_arg; charlie="$OPTARG" ;;
??* ) die "Illegal option --$OPT" ;; # bad long option
? ) exit 2 ;; # bad short option (error reported via getopts)
esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list
When the option is a dash (-), it is a long option. getopts will have parsed the actual long option into $OPTARG, e.g. --bravo=foo originally sets OPT='-' and OPTARG='bravo=foo'. The if stanza sets $OPT to the contents of $OPTARG before the first equals sign (bravo in our example) and then removes that from the beginning of $OPTARG (yielding =foo in this step, or an empty string if there is no =). Finally, we strip the argument's leading =. At this point, $OPT is either a short option (one character) or a long option (2+ characters).
The case then matches either short or long options (the pipe, |, indicates that "or" operation. A long-only option like delta ) delta=true ;; doesn't need a pipe). For short options, getopts automatically complains about options and missing arguments, so we have to replicate those manually using the needs_arg function, which fatally exits when $OPTARG is empty. The ??* condition will match any remaining long option (? matches a single character and * matches zero or more, so ??* matches 2+ characters), allowing us to issue the "Illegal option" error before exiting.
与正常的gnu风格的长选项一样,提供——将停止解析,因此- -- --bravo=4将把$alpha设置为true,但$bravo将保持不变,$1将-bravo=4。我不能说我建议用前导破折号来命名文件,但这是表示它们不是选项的方法。
小错误:如果有人给出了一个无效的单字符长选项(它也不是一个短选项),这将退出一个错误,但没有消息(这个实现假设它是一个短选项)。您可以在case之前的条件中使用一个额外的变量来跟踪它,然后在最后的case条件中测试它,但我认为这是一个太麻烦的角落情况。
大写变量名:一般情况下,建议保留全大写变量供系统使用。我将$OPT保留为全大写,以与$OPTARG保持一致,但这确实打破了这种约定。我认为这很合适,因为这是系统应该做的事情,它应该是安全的;我还没有听说过任何使用这个变量名的标准。
要抱怨长选项的非预期实参:用翻转测试模拟needs_argg来抱怨一个非预期实参:
no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }
要接受带空格分隔参数的长选项:您可以用eval "ARG_B=\"\$$OPTIND\""(或使用bash的间接展开,ARG_B="${!OPTIND}")拉入下一个参数,然后增加$OPTIND,正如这个答案的旧版本所指出的那样,但它不可靠;getopts可以过早地终止,假设参数超出了它的作用域,并且一些实现不太适合手动操作$OPTIND。
hm.
对纯bash选项不太满意。为什么不使用perl来得到你想要的。直接解析$*数组,并自动命名您的选项。
简单的帮助脚本:
#!/usr/bin/perl
use Getopt::Long;
my $optstring = shift;
my @opts = split(m#,#, $optstring);
my %opt;
GetOptions(\%opt, @opts);
print "set -- " . join(' ', map("'$_'", @ARGV)) . ";";
my $xx;
my $key;
foreach $key (keys(%opt))
{
print "export $key='$opt{$key}'; ";
}
然后你可以在你的脚本中使用一行,例如:
#!/bin/bash
eval `getopts.pl reuse:s,long_opt:s,hello $*`;
echo "HELLO: $hello"
echo "LONG_OPT: $long_opt"
echo "REUSE: $reuse"
echo $*
/tmp/script.sh hello——reuse me——long_opt whatever_you_want_except_spaces——hello 1 2 3
你好:1 LONG_OPT: whatever_you_want_except空格 重用:我
1, 2, 3
这里唯一需要注意的是空格不适用。但它避免了bash相当复杂的循环语法,适用于长参数,自动将它们命名为变量并自动调整$*的大小,因此99%的时间都是有效的。