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.

ilkkachu
  • 138,973
jayhendren
  • 8,384
  • 2
  • 33
  • 58
  • 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 04 '14 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 04 '14 at 20:26
  • give this a try a=ls /foo/* now you can match in a – Hackaholic Nov 04 '14 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 04 '14 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 '16 at 20:17
  • Also there is compgen -G "<glob-pattern>" for bash. – Wtower Mar 12 '17 at 09:21

6 Answers6

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.

jayhendren
  • 8,384
  • 2
  • 33
  • 58
11

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
mikeserv
  • 58,310
  • 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 05 '14 at 00: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 '16 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.

Hauke Laging
  • 90,279
  • 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 04 '14 at 20:54
  • @jayhendren Then you probably have to first convert the incoming pattern to those bash accepts. – Hauke Laging Nov 04 '14 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 04 '14 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. – Hauke Laging Nov 04 '14 at 21:32
  • 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 Nov 04 '14 at 21:38
  • 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 '16 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
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

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.

llua
  • 6,900