6

I would like to replace parts of one line with the result of the selection being piped into a command.

For example:

echo "hello $(echo "world" | base64)" | vim -

This will open a buffer with hello d29ybGQK in it. Now press wvw to visually select d29ybGQK.

Then I attempted :!base64 -d and I expected the buffer to contain hello world, which did not happen. Indeed, the whole line was piped into the command, and the whole line was replaced.

Is it possible to replace only the visual selection, and have only that selection piped into the command?

I also attempted c<c-r>=system('base64 -d') but that did not send the visual selection to the command's stdin.

2
  • :! does not pass the buffer's contents; you either used :.! or :%! for that. Commented Sep 24, 2018 at 8:57
  • 2
    it does in visual selection mode, which triggers :'<,'>!. Try to select one word and press : it will show : '<,'>, and then maybe !xxd -p to change just the selection to hex. All the line will be converted in hex, not just the selection (as per your answer). Commented Sep 24, 2018 at 11:03

2 Answers 2

12

Filtering with ! is always line-wise. Your solution with c and the expression register is an excellent way to solve this. You only forgot to pass the input to system(), which is its second optional argument.

Since you just changed the text selected, it went into the " register automatically. All you need to do is to grab it back and pass it to system with getreg():

c<C-R>=system('base64 -D', getreg('"'))

Note that base64 may echo a newline at the end. If you want to remove it, either wrap the whole thing in trim(), a new function in Vim 8, or use [:-2]:

c<C-R>=trim(system('base64 -D', getreg('"')))
c<C-R>=system('base64 -D', getreg('"'))[:-2]

This is a shorthand for [0:-2], meaning grab everything from character 0 to second-last in the resulting string.

Consider creating a visual map if you use it often:

vnoremap <leader>d c<C-R>=system('base64 -D', getreg('"'))[:-2]<CR>
Sign up to request clarification or add additional context in comments.

1 Comment

Wow this is so good, thanks. Exactly what I wanted! Note to readers: -D seems to be the BSD option for base64 (at least that's what I have on my mac), -d seems to be the GNU option (at least that's what I have on my work laptop/servers).
1

For historical reasons, the Ex commands are inherently line-based; venerable vi also didn't have visual mode yet. That limitation includes filtering through an external command with :range!; it will always filter complete lines.

manual solution

For simple input like in your example, it's probably easiest to temporarily split the line, filter, and then reassemble. For example:

:substitute/ /\r/ | execute '.!base64 -d' | -1join

plugin solution

For a universal solution, you need to use or implement a plugin that grabs the selected text, filters it (probably through system()), and then replaces the selection with the result.

My SubstituteExpression plugin has a {Visual}g= mapping that can filter through Vimscript expressions, Vim functions and commands, and external commands.

express.vim by Tom McDonald offers an almost identical implementation. It also allows on-the-fly creation of operators via :MapExpress and :MapSubpress, something for which I would use my TextTransform plugin, which you need to install as a dependency, anyway. My plugin offers more advanced (cross-mode) repeats, and the :Ex-command expression variant, but has two large dependencies you also need to install.

4 Comments

OK, TBH I didn't expect it wouldn't be possible without plugins, but I accept this answer nonetheless. Also it seems the manual solution is based on spaces, and my actual data doesn't (it's a compact JSON string with one of the field containing specific data that I wanted to replace using an external command). I couldn't adapt with a quote separator, but I didn't try hard enough yet maybe (couldn't figure out which line will be piped into the command). Thanks
If you'd limit processing to single-line characterwise visual selection and no error handling, this could be done in a custom mapping + function. There's no clear separation between plugin and customization; I both have very short plugins and very long mappings. But using a well-maintained plugin is highly recommended over trying to reinvent the wheel.
In case of non-space separator, you'd have to use :substitute instead of :join.
I believe the substitution places the caret at the last line, which would work if there is no trailing character. Doing so on {"hello":"d29ybGQK"} places the caret at the last line, being the closing curly brace, and the command wouldn't be executed on the desired token. So this works if it's the last token of the line, but that's far from my case (the actual data I wanted to convert is in the middle of the line). Re: plugin vs customization: agreed. I said "not a plugin" meaning I was expecting a single line, sub-20 chars command to make this happen.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.