Vim is a shell command, and its fast startup supports that use-case: shell tasks, whether ad-hoc (interactive) or orchestrated (pipeline, script), are cheap and thus frequent.
Yet Vim’s startup story is relatively unpolished. Shell tools are expected to consume standard input (“stdin”) and emit to standard output (“stdout”)—but Vim supports this awkwardly, at best. The endeavor is never mentioned in Vim tutorials, including the “Unix as IDE” hymnals. And it is puzzled out of Vim’s documentation only by careful inspection.
Vim is positioned as a script host (VimL,
if_python, …) , but not as
a participant. Yet Vim is a terminal tool, and terminal users expect their
tools to compose. Like this:
# Does not work! $ printf 'a\nb\nc\nb\n' | vim +'g/b/norm gUUixx' +2 +'norm yy2p' | tr x z
Why doesn’t that work? What can we do instead?
Let’s talk about -s-ex
The goal is to penetrate Vim with input, manipulate it non-interactively, and produce output consumable by other shell tools.
Sending text input to Vim requires the explicit
$ echo foo | vim -
Working non-interactively is less obvious. Vim’s testsuite does something like this:
$ vim -es -u NONE -U NONE -i NONE --noplugin -c ... -c "qall!"
But what is
-es? Not merely the combination of
-s, it is a special
“silent mode” described at
Switches off most prompts. ... The output of these commands is displayed (to stdout): :print ... Initializations are skipped.
-es does not draw the UI, and we can emit text to stdout using
$ echo foo | vim - -es +'%p' +'qa!' Vim: Reading from stdin... foo
:%p prints the entire buffer and
:qa! ensures that Vim quits. In Vim
version 8, that “Vim: Reading from stdin…” message can be avoided with
-se are not equivalent, the Vim
quite literally expects
-e to precede
$ echo foo | vim - -se +'%p' +'qa!' Garbage after option argument: "-se"
A similar order-sensitivity befalls the
- file argument:
vim - -es behaves
vim -es -! The former consumes stdin as text, while the
latter activates stdin as Ex commands.
If you run into trouble, use
-V1 to reveal why
-e isn’t working:
$ printf 'foo\n' | vim -es # No output. Non-zero error code. $ echo $? 1
$ printf 'foo\n' | vim -es -V1 Entering Ex mode. Type "visual" to go to Normal mode. :foo E492: Not an editor command: foo
So now we can light up our tinsel:
$ printf 'a\nb\nc\nb\n' | vim - -es --not-a-term +'g/b/norm gUUixx' +2 +'norm yy2p' '+%p' '+qa!' | tr x z a zzB zzB zzB c zzB
Yay! We did it. Wait, you’re going home already…?
Ugly sweater party
Apparently Vim thought this was an ugly sweater party. Vim’s sweater has
Reading from stdin... and forty-four
--help options. Let’s learn more
about Vim before seating it next to Grandpa vi.
Input at startup can take these forms:
- user (“keyboard”) input
- Ex commands
By default, even in non-interactive mode (
-es) Vim treats input as
“commands”. That’s a tradition from Grandpa vi. Note that “commands” in vi
parlance means general user-input (starting from Normal-mode).
-es) Vim treats input as Ex commands: those entered at the
prompt, or “statements” in Vim script.
$ printf "put ='foo'\n%%s/o/X\n%%print\n" | vim -es fXo
- file tells Vim to slurp input as plain text into a buffer. Then
commands can be given with
There’s another thing I want to announce at the dinner table: if you specify
- file, then other file arguments are not allowed.
characterizes these independent “editing ways”:
Exactly one out of the following five items may be used to choose how to start editing:
Should you try to invoke multiple “editing ways”, Vim will leave the table. For
example, asking Vim to read both
a.txt is asking too much:
$ echo foo | vim - -es --not-a-term '+%p' a.txt Too many edit arguments: "a.txt"
Santa POS is coming to town
Gathering from the POSIX vi specification we find these directives regarding non-interactive (“not a terminal”) cases:
Historically, vi exited immediately if the standard input was not a terminal. …
If standard input is not a terminal device, the results are undefined. The standard input consists of a series of commands and input text, …
If a read from the standard input returns an error, or if the editor detects an end-of-file condition from the standard input, it shall be equivalent to a SIGHUP
- Input is always interpreted as user input, even if non-interactive.
- When stdout is not a terminal, Vim may behave as it likes (“undefined”).
- EOF (that is, closed input stream) means quit.
We’ve uncovered the origin of Vim’s eagerness to consume stdin as something alive instead of something inert: ‘twas always done that way.
$ echo foo | vim Vim: Warning: Input is not from a terminal Vim: Error reading input, exiting... Vim: Finished.
Vim must warn about stdin-as-commands because (1) it’s potentially destructive and (2) it’s almost always accidental (does anyone actually use this feature?).
Vim exits after stdin closes (EOF), as prescribed by POSIX. (Party trick: use
vim -s <(echo ifoo) to convince it to keep running.)
POSIX does not mention
-E, a variant of
-e ignored by Vim’s own testsuite.
The distinction is not useful, and in Nvim they both invoke
Under the tree: Neovim
Nvim now treats non-terminal stdin as plain text by default (the explicit
file is not needed):
$ echo foo | nvim
That means Nvim never pauses to display a warning. Nvim also allows multiple “editing ways”:
$ echo foo | nvim file1.txt file2.txt
It also works with
-Es (but not
-es), and it exits automatically (no
$ echo foo | nvim -Es +"%p"
If you ever want to execute stdin-as-commands, use
$ echo ifoo | nvim -s -
With these improvements one can use
nvim -es as one might use
python -c or
perl -e. For example, I use it in my
.bashrc to configure the shell
depending on the Nvim version:
if nvim -es +'if has("nvim-0.3.2")|0cq|else|1cq|endif' ; then ... fi
The mechanisms and ergonomics for delivering data to Vim at invocation-time are essentially unchanged from vi—owing, yes, to deference to POSIX and our old friend backwards compatibility—but perhaps primarly to inertia.
It turns out that very few people actually care about the traditional behavior:
the precise behavior of
-es, for example, was broken in Nvim for years but no
one complained. And Vim’s own codebase (including testsuite) does not use
If no one uses a feature, it might be ok to change it.
Merry Textmas! This holiday, when you’re with your loved ones, think of Vim.