20

I would like to tell if a string $string would be matched by a glob pattern $pattern. $string may or may not be the name of an existing file. How can I do this?

Assume the following formats for my input strings:

string="/foo/bar"
pattern1="/foo/*"
pattern2="/foo/{bar,baz}"

I would like to find a bash idiom that determines if $string would be matched by $pattern1, $pattern2, or any other arbitrary glob pattern. Here is what I have tried so far:

  1. [[ "$string" = $pattern ]]

    This almost works, except that $pattern is interpreted as a string pattern and not as a glob pattern.

  2. [ "$string" = $pattern ]

    The problem with this approach is that $pattern is expanded and then string comparison is performed between $string and the expansion of $pattern.

  3. [[ "$(find $pattern -print0 -maxdepth 0 2>/dev/null)" =~ "$string" ]]

    This one works, but only if $string contains a file that exists.

  4. [[ $string =~ $pattern ]]

    This does not work because the =~ operator causes $pattern to be interpreted as an extended regular expression, not a glob or wildcard pattern.

6
  • 2
    The issue you're going to run into is that {bar,baz} isn't a pattern. It's parameter expansion. Subtle but critical difference in that {bar,baz} is expanded very early on into multiple arguments, bar and baz.
    – phemmer
    Nov 4, 2014 at 20:25
  • If the shell can expand parameters, then surely it can tell if a string is a potential expansion of a glob.
    – jayhendren
    Nov 4, 2014 at 20:26
  • give this a try a=ls /foo/* now you can match in a
    – Hackaholic
    Nov 4, 2014 at 20:32
  • 1
    @Patrick: after reading through the bash man page, I have learned that foo/{bar,baz} is actually a brace expansion (not a parameter expansion) while foo/* is pathname expansion. $string is parameter expansion. These are all done at different times and by different mechanisms.
    – jayhendren
    Nov 4, 2014 at 20:45
  • @jayhendren @Patrick is right, and then you learned that your question ultimately is not what the title leads one to believe. Rather, you want to match a string against various kinds of patterns. If you wanted to strictly match versus a glob pattern, the case statement performs Pathname Expansion ("globbing") as per the Bash manual.
    – Mike S
    Aug 16, 2016 at 20:17

7 Answers 7

13

There is no general solution for this problem. The reason is that, in bash, brace expansion (i.e., {pattern1,pattern2,...} and filename expansion (a.k.a. glob patterns) are considered separate things and expanded under different conditions and at different times. Here is the full list of expansions that bash performs:

  • brace expansion
  • tilde expansion
  • parameter and variable expansion
  • command substitution
  • arithmetic expansion
  • word splitting
  • pathname expansion

Since we only care about a subset of these (perhaps brace, tilde, and pathname expansion), it's possible to use certain patterns and mechanisms to restrict expansion in a controllable fashion. For instance:

#!/bin/bash
set -f

string=/foo/bar

for pattern in /foo/{*,foo*,bar*,**,**/*}; do
    [[ $string == $pattern ]] && echo "$pattern matches $string"
done

Running this script generates the following output:

/foo/* matches /foo/bar
/foo/bar* matches /foo/bar
/foo/** matches /foo/bar

This works because set -f disables pathname expansion, so only brace expansion and tilde expansion occur in the statement for pattern in /foo/{*,foo*,bar*,**,**/*}. We can then use the test operation [[ $string == $pattern ]] to test against pathname expansion after the brace expansion has already been performed.

10

I don't believe that {bar,baz} is a shell glob pattern (though certainly /foo/ba[rz] is) but if you want to know if $string matches $pattern you can do:

case "$string" in 
($pattern) put your successful execution statement here;;
(*)        this is where your failure case should be   ;;
esac

You can do as many as you like:

case "$string" in
($pattern1) do something;;
($pattern2) do differently;;
(*)         still no match;;
esac
2
  • 1
    Hi @mikeserv, as indicated in the comments and the answer that I provided above, I have already learned that what you say is true - {bar,baz} is not a glob pattern. I have already come up with a solution to my question that takes this into account.
    – jayhendren
    Nov 5, 2014 at 0:09
  • 3
    +1 This answers the question exactly as given in the title, and the first sentence. although the question later conflates other patterns with shell glob patterns.
    – Mike S
    Aug 16, 2016 at 20:29
3

As Patrick pointed out you need a "different type" of pattern:

[[ /foo/bar == /foo/@(bar|baz) ]]


string="/foo/bar"
pattern="/foo/@(bar|baz)"
[[ $string == $pattern ]]

Quotes are not necessary there.

6
  • Ok, this works, but strictly speaking, it doesn't answer my question. For instance, I would like to consider patterns that are coming from another source, i.e., the patterns are out of my control.
    – jayhendren
    Nov 4, 2014 at 20:54
  • @jayhendren Then you probably have to first convert the incoming pattern to those bash accepts. Nov 4, 2014 at 21:02
  • So all you've really done is transformed my question from "how do I tell if a filename is a potential expansion of an expression" to "how do I convert normal bash-style filename patterns to bash-style extended glob patterns."
    – jayhendren
    Nov 4, 2014 at 21:06
  • @jayhendren Considering that what you want seems impossible "all you've really done" sounds a bit strange to me but maybe that's just a foreign language thing. If you want to ask this new question then you must explain what the input patterns look like. Maybe it's a simple sed operation. Nov 4, 2014 at 21:32
  • 1
    @HaukeLaging is correct. People coming here to figure out how to match against glob patterns (aka "Pathname Expansion") are liable to get confused, as I did, because the title says "shell glob pattern" but the contents of his question use a non-glob pattern. At some point, jayhendren learned about the difference but his initial confusion is what caused Hauke to answer the way he did.
    – Mike S
    Aug 16, 2016 at 20:22
0

I tried to manually perform brace expansion using eval. It seems to work under most use cases, but I am not sure if there is any other caveats.

match() {
    exp=($(eval echo "$1")) # try to expand braces
    if [ ${#exp[@]} -eq 1 ]; then
        [[ "$2" == $1 ]]
        return $?
    else
        # match succeeds if one of the expanded patterns succeeds
        for expanded in ${exp[@]}; do
            if match "$expanded" "$2"; then
                return 0
            fi
        done
        return 1
    fi
}

Example usage:

file="abc.jpg"
pattern1="abc.{pdf,jpg}"
pattern2="*.{pdf,jpg}"
pattern3="*.jpg"
pattern4="[abcdef]??.jpg"

match "$pattern1" "$file" && echo "succeeds" || echo "fails"
match "$pattern2" "$file" && echo "succeeds" || echo "fails"
match "$pattern3" "$file" && echo "succeeds" || echo "fails"
match "$pattern4" "$file" && echo "succeeds" || echo "fails"

Output:

succeeds
succeeds
succeeds
succeeds
1
0

If it's about checking whether $string is among the filepaths that result from the expansion of the glob stored in $pattern (bearing in mind that, as others have said {foo,bar} is not a glob operator), then with zsh, you could do:

if ()(($#)) $~pattern(NY1e['[[ $REPLY = $string ]]']); then
  print -r -- "$string is among the result of the $pattern glob expansion"
fi

With bash, you could always use a loop:

among() (
  string=$1 pattern=$2 IFS=
  shopt -s nullglob extglob
  for file in $pattern@(); do
    [[ "$string" = "$file" ]] && return
  done
  false
)
if among "$string" "$pattern"; then
  printf '%s\n' "$string is among the result of the $pattern glob expansion"
fi

(with extglob enabled which enables a subset of ksh's extended operators, you can use things like foo/@(foo|bar). We add @() to the pattern above to force glob expansion; without it, among foo foo would return true even if foo didn't exist. That means however that patterns ending in / don't work (as in among /bin/ '/*/').

In ksh, brace expansion happens to be performed upon parameter expansion in addition to split+glob as long as noglob is not enabled and braceexpand is not disabled. ksh93 also has a ~(N) glob operator as an equivalent to zsh/bash's nullglob, so there you could do:

function among {
  typeset string="$1" pattern="$2" IFS= file
  set -o braceexpand +o noglob
  for file in ~(N)$pattern; do
    [[ "$string" = "$file" ]] && return
  done
  false
}

And among foo/bar 'foo/{bar,baz}' would return true as long as a foo/bar file exists.

Note that ksh's extended glob operators are not recognised when they are in the result of other expansions (so that a='@(x)'; echo $a outputs @(x) as POSIX requires).

Note that in all of those, among .foo '*' or among /etc/issue '*' or among /usr/local/bin '/*/bin' would return false even though the strings match the patterns.

-1

I would use grep thusly:

#!/bin/bash
string="/foo/bar"
pattern1="/foo/*"
pattern2="/foo/{bar,baz}"

if echo $string | grep -e "$pattern1" > /dev/null; then
    echo $string matches $pattern1
fi

if echo $string | grep -e "$pattern2" > /dev/null; then
    echo $string matches $pattern2
fi

Which gives the output:

./test2.bsh 
/foo/bar matches /foo/*
1
  • 1
    grep matches regular expressions, not shell glob wildcard patterns (and as other have said, {foo,bar} is not a glob operator, not is it a regexp operator). The ast-open implementation of grep does support a -K option to match against ksh wildcards though. Jun 8 at 5:19
-1

For those coming here with a similar question for zsh instead of bash:

[[ $string = $~pattern ]] && print true

Or

case $string in
  ($~pattern) print A;;
  ($~pattern2) print B
esac

Or its shorter form:

case $string {
  ($~pattern) print A;;
  ($~pattern2) print B;;
}

The ~ forces the contents of the variable to be interpreted as a pattern as if the globsubst option had been enabled. Without it [[ $string = $pattern ]] is just byte-to-byte string equality comparison.

Replace ;; with ;| for all patterns to be tested instead of stopping at the first match.

Like for bash and most other shells, {foo,bar} is not a glob operator in zsh. zsh's glob alternation operator is (foo|bar), like in regexps except that the parenthesis are required.

1
  • 1
    The question states that the bash shell will be used.
    – jayhendren
    Nov 5, 2014 at 20:32

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.