26

I have about 10 php.ini files on my system, located all over the place, and I wanted to quickly browse through them. I tried this command:

locate php.ini | xargs vi

But vi warns me Input is not from a terminal and then the console starts getting really weird - after which I need to press :q! to quit vi and then disconnect from the ssh session and reconnect to have the console behave normally again.

I think that I sort of understand what's happening here - basically the command hasn't finished when vi started so the command maybe hasn't finished and vi doesn't think that the terminal is in normal mode.

I have no idea how to fix it. I have searched Google and also unix.stackexchange.com with poor luck.

cwd
  • 45,389

7 Answers7

23

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.

Both the OS X/macOS/BSD and recent versions of GNU findutils' xargs (beginning with v4.6.0) have a -o option to address this exact scenario:

From the macOS/BSD man page:

-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 macOS, you could use the following command:

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

If you are stuck with an older version of GNU xargs, the following 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.

AdminBee
  • 22,803
darnir
  • 4,489
  • 4
    +1 thanks for the -o tip. i've been using xargs for years and never noticed that....just checked the man page on my system, that's because it's not a GNU xargs feature. The man page does provide xargs sh -c 'emacs "$@" < /dev/tty' emacs as what they claim is a more flexible and portable alternative (although it's kind of funny for GNU to be prefer portability to features :). – cas Jul 31 '12 at 22:22
  • Without dummy string: '<dev/tty "$0" "$@"' vim – darw Oct 21 '20 at 12:41
  • 3
    The -o (also --open-tty) has been ported to xargs (GNU findutils) 4.8.0. – Big McLargeHuge Jul 10 '21 at 20:21
  • @BigMcLargeHuge as of 4.6.0, which was back in 2017, so any recent installation of GNU findutils should have this option now. – Kevin E Sep 12 '21 at 16:18
15
vi $(locate php.ini)

Note: this will have issues if your file paths have space, tabs, newline (which are in the default value of $IFS) or glob characters, but it is functionally similar to your command (xargs does treat quote and backslash characters specially, which this doesn't do though).
This next version will properly handle space, tab and glob characters but is a bit more complicated (newlines in file names will still break it though)

(IFS=$'\n'; set -o noglob; 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.

phemmer
  • 71,831
  • 2
    xargs is wonderful, one of my favourite tools - it's just not suited for use with programs that use stdin for anything other than a data feed. i like your answer and your explanation other than that, so +1 anyway :) – cas Jul 31 '12 at 22:16
  • @CraigSanders I don't like it because it's too easy to abuse (use improperly) and end up breaking. I've never run into anything that I've absolutely had to use xargs for that couldn't be done directly with the shell (or find). However I can think of cases where it would be the best solution. So, so long as you understand what xargs is doing, how it splits up the arguments, how it runs the program, etc, and are using it properly, I'd say go for it :-P – phemmer Jul 31 '12 at 22:23
  • it can't be beat for things like ... | awk '{print $3}' | xargs | sed -e 's/ /+/g' | bc (to add up all the values of field 3). or with sed -e 's/ /|/g' to construct a regexp. and yes, like any tool, you do need to know how to use it and what its limitations and caveats are. – cas Jul 31 '12 at 22:29
  • The vi $(...) approach also has a problem with wildcards in shells other than zsh. – Stéphane Chazelas Jan 25 '20 at 18:30
  • Also note that with the xargs approach beside the whitespace issue, file names with single quotes, double quotes and backslashes are also a problem. – Stéphane Chazelas Jan 25 '20 at 20:08
  • Note that depending on the xargs implementation, in xargs cmd, cmd inherits the file descriptor or xargs makes cmd's stdin /dev/null opened in read-only mode which is generally a better idea as it avoids cmd interfering with xargs's input (that's the latter with GNU xargs). – Stéphane Chazelas Jan 25 '20 at 20:12
3

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.

2

This error happens when vim is invoked and it's connected to the previous pipeline's output, instead of the terminal and it's receiving different unexpected input (like NULs). The same happens when you run: vim < /dev/null, so reset command in this case helps. This is explained well by grawity at superuser.

On BSD/OSX or with GNU xargs since version 4.7.0, you can use xargs with -o parameter, like:

locate php.ini | xargs -o vim

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

With xargs implementations that don't support -o, try the following workaround:

locate php.ini | xargs sh -c 'exec vim < /dev/tty "$@"' sh

Alternatively use GNU parallel instead of xargs to force tty allocation, in example:

locate php.ini | parallel -X --tty vi

Note: parallelon Unix/OSX won't work as it has different parameters and it doesn't support tty.

Many other popular commands provides pseudo-tty allocation as well (like -t in ssh), so check for help.

Alternatively use find to pass file names to edit, so don't need xargs, just use -exec, in example:

find /etc -name php.ini -exec vim {} +
kenorb
  • 20,988
  • 1
    xargs -J is not available on GNU Linux. Can you give the output of bash -c 'echo $OSTYPE' (or zsh -c 'echo $OSTYPE') (or uname -o if available) on the system where you're able to use xargs -J ? – SebMa Oct 03 '20 at 00:18
0

Edit multiple php.ini within the same editor ?

Try: vim -o $(locate php.ini)

daisy
  • 54,555
0

@Patrick's IFS hack is only necessary for dumb shells like bash and zsh. fish splits the string on newlines by default.

$ vim (locate php.ini)

And God help us all if a single one of us actually has a file with a newline in its name. After 17 years using Linux, I haven't seen it even once. I'd only bother supporting filenames with newlines in them for scripts that have to work no matter what, but scripts like that probably aren't running vim interactively.

  • zsh splits on SPC, TAB, NL and NUL by default. The thing it doesn't do compared to bash is perform globbing on the result so wildcard characters in file names are not a problem. In zsh, you'd do IFS=$'\0'; vi $(locate -0 php.ini) or as I shown in my answer vi ${(0)"$(locate -0 php.ini)"} for an explicit splitting operator. Also note tcsh's vi "\locate php.ini`"` – Stéphane Chazelas Jan 25 '20 at 19:03
  • aw, crap. OK this works: $ f='not there'<ret>$ ls $f<ret> but this doesn't: ls echo not there. OK looks like I need to update this a bit. – enigmaticPhysicist Jan 29 '20 at 01:57
  • Yeah, zsh doesn't do the right thing when you do ls "$(echo test; echo other test)". Only fish does the right thing. – enigmaticPhysicist Jan 29 '20 at 02:04
  • Assuming you meant the same without the quotes, that's not "right", that's splitting on lines, it's just a different choice. zsh splits on words by default (like all other shells) and can be told to split on lines or on NULs, either via $IFS or via explicit operators (f and 0 parameter expansion flags). For arbitrary file names, splitting by word or splitting by line is equally wrong, you need to split on NUL or parse some encoding, which fish can't do. In zsh, that's IFS=$'\0'; ls -ld -- $(printf '%s\0' "$file1" "$file2") or ls -ld -- ${(0)"$(printf '%s\0' "$file1" "$file2")"} – Stéphane Chazelas Jan 29 '20 at 09:01
  • Meh. Splitting on newlines is good enough. Like the answer says, newlines in filenames are extremely rare. I have literally never seen it happen in 17 years. And newlines are way more convenient separators than nuls. – enigmaticPhysicist Jan 29 '20 at 21:44
  • mkdir -p $'/tmp/\n/etc/shadow\n/file~'. There, you've seen it now ;-), the excuse no longer stands and your rm -f (find /tmp -name '*~') will remove /etc/shadow. It's not about how common it is or not, it's about whether it's possible or not. File paths are sequences of bytes other than 0. To handle them reliably, you have to treat them as binary blobs. – Stéphane Chazelas Jan 30 '20 at 09:33
0

The "vi" way (vs the "ex way") is historically more like:

vi /tmp/$$.out
~
~
:r!locate php.ini

Instead of trying to pipe to vi from stdin or stdout (which is not normally supported outside of vim-alikes), you open vi and use ex Command mode to execute your "shell stuff". Then you can munge the results in-place.

This method should work in any proper ex implementation. vi is traditionally just a Visual Interface to ex.
Or putting it another way, ex -v is more commonly known / executed as "vi" for short.

You could even perform these kinds of operations with a macro (@) in Input mode and/or use undo to make changes and/or corrections.

The ex way

If you want to do it in a script then it could be with ex commands from a here-doc or something:

tmp=/tmp/$$.out
ex -s ${tmp} <<'EOS'
r!locate php.ini
wq
EOS
vi ${tmp}

You can use almost any combination of valid scripting / sh or ex commands with this method, such as the find suggestion from others.

This could also be done with ed but I was never one of the cool kids.

Again, ex is POSIX, so it should work in GNU/Linux, *BSD, HP-UX, AIX, etc.

Variation

Presuming there are no security concerns, you might redirect to a file first then munge it separately, depending on your use case:

tmp=/tmp/$$.out
locate php.ini > ${tmp}
vi ${tmp}

However...

If just browsing is truly your goal then you may only need something like:

locate php.ini | more

I am throwing that out there because we can all over-complicate simple things at times.

Kajukenbo
  • 307