10

Trying to have a variable meet a string of text that varies by length. The part of the string that matters is the first couple of characters.

var1=hello  
if [ $var1 == hello ]; then  
    echo success  
fi

Or

var1=hello  
if [ $var1 == h*o ]; then  
    echo success  
fi

Outputs: success

But since I care care about the first 3 characters or so, this sounds logical, but is not working:

var1=hello  
if [ $var1 == hel* ]; then  
    echo success  
fi

Outputs: -bash: [: too many arguments

Since I only care about the first couple of characters, I can do:

var1=hello 
if [ ${var1:0:3} == hel ]; then 
    echo success  
fi

That would work, but I am looking for an explanation of why I am getting that error and a possible better written solution.

5 Answers 5

13

When you use * in an if like that, it will do filename expansion. So in the first instance, h*o probably matched to a file in your current directory, and so the test passed.

The hel* matched to several files, and so became too many arguments for the if command.

Try using if [[ $var1 == hel* ]]; then

The double brackets turns the test into a regex, and the * wildcard will work as you expect.

7
  • That is fantastic. Much cleaner. I will have to look into the double brackets. Specifically, why I just shouldn't always use them. Thanks for your help.
    – EAG08
    Mar 22, 2016 at 22:23
  • 1
    It is always safest to use quotes: if [[ "$var1" == "hel*" ]]; then. Although file name expansion isn't performed between double square brackets, there will be a problem if var1 contains a space.
    – AFH
    Mar 22, 2016 at 22:29
  • 2
    @AFH: No, the wildcard needs to be outside the quotes! Inside quotes it is taken literally. So it's either as Paul wrote, or if you need spaces in the string to match, then if [[ "$var1" == "some string"* ]]; then ...
    – mivk
    May 2, 2019 at 12:50
  • @mivk - It does need to be taken literally, so that bash sees it as a match pattern, not a list of files, though as I said this won't happen between double square brackets.
    – AFH
    May 2, 2019 at 14:18
  • 2
    @AFH: well, try it: var1=hello; if [[ $var1 == "hel*" ]]; then echo OK; else echo No match; fi says "No match". With the wildcard outside the quotes, it says "OK": var1=hello; if [[ $var1 == "hel"* ]]; then echo OK; else echo No match; fi
    – mivk
    May 2, 2019 at 15:53
1

I have a trick I use to hack in regex without using builtin bash regex. Example is for #2. The way this works is grep returns no output (thus a nonexistant string) if it doesn't match anything. So there are two tests, -z means "null string" and -n is "has data."

if [ -n "`echo $var1 | grep -e 'h.*o'`" ] ; then
  echo 'water found'
fi
3
  • So in egrep, . is any character, and * is repeated 0 or more times. Mar 22, 2016 at 22:27
  • 1
    Please, don't... """ if echo $var1 | grep 'h.*o' ; then """ does it well. We never should forget that [ (a.k.a. test) is just a program that returns 0 or else just like any other shell tool and the last one in the pipe means a lot to if. Mar 22, 2016 at 22:59
  • I thought about that a lot and would rather use $? from test rather than $? from grep personally, but I suppose they're exactly equivalent. Mar 25, 2016 at 19:38
1

[* is a regular command, similar to grep, find, or cat. You should be able to find it in /bin. Since it's a separate program, the shell will perform its normal set of expansions before handing [ its arguments.

As has been mentioned, since you're using * in your tests, you're getting glob expansions. Note that even if you use quotes, such as 'hel*', this probably won't work as you hope, because [ does not support patterns. In the case of h*o working, that is likely due to the presence of a file named hello in your current directory, and no other files matching that pattern. If it does work without a hello file, you may have an odd implementation, and your script is likely to fail on other systems.

Depending on your needs, there are a couple of options. Bash, Zsh, and some other shells have the [[ builtin. Since it is a builtin, it can give its arguments special treatment, including avoiding glob expansion. Additionally, it can do pattern matching. Try

var1=hello  
if [[ "$var1" = hel* ]]; then  
    echo success  
fi

Also, note the lack of quotes around the pattern. Without quotes, hel* is treated as a pattern by [[, with quotes (single or double), "hel*" is treated literally.

If you need wider compatibility, such as for shells without [[, you can use grep:

var1=hello
if echo "$var1" | grep -qe 'hel.*' ; then
    echo success
fi

No [ or [[ necessary here, but the quotes around 'hel.*' are.

*Some shells actually do have [ builtin, but this is for efficiency purposes. It should still behave identically to the separate executable, including having its arguments subjected to the shell's normal "mangling."

0

How about this?

if [[ "$var1" == "hel"* ]]  

Give wildcard outside the quotes while doing a string comparison.

0

[[ is bash specific and using grep is unnecessarily slow as it needs to first start a new process. There is a way simpler, faster solution that works with every POSIX compatible shell:

var1=hello  
case $var1 in
    hel*) echo success
esac

More general the syntax is:

case $var in
    <pattern1>) action1 ; action2 ; action3 ; ... ; actionN ;;
    <pattern2>) action1 ; action2 ; action3 ; ... ; actionN ;;
    <pattern3>) action1 ; action2 ; action3 ; ... ; actionN ;;
    *) action1 ; action2 ; action2 ; ... ; actionN ;;
esac

The first pattern that matches defines which actions to perform. If you want else-actions that only run if no pattern matches, use * as pattern as that will always match. You terminate a set of actions with ;;, which are optional if the next instruction is esac anyway. Also you can use line breaks to separate instructions, so the following syntax is valid, too:

case $var in
    <pattern>) 
       action1
       action2
       action3
       :
       actionN
    ;;

    *)
       action2
       action3
       :
       actionN
esac

And in patterns you can use * and ?, as well as | for "or" and [...], as in [abcd] (matches either of these 4 letters) or as in [A-Z] (matches all letters A to Z, as well as ! to negate the match. E.g.

for var in abc 123 3a b5 934 "" 
do
    case $var in
        "" | *[!0-9]*) echo "\"$var\" is NOT an integer!" ;;
        *) echo "\"$var\" is an integer."
    esac
done

Output:

"abc" is NOT an integer!
"123" is an integer.
"3a" is NOT an integer!
"b5" is NOT an integer!
"934" is an integer.
"" is NOT an integer!

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.