Invoking vi through find | xargs breaks my terminal. Why?
When you invoke a program via xargs
, the program's stdin (standard input) points to /dev/null
. (Since xargs doesn't know the original stdin, it does the next best thing.)
$ true | xargs filan -s 0 chrdev /dev/null 1 tty /dev/pts/1 2 tty /dev/pts/1 $ true | xargs ls -l /dev/fd/
Vim expects its stdin to be the same as its controlling terminal, and performs various terminal-related ioctl's on stdin directly. When done on /dev/null
(or any non-tty file descriptor), those ioctls are meaningless and return ENOTTY, which gets silently ignored.
-
My guess at a more specific cause: On startup Vim reads and remembers the old terminal settings, and restores them back when exiting. In our situation, when the "old settings" are requested for a non-tty fd (file descriptor), Vim receives all values empty and all options disabled, and carelessly sets the same to your terminal.
You can see this by running
vim < /dev/null
, exiting it, then runningstty
, which will output a whole lot of<undef>
s. On Linux, runningstty sane
will make the terminal usable again (although it will have lost such options asiutf8
, possibly causing minor annoyances later).
You could consider this a bug in Vim, since it can open /dev/tty
for terminal control, but doesn't. (At some point during startup, Vim duplicates its stderr to stdin, which allows it to read your input commands – from a fd opened for writing – but even that is not done early enough.)
(Following on from grawity's explanation, that xargs
points stdin
to /dev/null
.)
The solution for this problem is to add the -o
parameter to xargs
.
From man xargs
:
-o
Reopen stdin as
/dev/tty
in the child process before executing the command. This is useful if you wantxargs
to run an interactive application.
Thus, the following line of code should work for you:
find . -name "*.txt" | xargs -o vim
GNU xargs supports this extension since some release in 2017
(with the long option name --open-tty
).
For older or other versions of xargs,
you can explicitly pass in /dev/tty
to solve the problem:
find . -name "*.txt" | xargs bash -c '</dev/tty vim "$@"' ignoreme
(The ignoreme
is there to take up $0,
so that $@ is all arguments from xargs.)