Bash function to robustly pipe terminal output to vim

I often re-direct my terminal output to vim, in this fashion, which I get tired of typing all the time:

ls | vim -

I would like to define a function v to do this for me, i.e I want to be able to type:

v ls and that this somehow 'expands' to the previous command.

I can do this do work in simple cases with a script in this kind (of course, this can be re-factored as a small function):

#!/bin/bash 
 
touch crrt_cmd

while (( "$#" )); do 
  echo -n $1 >> crrt_cmd
  echo -n " " >> crrt_cmd
  shift 
done

chmod +x crrt_cmd
bash crrt_cmd | vim -

rm crrt_cmd

This works fine for very simple commands, for example ls or ls -l, but it does not work at all when some piping is present for example.

Any idea on how to perform something similar, but that works with any valid bash command?


So just to be clear, I would like things to work in this way if it is possible:

v ls | grep keyword | head -5

would translate into:

ls | grep keyword | head -5 | vim -

Process substitution may offer the closest syntax to what you want.

This works, and may be to your liking:

vim <(cmd1 | cmd2 | ...)

That lets you keep the text of your whole command together, and to put it after (rather than before) the command that sends it to vim, which seem to be your main goals. The absence of a - argument is intentional.

You don't need to define a function or alias for that, but of course you could shorten vim to v by defining v as an alias or function:

alias v=vim
v() { vim "$@"; }

v <(cmd1 | cmd2 | ...) is one more character than v 'cmd1 | cmd2 | ...'. But it also avoids the quoting hell that muru cautions about. ', ", and \ can appear inside <( ) and they work in the usual way. This also nests fine: you can have other parentheses in your command, so long as they are used in a manner that is otherwise syntactically correct.

This is process substitution. Bash creates a pipe that is accessible with a path like /dev/fd/63 (not to be confused with the pipes involved in your command itself). It substitutes the path of that pipe for <(cmd1 | cmd2 | ...), so vim sees a filename like /dev/fd/63. Your command, cmd1 | cmd2 | ..., is run asynchronously in a subshell, and its output is sent to the pipe, which vim reads. (That's why you don't write -: vim reads from /dev/fd/63 or whatever it ends up being called, not from standard input.)

muru has pointed out that you can write an alias that expands to vim <(:

alias v='vim <('

You can put this alias definition in ~/.bash_aliases or at the end of ~/.bashrc.

That gets you even closer to the syntax you originally wanted--you just have to write a ) at the end of your command:

v cmd1 | cmd2 | cmd3)

Process substitution has the additional benefit that you can use it multiple times, writing multiple <( ) constructions in the same command, in case you want to run multiple commands separately and view their output in separate vim buffers.

vim <(cmd1 | cmd2 | cmd3) <(cmd4 <infile) <(FOO=bar cmd5) <(cmd6 | cmd7)

Of course, as muru says, postfix operations are still simpler for the use case of running a pipeline and opening its output in vim, as you need only tack on another command for vim at the end of your pipeline.

Thanks to muru for the insight that v can be made an alias for vim <(.


The simplest cases: a better way to implement your original v

vim <( ), detailed above, can be used even in the simplest cases. But it's syntactic overkill when you're not writing a pipeline of two or more commands (cmd1 | cmd2), applying redirections to your command (cmd <infile), or assigning environment variables for the duration or your command (FOO=bar cmd). So you may still want to have a function that does what your original v function did.

Your implementation runs a loop to concatenate the text of the arguments. This is complicated, and also breaks in most scenarios involving quoting, even if you fix $1 to "$1" to prevent initial splitting and globbing. In v 'foo bar', your v function (like any function) doesn't receive any quotes: it sees foo bar. That's fine, since it receives it as a single argument... until it constructs a script that contains it and runs that script, at which point foo bar gets parsed as two words, which become two arguments.

Fortunately, there's a more reliable way, which is also simpler and shorter. "$@" expands to all the arguments passed to the current function, or to the current script, if not in a function. Separate arguments are neither further split nor joined with one another. So all you need is:

v() { "$@" | vim -; }

(Of course, if you're defining v to do something else--perhaps as an alias for vim <( as described above--then you'll want to call this something else, perhaps u.)

Running that command defines a shell function v that runs the command named in its first argument and passes its subsequent arguments. This does not construct a separate script, and it does not use bash -c or eval. So it does not risk splitting or joining arguments incorrectly, because arguments are never split or joined: they're separate going into the function, and "$@" uses them separately.

You can put that at the end of ~/.bashrc so it will be defined for your interactive shells.

Or if you prefer it be a script, then make a file called v (or whatever you want the command name to be) with the contents:

#!/bin/bash
"$@" | vim -

Mark the file executable (chmod +x v) and put it in a directory listed in your $PATH. I suggest ~/bin, which is automatically added to your $PATH when you log in, if it exists (unless you've changed ~/.profile to not do that).


A function can't do this for arbitrary non-simple commands in bash, since a function itself is a simple command - by the time the function definition comes into play, it's way too late. Expansions of aliases can contain pipelines, so you can add piplines in an alias, but of course only at the beginning of the command:

$ alias e='echo foo |'
$ e grep bar  # expands to echo foo | grep bar

If you were to chose the insanity that will inevitably follow something like this:

v () { eval "$@" | vim -; }
# v 'ls -l | nc' 

You will end up in quoting hell sooner or later.

Go for postfix operations, it's just simpler.


Other options for insanity:

Global aliases in zsh

You can expand aliases anywhere in the command line (instead of just the beginning) in zsh:

% alias -g v='| vim -'
% echo foo | grep foo v
Vim: Reading from stdin...