4

我编写了一个 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

这不那么碍眼。但是,这可能会导致不良行为,例如 where do_commandis called when $1is given as "comblah" 或其他不应被视为有效参数的东西。

我的问题是:在用户可以提供任何不同截断形式的预期命令的情况下,正确调度此类命令的最优雅(如上定义)方式是什么?

4

4 回答 4

2

Bash 中命令调度的模式匹配

似乎你们中的一些人喜欢使用解析器在调度逻辑之前找到完整的命令匹配的想法。对于大型命令集或长词集,这可能是最好的方法。我把下面的乱七八糟的东西放在一起——它使用内置的参数扩展子字符串删除进行了 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"
于 2013-02-16T14:48:57.640 回答
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并不漂亮,但我希望你大部分时间都不必看这段代码。

于 2013-02-16T10:51:37.883 回答
0

更新1:

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直到命令式匹配。shift 会将第二个位置参数设置为第一个,含义;如果脚本被称为:

./script.sh imp say hello

将解决成

do_imperative say hello

如果您希望进一步解析速记命令,也可以在函数中使用相同的方法。

于 2018-08-26T10:33:49.150 回答
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) 
# ...
于 2018-09-09T16:33:39.250 回答