Skip to content

Don't print a stray blank line when try with catch or finally collects an external - #18811

Merged
fdncred merged 2 commits into
nushell:mainfrom
hexbinoct:fix/try-collect-uncaptured-external
Sep 2, 2026
Merged

Don't print a stray blank line when try with catch or finally collects an external#18811
fdncred merged 2 commits into
nushell:mainfrom
hexbinoct:fix/try-collect-uncaptured-external

Conversation

@hexbinoct

Copy link
Copy Markdown
Contributor

Description

Fixes #18765. Byte for byte, before:

> nu -c "try { ^nu -c 'print hi' } catch {}" | od -c
0000000   h   i  \n  \n

and after:

0000000   h   i  \n

Since 7f69d75, a try block followed by catch or finally collects the block's output first, so it can tell whether the block failed before anything starts streaming. If the tried command is an external whose stdout was not captured (it is inherited at the statement level, or redirected to a file inside the block), the output has already left the process by the time the collect runs. The collect then converted that data-less child stream into an empty string value, and printing the collected result rendered the empty string as an extra blank line.

The fix is in the shared collect helper in eval_ir.rs: when the collected data is a byte stream whose child has no captured stdout, wait for the child and produce Empty, which is exactly what the plain try { } path produces when it drains the same stream. Waiting keeps the failure semantics: a non-zero exit still becomes an error, so catch still runs (covered by a test).

Behavior that did not change, verified against an unpatched build:

  • Value context still collects and trims the captured output: let x = (try { ^cmd } catch {}) still gives the trimmed string.
  • $env.LAST_EXIT_CODE was not set by the collect path before and still is not.

Credit where due: neurolag bisected the inconsistency to 7f69d75, and weirdan showed it is the collection step that differs.

User-Facing Changes

try { ^external } catch { } and try { ^external } finally { } no longer print a stray blank line after the external's output. Collecting an uncaptured external (for example try { ^cmd o> file } catch {} as the value of an assignment) now produces nothing instead of an empty string.

Tests + Formatting

Added try_catch_inherited_external_output_has_no_extra_blank_line in tests/shell/mod.rs. It spawns the real nu binary and compares raw stdout bytes for the catch, finally, and catch plus finally cases, plus a failing external to prove catch still runs. The try sits in final statement position on purpose: only the final statement's value is printed, so that is the position where the fabricated empty string became visible. The test fails on current main with exactly the extra \n and passes with this change.

  • cargo fmt --all -- --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • shell:: suite 255 passed, commands::try_:: suite 41 passed (includes the pipefail experimental tests)
  • cargo check -p nu-engine --target wasm32-unknown-unknown --no-default-features compiles (the new block is gated on the os feature)

After Submitting

Release notes summary - What our users need to know

try with a catch or finally block no longer prints a stray blank line after an external command's output.

A `try` with a `catch` or `finally` block collects the block's output to
know whether it failed before anything streams (7f69d75). When the tried
command is an external whose stdout was inherited, its output has already
gone to the terminal, so there is nothing to collect. Collecting anyway
turned the data-less child stream into an empty string, and printing that
empty string produced a stray blank line after the external's own output:

    > nu -c "try { ^nu -c 'print hi' } catch {}"
    hi

    >

The same happened with a `finally` block and when the external's stdout
was redirected to a file inside the block.

Now `collect` waits for such a child, so a failure still surfaces and
`catch` still runs, and produces Empty, matching what draining the same
stream produces in the plain `try { }` case.

Thanks to neurolag for bisecting the inconsistency to 7f69d75 and to
weirdan for pinning it on collection.

Fixes nushell#18765
Comment thread tests/shell/mod.rs Outdated
@fdncred
fdncred merged commit cbb3333 into nushell:main Sep 2, 2026
14 checks passed
@fdncred

fdncred commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thanks

@github-actions github-actions Bot added this to the v0.116.0 milestone Sep 2, 2026
Dheebz pushed a commit to Dheebz/nushell that referenced this pull request Sep 3, 2026
…s an external (nushell#18811)

# Description

Fixes nushell#18765. Byte for byte, before:

```
> nu -c "try { ^nu -c 'print hi' } catch {}" | od -c
0000000   h   i  \n  \n
```

and after:

```
0000000   h   i  \n
```

Since 7f69d75, a `try` block followed by `catch` or `finally` collects
the block's output first, so it can tell whether the block failed before
anything starts streaming. If the tried command is an external whose
stdout was not captured (it is inherited at the statement level, or
redirected to a file inside the block), the output has already left the
process by the time the collect runs. The collect then converted that
data-less child stream into an empty string value, and printing the
collected result rendered the empty string as an extra blank line.

The fix is in the shared `collect` helper in `eval_ir.rs`: when the
collected data is a byte stream whose child has no captured stdout, wait
for the child and produce `Empty`, which is exactly what the plain `try
{ }` path produces when it drains the same stream. Waiting keeps the
failure semantics: a non-zero exit still becomes an error, so `catch`
still runs (covered by a test).

Behavior that did not change, verified against an unpatched build:

- Value context still collects and trims the captured output: `let x =
(try { ^cmd } catch {})` still gives the trimmed string.
- `$env.LAST_EXIT_CODE` was not set by the collect path before and still
is not.

Credit where due: neurolag bisected the inconsistency to 7f69d75, and
weirdan showed it is the collection step that differs.

# User-Facing Changes

`try { ^external } catch { }` and `try { ^external } finally { }` no
longer print a stray blank line after the external's output. Collecting
an uncaptured external (for example `try { ^cmd o> file } catch {}` as
the value of an assignment) now produces nothing instead of an empty
string.

# Tests + Formatting

Added `try_catch_inherited_external_output_has_no_extra_blank_line` in
`tests/shell/mod.rs`. It spawns the real `nu` binary and compares raw
stdout bytes for the `catch`, `finally`, and `catch` plus `finally`
cases, plus a failing external to prove `catch` still runs. The `try`
sits in final statement position on purpose: only the final statement's
value is printed, so that is the position where the fabricated empty
string became visible. The test fails on current main with exactly the
extra `\n` and passes with this change.

- `cargo fmt --all -- --check` clean
- `cargo clippy --workspace --all-targets -- -D warnings` clean
- `shell::` suite 255 passed, `commands::try_::` suite 41 passed
(includes the pipefail experimental tests)
- `cargo check -p nu-engine --target wasm32-unknown-unknown
--no-default-features` compiles (the new block is gated on the `os`
feature)

# After Submitting

## Release notes summary - What our users need to know

`try` with a `catch` or `finally` block no longer prints a stray blank
line after an external command's output.
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.

Byte Stream Line Endings Are Inconsistent in Try-Catch-Finally Blocks

2 participants