5

I have the given file

4 apples
2 bananas
6 peaches
3 grapes
1 pineapple
5 pears
3 tomatoes

What I want to do is to receive the following result instead:

apple apple apple apple
banana banana
peach peach peach peach peach peach
grape grape grape
pineapple
pear pear pear pear pear
tomato tomato tomato

I would imagine to do something along the lines of:

"wyw$x"eyBddO<Esc>"e("w)P the part in brackets is supposed to result in something like paste content of e for a w number of times which doesn't really seem to work out.

[clarification for the given instructions would boil down to:

  1. yank the current word into w
  2. jump to the end of line, delete character (because of plurals, leaves me with a problem for words that are singular though)
  3. yank the current word from its beginning to cursor position into e
  4. delete the current line
  5. insert in one inserted line above this one, exit insert mode
  6. use the content of e to paste it w times ]

I'm not exactly sure how i can use the number in w as an argument for a command, which I execute w times then.

Can this be achieved at all? I'm looking for an answer using default vim, no additional external macros or anything like that.

I would like to be able to just record this sequence and then to execute it for the number of lines on every line.

5 Answers 5

5

This is not exactly what you want, but you could visually select your lines, then run this global command:

g/^/exe 'norm! "adiwD' | exe 'norm! '.@a.'p'
               ├───┘│                 ├────┘
               │    │                 └ paste unnamed register as many times as the number in register `a`
               │    └ delete word in unnamed register
               └ delete count in register `a`

There will we be an extra space in front of each line; to remove it:

:'<,'>s/^ /

Edit: Peter Rincker has a shorter solution.

Broken down:

:g/^/exe 'norm x'|exe 'norm D'.@@.'p0x'
               │            │  ├───┘├┘
               │            │  │    └ delete extra space at the start of the line
               │            │  └ paste word count times
               │            └ delete word in unnamed register
               └ delete count in unnamed register

To make it more reliable, you could suffix :norm with a bang (to ignore custom mappings), and delete the count with diw (to handle multi-digit numbers):

:g/^/exe 'norm! diw'|exe 'norm! D'.@@.'p0x'
              ^ ^^^           ^

Alternatively, select your lines, then run this substitution command:

s/\(\d\+\)\s\+\(\k\+\)/\=repeat(submatch(2).' ', str2nr(submatch(1)))
  ├──────┘    ├──────┘ ├┘│      ├─────────────┘  ├─────────────────┘
  │           │        │ │      │                └ as many times as the count in the capturing group 1
  │           │        │ │      └ the capturing group 2 (word) followed by a space
  │           │        │ └ repeat the next string
  │           │        └ use evaluation of next expression for the replacement
  │           └ capture word
  └ capture count

There will we be an extra space at the end of each line; to remove it:

:'<,'>s/ $/

I'm not sure the call to str2nr() is required.

If the number starts with a leading 0, Vim will take it as octal. This is why, without str2nr(), the line:

010 foo

would be turned into:

foo foo foo foo foo foo foo foo 

Whereas, with str2nr(), it will be turned into:

foo foo foo foo foo foo foo foo foo foo 

Here is a more detailed explanation.

But if the OP is working with octal numbers, then you're right, and they should remove str2nr().


On the :s/// case you can even handle plurals easily with an s\? at the end of the match, outside the group.

You're right, although, I think you would need to also add s\@<! at the end of the second capturing group:

s/\(\d\+\)\s\+\(\k\+s\@<!\)s\=/\=repeat(submatch(2).' ', str2nr(submatch(1)))
                     ^^^^  ^^^

As explained by Christian Brabandt, you could also use \{-} and \>:

s/\(\d\+\)\s\+\(\k\{-}\)s\=\>/\=repeat(submatch(2).' ', str2nr(submatch(1)))
                  ^^^^     ^^

But no solution will correctly deal with words whose plural form adds more than a single s, like peach whose plural form adds e and s. I guess that for such words, you will have to fix them manually afterward.


Also, suggestion to use \v in your regex, do you don't need to backslash all parens and plusses.

For various reasons, I prefer \m, but if it helps, the command substitution can be rewritten like this:

s/\v(\d+)\s+(\k+s@<!)s=/\=repeat(submatch(2).' ', str2nr(submatch(1)))
6
  • 1
    I'm not sure the call to str2nr() is required. Commented Sep 6, 2019 at 14:41
  • 2
    On the :s/// case you can even handle plurals easily with an s\? at the end of the match, outside the group. This is one situation where I find using :s/// ends up being simpler than normal mode commands. Also, suggestion to use \v in your regex, do you don't need to backslash all parens and plusses.
    – filbranden
    Commented Sep 6, 2019 at 14:42
  • 6
    Vimgolf: :g/^/exe 'norm x'|exe 'norm D'.@@.'p0x' & :%s/\v(\d+)( .*)/\=repeat(submatch(2), submatch(1))[1:]/ Commented Sep 6, 2019 at 14:44
  • 1
    010 is a pretty unusual way of writing 10. If the line starts with 010, perhaps it should be turned into foo foo foo foo foo foo foo foo ;)
    – Rich
    Commented Sep 6, 2019 at 15:20
  • 2
    +1 for the :s command (you can skip the \@<! by using \{-} and forcing a word-end match: \(\w\{-}\)s\?\>). Note however, if I am not completely wrong, all solutions will not correctly convert peaches into peach Commented Sep 6, 2019 at 21:31
2

"e("w)P: the part in brackets is supposed to result in something like "paste content of e for a w number of times" which doesn't really seem to work out.

How to apply a register (as a count) to a command?

The direct answer to this is that you need to use :normal! to run a normal mode command and :execute to assemble it from a string. You can then refer to the register while assembling that string, so you can use the value of that register as a count.

In your particular case:

:execute 'normal! "e'.@w.'P'

Putting it all together:

"wyw$x"eyBddO<Esc>:exe 'norm! "e'.@w.'P'<CR>

You can record this sequence to a macro and repeat it on every line.

Note there's a bug on the pineapple line, it will consume the last e instead of an s.

The other answers actually offer better ways to approach this task (personally I found using :s/// with an expression in the replacement was the cleanest), but I wanted to address your direct query of how to use a register as part of a normal mode command, in this particular case as a count.

1

You can indeed do this by recording a macro in vanilla Vim.

Type the following series of keystrokes:

qqfSpaceraA SpaceCtrl-VEscEsc0d$@"x+q

You now have in register "q a macro that will convert a single line. You can replay this on the remaining 6 lines by typing 6@q.

Converting the plural nouns into singular nouns is left as an exercise for the reader.

How it works:

  • qq Start recording a macro.
  • fSpaceraA SpaceCtrl-VEscEsc This series of normal mode commands edits the line so that it now contains the series of keystrokes required to write out the desired output, starting from normal mode. So for the apples line, it starts with a count, and then appends the string "apples " four times. The pair of keystrokesCtrl-VEsc inserts a literal ESC character into the buffer.
  • 0d$ Move to the start of the line and delete the contents of the line. The deleted text will be stored in register the unnamed register ""
  • @" Playback the contents of register "" (the text we just deleted. This writes the output into the line.
  • x There's an extra space at the end of the line. Remove it.
  • + Move to the start of the next line.
  • q End the recording.
2
  • 2
    To make the macro more reliable, it could be started with 0 (not needed for the repetitions thanks to +, but may still be useful for the initial line), and l could be replaced with f (find a space) to handle the case of a multi-digit number.
    – user938271
    Commented Sep 6, 2019 at 13:45
  • @user938271 Good points. I've edited the answer to use f<Space>, as that was a particularly bone-headed omission.
    – Rich
    Commented Sep 6, 2019 at 13:53
1

Here's yet another method:

  1. Type qq to start recording a macro,
  2. Type "wdiw to delete the number into the "w register,
  3. Type "ede to delete the word into the "e register,
  4. Type iCtrl-R= to enter insert mode and start using the expression register. See :help i_CTRL-R_=.
  5. Type repeat(@e, @w), which is now an expression that evaluates to apples repeated four times, and press Enter to enter the evaluation of this expression into the buffer.
  6. Press Esc0x to exit insert mode, and delete the first space on the line.
  7. Press +q to move to the next line and end the recording.

Like in the other macro-based answer, you can then press 6@q to replay the recording on the next 6 lines.

0

I played around with it a little and made the commands to change your original string to the following:

4iapple <esc>o<esc>2ibanana <esc>o<esc>6ipeache <esc>o<esc>3igrape <esc>o<esc>1ipineapple <esc>o<esc>5ipear <esc>o<esc>3itomato

Then I yanked it into a register and executed it as a macro.

You can write the character <esc> in insert mode by pressing <ctrl> + v and pressing escape.

2
  • Welcome to Vi and Vim! Nice to show an alternate way to do things, though personally I would do something else—that's a lot of typing :)
    – D. Ben Knoble
    Commented Mar 26, 2021 at 14:32
  • 1
    @D.BenKnoble Thanks :D It's not that much typing assuming you had the original text from the question. Record a macro where you change the first line lri<esc>A ctrl+v <esc><esc> Repeat that macro for the other lines. Make them into one line with gJ (gJ because J would add a space which makes the macro only print apples) I know I ignored the removing of s and es in the ending here :) you could probably use a search replace for that.
    – Kim
    Commented Mar 27, 2021 at 15:36

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.