我写了一个bash脚本,它将一个命令作为第一个位置参数,并使用case构造作为类似于以下内容的调度:
do_command() {
# responds to invocation `$0 command ...`
}
do_copy() {
# respond to invocation: `$0 copy...`
}
do_imperative() {
# respond to invocation: `$0 imperative ...`
}
cmd=$1
shift
case $cmd in
command)
do_command $*
;;
copy)
do_copy $*
;;
imperative)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
此脚本根据$1
决定调用哪个函数,然后将剩余的参数传递给该函数。我想在不同的部分匹配上添加能力调度,但我希望以优雅的方式进行(优雅定义为既易于阅读又不会像眼睛或分心那样冗长的方式)。
明显的功能(但不是优雅)解决方案可能是这样的:
case $cmd in
command|comman|comma|comm|com)
do_command $*
;;
copy|cop)
do_copy $*
;;
imperative|imperativ|imperati|imperat|impera|imper|impe|imp|im|i)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
正如您所看到的,显式枚举每个命令名称的所有不同排列都会变得非常混乱。
有一会儿,我觉得使用这样的通配符可能没问题:
case $cmd in
com*)
do_command $*
;;
cop*)
do_copy $*
;;
i*)
do_imperative $*
;;
*)
echo "Usage: $0 [ command | copy | imperative ]" >&2
;;
esac
这不是一个眼睛。但是,这可能会导致不良行为,例如当do_command
被称为“comblah”时调用$1
或其他不应被识别为有效参数的内容。
我的问题是:正确调度此类命令的最优雅(如上所述)方法是什么,用户可以提供任何截然不同的预期命令形式?
答案 0 :(得分:2)
我想出了以下解决方案,它应该适用于任何解决方案 与bourne兼容的shell:
disambiguate() {
option="$1"
shift
found=""
all=""
comma=""
for candidate in "$@"; do
case "$candidate" in
"$option"*)
found="$candidate"
all="$all$comma$candidate"
comma=", "
esac
done
if [ -z "$found" ] ; then
echo "Unknown option $option: should be one of $@" >&2
return 1;
fi
if [ "$all" = "$found" ] ; then
echo "$found"
else
echo "Ambigious option $option: may be $all" >&2
return 1
fi
}
foo=$(disambiguate "$1" lorem ipsum dolor dollar)
if [ -z "$foo" ] ; then exit 1; fi
echo "$foo"
是的,disambiguate
的源代码并不漂亮,但我希望你
大部分时间都不必查看此代码。
答案 1 :(得分:2)
似乎有些人喜欢在调度逻辑之前使用解析器查找完整命令匹配的想法。这可能只是拥有长字的大型命令集或集合的最佳方式。我把以下乱糟糟的东西放在一起 - 它使用内置的参数扩展子字符串删除进行2次传递。我似乎工作得很好,它保持调度逻辑清除解决部分命令的分心。我的bash版本是4.1.5。
#!/bin/bash
resolve_cmd() {
local given=$1
shift
local list=($*)
local inv=(${list[*]##${given}*})
local OIFS=$IFS; IFS='|'; local pat="${inv[*]}"; IFS=$OIFS
shopt -s extglob
echo "${list[*]##+($pat)}"
shopt -u extglob
}
valid_cmds="start stop status command copy imperative empathy emperor"
m=($(resolve_cmd $1 $valid_cmds))
if [ ${#m[*]} -gt 1 ]; then
echo "$1 is ambiguous, possible matches: ${m[*]}" >&2
exit 1
elif [ ${#m[*]} -lt 1 ]; then
echo "$1 is not a recognized command." >&2
exit 1
fi
echo "Matched command: $m"
答案 2 :(得分:0)
match 使用(部分)命令作为第一个位置参数,后跟要测试的字符串来调用。在多个匹配项中,每个部分匹配项都将用大写字母提示。
# @pos 1 string
# @pos 2+ strings to compare against
# @ret true on one match, false on none|disambiguate match
match() {
local w input="${1,,}" disa=();
local len=${#input}; # needed for uppercase hints
shift;
for w in $*; do
[[ "$input" == "$w" ]] && return 0;
[[ "$w" == "$input"* ]] && disa+=($w);
done
if ! (( ${#disa[*]} == 1 )); then
printf "Matches: "
for w in ${disa[*]}; do
printf "$( echo "${w:0:$len}" | tr '[:lower:]' '[:upper:]')${w:${len}} ";
done
echo "";
return 1;
fi
return 0;
}
用法示例。可以调整 match 来打印/返回整个无歧义的命令,否则 do_something_with 将需要一些逻辑来解析部分命令。 (类似于我的第一个答案)
cmds="start stop status command copy imperative empathy emperor"
while true; do
read -p"> " cmd
test -z "$cmd" && exit 1;
match $cmd $cmds && do_something_with "$cmd";
done
#!/bin/bash
# script.sh
# set extended globbing, in most shells it's not set by default
shopt -s extglob;
do_imperative() {
echo $*;
}
case $1 in
i?(m?(p?(e?(r?(a?(t?(i?(v?(e))))))))))
shift;
do_imperative $*;
;;
*)
echo "Error: no match on $1";
exit 1;
;;
esac
exit 0;
i , im , imp ,直到命令性匹配为止。移位会将第二个位置参数设置为第一个,即;如果脚本被称为:
./script.sh imp say hello
将解析为
do_imperative say hello
如果您想进一步解决速记命令,请在功能内也使用相同的方法。
答案 3 :(得分:0)
这是一个仅适用于bash的简单(可能甚至是优雅的)解决方案,因为它依赖于bash特定的compgen
命令。
此版本假定操作函数始终称为do_X
,其中X
是命令名称。在调用此函数之前,您需要将$commands
设置为以空格分隔的合法命令列表。假设合法命令将是简单的单词,因为函数名不能包含特殊字符。
doit () {
# Do nothing if there is no command
if (( ! $# )); then return 0; fi;
local cmd=$1
shift
local -a options=($(compgen -W "$commands" "$cmd"));
case ${#options[@]} in
0)
printf "Unrecognized command '%b'\n" "$cmd" >> /dev/stderr;
return 1
;;
1)
# Assume that the action functions all have a consistent name
"do_$options" "$@"
;;
*)
printf "Ambigous command '%b'. Possible completions:" "$cmd";
printf " %s" "${options[@]}";
printf "\n";
return 1
;;
esac
}
do_command () { printf "command %s\n" "$@"; }
do_copy () { printf "copy %s\n" "$@"; }
do_imperative () { printf "imperative %s\n" "$@"; }
commands="command copy imperative"
试运行:
$ doit cop a "b c"
copy a
copy b c
$ doit comfoo a "b c"
Unrecognized command 'comfoo'
$ doit co a "b c"
Ambigous command 'co'. Possible completions: command copy
$ doit i a "b c"
如果您确信没有杂散的do_X
变量可用,则也可以使用compgen
来列出命令列表:
command=$(compgen -Afunction do_ | cut -c4-)
或者,您可以使用相同的系统来创建解析器,然后使用普通的case语句处理返回的选项:
# resolve cmd possible-commands
resolve () {
# Fail silently if there is no command
if [[ -z $1 ]]; then return 1; fi;
local cmd=$1
shift
local commands="$*"
local -a options=($(compgen -W "$commands" "$cmd"));
case ${#options[@]} in
0)
printf "Unrecognized command '%b'\n" "$cmd" >> /dev/stderr;
return 1
;;
1)
echo $options
return 0
;;
*)
printf "Ambigous command '%b'. Possible completions:" "$cmd";
printf " %s" "${options[@]}";
printf "\n";
return 1
;;
esac
}
$ resolve com command copy imperative && echo OK
command
OK
$ resolve co command copy imperative && echo OK
Ambigous command 'co'. Possible completions: command copy
$ resolve copx command copy imperative && echo OK
Unrecognized command 'copx'
目的是写类似这样的东西:
cmd=$(resolve "$1" "$commands") || exit 1
case "$cmd" in
command)
# ...