13

In my bash script, I am checking that the first argument is either -, 0, or +, using following if statement:

LEVEL=$1
if [ "$LEVEL" -ne "-" ] && [ "$LEVEL" -ne "0" ] && [ "$LEVEL" -ne "+" ]
then
     echo "The value of LEVEL must be either -, 0, or +!"
     exit 1
fi

But it's giving me the error [: -: integer expression expected, referring to the line with the if statement conditions.

I've been trying lots of different syntaxes (e.g., double vs single brackets, quoting vs non-quoting the variables and string literals), but I can't figure it out.

d4nyll
  • 11,811
  • 6
  • 54
  • 68
synaptik
  • 8,971
  • 16
  • 71
  • 98
  • 2
    `man test`. `-ne/-eq` are for numbers only. – Mat Mar 07 '14 at 19:44
  • 2
    FYI, using all-capital variable names is conventional for environment variables and for builtins -- not for unexported locals, as here. Following that rule avoids overwriting environment variables by mistake. – Charles Duffy Mar 07 '14 at 19:46
  • I believe this should be closed as a duplicate of https://stackoverflow.com/questions/21157435/bash-string-compare-to-multiple-correct-values. That other question is more clearly formulated, came sooner, has more votes, and has better answers than the other. I can't explicitly vote to close though because the other question is currently closed. – studgeek Jun 06 '20 at 17:40

2 Answers2

28

The conventional approach, compatible with POSIX sh rather than leveraging bashisms, is to use the case statement:

case $level in
   -|0|+)
     echo "Got it!" ;;
   *)
     echo "Not a valid value" ;;
esac

That said, if you wanted to use test, you could do that too:

if [ "$LEVEL" != "-" ] && [ "$LEVEL" != "0" ] && [ "$LEVEL" != "+" ]; then
  ...
fi

!= is the negating string comparison operator, and = (not ==) is the POSIX-compatible positive-matching one. (Bash extends POSIX to support == in test, but making a habit of using this extension will get you into trouble if you try to write code for a pure POSIX shell later).


Below here, there be bashisms! (Above is POSIX compliant).


In a comment, a follow-up question was asked, in terms of whether the set of possible characters could be read from a variable. In general, yes, though there are some caveats:

# no spaces in possible_levels, as we're using it to form a pattern
possible_levels='-0+'
possible_levels_pattern="[${possible_levels}]"
if [[ $value = $possible_levels_pattern ]]; then
  echo "value contains a valid level"
fi

...As an even shorter approach that allows your input string to be used unmodified, and is almost correct:

# caveat: will say some incorrect strings, like "- 0" or "0 +", are valid levels
possible_levels=' - 0 +'
check_level() {
  [[ " $possible_levels " = *" $1 "* ]] && echo "Value is a valid level"
}

...or, as yet another implementation (verbose, but correct in the particulars):

possible_levels=' - 0 +'
read -a possible_levels_array <<<"$possible_levels"
check_level() {
  local possible_level
  local value=$1
  for possible_level in "${possible_levels_array[@]}"; do
    [[ $value = "$possible_level" ]] && return 0
  done
  return 1
}

if check_level "$value"; then
  echo "$value is a valid level"
else
  echo "$value is not a valid level"
fi

Obviously, this is a lot of work, and in general, just hardcoding the comparisons when appropriate (and possible without a loss of safety) will save you trouble over trying to make things more generic than they truly need to be.

Now, if we could pass in the variable as an associative array, without needing to support the silly space-separated-list thing, that makes it much shorter:

declare -A possible_levels=( [-]=1 [+]=1 [0]=1 )
if [[ ${possible_levels[$value]} ]]; then
  echo "valid level"
else
  echo "invalid level"
fi
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Suppose I had a variable `possiblelevels="- 0 +"`, could I somehow use the variable `$possiblelevels` in the `case` statement to check the `$level` variable? – synaptik Mar 07 '14 at 20:07
  • 1
    @synaptik, I've followed up appropriately. Not in the case statement, and with the spaces like that, you'd be treating a space character as a valid level too, which is probably not what you want. – Charles Duffy Mar 07 '14 at 20:29
  • @synaptik, glad to help. I've added some further edits, including support for the exact space-separated list of levels you suggested. (It would be easier / less code to use an associative array with valid values as the keys for checking, but that assumes control over the format of the list). – Charles Duffy Mar 07 '14 at 20:38
8

You can use [[ and ]] use a selector:

if [[ "$LEVEL" == [0+-] ]]; then
  echo "got it"
fi
anubhava
  • 761,203
  • 64
  • 569
  • 643
  • 2
    +1 -- good use of bashisms, though I'd tend to use `=` rather than `==` here to avoid building finger memory that would compel folks to write non-POSIX code without meaning to (whereas `[[ ]]` is an incompatibility that actually adds value, `==` vs `=` is just sugar) – Charles Duffy Mar 07 '14 at 19:49
  • 2
    @CharlesDuffy Yeah, and not even *good* sugar. I've been programming in C/C++ for 20+ years, and even I think `==` is unsightly in a shell script. I have no idea why the bash folks thought it would *please* C programmers. In C, `==` is only a necessary evil, made necessary by the fact that you can do an `=` (assignment) just about anywhere, including inside conditional expressions. Bad enough I have to type `==` all day long in my C code. Why on Earth would I want to drag that ugliness with me into every other language I use? – Mike Holt Mar 07 '14 at 20:58
  • Also, speaking of bashisms here, as long as we're using `[[ ]]`, there's no need to put quotes around `$LEVEL`. – Mike Holt Mar 07 '14 at 21:06
  • [[ $LEVEL == [0+-] ]] && echo "got it" || echo "bad option" – philcolbourn Mar 08 '14 at 06:32
  • @philcolbourn, `foo && bar || baz` is not fully equivalent to `if foo; then bar; else baz; fi`, as if `bar` fails, then `baz` can be executed even if `foo` was truthy. Granted, if one `echo` fails, the other one almost certainly will do so as well, but it's not a great habit to be in regardless -- sometimes, that fallthrough behavior will matter. – Charles Duffy Oct 12 '15 at 19:33
  • 1
    @philcolbourn, ...also, this matters if `set -e` is enabled or an `ERR` trap is in use: With the `if` statement, then both `echo`s will be considered unchecked events, so either `set -e` or ERR trap behaviors will be activated if they fail; with the pseudo-ternary idiom, `bar` is considered "checked", so ERR traps won't activate if it fails. – Charles Duffy Oct 12 '15 at 19:34