April 29, 2020·4 min read·engineeringfishterminaldebugging

fish 3.1.2 restores terminal access through eval

fish 3.1.2 fixes an eval buffering regression that could hang interactive programs. The patch distinguishes actual pipelines from terminal output and file redirection.

An interactive program needs to observe the terminal while it runs. Capturing all its output until completion can break the interaction that would allow it to complete. fish 3.1.2 repairs that mistake in eval, following reports that interactive tools appeared to hang after the preceding patch release.

The April 29 release identifies excessive buffering in eval as a regression in fish 3.1.1. It names fzf and enhancd as affected commands. The fix narrows when fish adds output buffers, preserving terminal access when output is not piped to another process.

I like fish's interactive behavior because a shell should make ordinary terminal work easier to understand. A regression in the path used by interactive integrations matters for that reason. The command text can remain correct while the shell changes the environment in which the resulting program runs.

The report reduced the integration to one command

Issue 6955 was opened on April 28. The reporter explained that fzf's fish integration dynamically evaluates the executable command and supplied a smaller reproduction using eval (echo fzf).

eval (echo fzf)

This is the issue's reproduction for an environment where fzf is installed. It illustrates the failing path without requiring the integration's helper function. Command substitution supplies the command name, then eval evaluates that text.

The versioned eval documentation says that the builtin evaluates its parameters as a command, joining multiple parameters with spaces. That explains why this small example exercises dynamic evaluation even though the resulting command name is ordinary.

Other comments that day reported related effects with enhancd and commands that produced no visible output until interruption. Those reports helped identify the scope of the regression. They are observations from the issue, without establishing that every apparently stalled interactive command has this cause.

An earlier repair introduced unconditional buffering

The comparison between 3.1.1 and 3.1.2 explains the preceding change. A fix associated with issue 6806 had made eval buffer its output like other builtins. The comments in the affected code explain why buffering can be necessary when the next process in a pipeline has not yet launched.

That requirement does not apply uniformly to every invocation. When output goes directly to a terminal, inserting capture buffers changes what the evaluated command sees. The corrective commit explicitly says that this prevented commands wanting terminal access, particularly fzf, from operating correctly.

The original implementation created capture buffers for standard output and standard error without first distinguishing those cases. It then evaluated the command with those buffers as its output arrangement. The correction preserves buffering for the situation that needs it and avoids imposing it on ordinary terminal output.

This makes the regression more precise than a general statement that buffering is undesirable. The shell still needs buffering in a particular execution path. The error was applying that path to commands whose interaction depended on the terminal.

The fix tracks pipes separately from redirections

The patch adds out_is_piped and err_is_piped flags to the builtin's stream information. Existing redirected-output flags could establish that an output destination changed, but they did not specifically identify a pipeline.

The execution code obtains the I/O entries for standard output and standard error and checks whether each has pipe mode. That lets eval decide independently whether either stream needs a capture buffer.

A file redirection is consequently different from a pipe. The patch comments explicitly say not to buffer merely because output is redirected to a file. A broad redirected-output test would therefore still apply the capture behavior in more cases than intended.

The revised evaluator starts with the existing I/O chain and appends a buffer only for a stream that is actually piped. After evaluation, it finishes and copies back only the buffers it created. The implementation preserves the surrounding I/O arrangement while adding the specific capture needed for a pipeline.

Syntax and terminal behavior are separate concerns

The eval documentation mentions direct command execution through a variable when no pipeline or other compound syntax is needed. That provides an alternative for some dynamic commands. It does not change the obligation of eval to support interactive commands when the evaluated text requires it.

Similarly, a shell function can produce the expected command string while its eventual execution still fails. Inspecting the string answers a parsing question. Inspecting the command's I/O arrangement answers a different question about its runtime environment.

I find the patch's distinction useful beyond this individual report. Standard output connected to a terminal, a file and another process are different execution conditions. A shell implementation needs enough information to preserve those differences when it evaluates commands dynamically.

The 3.1.2 changelog concentrates on this regression after the broader 3.1.1 maintenance update. The April correction restores terminal access for the affected eval path while retaining capture for actual pipelines, where the execution machinery still needs it.