bash:部分匹配一个完整的单词

时间:2013-02-16 09:46:43

标签: bash command pattern-matching dispatch

我写了一个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或其他不应被识别为有效参数的内容。

我的问题是:正确调度此类命令的最优雅(如上所述)方法是什么,用户可以提供任何截然不同的预期命令形式?

4 个答案:

答案 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)

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"

答案 2 :(得分: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 ,直到命令性匹配为止。移位会将第二个位置参数设置为第一个,即;如果脚本被称为:

./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) 
# ...