Differences between revisions 12 and 20 (spanning 8 versions)
Revision 12 as of 2011-04-02 04:49:13
Size: 5781
Editor: sn18
Comment: printf %q quotes | character
Revision 20 as of 2023-04-29 04:33:04
Size: 2010
Editor: ormaaj
Comment: >:-|
Deletions are marked like this. Additions are marked like this.
Line 3: Line 3:
If your real question was ''How do I check whether one of my parameters was -v?'' then please see [[BashFAQ/035|FAQ #35]] instead.  Otherwise, read on.... If your real question is ''How do I check whether one of my parameters was -v?'' then see [[BashFAQ/035|FAQ #35]] instead. Otherwise, read on
Line 5: Line 5:
First of all, let's get the terminology straight. Bash has no notion of "lists" or "sets" or any such. Bash has strings and [[BashFAQ/005|arrays]]. Strings are a "list" of '''characters''', arrays are a "list" of '''strings'''. <<TableOfContents>>
=== Associative arrays ===
All we need to do is create an entry for each item and look it up by index. In this example, we test whether the user input `x` is a member of the set `a`:
Line 7: Line 9:
'''NOTE:''' In the general case, a string cannot possibly contain a list of other strings because there is no reliable way to tell where each substring begins and ends. {{{
# Bash etc.
Line 9: Line 12:
Given a traditional array, the only proper way to do this is to loop over all elements in your array and check them for the element you are looking for. Say what we are looking for is in `bar` and our list is in the array `foo`:
   {{{
   # Bash
   for element in "${foo[@]}"; do
      [[ $element = $bar ]] && echo "Found $bar."
   done
   }}}
function get_input {
 [[ -t 0 ]] || return
 printf 'hm? '
 IFS= read -r${BASH_VERSION+\e} -- "$1"
}
Line 17: Line 18:
If you need to perform this several times in your script, you might want to extract the logic into a function:
   {{{
   # Bash
   isIn() {
       local pattern="$1" element
       shift
set -- Bigfoot UFOs Republicans
typeset -A a
for x; do
 a+=([$x]=)
done
Line 24: Line 24:
       for element
       do
           [[ $element = $pattern ]] && return 0
       done
get_input x
Line 29: Line 26:
       return 1
   }
if [[ -v a[$x] ]]; then
 printf '%s exists!\n' "$x"
else
 printf $'%s doesn\'t exist.\\n' "$x"
fi
}}}
Line 32: Line 33:
   if isIn "jacob" "${names[@]}"
   then
       echo "Jacob is on the list."
   fi
   }}}
=== Indexed arrays ===
We can store a list of strings in an indexed array by looping over each element:
Line 38: Line 36:
Or, if you want your function to return the '''index''' at which the element was found:
   {{{
   # Bash 3.0 or higher
   indexOf() {
       local pattern=$1
       local index list
       shift
{{{
# Bash
Line 46: Line 39:
       list=("$@")
       for index in "${!list[@]}"
       do
           [[ ${list[index]} = $pattern ]] && {
               echo $index
               return 0
           }
       done
typeset -a haystack
for x in "${haystack[@]}"; do
 [[ $x == "$needle" ]] && printf 'Found %q!\n' "$needle"
done
}}}
Line 55: Line 45:
       echo -1
       return 1
   }
=== enum (ksh93) ===
In ksh93t or later, one may create enum types/variables/constants using the `enum` builtin. These work similarly to C enums (and the equivalent feature of other languages). These may be used to restrict which values may be assigned to a variable so as to avoid the need for an expensive test each time an array variable is set or referenced. Like types created using `typeset -T`, the result of an `enum` command is a new declaration command that can be used to instantiate objects of that type.
Line 59: Line 48:
   if index=$(indexOf "jacob" "${names[@]}")
   then
       echo "Jacob is the ${index}th on the list."
   else
       echo "Jacob is not on the list."
   fi
   }}}
{{{
# ksh93
Line 67: Line 51:
If your "list" is contained in a string, and for some half-witted reason you choose not to heed the warnings above, you can use the following code to search through "words" in a string. (The only real excuse for this would be that you're stuck in Bourne shell, which has no arrays.)
   {{{
   # Bourne
   set -f
   for element in $foo; do
      if test x"$element" = x"$bar"; then
         echo "Found $bar."
      fi
   done
   set +f
   }}}
 $ enum colors=(red green blue)
 $ colors foo=green
 $ foo=yellow
ksh: foo: invalid value yellow
}}}
Line 79: Line 57:
Here, a "word" is defined as any substring that is delimited by whitespace (or more specifically, the characters currently in IFS). The `set -f` prevents [[glob]] expansion of the words in the list. Turning glob expansions back on (`set +f`) is optional. `typeset -a` can also be used in combination with an enum type to allow enum constants as subscripts.
Line 81: Line 59:
If you're working in bash 4 or ksh93, you have access to associative arrays. These will allow you to restructure the problem -- instead of making a list of words that are allowed, you can make an ''associative array'' whose keys are the words you want to allow. Their values could be meaningful, or not -- depending on the nature of the problem. {{{
# ksh93
Line 83: Line 62:
   {{{
   # Bash 4
   declare -A good
   for word in "goodword1" "goodword2" ...; do
     good["$word"]=1
   done

   # Check whether $foo is allowed:
   if ((${good[$foo]})); then ...
   }}}

Here's a hack that you shouldn't use, but which is presented for the sake of completeness:
   {{{
   # Bash
   if [[ " $foo " = *" $bar "* ]]; then
      echo "Found $bar."
   fi
   }}}
(The problem here is that is assumes ''space'' can be used as a delimiter between words. Your elements might contain spaces, which would break this!)

That same hack, for Bourne shells:
   {{{
   # Bourne
   case " $foo " in
      *" $bar "*) echo "Found $bar.";;
   esac
   }}}

You can also use extended glob with printf to search for a word in an array.
''I haven't tested it enough, so it might break in some cases --sn18''

   {{{
   # Bash
   shopt -s extglob
   #convert array to glob
   printf -v glob '%q|' "${array[@]}"
   glob=${glob%|}
   [[ $word = @($glob) ]] && echo "Found $word"
   }}}

  . ''It will break when an array element contains a | character. Hence, I moved it down here with the other hacks that work in a similar fashion and have a similar limitation.'' -- GreyCat
   . ''printf %q quotes a | character too, so it probably should not'' --sn18

GNU's grep has a {{{\b}}} feature which allegedly matches the edges of words. Using that, one may attempt to replicate the shorter approach used above, but it is fraught with peril:

   {{{
   # Is 'foo' one of the positional parameters?
   egrep '\bfoo\b' <<<"$@" >/dev/null && echo yes

   # This is where it fails: is '-v' one of the positional parameters?
   egrep '\b-v\b' <<<"$@" >/dev/null && echo yes
   # Unfortunately, \b sees "v" as a separate word.
   # Nobody knows what the hell it's doing with the "-".

   # Is "someword" in the array 'array'?
   egrep '\bsomeword\b' <<<"${array[@]}"
   # Obviously, you can't use this if someword is '-v'!
   }}}

Since this "feature" of GNU grep is both non-portable and poorly defined, we recommend '''not''' using it. It is simply mentioned here for the sake of completeness.

== Bulk comparison ==
This method tries to compare the desired string to the entire contents of the array. It can potentially be very efficient, but it depends on a delimiter that must not be in the sought value or the array. Here we use $'\a', the BEL character, because it's extremely uncommon.

   {{{
   # usage: if has "element" list of words; then ...; fi
   has() {
     local IFS=$'\a' t="$1"
     shift
     [[ $'\a'"$*"$'\a' == *$'\a'$t$'\a'* ]]
   }
   }}}
 $ typeset -a '[colors]' bar
 $ bar[blue]=test1
 $ typeset -p bar
typeset -a '[colors]' bar=([blue]=test)
 $ bar[orange]=test
ksh: colors: invalid value orange
}}}

I want to check to see whether a word is in a list (or an element is a member of a set).

If your real question is How do I check whether one of my parameters was -v? then see FAQ #35 instead. Otherwise, read on…

Associative arrays

All we need to do is create an entry for each item and look it up by index. In this example, we test whether the user input x is a member of the set a:

# Bash etc.

function get_input {
        [[ -t 0 ]] || return
        printf 'hm? '
        IFS= read -r${BASH_VERSION+\e} -- "$1"
}

set -- Bigfoot UFOs Republicans
typeset -A a
for x; do
        a+=([$x]=)
done

get_input x

if [[ -v a[$x] ]]; then
        printf '%s exists!\n' "$x"
else
        printf $'%s doesn\'t exist.\\n' "$x"
fi

Indexed arrays

We can store a list of strings in an indexed array by looping over each element:

# Bash

typeset -a haystack
for x in "${haystack[@]}"; do
        [[ $x == "$needle" ]] && printf 'Found %q!\n' "$needle"
done

enum (ksh93)

In ksh93t or later, one may create enum types/variables/constants using the enum builtin. These work similarly to C enums (and the equivalent feature of other languages). These may be used to restrict which values may be assigned to a variable so as to avoid the need for an expensive test each time an array variable is set or referenced. Like types created using typeset -T, the result of an enum command is a new declaration command that can be used to instantiate objects of that type.

# ksh93

 $ enum colors=(red green blue)
 $ colors foo=green
 $ foo=yellow
ksh: foo:  invalid value yellow

typeset -a can also be used in combination with an enum type to allow enum constants as subscripts.

# ksh93

 $ typeset -a '[colors]' bar
 $ bar[blue]=test1
 $ typeset -p bar
typeset -a '[colors]' bar=([blue]=test)
 $ bar[orange]=test
ksh: colors:  invalid value orange


CategoryShell

BashFAQ/046 (last edited 2023-04-29 04:33:04 by ormaaj)