Skip to content

Prevent exec from permanently redirecting stderr#849

Open
yutkat wants to merge 1 commit into
marlonrichert:mainfrom
yutkat:main
Open

Prevent exec from permanently redirecting stderr#849
yutkat wants to merge 1 commit into
marlonrichert:mainfrom
yutkat:main

Conversation

@yutkat

@yutkat yutkat commented Mar 15, 2026

Copy link
Copy Markdown

Commit 2be4e7f introduced a cleanup mechanism in .autocomplete:async:start() to unhook and close the previous fd before starting a new async operation. However, the line:

exec {_autocomplete__async_fd}<&- 2>/dev/null

permanently redirects stderr to /dev/null for the current shell, because exec without a command applies all accompanying redirections to the current shell. While the intent was
only to suppress errors from closing a stale fd, it also silenced stderr for the entire session.

Additionally, because start() now proactively cleans up the previous fd, the complete:fd-widget callback can fire after the fd has already been unhooked and closed, producing
harmless but noisy errors:

No handler installed for fd N
failed to close file descriptor N: bad file descriptor

These were hidden by the same exec ... 2>/dev/null bug.

Fix

  • Wrap exec {fd}<&- calls in command groups { } so that 2>/dev/null only applies within the group and stderr is restored afterward
  • Suppress the expected race condition errors in complete:fd-widget individually

Fix #848

Before submitting your Pull Request (PR), please check the following:

  • There is no other PR (open or closed) similar to yours. If there is, please first discuss over there.
  • Your new code in each file follows the same style as the existing code in that file.
  • Each commit messages follows the Seven Rules of a Great Commit Message.
  • Each commit message includes Fixes #<bug> or Resolves #<issue> in its body (not subject, that is, the
    first line) for each issue it resolves (if any).
  • You have squashed any redundant or insignificant commits.

Copilot AI review requested due to automatic review settings March 15, 2026 05:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an issue in zsh-autocomplete’s async initialization where a redirection intended to silence an fd-close error accidentally silences stderr for the entire interactive shell session (Fix #848).

Changes:

  • Scope 2>/dev/null to a command group when closing the previous async fd, so stderr is restored afterward.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

# Unhook and close the previous fd. (If it no longer exists, then this will fail harmlessly.)
builtin zle -F "$_autocomplete__async_fd" 2>/dev/null
exec {_autocomplete__async_fd}<&- 2>/dev/null
{ exec {_autocomplete__async_fd}<&- } 2>/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the docs:

A list is a sequence of zero or more sublists, in which each sublist is terminated by ‘;’, ‘&’, ‘&|’, ‘&!’, or a newline. This terminator may optionally be omitted from the last sublist in the list when the list appears as a complex command inside ‘(...)’ or ‘{...}’. When a sublist is terminated by ‘;’ or newline, the shell waits for it to finish before executing the next sublist. If a sublist is terminated by a ‘&’, ‘&|’, or ‘&!’, the shell executes the last pipeline in it in the background, and does not wait for it to finish (note the difference from other shells which execute the whole sublist in the background). A backgrounded pipeline returns a status of zero.

@stephanschielke

Copy link
Copy Markdown

Just found your PR (while stumbling upon the same issue and trying to fix here) by ultra safely closing the file descriptor:

.autocomplete:async:close-fd-safely() {
  local fd=$1
  local saved_stderr
  exec {saved_stderr}>&2
  exec 2>/dev/null
  exec {fd}<&-
  exec 2>&$saved_stderr
  exec {saved_stderr}>&-
}
...
    # exec {_autocomplete__async_fd}<&- 2>/dev/null
    .autocomplete:async:close-fd-safely "$_autocomplete__async_fd"

but I much much more prefer your command group approach to scope and wrap the redirect.

I just tested your branch and fix and can confirm this solves the issue.

To reproduce the issue:

% cd $(mktemp -d)
% git clone --depth 1 -- https://github.com/marlonrichert/zsh-autocomplete.git
...
% > .zshrc <<EOF
setopt interactivecomments transientrprompt
PS1='%# '
PS2=
RPS2='%^'
fpath+=(zsh-autocomplete)
source zsh-autocomplete/zsh-autocomplete.plugin.zsh
EOF
% env -i HOME=$PWD PATH=/usr/bin:/bin TERM=$TERM zsh -d

# Check initial FD 2 status
% ls -l /proc/$$/fd/2
lrwx------ 1 xxx xxx 64 Mar 20 10:55 /proc/1725126/fd/2 -> /dev/pts/31

#
# Type any command to trigger autocomplete suggestions, e.g., type "ls " and wait for suggestions
#

# Then check FD 2 again - it may now be corrupted
% ls -l /proc/$$/fd/2
l-wx------ 1 xxx xxx 64 Mar 20 10:55 /proc/1725126/fd/2 -> /dev/null  
# BUG: stderr redirected to /dev/null

or to understand the issue:

% zsh -f  # Start clean zsh
% ls -l /proc/$$/fd/2
lrwx------ 1 xxx xxx 64 Mar 20 10:55 /proc/1725126/fd/2 -> /dev/pts/31
% exec {testfd}< <(echo "test")
% exec {testfd}<&- 2>/dev/null
% ls -l /proc/$$/fd/2
l-wx------ 1 xxx xxx 64 Mar 20 10:55 /proc/1725126/fd/2 -> /dev/null  

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stderr does not print to the terminal

3 participants