Search code examples
bashcommandpattern-matchingdispatch

bash: partial match up to a complete word for case


I wrote a bash script that takes a command as the first positional parameter and uses a case construct as a dispatch similar to the following:

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

This script decides what function to call based on $1 and then passes the remaining arguments to that function. I would like to add the ability dispatch on distinct partial matches, but I want to do it in an elegant way (elegant defined as a way that is both easy to read and is not so verbose as to be an eyesore or a distraction).

The obvious functioning (but not elegant) solution might be something like this:

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

As you can see, explicitly enumerating all distinct permutations of each command name can get really messy.

For a moment, I thought it might be alright to use a wildcard match like this:

case $cmd in
com*)
  do_command $*
  ;;
cop*)
  do_copy $*
  ;;
i*)
  do_imperative $*
  ;;
*)
  echo "Usage: $0 [ command | copy | imperative ]" >&2
  ;;
esac

This is less of an eyesore. However, this could result in undesirable behavior such as where do_command is called when $1 is given as "comblah" or something else that shouldn't be recognized as a valid argument.

My question is: What is the most elegant (as defined above) way to correctly dispatch such a command where the user can provide any distinct truncated form of the expected commands?


Solution

  • Pattern Matching for Command Dispatch in Bash

    It seems that a few of you like the idea of using a resolver to find the full command match prior to the dispatching logic. That's might just be the best way to go for large command sets or sets that have long words. I put together the following hacked up mess--it makes 2 passes using built-in parameter expansion substring removal. I seems to work well enough and it keeps the dispatch logic clean of the distraction of resolving partial commands. My bash version is 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"