Invoking vi through find | xargs breaks my terminal. Why?
Solution 1
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.)
Solution 2
(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.)
Solution 3
The easiest way:
vim $(find . -name "*foo*")
Solution 4
It should work just fine if you use the -exec option on find rather than piping into xargs.
find . -type f -name filename.txt -exec vi {} +
Solution 5
Use GNU Parallel instead:
find . -name "*.txt" | parallel -j1 --tty vim
Or if you want to open all the files in one go:
find . -name "*.txt" | parallel -Xj1 --tty vim
It even deals correctly with filenames like:
My brother's 12" records.txt
Watch the intro video to learn more: http://www.youtube.com/watch?v=OpaiGYxkSuQ
Related videos on Youtube
DevSolar
Computer addict since the C64, Software Engineer since 2000. Unless otherwise noted, all my sources are released under Creative Commons CC0 (i.e., Public Domain for all practical purposes except in name due to a braindead restriction in German copyright law). SOreadytohelp
Updated on September 18, 2022Comments
-
DevSolar over 1 year
When invoking
vim
throughfind | xargs
, like this:find . -name "*.txt" | xargs vim
you get a warning about
Input is not from a terminal
and a terminal with pretty much broken behaviour afterwards. Why is that?
This question was explicitly about the why, not about the how to avoid. This was asked, and answered, elsewhere.
-
Trevor Powell about 11 yearsSide note: You can perform this operation entirely within vim, not using
find
orxargs
at all. Open vim with no arguments, then run:args **/*.txt<CR>
to set vim's arguments from inside the editor. -
DevSolar about 11 years@TrevorPowell: In all these years, vim never ceased to amaze me.
-
kenorb about 9 yearsRelated:
grep -l .. | xargs vim
generates a warning, why? at unix SE -
kenorb about 9 yearsRelated: Terminal borked after invoking Vim with xargs at Vim SE.
-
kenorb over 6 yearsGitHub bug report: vim does not handle STDIN set to /dev/null.
-
-
DevSolar over 12 yearsNot ubiquitously available. Most of the day I am working on servers where I am not at liberty to install additional tools. But thanks for the hint anyway.
-
DevSolar over 12 yearsOK, tried that - but parallel does not open all the files, it does open them in succession. It's also quite a mouthful for a simple operation.
vim $(find . -name "*.txt")
is simpler, and you get all files opened at once. -
user1686 over 12 years@DevSolar: Somewhat unrelated, but both
find | xargs
and$(find)
will have big problems with spaces in file names. -
DevSolar over 12 years@grawity Correct, but there is no easy way around it (that I know of). You'd have to start fiddling with
$IFS
,-print0
and stuff, and then you left the realm of a one-shot command line solution and reached a point where you should come up with a script... there's a reason why spaces in filenames are discouraged. -
DevSolar about 11 yearsHuh... the trick there is the
+
(instead of "the usual"\;
) to get all the found files into one Vim session -- an option I keep forgetting about. You are right, of course, and +1 for that. I usevim $(find ...)
simply out of habit. However, I was actually asking for why the pipe operation screws up the terminal, and grawity nailed that with his explanation. -
Muhammad Rivan Febrian about 10 yearsThis is the best answer and it works on both BSD/OSX/GNU/Linux.
-
DevSolar about 10 yearsThe main question was "why", not "how to avoid it", and it's been answered to satisfaction two and a half years ago.
-
Chandranshu over 9 yearsAlso, find is not the only way of getting a list of files that have to be edited simultaneously by vim. I can use grep to find all files with a pattern and try editing them at the same time as well.
-
doc_id about 9 years+1, and for TL;DR people just run
stty sane
-
doc_id about 9 years@DevSolar Understood, but think about frustrated people like me who just google how to get rid of that behavior while not -unfortunately - have enough time right now to study "why", which is very interesting nonetheless.
-
Capi Etheriel about 9 yearswhen my terminal breaks, like this, i use
reset
instead ofstty sane
and it works fine after that. -
Dejay Clayton almost 9 yearsThis, of course, does not work properly when filenames contain spaces or other special characters, and is also a security risk.
-
zanegray over 8 yearsHow would you create a bash alias out of this?
$@
doesn't seem to be translating arguments correctly. -
Smylers about 8 years@grawity Using
find | xargs -d '\n'
works with filenames with spaces in them; it makesxargs
only split on line-breaks, which works becausefind
handily emits filenames on per line. (Obviously it wouldn't work with filenames that have line-breaks in them.) -
Travis Wilson over 7 yearsMy favorite answer because it works for every command that lists files, not just "find" or wildcards. It does require a little trust, as Dejay points out.
-
Good Person over 7 yearsThis is will not work with many use cases xargs is designed for: e.g., when the number of paths is very high (cc @TravisWilson)
-
Christopher about 7 years@zanegray -- you can't make an alias, but you can make it a function. Try:
function vimin () { xargs sh -c 'vim "$@" < /dev/tty' vim; }
-
wisbucky over 5 yearsFor a detailed explanation of how the GNU xargs solution works, and why you need the dummy
ignoreme
string, see vi.stackexchange.com/a/17813 -
wisbucky over 5 years@zanegray, You can make it an alias. The quotes are tricky. See solution at vi.stackexchange.com/a/17813
-
localhostdotdev almost 5 years
The -J, -o, -P and -R options are non-standard FreeBSD extensions which may not be available on other operating systems.
(It was not available on macOS for me because I installed xargs from homebrew (the GNU one)) -
DevSolar almost 5 yearsAs for several other answers, note that the actual question was "why", not "how to avoid it". (For which I would still point to Trevor's comment under my question as the most solid way that doesn't require scripting, aliases or anything.)
-
Chris Morgan over 4 yearsGNU xargs has
-o
since some release in 2017. -
James McGuigan over 3 years
vimgrep () { fgrep --no-messages "$@" | xargs -o -p vim }
-
James McGuigan over 3 years
fgrep () { rgrep --color=never -l "$@" }
-
James McGuigan over 3 years
rgrep () { if [[ $(argc "$@") < 2 ]]; then dir='./'; else dir=''; fi; GREP_COLOR='35;1' nice grep -rHIin --no-messages --color=always "$@" $dir --exclude-dir='*.vimbackup' --exclude-dir='.svn' --exclude-dir='target' --exclude-dir='.git' --exclude-dir='platforms' --exclude-dir='node_modules' --exclude='*.tmp' --exclude='*~' --exclude='*.min.*' --exclude='.vlt' --exclude='*.swp' | grep -v '^.{1000}' }
-
James McGuigan over 3 years
argc () { count=0; for arg in "$@"; do if [[ ! "$arg" =~ ^- ]]; then count=$(($count+1)); fi; done; echo $count }
-
James McGuigan over 3 yearsvimgrep will recursively search for text in the current directory tree and open each matching file in vim