29

I've sometimes tried invoking Vim using xargs, like this:

find . -name '*.java' | xargs vim

… which kind of works:

  1. As Vim launches, I see the following warning flash briefly:

    Vim: Warning: Input is not from a terminal
    
  2. Editing works — :files correctly enumerates all of the .java files as expected.
  3. I can save and quit.

However, after exiting Vim, my terminal is borked:

  • Whatever I type at the shell prompt is not echoed.
  • Carriage returns don't appear at all, and line feeds only sometimes appear.

This goes on until I issue a reset(1) command to reinitialize the terminal.

Is this a Vim bug, or is there a more satisfactory explanation for why it interacts with the terminal like that? I've seen it happen on Vim up to version 7.3 (the version doesn't seem to matter) on Linux and various Unices.

I'm aware of one workaround, namely vim $(find . -name '*.java'). Other workarounds would be welcome, though that's not my main question.

  • I kind of get your desire to fill up this site with as many questions as possible but… that question has been answered dozens of times over the years on SO/SU and elsewhere: xargs uses a dummy stdin that can't be used by Vim and breaks everything afterwards. – romainl Feb 5 '15 at 9:08
  • 1
    @romainl I'm not active on those sites, and I've never seen it asked there — honest. – 200_success Feb 5 '15 at 9:09
  • Related: superuser.com/questions/336016/… - perhaps someone can condense the top answers to a great answer. – muru Feb 5 '15 at 10:53
  • 2
    Great question - this has happened to me numerous times. Also, I don't use any other SO sites - if it's vim-related, I'd like to find it here. – craigp Feb 5 '15 at 19:38
22

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

If you're using find to pass file names to edit, you don't need xargs, just use -exec, in example:

find . -name '*.java' -exec vim {} +

If you'd like to use xargs, on Unix/macOS you should use -o parameter, like:

find . -name '*.java' | 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.

or:

find . -name "*.java" -type f -print0 | xargs -o -0 vim

Note: Using -print0/-0 will support filenames with whitespaces.


find + BSD xargs

On Unix/macOS you can try the following workaround:

find . -name '*.java' | xargs -J% sh -c 'vim < /dev/tty $@'

You can also use command substitution syntax, for example:

vim $(find . -name '*.java')
vim `find . -name '*.java`

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

find . -name '*.java' | 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.

Other suggestion would be to use:

Related:

  • My GNU xargs doesn't have -J. That seems to be a MacOS (BSD) option that's not present in the GNU version. – Dennis Williamson Nov 2 '18 at 21:34
12

Workaround suggestion: use a buffer as a filesystem navigator

Use the vim - command to read a list of paths from stdin. Vim's :help -- explains this:1

Start editing a new buffer, which is filled with text that is read from stdin. The commands that would normally be read from stdin will now be read from stderr. Example:

find . -name "*.c" -print | vim -

You can then use gf or Ctrl-wCtrl-f to navigate to the file in the same buffer or in a new split window respectively.

Workaround suggestion: argument list

Another great way is to use the Vim's argument list. The :argadd **/*.java command will populate Vim's argument list with all the java files found inside the current directory recursively. You can then use the :next and :prev to move between the files.


1 The first - tells Vim you want to search the help for a command line option, the second is actually the command line flag you're searching for. Read :help help-context for more tricks like this.

8

Besides reset, you can try:

stty sane

which should also make your terminal usable again.

See here for explanations. And somehow this can be considered a vim misbehavior, at least Neovim doesn't have this issue at the moment.

  • This doesn't exactly answer the question, but it helped me, so +1. – iamnotmaynard Oct 18 '16 at 16:16
  • This DOES answer my question ;) Though after reading the OP more I guess reset should too. – Sridhar Sarnobat Jun 20 at 17:47
1

The reason is that xargs sets stdin to /dev/null, whereas vim needs stdin to be /dev/tty.


BSD xargs (e.g. Mac) solution:

echo -e 'file1\nfile2' | xargs -o vim

-o sets the stdin of xarg's child process (vim in this case) to dev/tty.


GNU xargs (e.g. Linux) solution:

GNU xargs does not have the -o option. Instead you will have to use a more complicated workaround. (Note: It is very important to have the trailing zero string, don't forget it.)

echo -e 'file1\nfile2' | xargs bash -c '</dev/tty vim "$@"' zero

You can also make it an alias:

alias vimin='xargs bash -c '\''</dev/tty vim "$@"'\'' zero'
echo -e 'file1\nfile2' | vimin

Detailed explanation of GNU xargs solution

Let's break it down step by step:

echo -e 'file1\nfile2' | xargs bash -c '</dev/tty vim "$@"' zero

1. xargs simply appends the stdin to the end of the string, so xargs will be executing this:

    bash -c '</dev/tty vim "$@"' zero file1 file2

2. The format for bash -c is bash -c 'COMMAND_STRING' $0 $1 $2 etc.

"$@" expands to the positional parameters "$1", "$2", etc. It does not include "$0" because that is a special parameter for the script name, not a positional parameter. This is why we need to add the dummy string zero (it can be any string) to take the place for $0. Otherwise, you will lose the first file.

So after you expand "$@", you end up with:

    bash -c '</dev/tty vim file1 file2' 

3. bash -c will execute the COMMAND_STRING:

    </dev/tty vim file1 file2 

</dev/tty sets the stdin to /dev/tty so that vim can work in interactive mode.

  • +1 This would seem to be a duplicate of the answer with the highest number of votes, but it's not. The $0 placeholder (here "zero") is the key. – Dennis Williamson Nov 2 '18 at 21:36

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.