From b692f9f9318f078356f0749a7f98cdc768778699 Mon Sep 17 00:00:00 2001 From: mj006648 Date: Sat, 23 May 2026 11:19:48 +0000 Subject: [PATCH 1/2] Add --stdout / -S option to force stream-backed terminal Closes #10865. Follow-up to #12425 (which added --plain / -P as a discoverability alias for --non-ansi). That change made the ANSI-free output mode easier to find but did not address the underlying redirection bug: jline's default TerminalBuilder opens the controlling PTY (/dev/tty on POSIX) when a TTY is detected, so 'nessie-cli -c "..." > file' still wrote to the terminal instead of the file. This change adds a new --stdout / -S option that builds the jline Terminal with .system(false).streams(System.in, System.out).type("dumb"). The stream-backed Terminal honours shell redirection (>) and pipes (|) in non-interactive runs (-c, -s) and emits no ANSI control sequences, so the redirected file / piped consumer stays clean. The option is intentionally separate from --plain: --plain / --non-ansi : ANSI control on/off (existing behaviour) --stdout : stream routing (new, implies plain) This matches the orthogonal concerns called out in the #12425 review discussion. Signed-off-by: mj006648 --- .../nessie/cli/cli/NessieCliImpl.java | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java index 912c16ae6d..fcf6c83b50 100644 --- a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java @@ -92,6 +92,7 @@ public class NessieCliImpl extends BaseNessieCli implements Callable { public static final String OPTION_CONTINUE_ON_ERROR = "--continue-on-error"; public static final String OPTION_NON_ANSI = "--non-ansi"; public static final String OPTION_PLAIN = "--plain"; + public static final String OPTION_STDOUT = "--stdout"; public static final String HISTORY_FILE_DEFAULT = "~/.nessie/nessie-cli.history"; public static final AttributedStyle STYLE_ERROR_HIGHLIGHT = STYLE_ERROR.italic().bold(); @@ -124,6 +125,17 @@ public class NessieCliImpl extends BaseNessieCli implements Callable { defaultValue = "false") private boolean dumbTerminal; + @Option( + names = {"-S", OPTION_STDOUT}, + description = { + "Use standard System.in/System.out streams for the terminal instead of opening", + "the controlling PTY. This makes shell redirection (>) and pipes (|) work even", + "in environments where jline can still detect a TTY (e.g. interactive shells).", + "Implies " + OPTION_PLAIN + " (no ANSI control sequences are emitted)." + }, + defaultValue = "false") + private boolean stdoutTerminal; + @Option( names = {"-q", OPTION_QUIET}, description = {"Quiet option - omit the welcome and exit output."}) @@ -184,12 +196,23 @@ public void connected(NessieApiV2 nessieApi, RESTCatalog icebergClient) { @Override public Integer call() throws Exception { - Terminal terminal = - TerminalBuilder.builder() - .jansi(!dumbTerminal) - .dumb(dumbTerminal) - .provider(dumbTerminal ? TerminalBuilder.PROP_PROVIDER_DUMB : null) - .build(); + // --stdout implies --plain (no ANSI control sequences make sense on a redirected stream). + boolean plain = dumbTerminal || stdoutTerminal; + + TerminalBuilder builder = TerminalBuilder.builder().jansi(!plain); + if (stdoutTerminal) { + // Force a stream-backed terminal that honours shell redirection (>) and pipes (|). + // Background: jline's default builder opens the controlling PTY (/dev/tty on POSIX) when + // a TTY is detected, which bypasses stdout redirection. Pointing system(false).streams(...) + // at System.in/System.out produces a Terminal whose I/O follows the redirected streams. + // Setting type="dumb" suppresses ANSI control sequences so the file/pipe stays clean; + // we deliberately do NOT pass dumb(true) here because that triggers a TTY lookup which + // fails with IllegalStateException when stdout is a regular file or pipe. + builder.system(false).streams(System.in, System.out).type("dumb"); + } else { + builder.dumb(plain).provider(plain ? TerminalBuilder.PROP_PROVIDER_DUMB : null); + } + Terminal terminal = builder.build(); setTerminal(terminal); @@ -203,7 +226,7 @@ public Integer call() throws Exception { PrintWriter writer = writer(); try { if (!quiet) { - writer.print(readResource(dumbTerminal ? "banner-plain.txt" : "banner.txt")); + writer.print(readResource(plain ? "banner-plain.txt" : "banner.txt")); writer.printf("v%s%n%n", NessieVersion.NESSIE_VERSION); writer.print(readResource("welcome.txt")); From e38ddd3042ab30bfdec8c15cad20acc586c5f1cd Mon Sep 17 00:00:00 2001 From: mj006648 Date: Tue, 26 May 2026 03:26:00 +0000 Subject: [PATCH 2/2] Address review: simplify --stdout description wording --- .../java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java index fcf6c83b50..6538e7e12c 100644 --- a/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java +++ b/cli/cli/src/main/java/org/projectnessie/nessie/cli/cli/NessieCliImpl.java @@ -128,7 +128,7 @@ public class NessieCliImpl extends BaseNessieCli implements Callable { @Option( names = {"-S", OPTION_STDOUT}, description = { - "Use standard System.in/System.out streams for the terminal instead of opening", + "Use standard in/out streams for communicating with the operator instead of opening", "the controlling PTY. This makes shell redirection (>) and pipes (|) work even", "in environments where jline can still detect a TTY (e.g. interactive shells).", "Implies " + OPTION_PLAIN + " (no ANSI control sequences are emitted)."