18

System info:

macOS Sierra 10.12.6
zsh 5.4.2 (x86_64-apple-darwin16.7.0)
GNU bash, version 4.4.12(1)-release (x86_64-apple-darwin16.3.0)

Scroll to the EXAMPLES at the bottom if you just want to dig in to the simplified examples that I made.

NOTE: I am not a big zsh user.


I was looking at the fzf keybindings for bash and zsh.

Notice how they both run a variable command $(__fzfcmd). __fzfcmd by default outputs fzf to stdout and the parameter substitution just runs command (fzf) resulting from the output.

One difference between the bash and zsh script is that the bash one further pipes the output of $(__fzfcmd) but zsh just captures it inside an array. My guess is because of a problem in zsh when you further pipe the output of fzf where you can't input to fzf and the process piped to by fzf doesn't get any stdin. Your only choice is to ^Z or ^C. ^C seems to background the process for some reason. Or maybe they just wanted it in an array so they could could run zle vi-fetch-history on it. The bash version does some magic in the key binding with "\e^": history-expand-line

Now fzf isn't important. It seems like you just need a program that outputs to the tty to be called by parameter substitution to cause this problem. So I will show some simpler examples.

Here are some other commands that output to the tty that can cause this problem in zsh:

  • vipe (run editor in middle of a pipe)
  • 'vim -' (make vim read from stdin. similar to vipe but won't output to stdout)

In the examples below, replace every occurrence of vipe with vim - if you don't want to do a separate install. Just remember that vim - won't output the editor contents to stdout like vipe does.

EXAMPLES:

1) echo 1 | vipe | cat            # works in both bash and zsh
2) echo 1 | $(echo vipe) | cat    # works in bash only. zsh problem with no output until I hit `^C`:
   ^C
   zsh: done                    echo 1 | 
   zsh: suspended (tty output)  $(echo vipe) | 
   zsh: interrupt               cat
   # seems like the process is backgrounded. I can still see it in jobs command

3) cat <(echo 1 | $(echo vipe))   # zsh and bash has the problem. I'm guessing because
                                  # the file isn't finished writing and cat is
                                  # blocking vipe's tty output
                                  # both their `^C` output is just:
   ^C # nothing special, as expected

4) cat < <(echo 1 | $(echo vipe)) # works in both bash and zsh
5) echo 1 | $(echo vipe) > >(cat) # works in both bash and zsh

# The following don't have and input pipe to vipe.
# Type something then send EOF with ^D
6) vipe | cat                     # works for both
7) $(echo vipe) | cat             # works for both

Now, I'm mostly wondering why 2) has a problem for zsh but not for bash and why 4) and 5) fixes the problem for zsh.

The requirements for zsh to have this problem to seem to be exactly what I put in the title:

  • input pipe
  • command run by variable/parameter substitution that has tty output
  • output pipe

UPDATE

I added another workaround that doesn't cause zsh to have this problem, 5). It's similar to 4) but instead of redirecting stdout directly into stin, I redirect it into a file that redirects into stdin using process substitution.

10
  • 1
    As the output of ps will tell you, in none of these cases are the shells frozen or stuck. They are simply waiting for child processes in the normal way; and they will indeed loop back to prompting for input in the normal way once those child processes are suspended or terminated. Your question title and body include an implicit false premise. "Why does my shell freeze?" is an unanswerable loaded question when your shell is not actually freezing in the first place. You would have a better question for removing this implicit false premise.
    – JdeBP
    Dec 19, 2017 at 7:31
  • 1
    The shell is not waiting for input. It is waiting for its children. This question would be better put simply describing what happens. Don't form hypotheses and inferences such as "my shell is frozen" and then ask about the inferences. Describe what happens and ask about that: The special character terminal input sequences (that would normally suspend the foreground job, or interrupt or quit the job, or send an EOF indication to the process reading from the terminal) have no effect. What is happening? Why?. This is replicatable on Debian Linux and FreeBSD/TrueOS, by the way,
    – JdeBP
    Dec 19, 2017 at 10:18
  • 1
    I've reported the bug on the zsh development mailing list. For now, you should be able to work around it by wrapping it in a subshell (echo | $(echo vipe) | cat) Mar 23, 2018 at 16:19
  • 1
    The fact that process substitutions are started in background is documented I think (or at least known) Mar 23, 2018 at 16:22
  • 1
    Similar bug: unix.stackexchange.com/questions/585941/…
    – HappyFace
    May 12, 2020 at 7:07

1 Answer 1

0

I believe your issue boils down to improperly quoting your expansions.

Quoting from zsh:14 Expansion

A command enclosed in parentheses preceded by a dollar sign, like $(...), or quoted with grave accents, like ‘...’, is replaced with its standard output, with any trailing newlines deleted. If the substitution is not enclosed in double quotes, the output is broken into words using the IFS parameter. The substitution $(cat foo) may be replaced by the equivalent but faster $(<foo). In either case, if the option GLOB_SUBST is set, the output is eligible for filename generation.

Note that Example #2 in your question results in an infinite echo of NULL, due to:

If the substitution is not enclosed in double quotes, the output is broken into words using the IFS parameter.

In other words the shell infinitely waits for the echo, because the default delimiter is SPACE, the echo never completes. See TLDP: Internal Variables. This leaves a hung pipe for the cat command.

As a hunch, I believe 4 and 5 work due to output redirection.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .