15

Say I have the following file

<block>
    <foo val="bar"/>
    <foo val="bar"/>
</block>
<block>
    <foo val="bar"/>
    <foo val="bar"/>
</block>

How could I make that into

<block>
    <foo val="bar1"/>
    <foo val="bar"/>
</block>
<block>
    <foo val="bar1"/>
    <foo val="bar"/>
</block>

One thing I tried to do was record a macro with :%s/bar/bar1/gc and press y and n once each and then try to edit that macro. For some reason I cannot edit the macro. :(

ib.
  • 27,830
  • 11
  • 80
  • 100
Osada Lakmal
  • 891
  • 2
  • 8
  • 22

7 Answers7

43

Just to show that this can be done in a substitution:

:let a = ['', '1']
:%s/bar\zs/\=reverse(a)[0]/g

Overview

Replace at the end of every bar with the first element of array in variable a after the array is reversed in-place upon every substitution.

Glory of Details

  1. let a = ['', '1'] define an variable a to hold our array
  2. %s/.../.../ do a substitution on every line in the file
  3. %s/bar\zs/.../ do a substitution on bar but start the replacement after bar using \zs
  4. \= inside the replacement portion of the :s command uses the value of the following expression
  5. reverse(a) reverse simply reverses the array, but does so in-place
  6. reverse(a)[0] reverse returns the now reversed array so get the first element
  7. /g replace all occurances in the line (optional)

General Case

:let a = ['a', 'b', 'c']
:%s/bar\zs/\=add(a, remove(a, 0))[-1]/g

The general case "rotates" the array, a, in-place and uses the last position of the array as the value for the replacement of the substitution.

For more help see

:h :s
:h range
:h /\zs
:h :s\=
:h reverse(
:h :s_flags
:h Lists
:h add(
:h remove
Peter Rincker
  • 43,539
  • 9
  • 74
  • 101
5

You can use

:%s/bar/bar1/gc

And it will ask you in every match, if you want to replace it.

Else you have to specify the whole content in and just replace the first bar with bar1.

tuxtimo
  • 2,730
  • 20
  • 29
4

I'd do it with a macro:

qv            start recording in register v
/"bar"/e<cr>  search for "bar" and position the cursor at the end of the match
i1<esc>       insert 1 before the cursor and go back to normal mode
n             jump to next match
q             stop recording

After that, do {count}@v.

romainl
  • 186,200
  • 21
  • 280
  • 313
1

try this:

:%s/bar\(.*\)\n\(.*\)bar/bar1\1\r\2bar
none
  • 11,793
  • 9
  • 51
  • 87
  • But can we make it more general not just match consecutive lines. (I already gave you an upvote :) This does solve the immediate issue) – Osada Lakmal Dec 07 '12 at 13:42
  • @OsadaLakmal I was thinking about the same thing but I'm not sure how to do it. I tried `%s/bar\(\_.*\)bar/bar1\1bar` but it doesn't work probably because of greedy stuff. multiline regexps are a little complicated to me, maybe someone with more experience can come up with it.. – none Dec 07 '12 at 13:52
1

Here's a custom command that should do the trick. It uses a replace expression to count the replacements done, and uses a passed additional argument to decide whether a replacement should be done. (This allows more complex arrangements than every second one.) Your example would then be a simple:

:%SubstituteSelected/\<bar\>/&1/ yn

Here's the (unfortunately quite long) implementation:

":[range]SubstituteSelected/{pattern}/{string}/[flags] {answers}
"           Replace matches of {pattern} in the current line /
"           [range] with {string}, determining whether a particular
"           match should be replaced on the sequence of "y" or "n"
"           in {answers}. I.e. with "ynn", the first match is
"           replaced, the second and third are not, the fourth is
"           again replaced, ...
"           Handles & and \0, \1 .. \9 in {string}.
function! CountedReplace()
    let l:index = s:SubstituteSelected.count % len(s:SubstituteSelected.answers)
    let s:SubstituteSelected.count += 1

    if s:SubstituteSelected.answers[l:index] ==# 'y'
        if s:SubstituteSelected.replacement =~# '^\\='
            " Handle sub-replace-special.
            return eval(s:SubstituteSelected.replacement[2:])
        else
            " Handle & and \0, \1 .. \9 (but not \u, \U, \n, etc.)
            let l:replacement = s:SubstituteSelected.replacement
            for l:submatch in range(0, 9)
                let l:replacement = substitute(l:replacement,
                \   '\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@<!' .
                \       (l:submatch == 0 ?
                \           '\%(&\|\\'.l:submatch.'\)' :
                \           '\\' . l:submatch
                \       ),
                \   submatch(l:submatch), 'g'
                \)
            endfor
            return l:replacement
        endif
    elseif s:SubstituteSelected.answers[l:index] ==# 'n'
        return submatch(0)
    else
        throw 'ASSERT: Invalid answer: ' . string(s:SubstituteSelected.answers[l:index])
    endif
endfunction
function! s:SubstituteSelected( range, arguments )
    let l:matches = matchlist(a:arguments, '^\(\i\@!\S\)\(.*\)\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@<!\1\(.*\)\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@<!\1\(\S*\)\s\+\([yn]\+\)$')
    if empty(l:matches)
        echoerr 'Invalid arguments'
        return
    endif
    let s:SubstituteSelected = {'count': 0}
    let [l:separator, l:pattern, s:SubstituteSelected.replacement, l:flags, s:SubstituteSelected.answers] = l:matches[1:5]

    execute printf('%ssubstitute %s%s%s\=CountedReplace()%s%s',
    \   a:range, l:separator, l:pattern, l:separator, l:separator, l:flags
    \)
endfunction
command! -bar -range -nargs=1 SubstituteSelected call <SID>SubstituteSelected('<line1>,<line2>', <q-args>)

Edit

I've now published this (together with related commands) as the PatternsOnText plugin.

Ingo Karkat
  • 167,457
  • 16
  • 250
  • 324
1
:let dosubs=1
:%s/bar/\=[dosubs?'bar1':submatch(0),extend(g:,{'dosubs':!dosubs})][0]/g
ZyX
  • 52,536
  • 7
  • 114
  • 135
1

I have found a simpler solution:

:g/<block>/norm! j02f"i1

:g ............ global command
/<block>/ ..... every line with <block>
j0 ............ goes down one line and to the column 1
2f" ........... jumps to the second "
i1 ............ insert the number one

Another great option

:%s/<block>\n.*bar\zs/1

My old complicated solution

\v%(block>\_{-})\zsbar
%s,,&1,g



   \v ............ very magic (avoid lots of scapes)
   %  ............ ignore whats flows
   (  ............ starts (ignored) group
   \_ ............ multiline search
   .{-} .......... non-greedy 
   \zs ........... start pattern for substituition
   bar  .......... pattern we want to change 

   % ............. whole file
   s ............. substituition
   ,, ............ use last search (could be //)
   & ............. use searched pattern 
   1 ............. add 1 after it  
SergioAraujo
  • 11,069
  • 3
  • 50
  • 40