13

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.

EAG08
  • 131
  • 1
  • 1
  • 3

6 Answers6

17

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.

Paul
  • 59,223
  • 18
  • 147
  • 168
  • 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 '16 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 '16 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 02 '19 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 02 '19 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 02 '19 at 15:53
  • @mivk - I do apologise, you are absolutely correct. This is completely different from how I thought `bash` operates, so I'll need to investigate further. Thank you for putting me right. – AFH May 02 '19 at 19:17
  • This seems to work in testing, but I'm having difficulty finding a source for this usage of `*` with `[[`. This is in `man bash` though: "An additional binary operator, =~, is available, with the same precedence as == and !=. When it is used, the string to the right of the operator is considered a POSIX extended regular expression and matched accordingly (as in regex(3))." – Josh Cooley Mar 06 '20 at 16:15
2

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
Some Linux Nerd
  • 146
  • 1
  • 7
  • So in egrep, . is any character, and * is repeated 0 or more times. – Some Linux Nerd Mar 22 '16 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. – user556625 Mar 22 '16 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. – Some Linux Nerd Mar 25 '16 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."

8bittree
  • 2,900
  • 1
  • 17
  • 28
1

[[ 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!
Mecki
  • 858
  • 8
  • 11
1

Use double brackets to enable pattern matching in the right expression

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

or use regular expressions comparison operator.

if [[ "$var1" =~ ^hel.* ]]

(Pattern matching and regular expressions are not the same)

Rohit Gupta
  • 2,721
  • 18
  • 27
  • 35
  • How is this better than other answers? – Toto Apr 23 '23 at 11:39
  • Nobody else mentions regex operator =~ which is much more powerful than pattern matching. The currently highest ranked answer (Paul) actually wrongly mixes up pattern matching and regex. – Oskar Enoksson Apr 24 '23 at 14:32
0

How about this?

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

Give wildcard outside the quotes while doing a string comparison.

slm
  • 9,959
  • 10
  • 49
  • 57