Pointing at the error: compiler-style diagnostics in uutils coreutils
Pointing at the Error: Compiler-Style Diagnostics in uutils coreutils
By Sylvestre Ledru | August 31, 2026
For half a century, the Coreutils suite has undergone constant evolution. Today, we are pushing the boundaries of that innovation by fundamentally reimagining how the tools communicate errors to the user.
The Problem: Vague Failures in "Mini-Languages"
While many commands have simple inputs, several others accept arguments that function as small, specialized languages. Examples include:
testexpressionschmodmodessortkeystrsets
When a parser fails to process these, a generic error message is insufficient. The user needs to know exactly which argument, or even which specific character, caused the failure.
The Inspiration
The Rust compiler, rustc, has perfected this using visual carets to pinpoint errors. By leveraging the ariadne crate, this high-quality rendering is easily accessible. This approach was first piloted in uutils awk, and since coreutils arguments are essentially small languages, the same logic applies.
How it Works
When the stderr output is directed to a terminal, a parse error is no longer just a line of text; it becomes a diagnostic report.
The report consists of:
- The arguments echoed back as a source line.
- A
^(caret) marking the exact culprit. - A "Help" section providing syntax guidance.
Visual Comparisons
1. The tr Command
GNU's original message assumes the user is already familiar with collating sequences.
Before: tr: range-endpoints of 'y-b' are in reverse collating sequence order
After:
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
╰───╯
2. The cut Command
When dealing with field selections, the syntax is strict.
Comparison Table:
| Version | Input | Error Message | Visual Aid |
|---|---|---|---|
| Old | cut -f 1,4-2,9-12 | invalid decreasing range | None |
| New | cut -f 1,4-2,9-12 | invalid decreasing range | Caret at 4-2 |
New Output:
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-
╰───╯
3. The chmod Command
Invalid operators are now easy to spot.
Example:
chmod 'g+rw?x' notes.txtError: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
╰───╯
4. The sort Command
Because sort keys are compact, a single stray character can be hard to find.
New Output:
sort: stray character in field spec: invalid field specification '2.3x'
╭─[ sort:1:11 ]
│ 1 │ sort -k2.3 x notes.txt
│ ─
│ ╰─── Help : a key is FIELD[.CHAR][OPTS][,FIELD[.CHAR][OPTS]], as in -k2.3,4nr
╰───╯
5. The env -S Command
env -S splits a command line similarly to a shell. Previously, it could only quote the fragment. Now, it handles quoted strings while maintaining caret accuracy.
env: only ${VARNAME} expansion is supported, error at: ${1FOO}
╭─[ env:1:14 ]
│ 1 │ env -S 'echo ${1 FOO}'
│ ─┬─
│ ╰─── a variable name cannot start with a digit
│
│ Help : only $NAME and ${NAME} are expanded; the other shell forms are not
╰───╯
6. The test Command
The test tool builds expressions from separate arguments. The report isolates the expression from the test command itself.
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
╰───╯
7. The Universal SIZE Parser
A single parser handles size specifications across the entire suite. This means the same diagnostic style appears in:
-
head -c -
tail -c -
truncate -s -
split -b -
shred -s -
od -N -
sort -S -
du -B/du -t -
df -B -
ls --block-size
Example (head):
head: invalid number of bytes: '1fb'
╭─[ head:1:10 ]
│ 1 │ head -c 1 fb 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
╰───╯
8. The numfmt Command
The --format flag uses a printf-style syntax allowing one conversion.
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
╰───╯
9. The csplit Command
Since csplit patterns use regular expressions, the regex engine can pinpoint the exact character that caused the crash.
csplit: '/a{2,1}/': invalid pattern
╭─[ csplit:1:20 ]
│ 1 │ csplit notes.txt /a {2,1} /
│ ──┬──
│ ╰──── inva...
For more, try it in the playground!