6

I want to write a function that checks if a given variable, say, var, starts with any of the words in a given list of strings. This list won't change.

To instantiate, let's pretend that I want to check if var starts with aa, abc or 3@3.

Moreover, I want to check if var contains the character >.

Let's say this function is called check_func. My intended usage looks something like

if check_func "$var"; then
    do stuff
fi

For example, it should "do stuff" for aardvark, abcdef, 3@3com.com and 12>5.


I've seen this SO question where a user provides part of the work:

beginswith() { case $2 in "$1"*) true;; *) false;; esac; }

My idea is that I would iterate over the list mentioned above and use this function. My difficulty lies in not understanding exactly how exiting (or whatever replaces returning) should be done to make this work.

  • It just occurred to me that your question is ambiguous.  Do you mean (1) I want to check if var starts with aa.  I want to check if var starts with abc.  I want to check if var starts with 3@3.  And I want to check if var contains >. ?    Or do you mean (2) I want to check if var starts with aa, abc or 3@3, and it also contains >. ? – G-Man Says 'Reinstate Monica' Jun 7 at 21:23
  • @G-Man The first one. Thanks for bringing that up. – No Imaginatition Jun 7 at 22:44
10
check_prefixes () {
    value=$1

    for prefix in aa abc 3@3; do
        case $value in
            "$prefix"*) return 0
        esac
    done

    return 1
}

check_contains_gt () {
    value=$1

    case $value in
        *">"*) return 0
    esac

    return 1
}

var='aa>'
if check_prefixes "$var" && check_contains_gt "$var"; then
    printf '"%s" contains ">" and starts with one of the prefixes\n' "$var"
fi

I divided the tests up into two functions. Both use case ... esac and returns success (zero) as soon as this can be determined. If nothing matches, failure (1) is returned.

To make the list of prefixes more of a dynamic list, one could possibly write the first function as

check_prefixes () {
    value=$1
    shift

    for prefix do
        case $value in
            "$prefix"*) return 0
        esac
    done

    return 1
}

(the value to inspect is the first argument, which we save in value and then shift off the list of arguments to the function; we then iterate over the remaining arguments) and then call it as

check_prefixes "$var" aa abc 3@3

The second function could be changed in a similar manner, into

check_contains () {
    value=$1
    shift

    case $value in
        *"$1"*) return 0
    esac

    return 1
}

(to check for some arbitrary substring), or

check_contains_oneof () {
    value=$1
    shift

    for substring do
        case $value in
            *"$substring"*) return 0
        esac
    done

    return 1
}

(to check for any of a number of substrings)

  • Thanks. My intended logic is with a logical or instead of a logical and. Should it be value instead of var in the first function's definition? – No Imaginatition Jun 7 at 23:12
  • @NoImaginatition OK, just change the logic in the code calling the functions (|| instead of &&). Yes, that's a typo in my code, I'll fix it at once, thanks. – Kusalananda Jun 7 at 23:14
  • I'm trying to combine both functions into one by defining check_func() { if check_prefixes $1 || check_contains_gt $1; then; return 0; fi; return 1; }, but it won't work for echo a >file. Any idea why? – No Imaginatition Jun 7 at 23:16
  • @NoImaginatition Always double quote variable expansions ("$1") unless you know exactly in what contexts this is not needed. – Kusalananda Jun 7 at 23:19
  • Great, it worked. If it's not much to ask, what is it that it makes it fail without the double quotes? I tried to test if it would expand the second call of $1, and it seems to expand. I tested it with test (){ echo $1 $1; } and it worked fine. – No Imaginatition Jun 7 at 23:23
6

For bash:

Using the properties of regex you can write start with ^ and contain by nothing.

The list of regexes to check start with aa abc or 3@3 and contains > is:

^aa ^abc ^3@3 >

Make that a properly quoted list and ask bash to use regexes (=~):

check_func() {
               matched=1
               for test_regex in '^aa' '^abc' '^3@3' '>'; do
                   if [[ $var =~ $test_regex ]] ; then
                       matched=0
                       break 
                   fi
               done
               return "$matched"
              }

var='aaIsAMatch'
if check_func; then
    echo "A match was found"
fi

The function has hard-coded the list of matches and the name of the var.

Giving the list of regex in an array variable and the value to test on the first argument:

check_func() {
               local matched; matched=1
               for t in "${test_regex[@]}"; do
                   [[ $1 =~ $t ]] && { matched=0; break; } 
               done
               return "$matched"
              }


test_regex=('^aa' '^abc' '^3@3' '>')

if check_func 'aaIsAMatch'; then
    echo "A match was found"
fi

The function could be further improved to use the name of a variable (instead of a value) as the first argument.

posix

As there is no regex in posix shells and the only way to test is a case statement, we must use a case statement. Sadly, for older shells ([no extended globs available][1]) we must loop to make all tests. And, the globs need to be:

'aa*' 'abc*' '3@3*' '*>*'

An script example that tests several input strings against several globs:

check_func() { :
           matched=1
       value=$1; shift
           for t in "$@"; do
               case $value in $t) matched=0; #break;; esac
                  echo "matched $value with $t"
                  ;;
       esac
       done
           return "$matched"
         }


for var in abdg wabcde aadef abcde 3@3hello hmm3@3hell 'we>we' 'a>dfff' 'dfd>' 'a> de' 'a*> fg'; do
if check_func "$var" 'aa*' 'abc*' '3@3*' '*>*'; then
        echo "========A match was found for \"$var\""
fi
done

A simpler version of the function to exactly match your request:

check_func() { :
               matched=1
               value=$1; shift
                   for t in "$@"; do
                       case $value in $t) matched=0; break;; esac
                   done
               return "$matched"
             }
4

Here's what the case statement does: take the second parameter to the function ($2). If it matches the pattern "$1"*, i.e. the first argument to the function followed by anything, then execute true and end the case statement. true does nothing and returns the status 0. Otherwise, if it matches *, i.e. anything, execute false and end the case statement. false does nothing and returns the status 1. Thus the case statement has the status 0 if the second parameter starts with the first parameter and 1 otherwise. Since this is the last (and only) statement in the function, the function returns 0 if the second parameter starts with the first parameter and 1 otherwise.

Conditional statements such as if in the shell consider a statement to be true if it returns 0 and false otherwise. Hence if beginswith "$var" "string"; then echo yes; else echo no; fi prints yes if the value of var starts with string and no otherwise.

There are several alternative ways to write this function. For example the author could have used return 0 or return 1 instead of true and false, since they are the last statement in the function. The way the function was written makes it possible to use its body directly without wrapping it in a function, by just changing references to the function parameters ($1 and $2) to whatever strings you want to work with.

To allow multiple prefixes, iterate over them in a loop. As soon as you've found a matching prefix, return from the function, with a true status (0). If none of the prefixes match, return a false status (conventionally 1).

# begins_with STRING PREFIX1 PREFIX2...
# Test if STRING starts with any of PREFIX1, PREFIX2, ...
begins_with () {
  string=$1
  shift
  for prefix in "$@"; do
    case "$string" in
      "$prefix"*) return 0;;
    esac
  done
  return 1
}

if begins_with "$var" 'aa' 'abc' '3@3'; then
  echo "The value starts with one of the permitted prefixes"
fi

To test for a suffix, use the pattern *"$suffix" instead of "$prefix"*. To test for a substring, use *"$substring"*. Note that the double quotes are necessary here, otherwise the variable would be interpreted as a pattern. For example:

suffix='?'
case "$var" in
  *"$suffix") echo "The value of var ends with a question mark";; 
esac
case "$var" in
  *$suffix) echo "The value of var is not empty";; 
esac
4

Revised based on clarification to the question: This is less elegant (and much less flexible), but more compact than the other answers,

check_func() {
        case "$1" in
            ( aa* | abc* | 3@3*  | *">"*)
                return 0
        esac
        return 1
}

This returns true for aardvark, abcdef, 3@3com.com and 12>5.  And, of course, also aard>vark, abc<def>ghi and 3@3>3.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.