diff --git a/README.md b/README.md index 03873eeef3f..a573e60cd44 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,10 @@ are treated as bugs. Our key objectives include: - Matching GNU's output (stdout and error code) exactly -- Better error messages +- Better error messages: at a terminal, a parse error is shown as a + compiler-style report with a caret under the argument at fault, where GNU + prints a single line (see [error diagnostics](docs/src/extensions-errors.md)); + scripts and pipes still get the plain GNU message - Providing comprehensive internationalization support (UTF-8) - Improved performances - [Extensions](docs/src/extensions.md) when relevant (example: --progress) diff --git a/docs/src/extensions-errors.css b/docs/src/extensions-errors.css new file mode 100644 index 00000000000..2e05e08f22b --- /dev/null +++ b/docs/src/extensions-errors.css @@ -0,0 +1,42 @@ +/* Styling for the error reports shown on this page. The colors are the ones + the utilities actually emit, so the examples read here the way they do in a + terminal; they match the palette used on uutils.org. */ + +pre.diag { + margin: 1rem 0; + padding: 0.9rem 1.1rem; + border-radius: 0.6em; + background: #1e1e2e; + color: #cdd6f4; + font-size: 0.82rem; + line-height: 1.5; + overflow-x: auto; + white-space: pre; +} + +/* The prompt of the command being run. */ +pre.diag .a-p { + color: #89b4fa; +} + +/* ANSI colors as emitted by the utilities: frame, source line, gutter, + the span at fault, and the Help: label. */ +pre.diag .a-d { + color: #9399b2; +} + +pre.diag .a-s { + color: #cdd6f4; +} + +pre.diag .a-f { + color: #7f849c; +} + +pre.diag .a-e { + color: #f38ba8; +} + +pre.diag .a-h { + color: #a6e3a1; +} diff --git a/docs/src/extensions-errors.md b/docs/src/extensions-errors.md index fdfbc94e86e..f4ed061d7e9 100644 --- a/docs/src/extensions-errors.md +++ b/docs/src/extensions-errors.md @@ -1,65 +1,68 @@ - + + # Error diagnostics GNU coreutils reports every error as a single line on stderr. That line -answers *what* went wrong, but not *where*. For utilities whose arguments form -a small language of their own — a `test` expression, a `chmod` mode, a `sort` -key — the interesting question is usually which argument, or which character +answers *what* went wrong, but Not *where*. For utilities whose arguments form +a small language of their own - a `test` expression, a `chmod` mode, a `sort` +key - the interesting question is usually which argument, or which character inside an argument, broke the parse. When stderr is a terminal, uutils renders these errors as a compiler-style report instead: the command line is echoed back as a source line, and a caret points at the part that is at fault, often with a line of advice. Everywhere -else — a script, a pipe, a test harness — the plain one-line message is kept, +else - a script, a pipe, a test harness - the plain one-line message is kept, so nothing that reads stderr can tell the difference. + + ## Before and after -Each utility below shows the plain one-line message first — what uutils +Each utility below shows the plain one-line message first - what uutils prints when stderr is not a terminal, and in most cases exactly what GNU -prints — then the report rendered on a terminal. +prints - then the report rendered on a terminal, colors included. ### `test` -``` -$ test 7 -eq zap -test: invalid integer 'zap' -``` +Before: -``` -$ test 7 -eq zap -test: invalid integer 'zap' - ╭─[ test:1:7 ] - │ - 1 │ 7 -eq zap - │ ─── - │ - │ Help: -eq, -ne, -lt, -le, -gt and -ge compare integers; use =, !=, < or > to compare strings - │ -eq equal, -ne not equal, -lt less than, -le less than or equal, -gt greater than, -ge greater than or equal -───╯ -``` +
$ test 7 -eq zap
+test: invalid integer 'zap'
+ +After: + +
$ test 7 -eq zap
+test: invalid integer 'zap'
+   ╭─[ test:1:7 ]
+   │
+ 1 │ 7 -eq zap
+   │       ───
+   │
+   │ Help: -eq, -ne, -lt, -le, -gt and -ge compare integers; use =, !=, < or > to compare strings
+   │       -eq equal, -ne not equal, -lt less than, -le less than or equal, -gt greater than, -ge greater than or equal
+───╯
[Try it in the playground](https://uutils.org/playground/?cmd=test+7+-eq+zap). ### `expr` -``` -$ expr 9 + foo -expr: non-integer argument -``` +Before: -``` -$ expr 9 + foo +
$ expr 9 + foo
+expr: non-integer argument
+ +After: + +
$ expr 9 + foo
 expr: non-integer argument
-   ╭─[ expr:1:5 ]
-   │
- 1 │ 9 + foo
-   │     ───
-   │
-   │ Help: arithmetic operators need integers; use = or != to compare strings instead
-───╯
-```
+   ╭─[ expr:1:5 ]
+   │
+ 1 │ 9 + foo
+   │     ───
+   │
+   │ Help: arithmetic operators need integers; use = or != to compare strings instead
+───╯
[Try it in the playground](https://uutils.org/playground/?cmd=expr+9+%2B+foo). @@ -68,105 +71,106 @@ expr: non-integer argument The caret can point *inside* an argument, at the exact character that broke the parse: -``` -$ chmod g+rw?x notes.txt -chmod: invalid operator (expected +, -, or =, but found ?) -``` +Before: -``` -$ chmod g+rw?x notes.txt +
$ chmod 'g+rw?x' notes.txt
+chmod: invalid operator (expected +, -, or =, but found ?)
+ +After: + +
$ chmod 'g+rw?x' notes.txt
 chmod: invalid operator (expected +, -, or =, but found ?)
-   ╭─[ chmod:1:5 ]
-   │
- 1 │ g+rw?x notes.txt
-   │     ─
-   │
-   │ Help: a mode is either octal, as in 644, or clauses such as u+rwx,go-w
-───╯
-```
+   ╭─[ chmod:1:5 ]
+   │
+ 1 │ g+rw?x notes.txt
+   │     ─
+   │
+   │ Help: a mode is either octal, as in 644, or clauses such as u+rwx,go-w
+───╯
### `tr` -``` -$ tr 'qw[y-b]' x -tr: range-endpoints of 'y-b' are in reverse collating sequence order -``` +Before: -``` -$ tr 'qw[y-b]' x -tr: range-endpoints of 'y-b' are in reverse collating sequence order - ╭─[ tr:1:7 ] - │ - 1 │ tr qw[y-b] x - │ ─┬─ - │ ╰─── did you mean 'b-y'? - │ - │ Help: a range goes from the lower character to the higher one, as in a-z -───╯ -``` +
$ tr 'qw[y-b]' x
+tr: range-endpoints of 'y-b' are in reverse collating sequence order
+ +After: + +
$ tr 'qw[y-b]' x
+tr: range-endpoints of 'y-b' are in reverse collating sequence order
+   ╭─[ tr:1:7 ]
+   │
+ 1 │ tr qw[y-b] x
+   │       ─┬─
+   │        ╰─── did you mean 'b-y'?
+   │
+   │ Help: a range goes from the lower character to the higher one, as in a-z
+───╯
[Try it in the playground](https://uutils.org/playground/?cmd=tr+%27qw%5By-b%5D%27+x). ### `sort` -``` -$ sort -k2.3x notes.txt -sort: stray character in field spec: invalid field specification '2.3x' -``` +Before: -``` -$ sort -k2.3x notes.txt -sort: stray character in field spec: invalid field specification '2.3x' - ╭─[ sort:1:11 ] - │ - 1 │ sort -k2.3x notes.txt - │ ─ - │ - │ Help: a key is FIELD[.CHAR][OPTS][,FIELD[.CHAR][OPTS]], as in -k2.3,4nr -───╯ -``` +
$ sort -k2.3x notes.txt
+sort: stray character in field spec: invalid field specification '2.3x'
+ +After: + +
$ sort -k2.3x notes.txt
+sort: stray character in field spec: invalid field specification '2.3x'
+   ╭─[ sort:1:11 ]
+   │
+ 1 │ sort -k2.3x notes.txt
+   │           ─
+   │
+   │ Help: a key is FIELD[.CHAR][OPTS][,FIELD[.CHAR][OPTS]], as in -k2.3,4nr
+───╯
[Try it in the playground](https://uutils.org/playground/?cmd=sort+-k2.3x+fruits.txt). ### `numfmt` -``` -$ numfmt --format=%q 1000 -numfmt: invalid format '%q', directive must be %[0]['][-][N][.][N]f -``` +Before: -``` -$ numfmt --format=%q 1000 -numfmt: invalid format '%q', directive must be %[0]['][-][N][.][N]f - ╭─[ numfmt:1:18 ] - │ - 1 │ numfmt --format=%q 1000 - │ ─ - │ - │ Help: a format is [PREFIX]%[0]['][-][WIDTH][.PRECISION]f[SUFFIX], as in "%'-10.2f" -───╯ -``` +
$ numfmt --format=%q 1000
+numfmt: invalid format '%q', directive must be %[0]['][-][N][.][N]f
+ +After: + +
$ numfmt --format=%q 1000
+numfmt: invalid format '%q', directive must be %[0]['][-][N][.][N]f
+   ╭─[ numfmt:1:18 ]
+   │
+ 1 │ numfmt --format=%q 1000
+   │                  ┬
+   │                  ╰── f is the only conversion numfmt has; %d, %e, %g and the other C conversions are not accepted
+   │
+   │ Help: a format is [PREFIX]%[0]['][-][WIDTH][.PRECISION]f[SUFFIX], as in "%'-10.2f"
+───╯
[Try it in the playground](https://uutils.org/playground/?cmd=numfmt+--format%3D%25q+1000). ### `printf` -``` -$ printf %5.2c q -printf: %5.2c: invalid conversion specification -``` +Before: -``` -$ printf %5.2c q +
$ printf %5.2c q
+printf: %5.2c: invalid conversion specification
+ +After: + +
$ printf %5.2c q
 printf: %5.2c: invalid conversion specification
-   ╭─[ printf:1:8 ]
-   │
- 1 │ printf %5.2c q
-   │        ─────
-   │
-   │ Help: %d, %s, %x, %f and the other C conversions are accepted, plus %b and %q; a literal % is written %%
-───╯
-```
+   ╭─[ printf:1:8 ]
+   │
+ 1 │ printf %5.2c q
+   │        ─────
+   │
+   │ Help: %d, %s, %x, %f and the other C conversions are accepted, plus %b and %q; a literal % is written %%
+───╯
The same goes for a broken escape: `printf 'a\xzb'` puts the caret under the `\x` that is missing its hexadecimal digits. @@ -179,80 +183,170 @@ The same goes for a broken escape: `printf 'a\xzb'` puts the caret under the messages name an offset, which is precisely the thing a caret can show instead: -``` -$ env -S 'echo ${1FOO}' -env: only ${VARNAME} expansion is supported, error at: ${1FOO} -``` +Before: -``` -$ env -S 'echo ${1FOO}' -env: only ${VARNAME} expansion is supported, error at: ${1FOO} - ╭─[ env:1:14 ] - │ - 1 │ env -S 'echo ${1FOO}' - │ ─┬─ - │ ╰─── a variable name cannot start with a digit - │ - │ Help: only $NAME and ${NAME} are expanded; the other shell forms are not -───╯ -``` +
$ env -S 'echo ${1FOO}'
+env: only ${VARNAME} expansion is supported, error at: ${1FOO}
+ +After: -Note that the `-S` string holds spaces, so it is echoed back quoted — and the +
$ env -S 'echo ${1FOO}'
+env: only ${VARNAME} expansion is supported, error at: ${1FOO}
+   ╭─[ env:1:14 ]
+   │
+ 1 │ env -S 'echo ${1FOO}'
+   │              ─┬─
+   │               ╰─── a variable name cannot start with a digit
+   │
+   │ Help: only $NAME and ${NAME} are expanded; the other shell forms are not
+───╯
+ +Note that the `-S` string holds spaces, so it is echoed back quoted - and the caret still points inside it. ### `cut` A list of ranges is often long, and only one item in it is wrong: -``` -$ cut -f 1,4-2,9-12 notes.txt -cut: range '4-2' was invalid: high end of range less than low end -``` +Before: -``` -$ cut -f 1,4-2,9-12 notes.txt -cut: range '4-2' was invalid: high end of range less than low end - ╭─[ cut:1:10 ] - │ - 1 │ cut -f 1,4-2,9-12 notes.txt - │ ─┬─ - │ ╰─── this range ends before it starts - │ - │ Help: a list is N, N-M, N- or -M, separated by commas, as in -f1,4-6,9- -───╯ -``` +
$ cut -f 1,4-2,9-12 notes.txt
+cut: invalid decreasing range
+ +After: + +
$ cut -f 1,4-2,9-12 notes.txt
+cut: invalid decreasing range
+   ╭─[ cut:1:10 ]
+   │
+ 1 │ cut -f 1,4-2,9-12 notes.txt
+   │          ─┬─
+   │           ╰─── this range ends before it starts
+   │
+   │ Help: a list is N, N-M, N- or -M, separated by commas, as in -f1,4-6,9-
+───╯
### `head` A SIZE is a number and a unit, and the caret says which of the two was rejected: -``` -$ head -c 1fb notes.txt -head: invalid number of bytes: '1fb' -``` +Before: -``` -$ head -c 1fb notes.txt -head: invalid number of bytes: '1fb' - ╭─[ head:1:10 ] - │ - 1 │ head -c 1fb notes.txt - │ ─┬ - │ ╰── not a known unit - │ - │ Help: a size is a number and an optional unit: K, M, G and so on for 1024, KB, MB, GB for 1000 -───╯ -``` +
$ head -c 1fb notes.txt
+head: invalid number of bytes: '1fb'
+ +After: + +
$ head -c 1fb notes.txt
+head: invalid number of bytes: '1fb'
+   ╭─[ head:1:10 ]
+   │
+ 1 │ head -c 1fb notes.txt
+   │          ─┬
+   │           ╰── not a known unit
+   │
+   │ Help: a size is a number and an optional unit: K, M, G and so on for 1024, KB, MB, GB for 1000
+───╯
+ +### `dd` + +Every `dd` operand is a `KEY=VALUE` pair, and a value can be a comma-separated +list of flags, so there are three things the caret can pick out: the key, the +whole value, or one flag inside the list. A flag is underlined where it sits +in the list rather than wherever its text first turns up, and the advice names +the flags that operand accepts: + +Before: + +
$ dd conv=ucase,zap
+dd: invalid conversion: 'zap'
+ +After: + +
$ dd conv=ucase,zap
+dd: invalid conversion: 'zap'
+   ╭─[ dd:1:15 ]
+   │
+ 1 │ dd conv=ucase,zap
+   │               ─┬─
+   │                ╰─── not a known conversion
+   │
+   │ Help: conv= is one of ascii, ebcdic, ibm, lcase, ucase, block, unblock, swab, sync, noerror, sparse, excl, nocreat, notrunc, fdatasync or fsync
+───╯
+ +`iflag=` and `oflag=` are reported apart, so an output flag is no longer +blamed on the input, and each lists its own flags: + +Before: + +
$ dd oflag=zap
+dd: invalid output flag: 'zap'
+ +After: + +
$ dd oflag=zap
+dd: invalid output flag: 'zap'
+   ╭─[ dd:1:10 ]
+   │
+ 1 │ dd oflag=zap
+   │          ─┬─
+   │           ╰─── not a known output flag
+   │
+   │ Help: oflag= is one of direct, directory, dsync, sync, nocache, nonblock, noatime, noctty, nofollow, append or seek_bytes
+───╯
+ +An unknown key is underlined without its value, since the value is not what +was rejected: + +Before: + +
$ dd zap=1
+dd: unrecognized operand 'zap=1'
+ +After: + +
$ dd zap=1
+dd: unrecognized operand 'zap=1'
+   ╭─[ dd:1:4 ]
+   │
+ 1 │ dd zap=1
+   │    ───
+   │
+   │ Help: an operand is KEY=VALUE, as in if=file bs=4k count=10
+───╯
+ +A number that does not fit is rejected rather than quietly clamped, and the +caret covers the whole value: + +Before: + +
$ dd count=99999999999999999999999
+dd: invalid number: '99999999999999999999999': Value too large for defined data type
+ +After: + +
$ dd count=99999999999999999999999
+dd: invalid number: '99999999999999999999999': Value too large for defined data type
+   ╭─[ dd:1:10 ]
+   │
+ 1 │ dd count=99999999999999999999999
+   │          ───────────────────────
+   │
+   │ Help: a number may be followed by a multiplier: c, w, b, then K, M, G and so on for 1024, kB, MB, GB for 1000
+───╯
+ +[Try it in the playground](https://uutils.org/playground/?cmd=dd+conv%3Ducase%2Czap). ## Compatibility This is strictly an interactive nicety; nothing that reads our output can tell the difference: -- Reports are only rendered when **stderr is a terminal**. In a script, a - pipe, or a test harness, the utility keeps printing its plain one-line - message, so existing scripts that match on stderr keep working. +- Reports are only rendered when **stderr is a terminal**, unless `UUTILS_DIAG` + says otherwise (see below). In a script, a pipe, or a test harness, the + utility keeps printing its plain one-line message, so existing scripts that + match on stderr keep working. - Exit codes are unchanged. - Colors follow the usual conventions: they are used only on a terminal, and [`NO_COLOR`](https://no-color.org/) disables them. @@ -264,6 +358,44 @@ the difference: and its `ariadne` dependency, and every utility keeps its plain one-line messages. +## Turning it on and off + +The default keys off stderr being a terminal, and nothing else - which is +usually what you want, but not always. `UUTILS_DIAG` overrides it: + +| Value | Effect | +| ----- | ------ | +| `always` | Draw the report even when stderr is a file or a pipe. | +| `never` | Keep the plain one-line message even at a terminal. | +| `auto`, unset, anything else | Decide from stderr, as above. | + +An unrecognized value is deliberately not an error - this is the kind of +variable that gets exported from a shell profile once and forgotten, and no +spelling of it should be able to make a utility fail. + +`always` is the one to reach for when the error has to leave the terminal +it happened in - a CI log, or a report to paste into a bug: + +``` +$ UUTILS_DIAG=always sort -k2.3x notes.txt 2> parse.log +``` + +Colors are a separate question, and one the terminal still answers: a report +forced into a file is written without them, so nothing has to strip escape +sequences back out. [`NO_COLOR`](https://no-color.org/) is the middle setting +at a terminal - the report is still drawn, just in plain text. + +Without the variable, both directions are still one command away. To get the +plain line while sitting at a terminal, send stderr somewhere that is not one: + +``` +$ sort -k2.3x notes.txt 2>&1 | cat +sort: stray character in field spec: invalid field specification '2.3x' +``` + +And to get a report out of a command that has to run under a real terminal, +give it a pty with `script -qec "..." /dev/null`, or `unbuffer` from expect. + ## Supported utilities | Utility | What the caret points at | Try it | @@ -303,15 +435,15 @@ The rendering lives in `uucore::features::diagnostics` and is built on way the shell would) and remembers the byte range of each argument. 2. Maps its own error type to a position, and optionally a label and a line of advice, in a small per-utility `diagnostics.rs` module. A label is only - used when it adds something the message does not say — an expectation, or - a fix such as tr's `did you mean 'b-y'?` — never to restate it; with no + used when it adds something the message does not say - an expectation, or + a fix such as tr's `did you mean 'b-y'?` - never to restate it; with no label the span is drawn as a bare underline. Everything user-facing is passed in already localized. -3. Locates the argument the operand came from — with +3. Locates the argument the operand came from - with `Snapshot::index_of_value` for an option's value in whatever spelling it was given (`-k 2.3x`, `-k2.3x`, `-rk2.3x`, `--key 2.3x`, `--key=2.3x`), `Snapshot::index_of_positional` for a positional operand, or an index the - utility tracked itself — and calls `Snapshot::render` to point at the + utility tracked itself - and calls `Snapshot::render` to point at the whole argument, or `Snapshot::render_inside_at` to point at a byte range *inside* the operand it carries. Because the argument is named rather than searched for, a file, another option, or the program name that happens to @@ -322,7 +454,7 @@ The rendering lives in `uucore::features::diagnostics` and is built on An argument holding a space is echoed back quoted, and the caret still points inside it: the quotes only wrap the operand, so its bytes are found where they were printed and offsets count from there. An argument that could not be -printed as-is — a non-UTF-8 one, or one whose quoting had to be broken up — is +printed as-is - a non-UTF-8 one, or one whose quoting had to be broken up - is underlined as a whole instead, since no offset into it would line up with what the reader sees. @@ -341,13 +473,13 @@ repeated per utility. Three parsers work this way: and where it sat. - **Sizes** (`uucore::parser::parse_size`), for `head`, `tail`, `truncate`, `split`, `shred`, `stdbuf`, `sort` and `od` today, and available to the other callers of the parser. - `ParseSizeError::span` works out from the operand which of its two parts — - the number or the unit — was rejected, so the error type keeps the shape its + `ParseSizeError::span` works out from the operand which of its two parts - + the number or the unit - was rejected, so the error type keeps the shape its callers build by hand. Taking modes as the worked example: the parser reports errors as a structured `ModeError` carrying the byte range at fault, and its rendering places the -caret for every utility that takes a mode — `ModeError::render_option_value` +caret for every utility that takes a mode - `ModeError::render_option_value` finds the mode as the value of `-m`/`--mode` for `mkdir`, `mkfifo`, `mknod` and `install`, while `chmod`, which accepts modes clap cannot see (such as `chmod -w -r file`), tracks where each mode operand sat and passes the index diff --git a/src/uucore/src/lib/features/diagnostics.rs b/src/uucore/src/lib/features/diagnostics.rs index 6ae44e15cd4..e92b9b1dfd5 100644 --- a/src/uucore/src/lib/features/diagnostics.rs +++ b/src/uucore/src/lib/features/diagnostics.rs @@ -19,7 +19,8 @@ //! //! Rendering only happens when stderr is a terminal, so anything reading our //! output — a script, a pipe, a test suite — still sees the plain one-line -//! message it always did. +//! message it always did. `UUTILS_DIAG=always` or `never` overrides that +//! check. //! //! ```text //! tr: range-endpoints of 'y-b' are in reverse collating sequence order @@ -41,12 +42,45 @@ use std::ffi::{OsStr, OsString}; use std::fmt::Write as _; use std::io::IsTerminal; use std::ops::Range; +use std::sync::OnceLock; use ariadne::{CharSet, Color, Config, IndexType, Label, Report, ReportKind, Source}; use crate::display::Quotable; use crate::translate; +/// The variable that overrides the terminal check. +const MODE_VAR: &str = "UUTILS_DIAG"; + +/// When to render an error against its argument list. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Mode { + /// Let stderr decide. + Auto, + Always, + Never, +} + +impl Mode { + /// What [`MODE_VAR`] asks for. + /// + /// Anything but `always` and `never` is [`Mode::Auto`] rather than an + /// error — this gets exported from shell profiles, and no spelling of it + /// should make a utility fail. + fn from_env(value: Option<&OsStr>) -> Self { + let Some(value) = value.and_then(OsStr::to_str) else { + return Self::Auto; + }; + if value.eq_ignore_ascii_case("always") { + Self::Always + } else if value.eq_ignore_ascii_case("never") { + Self::Never + } else { + Self::Auto + } + } +} + /// Whether errors should be rendered against their argument list. /// /// Callers should check this before doing any work that only a diagnostic needs, @@ -54,11 +88,18 @@ use crate::translate; /// /// # Returns /// -/// `true` when stderr is a terminal — a person is watching, and gets the rich -/// form. `false` in a script or a pipe, where whatever reads stderr gets the -/// plain message it can grep for. +/// What `UUTILS_DIAG` asks for, when it asks. Otherwise `true` when stderr is +/// a terminal — a person is watching, and gets the rich form — and `false` in +/// a script or a pipe, where whatever reads stderr gets the plain message it +/// can grep for. pub fn enabled() -> bool { - std::io::stderr().is_terminal() + // Read once: this runs before the argument capture of every caret. + static MODE: OnceLock = OnceLock::new(); + match MODE.get_or_init(|| Mode::from_env(env::var_os(MODE_VAR).as_deref())) { + Mode::Always => true, + Mode::Never => false, + Mode::Auto => std::io::stderr().is_terminal(), + } } /// Keep the arguments a diagnostic would point at, as they were typed. @@ -730,6 +771,23 @@ mod tests { Snapshot::new(args) } + /// Everything but `always` and `never` leaves the terminal check in + /// charge, rather than failing. + #[test] + fn the_mode_variable_decides_only_when_it_is_spelled_out() { + let mode = |value| Mode::from_env(Some(OsStr::new(value))); + + assert_eq!(mode("always"), Mode::Always); + assert_eq!(mode("ALWAYS"), Mode::Always); + assert_eq!(mode("never"), Mode::Never); + assert_eq!(mode("Never"), Mode::Never); + + for undecided in ["auto", "", "yes", "1", "sometimes"] { + assert_eq!(mode(undecided), Mode::Auto, "{undecided:?}"); + } + assert_eq!(Mode::from_env(None), Mode::Auto); + } + /// `localize_help` and `is_label_row` both address ariadne's output by row /// number, which only they know is right. Render a report whose shape is /// known and check it, so that an ariadne that adds or drops a row fails diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 07df2388028..63e422e0878 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -2444,4 +2444,50 @@ dd: unrecognized operand 'bsx=1' .stderr_contains("--help' for more information.") .stderr_does_not_contain("╭─"); } + + // The three below cover the shared switch in `uucore::diagnostics`. + + #[test] + fn test_report_is_drawn_into_a_pipe_when_asked_for() { + let result = new_ucmd!() + .env("UUTILS_DIAG", "always") + .args(&["bsx=1"]) + .pipe_in("") + .fails_with_code(1); + let stderr = result.stderr_str(); + + // No terminal anywhere, and the report is drawn all the same. + assert!(stderr.contains("dd:1:4"), "{stderr}"); + assert!(stderr.contains("1 \u{2502} dd bsx=1"), "{stderr}"); + } + + #[cfg(unix)] + #[test] + fn test_plain_message_at_a_terminal_when_asked_for() { + let result = new_ucmd!() + .env("UUTILS_DIAG", "never") + .terminal_sim_stderr() + .args(&["bsx=1"]) + .pipe_in("") + .fails_with_code(1); + let stderr = result.stderr_as_displayed(); + + assert!( + stderr.contains("dd: unrecognized operand 'bsx=1'"), + "{stderr}" + ); + assert!(!stderr.contains('\u{256d}'), "{stderr}"); + } + + #[test] + fn test_unknown_mode_leaves_the_terminal_in_charge() { + // A value nobody meant behaves as if the variable were unset. + new_ucmd!() + .env("UUTILS_DIAG", "sometimes") + .args(&["bsx=1"]) + .pipe_in("") + .fails_with_code(1) + .stderr_contains("dd: unrecognized operand 'bsx=1'\n") + .stderr_does_not_contain("╭─"); + } } diff --git a/util/run-gnu-test.sh b/util/run-gnu-test.sh index b562002dbfc..0480442e116 100755 --- a/util/run-gnu-test.sh +++ b/util/run-gnu-test.sh @@ -90,6 +90,10 @@ fi export RUST_BACKTRACE=1 +# The GNU tests compare stderr byte for byte: never let a dev shell force a +# caret report into their pipes. +unset UUTILS_DIAG + # Determine if we have SELinux tests has_selinux_tests=false if test $# -ge 1; then