5

Lets say that I have a directory with the following files (among others):

touch doc-mike.txt doc-jane.txt doc-susan.txt

I can do something in a script with those files using a construct like:

for fname in doc-*.txt; do
    echo input: ${fname}
done

But if I want to get the substring of the filename that matches the wildcard I have to jump through some ugly hoops:

for fname in doc-*.txt; do
    wildcard=${fname#doc-}
    wildcard=${wildcard%.txt}

    echo input: ${fname} output: output-${wildcard}.results
done

That works:

input: doc-jane.txt output: output-jane.results
input: doc-mike.txt output: output-mike.results
input: doc-susan.txt output: output-susan.results

but I feel like there has to be a better/easier way to get the substring that matches the "*" glob wildcard

  • 1
    What's "ugly" about those "hoops"? It would additionally work in any POSIX shell. The only thing is that you need to quote your variable expansions properly. – Kusalananda Apr 7 '18 at 7:20
  • to add a shopt -s nullglob at the top and a shopt -u nullglob after would be useful but only in bash. – D'Arcy Nader Apr 7 '18 at 8:03
  • 1
    The top of the list of what I dislike probably starts with: 1) I have to use two expressions instead of one, 2) i need an intermediate variable (the first expressions acts directly on $fname - the second expression can't do the same), and 3) the things I'm matching in those two expressions are themselves fragments of stuff I want excluded - it's not readily apparent to me what I actually want from those expressions – Michael Burr Apr 7 '18 at 8:07
  • Using a regular expression match like muru shows in his answer works, but is a far more complicated way of doing it, and also uses an extra variable. – Kusalananda Apr 7 '18 at 13:42
6

The closest I can think of is BASH_REMATCH, since bash stores the results of a regex text in the that variable:

$ for fname in doc-*.txt; do 
    [[ $fname =~ doc-(.*).txt ]];
    echo "input: ${fname} output: output-${BASH_REMATCH[1]}.results";
done
input: doc-jane.txt output: output-jane.results
input: doc-mike.txt output: output-mike.results
input: doc-susan.txt output: output-susan.results

As (.*) is the first group in the regex, it's in BASH_REMATCH[1]. I think this is the behaviour you want, but with globs, I don't think bash makes that available in any way.

  • 3
    You could also do something like for fname in *; do [[ $fname =~ doc-(.*).txt ]] || continue; ... ; done to avoid duplicating the pattern – ilkkachu Apr 7 '18 at 9:44
  • Be advised that the glob * represents [^/] as a regular expression. – kvantour Sep 26 at 13:00
4

With zsh you can use a mix of glob qualifiers, history expansion modifiers and globbing flags to do it.
Assuming you have enabled extended globbing and history pattern substitution mode via

setopt extendedglob
setopt histsubstpattern

you can then run

print doc-*.txt(#q:s/(#b)doc-(*).txt/'${match[1]}'/)

which will output

jane mike susan

As usual, you can combine this with other qualifiers/modifiers/flags e.g. select only regular files, anchor the expression to beginning/end of word etc.


Though if you wanted to iterate over each file name, with zsh you can nest parameter expansions so your code could be simplified to

for fname in doc-*.txt; do
    print input: ${fname} output: output-${${fname#doc-}%.txt}.results
done

The above (re:your comment) uses a single expression and doesn't need an intermediate variable.

1

You can also try

for fname in doc-*.txt; do 
  echo "input: ${fname} output: output-${fname:4:$((${#fname}-8))}.results"
done

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.