7

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 -
  • alias v='vim -' and then ls | v ? – pLumo Jun 30 at 7:30
  • yes, but this is more typing that the kind of 'polish notation' v ls of v ls | grep keyword:) – Zorglub29 Jun 30 at 7:36
  • Thank you for a very detailed comment :) . Ok, so that confirms that ths may be hard to do... I would like to avoid as boilerplate as much as possible, so a v command that works a bit like time would be best, but it looks like you think it is not possible. – Zorglub29 Jun 30 at 8:06
  • I think that something a la v 'seq 100 | grep 7'would be the closest to what I hope for, it is 'only' two boilerplate 'more than the syntax I would love best. – Zorglub29 Jun 30 at 8:10
13

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).

| improve this answer | |
  • Thanks for a really amazing answer :) I will let the thread opened a bit more to see if people come with other propositions, but I expect there will not be a better answer than that, and will probably accept this evening or tomorrow :) . – Zorglub29 Jun 30 at 13:24
6

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...
| improve this answer | |
  • Mmmh, ok, thank you. So this looks difficult to perform then... Do you think there may be another way around, for example re-directing temporarily the output of the current command to a file, so that it can be opened by vim after? – Zorglub29 Jun 30 at 11:37
  • 2
    Eliah Kagan'd process substitution idea is nice, and could probably be shortened with an alias as well (alias v='vim <(', v ls -l | grep foo)) – muru Jun 30 at 12:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.