Capture the output of a Vim command

Posted on Jun 24, 2020

Vim ships with an internal pager named “more” for displaying command output that is used when the entire screen would be filled. Despite it’s name, more, it is not the command line utility that you might be familiar with, and because it’s a builtin feature vim cannot be configured to use an alternative (like less).

Much like it’s namesake, the internal more pager leaves a lot to be desired, like any ability to search the output. Fortunately, you can capture the output of any vim expression and display however you want.

Vim provides three means of capturing messages: :redir, execute() and :set verbosefile. We will be focusing on execute() for our purposes. :redir can be useful if you just want to quickly redirect output to a variable, file, register, or some other source but you generally will rely on execute() while scripting.

General usage is execute({expr}), or execute([{expr}, {expr}]) for multiple commands. Notice below how the entire output is concatenated into a single string.

:execute('echon "foo"')
'foo'
:execute(['echon "foo"', 'echon "bar"'])
'foobar'

Now that we have the output as a string we can write a function that creates a temporary buffer with the output and opens a window in the bottom of the current tabpage.

function! s:split(expr) abort
let lines = split(execute(a:expr, 'silent'), "[\n\r]")
let name = printf('capture://%s', a:expr)

if bufexists(name) == v:true
execute 'bwipeout' bufnr(name)
endif

execute 'botright' 'new' name

setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal filetype=vim

call append(line('$'), lines)
endfunction

Which when called will produce the output below (command :P is defined later). The show capturing the output of :command to get a listing of all user defined commands.

Or, if you use fzf-vim and you want to search the output you could use it like the below function.

function! s:fzf(expr) abort
let lines = split(execute(a:expr, 'silent'), "[\n\r]")

return fzf#run({
\  'source': lines,
\  'options': '--tiebreak begin --ansi --header-lines 1'
\})
endfunction

And as a example usage we could execute :P! function to search for all projectionist.vim functions.

Now we can tie it all together with a command that we easily use from command mode, and function that will let us choose dumping to a buffer or searching with fzf on demand.

function s:capture(expr, bang) abort
if a:bang
call s:fzf(a:expr)
else
call s:split(a:expr)
endif
endfunction

command! -nargs=1 -bang -complete=command P call s:capture(<q-args>, <bang>0)

Below is the script in its entirety:

function! s:split(expr) abort
let lines = split(execute(a:expr, 'silent'), "[\n\r]")
let name = printf('capture://%s', a:expr)

if bufexists(name) == v:true
execute 'bwipeout' bufnr(name)
end

execute 'botright' 'new' name

setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal filetype=vim

call append(line('$'), lines)
endfunction

function! s:fzf(expr) abort
let lines = split(execute(a:expr, 'silent'), "[\n\r]")

return fzf#run({
\  'source': lines,
\  'options': '--tiebreak begin --ansi --header-lines 1'
\})
endfunction

function s:capture(expr, bang) abort
if a:bang
call s:fzf(a:expr)
else
call s:split(a:expr)
endif
endfunction

command! -nargs=1 -bang -complete=command P call s:capture(<q-args>, <bang>0)