Skip to content

Instantly share code, notes, and snippets.

@romainl
Last active November 14, 2024 18:54
Show Gist options
  • Save romainl/eae0a260ab9c135390c30cd370c20cd7 to your computer and use it in GitHub Desktop.
Save romainl/eae0a260ab9c135390c30cd370c20cd7 to your computer and use it in GitHub Desktop.
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>)
@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.

@habamax
Copy link

habamax commented Mar 10, 2023

not exactly the same, but similar in vim9script: https://github.com/habamax/vim-shout

@ubaldot
Copy link

ubaldot commented May 2, 2023

The same in Vim9script:

export def g:Redir(cmd: string, rng: number, start: number, end: number)
	for win in range(1, winnr('$'))
		if !empty(getwinvar(win, 'scratch'))
			execute win .. 'windo close'
		endif
	endfor
        var output = []
	if cmd =~ '^!'
		var cmd_filt = cmd =~ ' %'
			\ ? matchstr(substitute(cmd, ' %', ' ' .. shellescape(escape(expand('%:p'), '\')), ''), '^!\zs.*')
			\ : matchstr(cmd, '^!\zs.*')
		if rng == 0
			output = systemlist(cmd_filt)
		else
			var joined_lines = join(getline(start, end), '\n')
			var cleaned_lines = substitute(shellescape(joined_lines), "'\\\\''", "\\\\'", 'g')
			output = systemlist(cmd_filt .. " <<< $" .. cleaned_lines)
		endif
	else
                var tmp: string
		redir => tmp
		execute cmd
		redir END
		output = split(tmp, "\n")
	endif
	vnew
	w:scratch = 1
	setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile
	setline(1, output)
enddef


@HCY-ASLEEP
Copy link

HCY-ASLEEP commented May 30, 2023

In fact, things can be easier now use the vim command read. For example, :read !ls -arlh can exec the external shell command ls -arlh to the buffer in one step directly. If you wanna redirect the command result from the vim ex mode, you can also use the vim command like :put=execute('ls') or :put=execute('buffers'). The variable put is very special in vim. I have used this feature to make my "quickfix" with ripgrep. Here is my vim config -> https://github.com/HCY-ASLEEP/NVIM-Config/blob/main/init.vim.

@romainl
Copy link
Author

romainl commented May 30, 2023

Well… of course :read !foo can be used to insert the output of external command foo in the buffer, or :put=execute('hi') to insert the output of Ex command :hi. But that's, like… in the current buffer, and 10% of what this :Redir does.

:Redir !foo is functionally similar to the following crude command:

:vnew | read !foo

and :Redir foo is functionally "similar" to:

:vnew | put=execute('foo')

which is already a bit more than :read.

That is more or less what I did for years but it had a bunch of issues that I fixed gradually to obtain this command, which now fits my workflow perfectly. Those issues, in no particular order:

  • Whether you do :read or :0read, you always end up with an extraneous empty line, which is something I don't like at all. My first attempt to fix that was to switch to :put, which allowed me to use system() or :redir, which allowed me to add a third command that removed the extra line:

    :vnew | :put=system('foo') | 1d_
    

    Which worked, but it started to become quite a mouthful. That is the point where you start to consider turning it into a an Ex command. And of course, it couldn't be turned into a command as-is because, depending on whether the command was internal or external, I would have to decide whether to use system() or :redir.

    Note that I wrote this command before execute() was introduced, but that wouldn't change much. :redir is perfectly fine.

    Taking one path or another depending on the argument is not super hard, mind you, but solving that specific "extra line" issue, made the problem—and the solution—a little bit more complex than a "simple" :read or a "simple" :put.

  • The resulting buffer lingered behind because it was a regular one. Since I intended it to be a "scratch buffer", I had to set a few options. Once again, that's not hard, but that's more complexity added to the mix. It makes the whole thing much cleaner, though, so the added complexity is very much worth it.

    And that's one more step further away from a simple :read or :put.

  • In some cases, :read changes the alternate file, which is something I wanted to avoid in this scenario but not all the time. Another reason why suggesting a simple :read as an alternative would be disingenuous at best.

  • I didn't want the command to spawn more than one scratch window. Solving it was, again, relatively easy, but that's still more complexity than that mythical :read.

  • I wanted my command to be usable as a filter, which, yet again, added more complexity because I needed to handle ranges.

  • Etc.

So no, :read and :put don't make things easier at all.

@HCY-ASLEEP
Copy link

Thanks for your serious reply, now that I have a better understanding of these commands, I'm considering taking your methods to improve my config.

@gachikuku
Copy link

guys can you give me something in luascript?!

@Leenuus
Copy link

Leenuus commented Jun 17, 2024

guys can you give me something in luascript?!

Thanks romainl for amazing work! I implement this command with lua script, with some extra commands to get repl-like features.

Also, this lua implementations support vertical and horizontal modifiers.

And note that this lua script doesn't use sh -c to do things, it spawns process directly, so no shell commands available but also no shell escaping needed.

My implementation in lua

@romainl
Copy link
Author

romainl commented Jun 18, 2024

Thanks, @Leenuus.

@gachikuku it looks like your wish came true.

@gachikuku
Copy link

Thank you @Leenuus! 🤗
@romainl it did.

@Leenuus
Copy link

Leenuus commented Jul 9, 2024

Thank you @Leenuus! 🤗 @romainl it did.

Thanks, @Leenuus.

@gachikuku it looks like your wish came true.

Hi, @gachikuku , I rewrite my ugly codes and write a README like @romainl did.

And with vim.uv power, this implementation is totally async.

Split direction and stderr are handled too.

Can't thank @romainl more for such a simple, elegant, and powerful solution.

My gist here

@romainl
Copy link
Author

romainl commented Jul 9, 2024

You are very welcome, @Leenuus. Your gist seems almost plugin-ready.

@Leenuus
Copy link

Leenuus commented Jul 9, 2024

You are very welcome, @Leenuus. Your gist seems almost plugin-ready.

Hope that I don't overengineer. 😂

@gachikuku
Copy link

@Leenuus @romainl is right!
That thing reserves a repo not a gist.
Plus it would be easier for folks to set it up, so make it a plugin.

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