4

コマンドを最初の位置パラメーターとして受け取り、次のようなディスパッチとしてcase構文を使用するbashスクリプトを作成しました。

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

これは目障りではありません。ただし、これにより、 「comblah」として指定さdo_commandれたときに呼び出される場所$1や、有効な引数として認識されるべきではない他の何かなど、望ましくない動作が発生する可能性があります。

私の質問は次のとおりです。ユーザーが期待されるコマンドの明確な切り捨てられた形式を提供できる場合に、そのようなコマンドを正しくディスパッチするための最も洗練された(上記で定義された)方法は何ですか?

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

私は次の解決策を思いつきました。これは、ボーン互換のシェルで機能するはずです。

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:

一致は、最初の位置パラメーターとして(部分)コマンドを使用して呼び出され、その後にテスト対象の文字列が続きます。複数の一致では、各部分一致は大文字でヒントされます。

# @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;

iimimpは、命令が一致するまでアップします。シフトにより、2番目の位置パラメータが最初に設定されます。スクリプトが次のように呼び出された場合:

./script.sh imp say hello

に解決されます

do_imperative say hello

ショートハンドコマンドをさらに解決したい場合は、関数内でも同じアプローチを使用してください。

于 2018-08-26T10:33:49.150 に答える
0

compgenこれは、bash固有のコマンドに依存しているため、bashでのみ機能する単純な(場合によってはエレガントな)ソリューションです。

このバージョンでは、アクション関数が常に呼び出されることを前提としています。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 に答える