How can I programmatically tell if a filename matches one of a set of brace-expanded glob patterns?

17,681

Solution 1

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.

Solution 2

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

Solution 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.

Solution 4

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/*
Share:
17,681

Related videos on Youtube

jayhendren
Author by

jayhendren

Updated on September 18, 2022

Comments

  • jayhendren
    jayhendren almost 2 years

    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.

    • phemmer
      phemmer over 9 years
      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.
    • jayhendren
      jayhendren over 9 years
      If the shell can expand parameters, then surely it can tell if a string is a potential expansion of a glob.
    • Hackaholic
      Hackaholic over 9 years
      give this a try a=ls /foo/* now you can match in a
    • jayhendren
      jayhendren over 9 years
      @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.
    • Mike S
      Mike S almost 8 years
      @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.
    • Wtower
      Wtower over 7 years
      Also there is compgen -G "<glob-pattern>" for bash.
  • jayhendren
    jayhendren over 9 years
    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.
  • Hauke Laging
    Hauke Laging over 9 years
    @jayhendren Then you probably have to first convert the incoming pattern to those bash accepts.
  • jayhendren
    jayhendren over 9 years
    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."
  • Hauke Laging
    Hauke Laging over 9 years
    @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.
  • jayhendren
    jayhendren over 9 years
    What I mean is this: your proposed solution works, but it introduces a new problem: how to transpose patterns from one form of expression to another. I have figured out the answer to my question and posted it here as an answer.
  • jayhendren
    jayhendren over 9 years
    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
    jayhendren over 9 years
    The question states that the bash shell will be used.
  • Mike S
    Mike S almost 8 years
    @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
    Mike S almost 8 years
    +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.
  • Admin
    Admin about 2 years
    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.
  • Admin
    Admin about 2 years
    Try match "*" hello.txt, or f='foo bar.txt'; match "$f" "$f". Or pat='$(uname -a >&2)'; match "$pat" hello.txt. The first two break because of word splitting and globbing in that exp=($(...)) assignment (Why does my shell script choke on whitespace or other special characters?).