2

I want to edit files by executing Vim commands from inside node.js as the following:

const child_process = require("child_process");
child_process.execSync(`time vim /home/user/file -c ':%s/^/newstring-/g | x'`);

When I run this code I get the following result:

node vimCommand.js
Vim: Warning: Output is not to a terminal
Vim: Warning: Input is not from a terminal
0.07user 0.00system 0:02.08elapsed 3%CPU (0avgtext+0avgdata 23168maxresident)k
0inputs+80outputs (0major+2611minor)pagefaults 0swaps

The functionality of this code works perfectly fine. However, for some reason, Vim is taking more than 2 seconds to execute this command in a very small file. That's not necessarily an issue with node-js because if I run echo commands instead, it executes immediately. Also, besides that, I've realized that when I use Neovim instead of Vim, this issue doesn't exist and my code works perfectly fine and fast:

node vimCommand.js
0.02user 0.01system 0:00.04elapsed 85%CPU (0avgtext+0avgdata 14164maxresident)k
0inputs+256outputs (0major+2218minor)pagefaults 0swaps

Here is the result of changing vim to nvim inside my node.js code. It doesn't have any warning telling me Output is not to a terminal. So, is there any extra procedure that nvim does automatically that I should manually do on vim? Just so it doesn't take too long to start executing the vim commands that I'm using? Or should I just consider it a vim bug and switch to neovim for doing this kind of stuff?


OBS1: When I run the command time vim /home/user/file -c 'execute ":%s/^/newstring-/g" | x' directly inside a terminal it executes very fast and I don't see any warnings. So the issue happens when I'm trying to use vim indirectly without a terminal window.

OBS2: the file I'm editing in my example is a very small one. My main issue here is why there's a 2 seconds delay for executing vim commands when I'm not running it directly from a terminal.

OBS3: I know that for this example I could just have used sed on the command line for doing exactly the same thing that my example is doing. But that was just an example for illustrating the problem, and executing ":%s/^/newstring-/g" is not the main issue.

1
4

The reason for the warning message is that Vim, in its default configuration, is an interactive application which expects to communicate using a particular terminal configuration. When you instead run things non-interactively from a shell command-line context Vim doesn't like it and complains.

For a slightly more technical explanation I'll quote Invoking vi through find | xargs breaks my terminal. Why?

Vim expects its stdin to be the same as its controlling terminal, and performs various terminal-related ioctl's on stdin directly. When done [with]...any non-tty file descriptor...those ioctls are meaningless and return ENOTTY.

("ENOTTY" is a standard error code on most Unix-type systems that indicates unexpected things happened while setting up input/output. More on this below.)

The solution is to put Vim in a mode where that command-line context is expected and there are, depending on how you count them, two or three ways to do this:

  1. Calling ex instead of vim
  2. Passing -e to Vim
  3. Passing -E to Vim

All of these put Vim into "Ex" mode which, in the simplest terms, is a mode where you you stay on the Ex command line even after you hit Enter to execute the current command(s). Normally, of course, you are dropped into to Normal mode after hitting Enter on the command line. Obviously another effect is to cause Vim to not look for the terminal configuration I mentioned above.

(The reason I said "two or three ways" is because vim -e and ex are functionally identical. If your installation has ex and you do ls -l /usr/bin/ex or equivalent you'll probably find that it's just a symbolic link to vim. Vim has some logic that looks at the name it was invoked with and if it sees ex it enables the same internal state that is triggered by vim -e.)

There are many Q&As out there already that explain this stuff in more detail, e.g How to edit files non-interactively (e.g. in pipeline)?, so I'll leave it at that except for your question about that two second delay.

I ran both incorrect and correct Vim invocations through strace. The results are very nearly identical except for this small section. Note the comments that delimit the lines that are present only with the incorrect invocation.

ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
# The next three lines are seen only with Vim without proper config (-e)
ioctl(2, TCGETS, 0x7fff35f74f10) = -1 ENOTTY (Inappropriate ioctl for device)
write(2, "Vim: Warning: Output is not to a"..., 42Vim: Warning: Output is not to a terminal
nanosleep({tv_sec=2, tv_nsec=5000000}, NULL) = 0
# Shared output resumes here
ioctl(1, TCGETS, 0x7fff35f745d0) = -1 ENOTTY (Inappropriate ioctl for device)
stat("/home/blayer/.terminfo", 0x55c66d2be890) = -1 ENOENT (No such file or directory)

Other things to note:

  • There's that ENOTTY error
  • It is caused by the unexpected state of ioctl (input/output control)
  • There's the warning that you saw
  • And the pièce de résistance ... there's a deliberate 2 second sleep period inserted here. (It's actually 2.005 seconds, oddly. Don't know what the extra 5 milliseconds is for.)

Now, I'd be lying if I claimed to know what purpose the sleep serves but you didn't ask that question so I think it's reasonable to put this baby to bed now. :)

4
  • The sleep is there to give the user a chance to read it. Relevant source code: github.com/vim/vim/blob/… I think you can skip it using the --not-a-term option. Apr 20 at 5:58
  • @ChristianBrabandt That occurred to me...but it didn't seem to make sense. Wouldn't a "pause so user can read it" delay make more sense in the opposite case, i.e. if communication was through a tty/terminal? Since a common non-tty scenario is outputting to a file why have a delay in that case? Or am I completely missing something? BTW, I just noticed that I got 2.5 seconds wrong. It's two seconds plus 5 milliseconds (5M nanoseconds). Not a big deal but do you know why such an odd interval was chosen? Thanks!
    – B Layer
    Apr 20 at 23:11
  • Of course, when I say "make more sense" I'm speaking about the general case of pausing an error message. I know you're not going to get a "Not a tty" error if you're using a tty. ;) Also, when outputting to a file, yes, stderr will still come through unless it is explicitly redirected it to the file but I think my point/question is still valid. Pausing like this seems pretty unconventional except for the case when Vim is being used normally as a TUI/GUI.
    – B Layer
    Apr 20 at 23:35
  • @ChristianBrabandt I looked a bit closer and noticed that you were the committer of this code and found the reason for the seemingly arbitrary sleep duration. BTW, the name of the sleep routine ui_delay...kinda seems to suggest that the last sentence of my last comment maybe isn't totally crazy. :)
    – B Layer
    Apr 21 at 9:50

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.