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

问题描述 投票:4回答: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

这不是一个眼睛。然而,这可能导致不良行为,例如当do_command被称为“comblah”或其他不应被视为有效参数的东西时调用$1

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

bash command pattern-matching dispatch
4个回答
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的源代码并不漂亮,但我希望你不必在大多数时候都看这个代码。


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"

0
投票

Update 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

First answer: Case approach; would require some logic before use, to solve disambiguate partial matches.

#!/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;

我,我,直到命令将匹配。该移位将第二个位置参数设置为第一个,意思是;如果脚本被调用为:

./script.sh imp say hello

将解决

do_imperative say hello

如果您希望进一步解决短手命令,请在功能中使用相同的方法。


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) 
# ...
© www.soinside.com 2019 - 2024. All rights reserved.