In bash, is there a way I can make shorter (if or) statements?

Solution 1:

It looks like a bug in your code, but we can refactor it down really small...

If you had really written your expression with [ foo = bar -o foo = car -o foo = jar -o foo = foo ], then it could've been simplified to:

echo yes

This test is always true because foo = foo is always true, which in turn is because the literal string foo is always the same as itself.

Though you've clarified this syntax didn't appear in code you were really using, I've kept this introductory section as it's a common mistake to do so, especially among novices. Attempting to check the value of foo this way doesn't work in any Bourne-style shell--to examine the value of a variable with [, you must perform parameter expansion with $ (see below). This particular aspect of the problem is not shell-specific; for example, here's a test in 7 Bourne-style shells.

Fixing the Original Code

Perhaps instead you meant to check the value of the foo variable (i.e., its contents, rather than its name) against those strings. In that case, your original code would become like:

if [ "$foo" = bar -o "$foo" = car -o "$foo" = jar -o "$foo" = foo ]; then
    echo yes
fi

This can be shortened slightly with the short-circuit && operator:

[ "$foo" = bar -o "$foo" = car -o "$foo" = jar -o "$foo" = foo ] && echo yes

But it's still longer and more complicated and repetitive than you probably prefer. So, moving on...

Shorter Forms via Pattern Matching

You can use the [[ facility's built-in regular expression matching operator:

[[ $foo =~ ^(bar|car|jar|foo)$ ]] && echo yes

While using case is probably the most common way, [[ with =~ is:

  • the same length as a compact use of case (see below)
  • arguably truer to the logic of your original code, in that it uses [[, which works much like a test expression.

So you might prefer that. It's the method I would probably use, to do this in bash.

If you wanted to do something like that but match using grep instead of [[ with =~, this is similar:

grep -Eq '^(bar|car|jar|foo)$' <<< "$foo" && echo yes

Or this:

grep -Fqe{bar,car,jar,foo} <<< "$foo" && echo yes

A One-Liner with case

As Serg says, can be simplified using case instead of if and [. That's the classic and probably the most common way.

The method in Serg's answer works perfectly well, but for the particular code you're refactoring, we can write something simpler, by using only one pattern for case. There is no need to have a catch-all *) match at the end. If the string doesn't match any of the patterns, control simply falls through the case construct and no commands are executed, which is equivalent to your if construct with no else.

case $foo in bar|car|jar|foo) echo yes;; esac

That makes it short and simple enough to easily fit--and be readable--on a single line (which seems to be what you're looking for). This method is also portable to Bourne-style shells other than bash.

Solution 2:

I suggest using case structure. Something like:

case $foo in
    foo|car|bar|jar) echo yes ;;
    *) ;;
esac

If you like, you can add a command between *) and ;; to be executed when $foo doesn't match foo, car, bar, or jar. For example, you could print a message with echo 'wrong input' or something like that.

Solution 3:

Even shorter:

[[ $foo =~ ^([bcj]ar|foo)$ ]] && echo yes

This way $foo is matched against a regex either starting with b, c or j followed by ar or matching exactly foo.

This uses the && operator as a short-circuit to run echo yes only if the [[ $foo =~ ^([bcj]ar|foo)$ ]] expression evaluates to true.

If this is too compact in order to be read with ease, you can still use the full if construct, which does exactly the same but is more readable:

if [[ $foo =~ ^([bcj]ar|foo)$ ]]
then
    echo yes
fi