指出错误:uutils coreutils 中的编译器风格诊断信息
Pointing at the error: compiler-style diagnostics in uutils coreutils

原始链接: https://uutils.org/blog/2026-08-error-diagnostics/

从 0.11.0 版本开始,**uutils coreutils** 引入了编译器风格的诊断错误报告。过去,Unix 工具仅提供单行错误信息,指出了问题但并未明确具体位置。许多 coreutils 命令使用的参数类似于“微型语言”(例如排序键、chmod 模式或正则表达式),这使得定位错误变得非常困难。 受 `rustc` 和 `ariadne` crate 的启发,新系统使用基于终端的插入符来回显错误参数,高亮显示导致失败的具体字符或范围,并提供有用的建议。 为确保与现有脚本和自动化工具的兼容性,这些图形化报告仅在输出到交互式终端时才会出现;管道和日志将继续接收标准的单行错误消息。用户可以通过 `UUTILS_DIAG=always` 环境变量强制开启此输出。该功能支持 `NO_COLOR` 规范并已实现完全本地化。 目前,已有 28 个实用工具支持这些诊断功能,并计划将此实现扩展到 `findutils` 和 `sed` 等其他项目。这一改进将错误处理从“猜谜游戏”转变为清晰、可操作的调试体验,同时保持了对传统 Unix 工具预期的严格遵循。

所提供的文本探讨了高质量编译器诊断信息的重要性,并以 `uutils coreutils` 作为案例研究。 作者认为,由于编译器交互中很大一部分会产生错误,因此这些错误信息的质量至关重要。虽然追踪代码跨度(spans)并进行额外分析以查明故障的确切原因需要耗费额外的计算资源,但这种投入是非常值得的。精心设计的诊断信息能够提供清晰、可操作的反馈,使开发人员能够迅速识别并解决错误。归根结底,将晦涩难懂的错误信息转变为精确且信息丰富的提示,能够减少开发人员的挫败感,并简化调试流程。
相关文章

原文

Pointing at the error: compiler-style diagnostics in uutils coreutils

Aug 31, 2026

Sylvestre Ledru

For 50 years, Coreutils have never stopped evolving. Now, we're pushing that innovation further by rethinking how they report errors.

Unix tools report errors as a single line on stderr. That line says what went wrong, not where. For most commands there is nowhere else to point anyway, but a few take arguments that are small languages: a test expression, a chmod mode, a sort key, a tr set. When one of those fails to parse, what you actually want to know is which argument, or which character of it, the parser tripped over.

rustc has been answering that question with a caret for years, and ariadne puts the same rendering one dependency away. The idea of bringing it to a command-line tool comes from uutils awk, which already reports errors in an awk program that way; coreutils arguments are smaller languages, but they parse just the same. Starting with 0.11.0, coreutils uses it. When stderr is a terminal, a parse error is printed as a report: the arguments are echoed back as a source line, a caret marks the culprit, and a help line explains the syntax when we have something useful to say about it.

What it looks like

Start with tr, whose GNU message assumes you already know what a collating sequence is.

Before:

tr

$ tr 'qw[y-b]' x

tr: range-endpoints of 'y-b' are in reverse collating sequence order

After:

tr

$ 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.

A cut list can be long, with a single bad item in it.

Before:

cut

$ cut -f 1,4-2,9-12 notes.txt

cut: invalid decreasing range
Try 'cut --help' for more information.

After:

cut

$ 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-
───╯
Try 'cut --help' for more information.

Try it in the playground.

The caret does not have to cover a whole argument. It can land on one character.

Before:

chmod

$ chmod 'g+rw?x' notes.txt

chmod: invalid operator (expected +, -, or =, but found ?)

After:

chmod

$ 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
───╯

sort keys are short enough that a stray character is easy to miss.

Before:

sort

$ sort -k2.3x notes.txt

sort: stray character in field spec: invalid field specification '2.3x'

After:

sort

$ 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.

env -S takes a whole command line and splits it the way a shell would. The old message could only quote the offending fragment back at you. Note that the string contains spaces, so it is echoed back quoted, and the caret still lands inside the quotes.

Before:

env

$ env -S 'echo ${1FOO}'

env: only ${VARNAME} expansion is supported, error at: ${1FOO}

After:

env

$ 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
───╯

test builds its expression out of separate arguments. The report echoes the expression on its own, without the test in front, and marks the argument that broke it.

Before:

test

$ test 7 -eq zap

test: invalid integer 'zap'

After:

test

$ 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.

A SIZE is a number followed by a unit. The report says which half was rejected.

Before:

head

$ head -c 1fb notes.txt

head: invalid number of bytes: '1fb'

After:

head

$ 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
───╯

Try it in the playground.

One parser handles every SIZE in the suite, so the same report shows up for tail -c, truncate -s, split -b, shred -s, od -N, sort -S, the block sizes of du -B, df -B and ls --block-size, and the threshold of du -t.

numfmt --format is a printf-style format that allows exactly one conversion. The old message just restated the rule. The annotation names the conversion you actually wrote.

Before:

numfmt

$ numfmt --format=%q 1000

numfmt: invalid format '%q', directive must be %[0]['][-][N][.][N]f

After:

numfmt

$ 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.

csplit patterns contain regexes, and the regex engine already knows which character it choked on. We were simply throwing that position away.

Before:

csplit

$ csplit notes.txt '/a{2,1}/'

csplit: '/a{2,1}/': invalid pattern

After:

csplit

$ csplit notes.txt '/a{2,1}/'

csplit: '/a{2,1}/': invalid pattern
   ╭─[ csplit:1:20 ]
   
 1 │ csplit notes.txt /a{2,1}/
                     ──┬──
                       ╰──── invalid repetition count range, the start must be <= the end
 
  Help: a pattern is a line number N, /REGEXP/[OFFSET] or %REGEXP%[OFFSET], each optionally followed by {N} or {*}
───╯

Try it in the playground.

That label comes straight from the regex engine and is not translated, since it is the only place the wording exists.

Where it applies

28 utilities use it in 0.11.0. The linked examples run in the playground; the others are for utilities the WebAssembly build does not ship, so try those locally:

UtilityWhat the caret points atTry it
testthe argument that made the expression failtest 7 -eq zap
exprthe argument that made the expression failexpr 9 + foo
chmodthe failing clause (or character) of an invalid symbolic or octal modechmod 'g+rw?x' fruits.txt
mkdirthe failing part of the mode given to -m/--modemkdir -m u+q mydir
mkfifothe failing part of the mode given to -m/--modemkfifo -m u+q mypipe
mknodthe failing part of the mode given to -m/--modemknod -m u+q mydev c 1 3
installthe failing part of the mode given to -m/--modeinstall -m u+q fruits.txt dest
trthe part of a set that is at fault (bad class, backwards range, bad repeat count, …)tr 'qw[y-b]' x
sortthe failing part of a -k/--key or field specification, or of the SIZE given to -Ssort -k2.3x fruits.txt
numfmtthe failing part of a --format or --field specification, the value given to --from, --to, --from-unit, --to-unit, --padding or --header, or the input number itselfnumfmt --format=%q 1000
printfthe failing conversion or escape in the format stringprintf %5.2c q
seqthe failing conversion in the format given to -f/--formatseq -f %5.2c 1 3
statthe failing directive of a -c/--format or --printf formatstat -c %d%.3 fruits.txt
envthe failing part of a -S/--split-string stringenv -S 'echo ${1FOO}'
ddthe failing key, value or flag of a KEY=VALUE operanddd conv=ucase,zap
jointhe failing field of the output format given to -ojoin -o 1.2,2.x fruits.txt fruits.txt
cutthe failing range in the list given to -b, -c, -f or -Fcut -f 1,4-2 fruits.txt
csplitthe failing pattern operand, the character of its regex that broke, or the format given to -b/-ncsplit fruits.txt '/a(b/'
splitthe failing part of the SIZE given to -b, -C or -lsplit -b 7zq fruits.txt
shredthe failing part of the SIZE given to -s/--sizeshred -s 4vv fruits.txt
headthe failing part of the SIZE given to -c or -nhead -c 1fb fruits.txt
tailthe failing part of the SIZE given to -c or -ntail -c 1fb fruits.txt
truncatethe failing part of the SIZE given to -s/--sizetruncate -s 10fb fruits.txt
odthe failing part of the SIZE given to -j, -N, -S or -wod -N 3zz fruits.txt
duthe failing part of the SIZE given to -B/--block-size or -t/--thresholddu -B 1fb
dfthe failing part of the SIZE given to -B/--block-sizedf -B 1fb
lsthe failing part of the SIZE given to --block-size (also dir and vdir)ls --block-size=1fb
stdbufthe failing part of the buffering mode given to -i, -o or -estdbuf -o 6pq head

Compatibility first

Being a drop-in replacement for GNU coreutils comes first, so this is strictly an interactive nicety:

  • Reports are only rendered when stderr is a terminal. In a script, a pipe or a test suite, each utility keeps printing exactly the plain one-line message shown as "Before" in the examples above, so anything that greps stderr keeps working.
  • Exit codes are unchanged.
  • Colors are only used on a terminal and respect NO_COLOR.
  • Like the rest of uutils, the messages, labels and help lines are localized; translations are managed on Weblate.
  • It can be compiled out to save a little space: the rendering sits behind the feat_diagnostics cargo feature (on by default), and building without it drops the ariadne dependency while every utility keeps its plain messages.

Turning it on and off

By default the rendering keys off stderr being a terminal and nothing else, which is usually but not always what you want. UUTILS_DIAG overrides it: always draws the report even into a file or a pipe, never keeps the plain line even at a terminal, and auto, or an unset variable, decides from stderr as before. An unrecognized value is deliberately not an error. This is the kind of variable people export from a shell profile once and forget about, and a typo in it should not be able to make a utility fail.

There is no command-line flag to go with it. The utilities that would need one most cannot have it: in test, printf and expr, a new option would be either illegal or ambiguous with the operands themselves.

To get a report out of a script or a CI log, to paste into a bug report for instance:

$ UUTILS_DIAG=always sort -k2.3x notes.txt 2> parse.log
$ cat parse.log
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
───╯

Colors are decided separately, and still by the terminal: a report forced into a file is written without them, so there are no escape sequences to strip back out. NO_COLOR sits in between at a terminal, where the report is still drawn, just in plain text.

The two tricks people used before the variable existed still work. Sending stderr somewhere that is not a terminal gets the plain line:

$ sort -k2.3x notes.txt 2>&1 | cat
sort: stray character in field spec: invalid field specification '2.3x'

And giving a command a pty (script -qec "sort -k2.3x notes.txt" /dev/null, or unbuffer from expect) gets the report back, which is handy when the command has to run under a terminal for other reasons.

What comes next

coreutils is where this starts, not where it stops. The rendering lives in uucore::diagnostics, which the other uutils projects already depend on, so picking it up is mostly a matter of handing the parser's error a span. findutils and sed are being wired up now; a find expression and a sed script are exactly the kind of small languages a caret helps with, and grep, awk and the rest have the same regexes and format strings to point at.

If a utility you use still prints an unhelpful one-liner, patches are welcome!

联系我们 contact @ memedata.com