我希望使用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,我还不能实现这些?
当前回答
我研究那个课题已经很长时间了。并制作了我自己的库,你将需要在你的主脚本的来源。 有关示例,请参见libopt4shell和cd2mpc。 希望能有所帮助!
其他回答
我想要一个没有外部依赖关系、有严格bash支持(-u)的东西,而且我需要它能在较旧的bash版本上工作。这个函数处理各种类型的参数:
短bool (-h) 短选项(-i "image.jpg") 长boos(——救命) = options(——file="filename.ext") 空格选项(——file "filename.ext") 连接的bool (-hvm)
只需在脚本顶部插入以下内容:
# Check if a list of params contains a specific param
# usage: if _param_variant "h|?|help p|path f|file long-thing t|test-thing" "file" ; then ...
# the global variable $key is updated to the long notation (last entry in the pipe delineated list, if applicable)
_param_variant() {
for param in $1 ; do
local variants=${param//\|/ }
for variant in $variants ; do
if [[ "$variant" = "$2" ]] ; then
# Update the key to match the long version
local arr=(${param//\|/ })
let last=${#arr[@]}-1
key="${arr[$last]}"
return 0
fi
done
done
return 1
}
# Get input parameters in short or long notation, with no dependencies beyond bash
# usage:
# # First, set your defaults
# param_help=false
# param_path="."
# param_file=false
# param_image=false
# param_image_lossy=true
# # Define allowed parameters
# allowed_params="h|?|help p|path f|file i|image image-lossy"
# # Get parameters from the arguments provided
# _get_params $*
#
# Parameters will be converted into safe variable names like:
# param_help,
# param_path,
# param_file,
# param_image,
# param_image_lossy
#
# Parameters without a value like "-h" or "--help" will be treated as
# boolean, and will be set as param_help=true
#
# Parameters can accept values in the various typical ways:
# -i "path/goes/here"
# --image "path/goes/here"
# --image="path/goes/here"
# --image=path/goes/here
# These would all result in effectively the same thing:
# param_image="path/goes/here"
#
# Concatinated short parameters (boolean) are also supported
# -vhm is the same as -v -h -m
_get_params(){
local param_pair
local key
local value
local shift_count
while : ; do
# Ensure we have a valid param. Allows this to work even in -u mode.
if [[ $# == 0 || -z $1 ]] ; then
break
fi
# Split the argument if it contains "="
param_pair=(${1//=/ })
# Remove preceeding dashes
key="${param_pair[0]#--}"
# Check for concatinated boolean short parameters.
local nodash="${key#-}"
local breakout=false
if [[ "$nodash" != "$key" && ${#nodash} -gt 1 ]]; then
# Extrapolate multiple boolean keys in single dash notation. ie. "-vmh" should translate to: "-v -m -h"
local short_param_count=${#nodash}
let new_arg_count=$#+$short_param_count-1
local new_args=""
# $str_pos is the current position in the short param string $nodash
for (( str_pos=0; str_pos<new_arg_count; str_pos++ )); do
# The first character becomes the current key
if [ $str_pos -eq 0 ] ; then
key="${nodash:$str_pos:1}"
breakout=true
fi
# $arg_pos is the current position in the constructed arguments list
let arg_pos=$str_pos+1
if [ $arg_pos -gt $short_param_count ] ; then
# handle other arguments
let orignal_arg_number=$arg_pos-$short_param_count+1
local new_arg="${!orignal_arg_number}"
else
# break out our one argument into new ones
local new_arg="-${nodash:$str_pos:1}"
fi
new_args="$new_args \"$new_arg\""
done
# remove the preceding space and set the new arguments
eval set -- "${new_args# }"
fi
if ! $breakout ; then
key="$nodash"
fi
# By default we expect to shift one argument at a time
shift_count=1
if [ "${#param_pair[@]}" -gt "1" ] ; then
# This is a param with equals notation
value="${param_pair[1]}"
else
# This is either a boolean param and there is no value,
# or the value is the next command line argument
# Assume the value is a boolean true, unless the next argument is found to be a value.
value=true
if [[ $# -gt 1 && -n "$2" ]]; then
local nodash="${2#-}"
if [ "$nodash" = "$2" ]; then
# The next argument has NO preceding dash so it is a value
value="$2"
shift_count=2
fi
fi
fi
# Check that the param being passed is one of the allowed params
if _param_variant "$allowed_params" "$key" ; then
# --key-name will now become param_key_name
eval param_${key//-/_}="$value"
else
printf 'WARNING: Unknown option (ignored): %s\n' "$1" >&2
fi
shift $shift_count
done
}
像这样使用它:
# Assign defaults for parameters
param_help=false
param_path=$(pwd)
param_file=false
param_image=true
param_image_lossy=true
param_image_lossy_quality=85
# Define the params we will allow
allowed_params="h|?|help p|path f|file i|image image-lossy image-lossy-quality"
# Get the params from arguments provided
_get_params $*
这需要一些时间,但我想要全部:
短选项 长选项 不管有没有论点 非选项参数(没有“-”或“——”的参数) 顺序不重要(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解析长选项名
公认的答案很好地指出了bash内置getopts的所有缺点。答案以:
因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。
尽管我原则上同意这种说法,但我觉得我们在各种脚本中实现这个特性的次数证明,我们应该花一些精力来创建一个“标准化”的、经过良好测试的解决方案。
因此,我通过在纯bash中实现getopts_long“升级”了内置在getopts中的bash,没有外部依赖。该函数的使用与内置的getopts 100%兼容。
通过在脚本中包含getopts_long(在GitHub上托管),可以简单地实现原始问题的答案:
source "${PATH_TO}/getopts_long.bash"
while getopts_long ':c: copyfile:' OPTKEY; do
case ${OPTKEY} in
'c'|'copyfile')
echo 'file supplied -- ${OPTARG}'
;;
'?')
echo "INVALID OPTION -- ${OPTARG}" >&2
exit 1
;;
':')
echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
exit 1
;;
*)
echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
exit 1
;;
esac
done
shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
可以考虑以下三种实现方式:
Bash内置的getopts。这不支持带有双破折号前缀的长选项名。它只支持单字符选项。 BSD UNIX实现的独立getopt命令(这是MacOS使用的)。这也不支持长选项。 独立getopt的GNU实现。GNU getopt(3)(由Linux上的命令行getopt(1)使用)支持解析长选项。
其他一些答案给出了使用bash内置getopts模拟长选项的解决方案。该解决方案实际上生成了一个字符为“-”的短选项。所以你得到“——”作为标志。然后,后面的所有内容都变成OPTARG,并使用嵌套的case测试OPTARG。
这很聪明,但也需要注意:
getopts不能强制执行opt规范。如果用户提供了无效的选项,它不能返回错误。在解析OPTARG时,您必须自己进行错误检查。 OPTARG用于长选项名称,当长选项本身有参数时,这会使使用复杂化。你最终不得不自己编写代码作为一个额外的案例。
因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。
为了保持跨平台兼容性,避免依赖外部可执行文件,我从另一种语言移植了一些代码。
我发现它很容易使用,这里有一个例子:
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