我希望使用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,我还不能实现这些?
当前回答
Bash内置的getopts函数可以通过在optspec中放入破折号和冒号来解析长选项:
#!/usr/bin/env bash
optspec=":hv-:"
while getopts "$optspec" optchar; do
case "${optchar}" in
-)
case "${OPTARG}" in
loglevel)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2;
;;
loglevel=*)
val=${OPTARG#*=}
opt=${OPTARG%=$val}
echo "Parsing option: '--${opt}', value: '${val}'" >&2
;;
*)
if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
echo "Unknown option --${OPTARG}" >&2
fi
;;
esac;;
h)
echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2
exit 2
;;
v)
echo "Parsing option: '-${optchar}'" >&2
;;
*)
if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
echo "Non-option argument: '-${OPTARG}'" >&2
fi
;;
esac
done
拷贝到当前工作目录中的可执行文件name=getopts_test.sh后,可以生成如下输出
$ ./getopts_test.sh
$ ./getopts_test.sh -f
Non-option argument: '-f'
$ ./getopts_test.sh -h
usage: code/getopts_test.sh [-v] [--loglevel[=]<value>]
$ ./getopts_test.sh --help
$ ./getopts_test.sh -v
Parsing option: '-v'
$ ./getopts_test.sh --very-bad
$ ./getopts_test.sh --loglevel
Parsing option: '--loglevel', value: ''
$ ./getopts_test.sh --loglevel 11
Parsing option: '--loglevel', value: '11'
$ ./getopts_test.sh --loglevel=11
Parsing option: '--loglevel', value: '11'
显然,getopts既不执行OPTERR检查,也不对长选项执行选项参数解析。上面的脚本片段展示了如何手动完成这一工作。基本原理也适用于Debian Almquist shell(“破折号”)。注意特殊情况:
getopts -- "-:" ## without the option terminator "-- " bash complains about "-:"
getopts "-:" ## this works in the Debian Almquist shell ("dash")
请注意,正如http://mywiki.wooledge.org/BashFAQ上的GreyCat指出的那样,这个技巧利用了shell的非标准行为,允许选项参数(即“-ffilename”中的文件名)连接到选项(如“-ffilename”)。POSIX标准规定它们之间必须有一个空格,在"——longoption"的情况下,空格将终止选项解析并将所有长选项转换为非选项参数。
其他回答
Getopts“可以用来”解析长选项,只要你不希望它们有参数……
以下是如何:
$ cat > longopt
while getopts 'e:-:' OPT; do
case $OPT in
e) echo echo: $OPTARG;;
-) #long option
case $OPTARG in
long-option) echo long option;;
*) echo long option: $OPTARG;;
esac;;
esac
done
$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test
如果尝试使用OPTIND获取长选项的参数,getopts将把它作为第一个不可选的位置参数,并停止解析任何其他参数。 在这种情况下,您最好使用简单的case语句手动处理它。
这将“总是”工作:
$ cat >longopt2
while (($#)); do
OPT=$1
shift
case $OPT in
--*) case ${OPT:2} in
long1) echo long1 option;;
complex) echo comples with argument $1; shift;;
esac;;
-*) case ${OPT:1} in
a) echo short option a;;
b) echo short option b with parameter $1; shift;;
esac;;
esac
done
$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test
尽管它不像getopts那么灵活,而且你必须在用例实例中自己做很多错误检查代码……
但这是一种选择。
内置的getopts只解析短选项(ksh93除外), 但是您仍然可以添加几行脚本来使getopts处理长选项。
以下是在http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts中找到的部分代码
#== set short options ==#
SCRIPT_OPTS=':fbF:B:-:h'
#== set long options associated with short one ==#
typeset -A ARRAY_OPTS
ARRAY_OPTS=(
[foo]=f
[bar]=b
[foobar]=F
[barfoo]=B
[help]=h
[man]=h
)
#== parse options ==#
while getopts ${SCRIPT_OPTS} OPTION ; do
#== translate long options to short ==#
if [[ "x$OPTION" == "x-" ]]; then
LONG_OPTION=$OPTARG
LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2)
LONG_OPTIND=-1
[[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1)
[[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND"
OPTION=${ARRAY_OPTS[$LONG_OPTION]}
[[ "x$OPTION" = "x" ]] && OPTION="?" OPTARG="-$LONG_OPTION"
if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then
if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then
OPTION=":" OPTARG="-$LONG_OPTION"
else
OPTARG="$LONG_OPTARG";
if [[ $LONG_OPTIND -ne -1 ]]; then
[[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 ))
shift $OPTIND
OPTIND=1
fi
fi
fi
fi
#== options follow by another option instead of argument ==#
if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then
OPTARG="$OPTION" OPTION=":"
fi
#== manage options ==#
case "$OPTION" in
f ) foo=1 bar=0 ;;
b ) foo=0 bar=1 ;;
B ) barfoo=${OPTARG} ;;
F ) foobar=1 && foobar_name=${OPTARG} ;;
h ) usagefull && exit 0 ;;
: ) echo "${SCRIPT_NAME}: -$OPTARG: option requires an argument" >&2 && usage >&2 && exit 99 ;;
? ) echo "${SCRIPT_NAME}: -$OPTARG: unknown option" >&2 && usage >&2 && exit 99 ;;
esac
done
shift $((${OPTIND} - 1))
下面是一个测试:
# Short options test
$ ./foobar_any_getopts.sh -bF "Hello world" -B 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello world
files=file1 file2
# Long and short options test
$ ./foobar_any_getopts.sh --bar -F Hello --barfoo 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello
files=file1 file2
另外,在最近的Korn Shell ksh93中,getopts可以自然地解析长选项,甚至显示类似的手册页。(参见http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options)
在ksh93中,getopts支持长名字…
while getopts "f(file):s(server):" flag
do
echo "$flag" $OPTIND $OPTARG
done
至少我找到的教程是这么说的。试试看。
这需要一些时间,但我想要全部:
短选项 长选项 不管有没有论点 非选项参数(没有“-”或“——”的参数) 顺序不重要(script.sh /file -V或script.sh -V /file) 抓住错误用法 在不同的脚本中使用它作为模块,而不需要更改多行代码
最后,我提出了下面的解决方案,它使用getopt来捕获错误并将非选项移动到列表的末尾,然后再使用getopts来解析短选项和长选项。
所有的选项都会自动解析,将它们的长选项名作为变量名(请看例子):
# create string of short options
opt_short=$(printf "%s" "${!options[@]}")
# create string of long options
opt_long="$(printf ",%s" "${options[@]}")"
# catch wrong options and move non-options to the end of the string
args=$(getopt -l "$opt_long" "$opt_short" "$@" 2> >(sed -e 's/^/stderr/g')) || echo -n "Error: " && echo "$args" | grep -oP "(?<=^stderr).*" && exit 1
# create new array of options
mapfile -t args < <(xargs -n1 <<< "$(echo "$args" | sed -E "s/(--[^ ]+) '/\1='/g")" )
# overwrite $@ (options)
set -- "${args[@]}"
# parse options ([h]=help sets the variable "$opt_help" and [V]="" sets the variable "$opt_V")
while getopts "$opt_short-:" opt; do
echo "$opt:$OPTARG"
# long option
if [[ "$opt" == "-" ]]; then
# extract long option name
opt="${OPTARG%%=*}"
# extract long option argument (may be empty)
OPTARG="${OPTARG#"$opt"}"
# remove "=" from long option argument
OPTARG="${OPTARG#=}"
# set variable name
opt=opt_$opt
# short option without argument uses long option name as variable name
elif [[ "${options[$opt]+x}" ]] && [[ "${options[$opt]}" ]]; then
opt=opt_${options[$opt]}
# short option with argument uses long option name as variable name
elif [[ "${options[$opt:]+x}" ]] && [[ "${options[$opt:]}" ]]; then
opt=opt_${options[$opt:]}
# short option without long option name uses short option name as variable name
else
opt=opt_$opt
fi
# remove double colon
opt="${opt%:}"
# options without arguments are set to 1
[[ ! $OPTARG ]] && OPTARG=1
# replace hyphen against underscore
opt="${opt//-/_}"
# set variable variables (replaces hyphen against underscore)
printf -v "$opt" '%s' "$OPTARG"
done
现在,我只需要定义所需的选项名称和源代码脚本:
# import options module
declare -A options=( [h]=help [f:]=file: [V]=verbose [0]=long_only: [s]="" )
source "/usr/local/bin/inc/options.sh";
# display help text
if [[ $opt_help ]]; then
echo "help text"
exit
fi
# output
echo "opt_help:$opt_help"
echo "opt_file:$opt_file"
echo "opt_verbose:$opt_verbose"
echo "opt_long_only:$opt_long_only"
echo "opt_short_only:$opt_s"
echo "opt_path:$1"
echo "opt_mail:$2"
在调用脚本时,可以完全随机地传递所有选项和非选项:
# $opt_file $1 $2 $opt_V $opt_long_only $opt_s
# /demo.sh --file=file.txt /dir info@example.com -V --long_only=yes -s
opt_help:1
opt_file:file.txt
opt_verbose:1
opt_long_only:yes
opt_short_only:1
opt_path=/dir
opt_mail:info@example.com
笔记
在选项数组中,在选项名称后添加:以启用参数。 如果没有给出较长的选项名,则变量名将是$opt_X,其中X是较短的选项名。 如果您希望使用较长的选项名而不定义较短的选项名,则将数组索引设置为一个数字,如上面示例中使用[0]=long_only所做的那样。当然,每个数组下标必须是唯一的。
使用的技术
不使用临时文件捕获stderr 将字符串转换为数组 使用:来解析getopt参数 使用getopts解析长选项名
如果你不想要getopt依赖,你可以这样做:
while test $# -gt 0
do
case $1 in
# Normal option processing
-h | --help)
# usage and help
;;
-v | --version)
# version info
;;
# ...
# Special cases
--)
break
;;
--*)
# error unknown (long) option $1
;;
-?)
# error unknown (short) option $1
;;
# FUN STUFF HERE:
# Split apart combined short options
-*)
split=$1
shift
set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$@"
continue
;;
# Done with options
*)
break
;;
esac
# for testing purposes:
echo "$1"
shift
done
当然,这样你就不能使用长样式选项。如果你想添加缩短的版本(例如——verbos而不是——verbose),那么你需要手动添加这些。
但是如果您希望获得getopts功能和长选项,这是一种简单的方法。
我也把这个片段作为一个主旨。