2

I have to go from this:

text match$something_here and match$xxx blablabla
text match$something_else_here and match$xxx blablabla
...

to this:

text match$something_here and match$something_here blablabla
text match$something_else_here and match$something_else_here blablabla
...

So I need to change, for every line in my file, the xxx after the 2nd occurrence of match$ with whatever is after the first occurrence of match$.

2
  • 1
    What if there is third match$ in the line?
    – jimmij
    Feb 7 '15 at 0:35
  • Luckily, that's not the case in my file, at least for now. However, I think the answers provided can be adapted to that scenario.
    – Juan
    Feb 9 '15 at 17:27
2
:%s/match\$\zs\(\w\+\)\(.*match\$\)xxx/\1\2\1/

Explanation

  • match\$\zs: anchor the match at the first match$; I use \zs to start the match after that, to avoid yet another capturing group
  • \(\w\+\): capture the text after the first occurrence of match$
  • \(.*match\$\): capture what's after that, up to the second occurrence of match$, and capture that as we want to keep it
  • xxx: match what gets replaced

Replacement: The text after the first occurrence, then the text in between, then the first text again (replacing the xxx).

0
1

Try this:

sed -e 's/\(match\$\)\([a-zA-Z_]\+\)\([a-zA-Z ]\+match\$\)[a-zA-Z]\+/\1\2\3\2/' < input.txt > output.txt

Using an input.txt of:

text match$something_here and match$xxx blablabla
text match$something_else_here and match$xxx blablabla

I get an output.txt of:

text match$something_here and match$something_here blablabla
text match$something_else_here and match$something_else_here blablabla
5
  • Your answer works almost perfectly in my PC, although it has a strange behaviour: I get 2 copies for every line (ie: 2 lines in input.txt, 4 lines in output.txt). I didn't research why would that be yet, maybe it has something to do with cygwin. I'll give the answer to Ingo since it works for Vim. Thanks again.
    – Juan
    Feb 9 '15 at 14:03
  • oh, sorry, i updated my answer to fix that.
    – jayhendren
    Feb 9 '15 at 17:01
  • This depends on a GNU sed and an ASCII locale.
    – mikeserv
    Feb 9 '15 at 19:03
  • Thanks for the infor, @mikeserv. I can tell you're right because this fails with the --posix flag to sed. But I don't understand why - the sed man page doesn't do much to explain the non-posix features of GNU sed.
    – jayhendren
    Feb 9 '15 at 19:13
  • It's the \+ metacharacters and similar - POSIX only specifies those for Extended regexp syntax, not for Basic regexp. And anyway, BRE can be more useful in scripts because it is easier to use information in variables without as much validation - you typically need only handle \*$^[] and then check whatever follows the backslash. Anyway, the [A-Za-z0-9] is the other culprit - the robust version is [[:alnum:]] and is probably what you should use instead - it is not less understandable but is more flexible
    – mikeserv
    Feb 9 '15 at 20:00
1
:%s/\v(match\$(\w+).*match\$)xxx/\1\2/
  • \v very magical (we can use less \\)
1
sed -e 's/\(match\)\([_[:alnum:]]*\)\(\(.*\)\n\)*/\1\
/2;tc' -e b -e :c -e 's//\1\2\4\2/'

The above sequence will always handle only the first and second occurrence of match on a line - regardless of how many there may be on a line.

It works by doing the the first s///ubstitution on the s///2cd occurrence of the pattern, then, if the substitution tests successful, branching to the :continue label, or, if not, branching out of the script.

So when there is a second match of the pattern, the pattern is repeated for the 2cd s///ubstitution command. But when there isn't the lines are printed as usual.

The important thing is the \(\(.*\)\n\)* subexpression. The subexpression matches only a null string for the first s///2 command because a \newline can only occur in a sed pattern space as a result of an edit. But when the pattern is repeated the \(\(.*\)\n\)* matches any/all characters occurring between the two matches because the previous s///ubstitution inserted a \newline when replacing [_[:alnum:]]*. And so the same repeated pattern can mean two different things depending on its context in the script.

While the above version should work as written for any POSIX sed (emphasis on should - many seds don't conform to repeated subexpression standards), w/ GNU sed you can write it a little shorter:

sed -E 's/(match)(\w*)((.*)\n)*/\1\n/2;T;s//\1\2\4\2/
' <<\IN
text match_something_here and !m!atch_xxx blablabla
text match_something_here and match_xxx blablabla
text match_something_else_here and match_xxx blablabla
text match_something_here and match_xxx blablabla match_xxx blablabla
text match_something_else_here and match_xxx blablabla match_xxx blablabla match_xxx blablabla
IN

...The <<\IN through IN bit are just a here-document input so I can demonstrate how it works - you should probably use <input_file in its place. Also note that I changed your $something and $xxx for _something and _xxx because it is my understanding that those dollar-signs should not actually be included in the replacement pattern but will be substituted in for something else. If true, then you can keep the \w*ord escape there, or, If you do want literal dollar signs included, then you should still define your own character class and add it like: [$_[:alnum:]]*.

Notice that the tc' -e b -e :c -e is shortened to - with GNU sed - just a T. Whereas portably the test resulted in a successful substitution branching over the point where unsuccessful lines branched away, with GNU sed you can Test for unsuccessful results - which directly branches them away and the only lines left still executing the script at that point are those which did successfully substitute the second match pair for a \newline.

In any case, (depending on the sed) either of the above will print:

text match_something_here and !m!atch_xxx blablabla
text match_something_here and match_something_here blablabla
text match_something_else_here and match_something_else_here blablabla
text match_something_here and match_something_here blablabla match_xxx blablabla
text match_something_else_here and match_something_else_here blablabla match_xxx blablabla match_xxx blablabla
4
  • Didn't work in cygwin at least: $ sed -e 's/\(match\)\([_[:alnum:]]*\)(\(.*\)\n\)*/\1\2;tc' -e b -e :c -e 's//\1\2\4\2/' input.txt gave the error message: sed: -e expression #1, char 48: unterminated s' command`.
    – Juan
    Feb 10 '15 at 13:15
  • 1
    @Juan - You didn't put it all in...? It needs to be s/\(match\).../\1\<newline>/2... - see?
    – mikeserv
    Feb 10 '15 at 16:25
  • I tried that too, but I get another error: sed: -e expression #1, char 48: Unmatched ) or \)
    – Juan
    Feb 10 '15 at 18:46
  • 1
    @Juan - that's my fault. A typo - I forgot a backslash - for what it's worth you can do the same thing w/ GNU sed like sed -E "s/(match)([_[:alnum:]]*)((.*)\n)*/\1\n/2;tc;b;:c;s//\1\2\4\2/"
    – mikeserv
    Feb 10 '15 at 23:12

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.