0

I have a bash script that uses a space-delimited string to keep track of all the possible valid arguments:

FRUITS="apple orange pear banana cherry"

I use a bash case statement because some of the arguments have special actions. The script can also loop on all valid arguments if called with no arguments

if ! [ "$1" = "" ]; then FRUITS=$*; fi
for fruit in $FRUITS; do
  case "$fruit" in
    apple)
      action_1 ;;
    cherry)
      action_2 ;;
    apple | orange | pear | banana | cherry)
      default_action ;;
    *)
      echo "Invalid fruit"
      exit 1 ;;
  esac
done

Every time I add a new valid argument I have to add it in 2 places, e.g. if I add "kiwi":

FRUITS="apple orange pear banana cherry kiwi" # KIWI ADDED HERE
if ! [ "$1" = "" ]; then FRUITS=$*; fi
for fruit in $FRUITS; do
  case "$fruit" in
    apple)
      action_1 ;;
    cherry)
      action_2 ;;
    apple | orange | pear | banana | cherry | kiwi) # KIWI ADDED HERE
      default_action ;;
    *)
      echo "Invalid fruit"
      exit 1 ;;
  esac
done

How can I use the $FRUITS variable instead of modifying the default_action pattern?

FRUITS="apple orange pear banana cherry kiwi"
if ! [ "$1" = "" ]; then FRUITS=$*; fi
for fruit in $FRUITS; do
  case "$fruit" in
    apple)
      action_1 ;;
    cherry)
      action_2 ;;
    $FRUITS)   # THIS DOESN'T HAVE "|"s, SO DOESN'T WORK
      default_action ;;
    *)
      echo "Invalid fruit"
      exit 1 ;;
  esac
done
8
  • 2
    You could use a bash array instead to avoid these kind of errors, see stackoverflow.com/a/79719357/2494754 Commented Sep 3 at 4:44
  • 2
    Looping on the contents of $FRUITS and using $FRUITS as one of the cases means you could never reach the * case so your sample code is more confusing than it could be. Commented Sep 3 at 11:41
  • you have apple and cherry defined with 2 different actions in the case statement; is this a typo or is your intention to have apple and cherry fire dual actions? Commented Sep 3 at 20:52
  • 1
    Following your last edit, i would say that going for a basic IF .. ELSE .. FI as shown by @EdMorton answer on the last code block, would be simpler. Commented Sep 4 at 17:50
  • 2
    @NVRM If I was writing the script from scratch, I would use EdMorton's techniques. The code is a bit "spaghettified" at this point. I just wanted to 1 part of it a bit easier. Commented Sep 4 at 18:11

4 Answers 4

4

With bash 4 or 5 (bash 3 doesn't have associative arrays), you could use a dispatch table:

FRUITS="apple orange pear banana cherry kiwi" 

declare -A dispatch

# set default action
# note: will expand pathnames, etc (consider FRUITS="mango /*")
for action in $FRUITS; do
    dispatch[$action]=default_action
done

# add overrides
dispatch[apple]=action_1
dispatch[cherry]=action_2

# define actions
action_1(){ echo action_1; }
action_2(){ echo action_2; }
default_action(){ echo default_action; }
invalid_action(){ echo "Invalid fruit"; exit 1; }

# loop over arguments
for fruit in "$@"; do
     ${dispatch[$fruit]:-invalid_action}
done
Sign up to request clarification or add additional context in comments.

3 Comments

What prevents this from working with bash 3?
no associative arrays
Ah right; Bash had arrays even in version 2, but numerically indexed, not associative.
2

Use another loop to match against the patterns like so:

for fruit in $FRUITS; do
  case $fruit in
  apple)
    action_1 ;;
  cherry)
    action_2 ;;
  *)
    for pat in $FRUITS; do
      case $fruit in "$pat")
        default_action
        continue 2
      esac
    done
    echo Invalid fruit
    exit 1
  esac
done

Comments

2

I ended up using sed, shopt -s extglob and a new variable. It was the easiest way to get what I desired without modifying the script too much:

fruits="apple orange pear banana cherry kiwi"
fruits_pattern=$(echo "$fruits" | sed 's/[[:space:]]/|/g')
shopt -s extglob
if ! [ "$1" = "" ]; then fruits=$*; fi
for fruit in $fruits; do
  case "$fruit" in
    apple)
      action_1 ;;
    cherry)
      action_2 ;;
    $fruits_pattern)
      default_action ;;
    *)
      echo "Invalid fruit"
      exit 1 ;;
  esac
done

Thanks to @oguzismail for continue 2. I did not know about it... VERY useful in the future.

Thanks to @EdMorton for a better future method using bash arrays.

2 Comments

You could use the string manipulation features in Bash instead of sed: fruits_pattern="@(${fruits//[[:space:]]/|})" . See Substituting part of a string (BashFAQ/100 (How do I do string manipulation in bash?)).
Nice. I've already modified my script.
1

Your script could never print Invalid fruit if you want to use $FRUITS before the * case since you're looping on the contents of $FRUITS. Maybe you want to get rid of if ! [ "$1" = "" ]; then FRUITS=$*; fi and then change for fruit in $FRUITS; do to for fruit in "$@"; do? Assuming that's the case, the following would do what you want:

$ cat tst.sh
#!/usr/bin/env bash

declare -A fruits
for fruit in apple orange pear banana cherry kiwi; do
    fruits["$fruit"]=1
done

for fruit in "$@"; do
    case "$fruit" in
    apple)
        echo action_1 ;;
    cherry)
        echo action_2 ;;
    *)
        if [[ -v "fruits[$fruit]" ]]; then
            echo default_action
        else
            echo "Invalid fruit"
            exit 1
        fi
        ;;
    esac
done

but in reality I'd reorganize it to test for valid fruit before the case statement which would then just test the valid values:

#!/usr/bin/env bash

declare -A fruits
for fruit in apple orange pear banana cherry kiwi; do
    fruits["$fruit"]=1
done

for fruit in "$@"; do
    if [[ -v "fruits[$fruit]" ]]; then
        case "$fruit" in
        apple )
            echo action_1 ;;
        cherry )
            echo action_2 ;;
        * )
            echo default_action ;;
        esac
    else
        echo "Invalid fruit"
        exit 1
    fi
done

I'm using lower case letters in the variable name fruits since it's not an environment variable (see Correct Bash and shell script variable capitalization) and an associative array instead of a scalar so I can quote its expansion (see https://mywiki.wooledge.org/Quotes). I made fruits an associative instead of indexed array so I could test for a given fruit existing as an index using -v.

If you really did want your code to start by possibly populating fruits from arguments, you could add an initial scalar array:

if (( $# == 0 )); then
    vals=( apple orange pear banana cherry kiwi )
else
    vals=( "$@" )
fi

declare -A fruits
for fruit in "${vals[@]}"; do
    fruits["$fruit"]=1
done

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.