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 running stty, which will output a whole lot of <undef>s. On Linux, running stty sane will make the terminal usable again (although it will have lost such options as iutf8, 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 want xargs 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.)