[linux] Check if a variable exists in a list in Bash

I am trying to write a script in bash that check the validity of a user input.
I want to match the input (say variable x) to a list of valid values.

what I have come up with at the moment is:

for item in $list
do
    if [ "$x" == "$item" ]; then
        echo "In the list"
        exit
    fi
done

My question is if there is a simpler way to do this,
something like a list.contains(x) for most programming languages.

Say list is:

list="11 22 33"

my code will echo the message only for those values since list is treated as an array and not a string, all the string manipulations will validate 1 while I would want it to fail.

This question is related to linux bash

The answer is


I find it's easier to use the form echo $LIST | xargs -n1 echo | grep $VALUE as illustrated below:

LIST="ITEM1 ITEM2"
VALUE="ITEM1"
if [ -n "`echo $LIST | xargs -n1 echo | grep -e \"^$VALUE`$\" ]; then
    ...
fi

This works for a space-separated list, but you could adapt it to any other delimiter (like :) by doing the following:

LIST="ITEM1:ITEM2"
VALUE="ITEM1"
if [ -n "`echo $LIST | sed 's|:|\\n|g' | grep -e \"^$VALUE`$\"`" ]; then
   ...
fi

Note that the " are required for the test to work.


Consider exploiting the keys of associative arrays. I would presume this outperforms both regex/pattern matching and looping, although I haven't profiled it.

declare -A list=( [one]=1 [two]=two [three]='any non-empty value' )
for value in one two three four
do
    echo -n "$value is "
    # a missing key expands to the null string, 
    # and we've set each interesting key to a non-empty value
    [[ -z "${list[$value]}" ]] && echo -n '*not* '
    echo "a member of ( ${!list[*]} )"
done

Output:

one is a member of ( one two three )
two is a member of ( one two three )
three is a member of ( one two three )
four is *not* a member of ( one two three )

If your list of values is to be hard-coded in the script, it's fairly simple to test using case. Here's a short example, which you can adapt to your requirements:

for item in $list
do
    case "$x" in
      item1|item2)
        echo "In the list"
        ;;
      not_an_item)
        echo "Error" >&2
        exit 1
        ;;
    esac
done

If the list is an array variable at runtime, one of the other answers is probably a better fit.


Matvey is right, but you should quote $x and consider any kind of "spaces" (e.g. new line) with

[[ $list =~ (^|[[:space:]])"$x"($|[[:space:]]) ]] && echo 'yes' || echo 'no' 

so, i.e.

# list_include_item "10 11 12" "2"
function list_include_item {
  local list="$1"
  local item="$2"
  if [[ $list =~ (^|[[:space:]])"$item"($|[[:space:]]) ]] ; then
    # yes, list include item
    result=0
  else
    result=1
  fi
  return $result
}

end then

`list_include_item "10 11 12" "12"`  && echo "yes" || echo "no"

or

if `list_include_item "10 11 12" "1"` ; then
  echo "yes"
else 
  echo "no"
fi

Note that you must use "" in case of variables:

`list_include_item "$my_list" "$my_item"`  && echo "yes" || echo "no"

how about

echo $list | grep -w -q $x

you could either check the output or $? of above line to make the decision.

grep -w checks on whole word patterns. Adding -q prevents echoing the list.


The shell built-in compgen can help here. It can take a list with the -W flag and return any of the potential matches it finds.

# My list can contain spaces so I want to set the internal
# file separator to newline to preserve the original strings.
IFS=$'\n'

# Create a list of acceptable strings.
accept=( 'foo' 'bar' 'foo bar' )

# The string we will check
word='foo'

# compgen will return a list of possible matches of the 
# variable 'word' with the best match being first.
compgen -W "${accept[*]}" "$word"

# Returns:
# foo
# foo bar

We can write a function to test if a string equals the best match of acceptable strings. This allows you to return a 0 or 1 for a pass or fail respectively.

function validate {
  local IFS=$'\n'
  local accept=( 'foo' 'bar' 'foo bar' )
  if [ "$1" == "$(compgen -W "${accept[*]}" "$1" | head -1)" ] ; then
    return 0
  else
    return 1
  fi
}

Now you can write very clean tests to validate if a string is acceptable.

validate "blah" || echo unacceptable

if validate "foo" ; then
  echo acceptable
else 
  echo unacceptable
fi

If it isn't too long; you can just string them between equality along a logical OR comparison like so.

if [ $ITEM == "item1" -o $ITEM == "item2" -o $ITEM == "item3" ]; then
    echo In the list
fi 

I had this exact problem and while the above is ugly it is more obvious what is going on than the other generalized solutions.


This is almost your original proposal but almost a 1-liner. Not that complicated as other valid answers, and not so depending on bash versions (can work with old bashes).

OK=0 ; MP_FLAVOURS="vanilla lemon hazelnut straciatella"
for FLAV in $MP_FLAVOURS ; do [ $FLAV == $FLAVOR ] && { OK=1 ; break; } ; done
[ $OK -eq 0 ] && { echo "$FLAVOR not a valid value ($MP_FLAVOURS)" ; exit 1 ; }

I guess my proposal can still be improved, both in length and style.


You can use (* wildcards) outside a case statement, too, if you use double brackets:

string='My string';

if [[ $string == *My* ]]
then
echo "It's there!";
fi

Thought I'd add my solution to the list.

# Checks if element "$1" is in array "$2"
# @NOTE:
#   Be sure that array is passed in the form:
#       "${ARR[@]}"
elementIn () {
    # shopt -s nocasematch # Can be useful to disable case-matching
    local e
    for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done
    return 1
}

# Usage:
list=(11 22 33)
item=22

if elementIn "$item" "${list[@]}"; then
    echo TRUE;
else
    echo FALSE
fi
# TRUE

item=44
elementIn $item "${list[@]}" && echo TRUE || echo FALSE
# FALSE

If the list is fixed in the script, I like the following the best:

validate() {
    grep -F -q -x "$1" <<EOF
item 1
item 2
item 3
EOF
}

Then use validate "$x" to test if $x is allowed.

If you want a one-liner, and don't care about whitespace in item names, you can use this (notice -w instead of -x):

validate() { echo "11 22 33" | grep -F -q -w "$1"; }

Notes:

  • This is POSIX sh compliant.
  • validate does not accept substrings (remove the -x option to grep if you want that).
  • validate interprets its argument as a fixed string, not a regular expression (remove the -F option to grep if you want that).

Sample code to exercise the function:

for x in "item 1" "item2" "item 3" "3" "*"; do
    echo -n "'$x' is "
    validate "$x" && echo "valid" || echo "invalid"
done

Assuming TARGET variable can be only 'binomial' or 'regression', then following would do:

# Check for modeling types known to this script
if [ $( echo "${TARGET}" | egrep -c "^(binomial|regression)$" ) -eq 0 ]; then
    echo "This scoring program can only handle 'binomial' and 'regression' methods now." >&2
    usage
fi

You could add more strings into the list by separating them with a | (pipe) character.

Advantage of using egrep, is that you could easily add case insensitivity (-i), or check more complex scenarios with a regular expression.


Prior answers don't use tr which I found to be useful with grep. Assuming that the items in the list are space delimited, to check for an exact match:

echo $mylist | tr ' ' '\n' | grep -F -x -q "$myitem"

This will return exit code 0 if the item is in the list, or exit code 1 if it isn't.

It's best to use it as a function:

_contains () {  # Check if space-separated list $1 contains line $2
  echo "$1" | tr ' ' '\n' | grep -F -x -q "$2"
}

mylist="aa bb cc"

# Positive check
if _contains "${mylist}" "${myitem}"; then
  echo "in list"
fi

# Negative check
if ! _contains "${mylist}" "${myitem}"; then
  echo "not in list"
fi

Examples

$ in_list super test me out
NO

$ in_list "super dude" test me out
NO

$ in_list "super dude" test me "super dude"
YES

# How to use in another script
if [ $(in_list $1 OPTION1 OPTION2) == "NO" ]
then
  echo "UNKNOWN type for param 1: Should be OPTION1 or OPTION2"
  exit;
fi

in_list

function show_help()
{
  IT=$(CAT <<EOF

  usage: SEARCH_FOR {ITEM1} {ITEM2} {ITEM3} ...

  e.g. 

  a b c d                    -> NO
  a b a d                    -> YES
  "test me" how "test me"    -> YES

  )
  echo "$IT"
  exit
}

if [ "$1" == "help" ]
then
  show_help
fi

if [ "$#" -eq 0 ]; then
  show_help
fi

SEARCH_FOR=$1
shift;

for ITEM in "$@"
do
  if [ "$SEARCH_FOR" == "$ITEM" ]
  then
    echo "YES"
    exit;
  fi
done

echo "NO"

IMHO easiest solution is to prepend and append the original string with a space and check against a regex with [[ ]]

haystack='foo bar'
needle='bar'

if [[ " $haystack " =~ .*\ $needle\ .* ]]; then
    ...
fi

this will not be false positive on values with values containing the needle as a substring, e.g. with a haystack foo barbaz.

(The concept is shamelessly stolen form JQuery's hasClass()-Method)


An alternative solution inspired by the accepted response, but that uses an inverted logic:

MODE="${1}"

echo "<${MODE}>"
[[ "${MODE}" =~ ^(preview|live|both)$ ]] && echo "OK" || echo "Uh?"

Here, the input ($MODE) must be one of the options in the regular expression ('preview', 'live', or 'both'), contrary to matching the whole options list to the user input. Of course, you do not expect the regular expression to change.