Skip to content

Instantly share code, notes, and snippets.

@romainl
Last active December 19, 2022 22:41
Embed
What would you like to do?
Redirect the output of a Vim or external command into a scratch buffer

Redirect the output of a Vim or external command into a scratch buffer

Usage (any shell)

Show full output of command :hi in scratch window:

:Redir hi

Show full output of command :!ls -al in scratch window:

:Redir !ls -al 

Additional usage (depends on non-standard shell features so YMMV)

Evaluate current line with node and show full output in scratch window:

" current line
console.log(Math.random());

" Ex command
:.Redir !node

" scratch window
0.03987581000754448

Evaluate visual selection + positional parameters with bash and show full output in scratch window:

" content of buffer
echo ${1}
echo ${2}

" Ex command
:%Redir !bash -s foo bar

" scratch window
foo
bar

My Vim-related gists.

function! Redir(cmd, rng, start, end)
for win in range(1, winnr('$'))
if getwinvar(win, 'scratch')
execute win . 'windo close'
endif
endfor
if a:cmd =~ '^!'
let cmd = a:cmd =~' %'
\ ? matchstr(substitute(a:cmd, ' %', ' ' . shellescape(escape(expand('%:p'), '\')), ''), '^!\zs.*')
\ : matchstr(a:cmd, '^!\zs.*')
if a:rng == 0
let output = systemlist(cmd)
else
let joined_lines = join(getline(a:start, a:end), '\n')
let cleaned_lines = substitute(shellescape(joined_lines), "'\\\\''", "\\\\'", 'g')
let output = systemlist(cmd . " <<< $" . cleaned_lines)
endif
else
redir => output
execute a:cmd
redir END
let output = split(output, "\n")
endif
vnew
let w:scratch = 1
setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
call setline(1, output)
endfunction
" This command definition includes -bar, so that it is possible to "chain" Vim commands.
" Side effect: double quotes can't be used in external commands
command! -nargs=1 -complete=command -bar -range Redir silent call Redir(<q-args>, <range>, <line1>, <line2>)
" This command definition doesn't include -bar, so that it is possible to use double quotes in external commands.
" Side effect: Vim commands can't be "chained".
command! -nargs=1 -complete=command -range Redir silent call Redir(<q-args>, <range>, <line1>, <line2>)
@dougpagani
Copy link

Cool, thanks. Vim's internal pager is so unusable when trying to troubleshoot mappings, highlight groups, etc.

This was a very difficult problem to google, actually.

@romainl
Copy link
Author

romainl commented Jan 20, 2018

@dougpagani, glad it helped.

@koepnick
Copy link

Eighteen lines of code really should not bring me this much happiness. Thank you very much!

@g0xA52A2A
Copy link

The last revision appears to have changed the call from Redir to redir#Redir which is not a function defined in the snippet as is.

@romainl
Copy link
Author

romainl commented Oct 12, 2018

Ooops, sorry for the copy-pasta.

@3N4N
Copy link

3N4N commented Apr 9, 2019

Hi, @romainl,

I learnt about :h execute() recently and noticed that it can be used instead of :h redir. Any thoughts?

" Redirect the output of a Vim or external command into a scratch buffer
function! Redir(cmd) abort
    let output = execute(a:cmd)
    tabnew
    setlocal nobuflisted buftype=nofile bufhidden=wipe noswapfile
    call setline(1, split(output, "\n"))
endfunction
command! -nargs=1 Redir silent call Redir(<f-args>)

@romainl
Copy link
Author

romainl commented Apr 11, 2019

@3N4N I try to share portable snippets so :help execute() is too recent for me.

@097115
Copy link

097115 commented Jun 23, 2019

Checking for % my be of some interest, too:

[...]
if cmd =~ '^!'
    if cmd =~' %'
        let cmd = substitute(cmd, ' %', ' ' . expand('%:p'), '')
    endif
    let output = system(matchstr(cmd, '^!\zs.*'))
else
[...]

@romainl
Copy link
Author

romainl commented Jun 23, 2019

@097115 good point. #, %< and friends should probably be transformed, too.

By the way, this was the very first gist notification I've ever get. Did they change something? Oh, right, there's an "Unsubscribe" button, now. Good.

@yangsibai
Copy link

Checking for % my be of some interest, too:

[...]
if cmd =~ '^!'
    if cmd =~' %'
        let cmd = substitute(cmd, ' %', ' ' . expand('%:p'), '')
    endif
    let output = system(matchstr(cmd, '^!\zs.*'))
else
[...]

Can you paste the full script? Sorry, I can't execute if after insert your snippets. Thank you.

@romainl
Copy link
Author

romainl commented Sep 26, 2019

Checking for % my be of some interest, too:

[...]
if cmd =~ '^!'
    if cmd =~' %'
        let cmd = substitute(cmd, ' %', ' ' . expand('%:p'), '')
    endif
    let output = system(matchstr(cmd, '^!\zs.*'))
else
[...]

Can you paste the full script? Sorry, I can't execute if after insert your snippets. Thank you.

097115's snippet incorrectly uses cmd instead of a:cmd. Check the main snippet for a working version.

@yangsibai
Copy link

Checking for % my be of some interest, too:

[...]
if cmd =~ '^!'
    if cmd =~' %'
        let cmd = substitute(cmd, ' %', ' ' . expand('%:p'), '')
    endif
    let output = system(matchstr(cmd, '^!\zs.*'))
else
[...]

Can you paste the full script? Sorry, I can't execute if after insert your snippets. Thank you.

097115's snippet incorrectly uses cmd instead of a:cmd. Check the main snippet for a working version.

It works! Thank you for this. It has wasted me several hours.

@nebbish
Copy link

nebbish commented May 3, 2022

I'm curious why you added the -bar option to the command definition?
Im on both MacOS and Windows -- with most of my time in Windows. When I try to redirect a shell command that has an argument with a space, it fails because the " I try to use just becomes the beginning of the Ex command's comment.

:Redir !p4 annotate -u "file with spaces"

Ends up trying to invoke:

system('p4 annotate -u')

...because the <q-args> never includes anything from the first " onwards.

Do you have a use case of invoking this command in a | joined chain of commands?

Side note...

I also changed the expand('%:p') to be "more" compatible for windows, into: shellescape(escape(expand('%:p'), '\'))
I "think" this change would still remain compatible with the *nix systems most people use most of the time.

@romainl
Copy link
Author

romainl commented May 4, 2022

@nebbish The use case for -bar is to allow things like:

:Redir <cmd> | v/foo/d    " insert output of <cmd> in the buffer then delete every line matching foo
:Redir !<cmd> | $    " insert output of <cmd> in the buffer then move cursor to some arbitrary line

With -bar I can "postprocess" the buffer on the command-line using Vim tools. That's the whole point.

The problem is that I overlooked this sentence from :help :command-bar:

Also checks for a " to start a comment.

which is not super intuitive when using something called -bar.

Anyway, right now I don't see a possible middle ground:

  • on one hand, I really want to be able to do the things described above,
  • on the other hand, your example is very real and reasonable and should work and -bar prevents it.

For now, I think I'm going to keep the -bar and provide a non-bar alternative.


As for your escaping snippets… my gists often miss that kind of refinement because I usually stop at "works for me" and "me". A plugin would be treated differently :-). It seems to work fine on my end so I will add it ASAP.

Thank you.

@nebbish
Copy link

nebbish commented May 4, 2022

👍

@younger-1
Copy link

How to capture the output of the job? In this case, redir is finished before out_cb is called.

call job_start("python -m this", {'out_cb': {_, msg -> execute('echom ' .. msg)}})

@romainl
Copy link
Author

romainl commented Jun 1, 2022

@younger-1 Redir() would have to be rewritten to make that possible. As of now, it is 100% synchronous.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment