109

I have the following piece of Bash script:

function get_cms {
    echo "input cms name"
    read cms
    cms=${cms,,}
    if [ "$cms" != "wordpress" && "$cms" != "meganto" && "$cms" != "typo3" ]; then
        get_cms
    fi
}

But no matter what I input (correct and incorrect values), it never calls the function again, because I only want to allow 1 of those 3 inputs.

I have tried it with ||, with [ var != value ] or [ var != value1 ] or [ var != value1 ], but nothing works.

Can someone point me in the right direction?

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
eagle00789
  • 1,211
  • 2
  • 8
  • 6
  • @triplee I voted to reopen. I believe the other question https://stackoverflow.com/questions/22259259 should be closed instead. This question is more clearly forumulated, came sooner, and has better answers than the other. – studgeek Jun 06 '20 at 16:44
  • 1
    @studgeek Thanks for your suggestion; I have now done so. – tripleee Jun 06 '20 at 17:56
  • The duplicate is better IMHO in that it has `case` as the accepted answer, but I guess this is mainly a matter of personal taste. Note that `case` is portable back to the original Bourne shell., and of course also to modern POSIX shell. Maybe see also [Difference between sh and bash](https://stackoverflow.com/questions/5725296/difference-between-sh-and-bash) – tripleee Jun 06 '20 at 18:00
  • 1
    Thanks. This one does have a case answer as well. It isn't the accepted one, but personally I always look at all the answers and their votes more then then which one is accepted (since the latter is just one person's opinion, while the votes are from many folks). – studgeek Jun 08 '20 at 01:21

5 Answers5

174

If the main intent is to check whether the supplied value is not found in a list, maybe you can use the extended regular expression matching built in BASH via the "equal tilde" operator (see also this answer):

if ! [[ "$cms" =~ ^(wordpress|meganto|typo3)$ ]]; then get_cms ; fi
vvvvv
  • 25,404
  • 19
  • 49
  • 81
Edgar Grill
  • 1,876
  • 2
  • 11
  • 7
  • 3
    Extra points for the cleaner answer. I prefer putting the ! inside [[ ]] but both are equally valid. – foob.ar Mar 02 '20 at 17:35
  • Thank you so much for this answer. Helped me to solve an issue at work! – maoyi Oct 02 '20 at 13:48
  • for some reason this doesn't work for me. I'm using (id=2015|id=2016) and I want the IF to execute if the variable has one of those strings anywhere, but it doesn't work – Freedo Dec 08 '21 at 08:55
58

Maybe you should better use a case for such lists:

case "$cms" in
  wordpress|meganto|typo3)
    do_your_else_case
    ;;
  *)
    do_your_then_case
    ;;
esac

I think for long such lists this is better readable.

If you still prefer the if you can do it with single brackets in two ways:

if [ "$cms" != wordpress -a "$cms" != meganto -a "$cms" != typo3 ]; then

or

if [ "$cms" != wordpress ] && [ "$cms" != meganto ] && [ "$cms" != typo3 ]; then
Alfe
  • 56,346
  • 20
  • 107
  • 159
  • 1
    it works great! btw, arent "do_your_then|else_case" text tip inverted? :) – Aquarius Power Aug 01 '14 at 19:52
  • 2
    OP was formulating his original condition using ≠ (`!=`); my version with the `case` is matching (so it is rather using a comparison with `=`), hence the inversion. – Alfe Aug 01 '14 at 23:30
  • 1
    Beautiful solution. Are there any gotchas associated with using case like this? – Xiao Jan 08 '16 at 01:48
  • Shell programming is never free of "gotchas", as you put it ;-) I can think of problems using comparison strings with spaces or pipe symbols or other strange characters in them; in this case you would need to quote them properly. This would take time to find the right syntax and would at least be hard to read for the next maintainer of the code. – Alfe Jan 09 '16 at 22:56
  • @Alfe, as long as you aren't using the `-a` version (which is marked obsolescent in current versions of the POSIX standard for `test`), the content of the string being compared shouldn't have any impact at all. `[ "$foo" != "$bar" ]` is completely unambiguous, ever if your `$foo` or `$bar` are `!`, `(`, `)`, or other characters that might with a longer argument list be potentially meaningful to `test`. – Charles Duffy Feb 18 '18 at 17:07
  • @CharlesDuffy I've never heard of `-a` being deprecated, and my (rather recent) versions of `test` (coreutils 8.25) and of `bash` (4.3.48) don't mention that either. Can you give a pointer to a documentation stating that `-a` is (going to be) deprecated? – Alfe Feb 19 '18 at 10:20
  • 2
    @Alfe, see the `[OB XSI]` notation in http://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html. If you click on it, it'll link you to a definition: *The functionality described may be removed in a future version of this volume of POSIX.1-2008. Strictly Conforming POSIX Applications and Strictly Conforming XSI Applications shall not use obsolescent features*. I doubt GNU coreutils or bash will actually remove it, but it *is* scheduled to in the future no longer be POSIX-mandated behavior (for good cause, given the ambiguities introduced). – Charles Duffy Feb 19 '18 at 13:22
  • Interesting. They don't mention an alternative there (like `\&` for `-a` or similar), so they seem to intend to drop the combination option completely. I think I have to think about that. Currently it strikes me as a drop of an important feature. Having to use `[ a = b ] && [ c = d ]` seems a bit cumbersome to me right now. – Alfe Feb 19 '18 at 16:15
56

Instead of saying:

if [ "$cms" != "wordpress" && "$cms" != "meganto" && "$cms" != "typo3" ]; then

say:

if [[ "$cms" != "wordpress" && "$cms" != "meganto" && "$cms" != "typo3" ]]; then

You might also want to refer to Conditional Constructs.

devnull
  • 118,548
  • 33
  • 236
  • 227
  • 1
    Repeating `"$cms" !=` again and a again isn't very DRY. It leaves room for typos in any of the occasions (e. g. `"$csm" !=`), and you have no compiler to point that out to you in shells. Better try to avoid repeating the variable name. (See my answer for a way to do that.) – Alfe Jan 12 '16 at 11:13
  • I had to remove the `;` at the end so instead of being `... "typo3" ]]; then` it is `..."typo3"]] then`, but otherwise this worked just fine. – Gharbad The Weak Jul 11 '19 at 20:57
16

As @Renich suggests, you can also use extended globbing for pattern matching. So you can use the same patterns you use to match files in command arguments (e.g. ls *.pdf) inside of bash comparisons.

For your particular case you can do the following.

if [[ "${cms}" != @(wordpress|magento|typo3) ]]

The @ means "Matches one of the given patterns". So this is basically saying cms is not equal to 'wordpress' OR 'magento' OR 'typo3'. In normal regular expression syntax @ is similar to just ^(wordpress|magento|typo3)$.

Mitch Frazier has two good articles in the Linux Journal on this Pattern Matching In Bash and Bash Extended Globbing.

For more background on extended globbing see Pattern Matching (Bash Reference Manual).

Abdull
  • 26,371
  • 26
  • 130
  • 172
studgeek
  • 14,272
  • 6
  • 84
  • 96
8

Here's my solution

if [[ "${cms}" != @(wordpress|magento|typo3) ]]; then
Renich
  • 159
  • 2
  • 3
  • 8
    Not `+` but `@` in your extglob. Otherwise you'll get wrong results for, e.g., `wordpressmagento` or `typo3typo3`. – gniourf_gniourf Mar 05 '15 at 10:26
  • @gniourf_gniourf I believe Edgar Grill answer is the same problem using `^` can result in matching any result that begins with "wordpress..." etc. – Jesse Nickles Jul 19 '21 at 17:58