xargs and vi - "Input is not from a terminal"

This question has previously been asked on the Super User forum.

Quoting from @grawity's answer on that question:

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

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.

This is mentioned in the manual pages for xarg. From OSX/BSD:

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

Hence, on OSX, you could use the following command:

find . -name "php.ini" | xargs -o vim

While, there is no direct switch on the GNU version, this command will work. (Make sure to include the dummy string, otherwise it will drop the first file.)

find . -name "php.ini" | xargs bash -c '</dev/tty vim "$@"' dummy

The above solutions are courtesy Jaime McGuigan on SuperUser. Adding them here for any future visitors searching the site for this error.


vi $(locate php.ini)

Note: this will have issues if your file paths have spaces, but it is functionally equivalent to your command.
This next version will properly handle spaces but is a bit more complicated (newlines in file names will still break it though)

(IFS=$'\n'; vi $(locate php.ini))


Explanation:

What's happening is that programs inherit their file descriptors from the process that spawned them. xargs has its STDIN connected to the STDOUT of locate, so vi has no clue what the original STDIN really in.


With GNU findutils, and a shell with support for process substitution (ksh, zsh, bash), you can do:

xargs -r0a <(locate -0 php.ini) vi

The idea being to pass the file list via a -a filename rather than stdin. Using -0 makes sure it works regardless of what characters or non-characters the file names may contain.

With zsh, you could do:

vi ${(0)"$(locate -0 php.ini)"}

(where 0 is the parameter expansion flag to split on NULs).

However note that contrary to xargs -r that still runs vi without argument if no file is found.