Why is the following code not working for completion of the foo command? When I source it then type foo <Tab>, the shell hangs and doesn't take any input until I press ^C (which exits the command completion).

My hypothesis is that /dev/tty is already being read by the shell and it somehow messes with cat being able to read from it as well, but in that case I still need a workaround.

_foo() {
    _values 'foo' "$(cat < /dev/tty)"

compdef _foo foo

Note that this example is deliberately simplified: the actual use case is running a terminal interface program (think ncurses) instead of cat.

  • 1
    What are you trying to achieve with this particular completion? – mik Jan 23 '18 at 21:49
  • 1
    I just want to implement my own completion UI. I already have the ncurses GUI binary implemented, it works when I call it from the regular shell (even when its input is a pipe or it's invoked from a subshell, in which case I just open /dev/tty myself), the only issue is when used with completion. – Nison Maël Jan 23 '18 at 21:52
  • Note that zsh has builtin ncurses support in its zsh/curses module. – Stéphane Chazelas Jan 29 '18 at 17:13

During completion, you're in the zsh line editor, so the terminal line discipline's own line editor is disabled as if you had run:

stty -icanon -echo

In that mode, cat cannot exit as there's no way you can signify it the end of input (^D is part of the icanon line discipline's line editor behaviour) and you won't see the echo of what you type.

You could do:

_foo() {
  _values 'foo' "$(
      s=$(stty -g)
      stty sane
      stty $s
    } < /dev/tty)"
  zle -I

That is, put the terminal device in the state expected by cat (where you can press ^D on an empty line or twice to end the input) before running cat and restore it afterwards. And we tell zle it has to redraw its prompt and buffer as the echo of what you'd type within the line discipline line editor would mess things up (zle -I to invalidate).


This is probably too simplified, cat will read from /dev/tty for a good long while. With a _foo completion of

#compdef foo
_values 'foo' "$(promptfor)"

and a promptfor of

#!/usr/bin/env expect
set fh [open /dev/tty r+]
stty raw -echo
set key [read $fh 1]            ;# read from tty
puts stdout $key                ;# to ZSH
flush stdout
stty -raw echo

my ZSH on footab will then complete whatever key is then pressed while promptfor runs.

If you have a daemon of sorts sitting around, then you'll probably need something fancier like a socket that ZSH and your daemon can use to carry out any necessary communications; read in ZSH can read from an arbitrary file descriptor or coprocess...

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.