17

How can I create a recursive macro so that it only runs until the end of the line?

Or how to run a recursive macro until the end of the line only?

2 Answers 2

13

There is probably a simpler method but maybe you could try the following.

Let's say you will use register q to record your recursive macro.

At the very beginning of the recording, type:

:let a = line('.')

Then, at the very end of the recording, instead of hitting @q to make the macro recursive, type the following command:

:if line('.') == a | exe 'norm @q' | endif

Finally end the recording of the macro with q.

The last command you typed will replay the macro q (exe 'norm @q') but only if the current line number (line('.')) is the same as the one initially stored in variable a.

The :normal command allows you to type normal commands (like @q) from Ex mode.
And the reason why the command is wrapped into a string and executed by the command :execute is to prevent :normal from consuming (typing) the rest of the command (|endif).


Usage example.

Let's say you have the following buffer:

1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4

And you want to increment all the numbers from an arbitrary line with a recursive macro.

You could type 0 to move your cursor to the beginning of a line then start the recording of the macro :

qqq
qq
:let a=line('.')
<C-a>
w
:if line('.')==a|exe 'norm @q'|endif
q
  1. qqq clears the contents of register q so that when you initially call it during the definition of the macro, it will not interfere
  2. qq starts the recording
  3. :let a=line('.') stores current line number inside variable a
  4. Ctrl+a increments the number under the cursor
  5. w moves the cursor to the next number
  6. :if line('.')==a|exe 'norm @q'|endif recalls the macro but only if the line number didn't change
  7. q stops the recording

Once you have defined your macro, if you position your cursor on the third line, hit 0 to move it to the beginning of the line, then hit @q to replay the macro q, it should only affect the current line and not the others:

1 2 3 4
1 2 3 4
2 3 4 5
1 2 3 4

Make a macro recursive after the recording

If you want, you can make your macro recursive after its recording using the fact that it's stored in a string inside a register and that you can concatenate two strings with the dot . operator.

This would give you several benefits:

  • no need of clearing the register before the recording, because the characters @q will be added in the macro after it has been defined, and after you have overwritten whatever old contents was there
  • no need of typing anything unusual during the recording, you could focus on making a simple, working macro
  • possibility of testing it before making it recursive to see how it behaves

If you record your macro as usual (non-recursively), you can make it recursive afterwards with the following command:

let @q = @q . "@q"

Or even shorter: let @q .= "@q"
.= is an operator which allows to append a string to another one.

This should add the 2 characters @q at the very end of the sequence of keystrokes stored inside register q. You could also define a custom command:

command! -register RecursiveMacro let @<reg> .= "@<reg>"

It defines the command :RecursiveMacro which waits for the name of a register as an argument (because of the -register attribute passed to :command).
It's the same command as before, the only difference is you replace every occurrence of q with <reg>. When the command will be executed, Vim will automatically expand every occurrence of <reg> with the register name you provided.

Now, all you have to do is record your macro as usual (non-recursively), then type :RecursiveMacro q to make the macro stored inside register q recursive.


You could do the same thing to make a macro recursive on the condition it stays on the current line:

let @q = ":let a=line('.')\r" . @q . ":if line('.')==a|exe 'norm @q'|endif\r"

It's the exact same thing described at the beginning of the post, except this time you do it after the recording. You just concatenate two strings, one before and one after whatever keystrokes the q register currently contains:

  1. let @q = redefines the contents of register q
  2. ":let a=line('.')\r" stores the current line number inside the variable a before the macro does its work
    \r is necessary to tell Vim to press Enter and execute the command, see :help expr-quote for a list of similar special characters,
  3. . @q . concatenates the current contents of the q register with the previous string and the next one,
  4. ":if line('.')==a|exe 'norm @q'|endif\r" recalls the macro q on the condition that the line didn't change

Again, to save some keystrokes, you can automate the process by defining the following custom command:

command! -register RecursiveMacroOnLine let @<reg> = ":let a=line('.')\r" . @<reg> . ":if line('.')==a|exe 'norm @<reg>'|endif\r"

And again, all you have to do is record your macro as usual (non-recursively), then type :RecursiveMacroOnLine q to make the macro stored inside register q recursive on the condition it stays on the current line.


Merge the 2 commands

You could also tweak :RecursiveMacro so that it covers the 2 cases:

  • make a macro recursive unconditionally,
  • make a macro recursive on the condition it stays on the current line

To do this, you could pass a second argument to :RecursiveMacro. The latter would simply test its value and, depending on the value, would execute one of the 2 previous commands. It would give something like this:

command! -register -nargs=1 RecursiveMacro if <args> | let @<reg> .= "@<reg>" | else | let @<reg> = ":let a=line('.')\r" . @<reg> . ":if line('.')==a|exe 'norm @<reg>'|endif\r" | endif

Or (using line continuations/backslashes to make it a little more readable):

command! -register -nargs=1 RecursiveMacro
           \ if <args> |
           \     let @<reg> .= "@<reg>" |
           \ else |
           \     let @<reg> = ":let a = line('.')\r" .
           \                  @<reg> .
           \                  ":if line('.')==a | exe 'norm @<reg>' | endif\r" |
           \ endif

It's the same as before, except this time you have to provide a 2nd argument to :RecursiveMacro (because of the -nargs=1 attribute).
When this new command will be executed, Vim will automatically expand <args> with the value you provided.
If this 2nd argument is non-zero/true (if <args>) the first version of the command will be executed (the one which makes a macro recursive unconditionally), otherwise if it's zero/false then the second version will be executed (the one which makes a macro recursive on the condition it stays on the current line).

So going back to the previous example, it would give the following thing:

qq
<C-a>
w
q
:RecursiveMacro q 0
3G
0@q
  1. qq begins the recording of a macro inside register q
  2. <C-a> increments the number under the cursor
  3. w moves the cursor to the next number
  4. q ends the recording
  5. :RecursiveMacro q 0 makes the macro stored inside register q recursive but only until the end of the line (because of the second argument 0)
  6. 3G moves your cursor to an arbitrary line (3 for example)
  7. 0@q replays the recursive macro from the beginning of the line

It should give the same result as before:

1 2 3 4
1 2 3 4
2 3 4 5
1 2 3 4

But this time you didn't have to type the distracting commands during the recording of your macro, you could simply focus on making a working one.

And during step 5, if you had passed a non-zero argument to the command, that is if you had typed :RecursiveMacro q 1 instead of :RecursiveMacro q 0, the macro q would have become recursive unconditionally, which would have given the following buffer:

1 2 3 4
1 2 3 4
2 3 4 5
2 3 4 5

This time the macro wouldn't have stopped at the end of the 3rd line but at the end of the buffer.


For more information, see:

:help line()
:help :normal
:help :execute
:help :command-nargs
:help :command-register
6
  • 2
    The location list can be used to advance over search matches in a macro, as long as the macro doesn't change the position of the matches, e.g. :lv /\%3l\d/g %<CR>qqqqq<C-a>:lne<CR>@qq@q will increment all numbers on line 3. Maybe there's a way to make this solution less fragile?
    – djjcast
    Commented Feb 10, 2016 at 7:45
  • @djjcast You could post it as an answer, I've tried it and it works really great. There is just one case I don't understand, when I execute the macro on the following line 1 2 3 4 5 6 7 8 9 10, I get 2 3 4 5 6 7 8 9 10 12 instead of 2 3 4 5 6 7 8 9 10 11. I don't know why, maybe I mistyped something. Anyway it seems more sophisticated than my simple approach, and it involves regexes to describe where the macro should move the cursor, as well as a location list which I have never seen used in this way. I like it a lot!
    – saginaw
    Commented Feb 10, 2016 at 8:07
  • @djjcast Sorry, I've just understood, the problem came simply from my regex, I should have used \d\+ to describe multiple digits numbers.
    – saginaw
    Commented Feb 10, 2016 at 8:19
  • @djjcast Ah now I understand what you meant when you said the macro should not change the position of the matches. But I don't know how to solve this issue. The only idea I have would be to update the location list from inside the macro but I'm not used to the location list, it's too complex for me, really sorry.
    – saginaw
    Commented Feb 10, 2016 at 10:16
  • 1
    @saginaw Iterating over the matches in reverse order seems like it would resolve the issue in most cases since it seems like it's less likely for a macro to change the positions of previous matches. So after the :lv ... command, the :lla command can be used to jump to the last match and the :lp command can be used to advance over the matches in reverse order.
    – djjcast
    Commented Feb 10, 2016 at 14:03
12

Use l

A recursive macro will stop as soon as it encounters a command that fails. Therefore, to stop at the end of a line, you need a command that will fail at the end of the line.

By default*, the l command is such a command, so you can use it to stop a recursive macro. If the cursor is not at the end of the line, then you just need to move it back afterwards with the command h.

Example

So, using the same example macro as saginaw:

qqqqq<c-a>lhw@qq

Broken down:

  1. qqq: Clear out the q register,
  2. qq: Start recording a macro in the q register,
  3. <c-a>: Increment the number under the cursor,
  4. lh: If we are at the end of the line, abort the macro. Otherwise, do nothing.
  5. w: Advance to the next word on the line.
  6. @q: Recurse
  7. q: Stop recording.

You can then run the macro with the same 0@q command as described by saginaw.


* The 'whichwrap' option allows you to define which movement keys will wrap around to the next line when you are at the beginning or end of a line (See :help 'whichwrap'). If you have l set in this option, then it will break the solution described above.

However, it is likely that you only use one of the three default normal-mode commands for advancing a single character (<Space>, l, and <Right>), so if you have l included in your 'whichwrap' setting, you can remove one that you don't use from the 'whichwrap' option, e.g. for <Space>:

:set whichwrap-=s

Then you can replace the l command in step 4 of the macro with a <Space> command.

2
  • 1
    Note also that setting virtualedit=onemore will interfere with using l to detect end-of-line, albeit not as severely as whichwrap=l.
    – Kevin
    Commented Jul 22, 2019 at 18:02
  • @Kevin Good point! I'll update my answer to mention 've'
    – Rich
    Commented Jul 23, 2019 at 10:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.