3

Is there any concise zsh regular expression parameter expansion to replace the last match of a pattern?

I know very ugly way of doing it, but I'd prefer a cleaner, more concise syntax:

# param, pattern & replacement are placeholders below for real values
${(*S)param/%pattern(#b)(*)/replacement${match[1]}}

Which enables extended_glob, chooses shortest match, matches the pattern against the end of the value of $param, specifies the pattern, enables back references, saves the text from after the last match of the pattern in a back reference, and then replaces the match with my replacement, using the back reference to reinstate the text from after the match.

4
  • Please add examples of the input and corresponding expected output to your question. Does the input value always match the pattern? Is the last match of the pattern at the end or anywhere inside the value?
    – Bodo
    Commented Aug 13 at 16:43
  • Note that ${match[1]} can be shortened to $match[1] and * omitted if you turn extendedglob on. Note that with FooFoo as the pattern, on an input like FooFooFooFooFoo, that can be seen as having two occurrences of FooFoo followed by Foo, that gives FooFooFooreplacement instead of FooFooreplacementFoo. Commented Aug 13 at 18:24
  • @Bodo For my case, the input always contains the pattern. The last match of the pattern is never at the end of the value, but it is near the end
    – XDR
    Commented Aug 14 at 8:04
  • @StéphaneChazelas Thanks for the insights. My pattern doesn't have any repetitions. I personally prefer to use braces for ${param}, even when unnecessary, for consistency (& I think it's easier to read). I also like to use (*) if I can instead of setopt. Good that you mentioned options to shorten the solution for others, though, who might have other stylistic preferences.
    – XDR
    Commented Aug 14 at 8:08

2 Answers 2

4

If by last occurrence, you mean the last after having scanned for them from left to right like ${param//pattern/replacement} does (which is not what your attempt does), you could do:

set -o extendedglob
param=foobarbazfoofoofoo pattern=foo replacement=replacement

: ${param//(#m)$~pattern}
param[MBEGIN,MEND]=$replacement

A function that replaces the nth occurrence (possibly counted from the last when n is negative) could be written as:

replace() {
  set -o localoptions -o extendedglob
  local _param=$1 _n=$2 _pat=$3 _repl=$4 _found MATCH MBEGIN MEND
  local -a _b _e
  : ${(P)_param//(#m)$~_pat/${_b[++_found]=$MBEGIN}${_e[_found]=$MEND}}
  (( _n > 0 && _found >= _n )) ||
    (( _n < 0 && - _found <= _n )) &&
    eval $_param'[_b[_n],_e[_n]]=$_repl'
}

To be called for instance as:

replace param -1 $pattern $replacement
3
  • Are you saying that my attempt doesn't work? I know that my attempt will get the match that starts the closest to the end, not necessarily the one that ends the closest to the end. For my current use case, those are the same as my pattern doesn't have any wildcards or other variability.
    – XDR
    Commented Aug 14 at 3:18
  • @XDR, no, you're attempt is good, I'm just point out that it's different from replacing the last match is the usual meaning of an ordered list of matches done by things like s/.../../g or zshs ${...//.../...} or regexp-replace and may yield different result depending on pattern and subject. Commented Aug 14 at 8:42
  • Thanks for the clarification
    – XDR
    Commented Aug 14 at 8:50
3

There's no explicit way to select the last match. But you can match * followed by your pattern: then your pattern will be the last match, since * matches the longest possible prefix that's followed by your pattern. Use the b globbing flag to gain access to the parts of the matched string in the replacement.

This requires extended_glob to be enabled. With zsh ≥5.9, you can activate it with the * parameter expansion flag.

For example, the following snippet matches a1b2c3d against the pattern (#b)(*)([0-9]). (#b) causes following parenthesized groups to record what they match in $match. (The positions are also available in $mbegin and $mend if you need them.) * matches the longest prefix, so [0-9] matches the last digit. The replacement keeps the prefix matched by *, replaces the digit by something else, and keeps the unmatched suffix, so the snippet outputs a1b2c-d.

setopt extended_glob
a=a1b2c3d
echo ${a/(#b)(*)([0-9])/$match[1]-}'

In zsh ≥5.9, you don't need to make sure that extended_glob is active.

a=a1b2c3d
echo ${(*)a/(#b)(*)([0-9])/$match[1]-}'
5
  • You probably were writing your answer as I was editing my question. I did a similar thing, just from the end. From the front is a little more concise. I normally would have written from the front, but was then thinking about finding a nicer syntax from the end… Thanks for the answer.
    – XDR
    Commented Aug 13 at 17:25
  • 1
    @XDR Right, I hadn't noticed your edit. I think it's as good as it gets. Few pattern matching libraries have a convenient way to get the last match. Commented Aug 13 at 17:35
  • A syntactically simple way zsh could support this would be to support negative numbers for the (I:number:) parameter expansion flag, with -1 being the last match, -2 the penultimate, etc.
    – XDR
    Commented Aug 13 at 18:26
  • 1
  • That is very funny. I definitely tried using (I:-1:) to see if it would work, despite negatives not being mentioned in the docs. Do you know if the next version of zsh will be released anytime soon? I'm looking forward to being able to replace pcre with pcre2, as zsh is the last software I have that depends on that old package, and the upgrade to pcre2 has already been committed to the post-5.9 zsh master. Will it be 5.10, 6.0, 5.9.1, …?
    – XDR
    Commented Aug 13 at 19:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .