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

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

指向错误:uutils coreutils 中的编译器风格诊断

Aug 31, 2026 | Sylvestre Ledru 2026年8月31日 | Sylvestre Ledru

For 50 years, Coreutils have never stopped evolving. Now, we’re pushing that innovation further by rethinking how they report errors. 50年来,Coreutils 一直在不断演进。现在,我们通过重新思考错误报告的方式,将这种创新推向了新的高度。

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. Unix 工具通常在标准错误(stderr)中以单行形式报告错误。这一行只说明了“出了什么问题”,而不是“在哪里出的问题”。对于大多数命令来说,确实没有其他地方可以指向,但少数命令接受的参数实际上是小型语言:例如 test 表达式、chmod 模式、sort 键或 tr 集合。当其中一个解析失败时,你真正想知道的是解析器在哪个参数或哪个字符上卡住了。

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. 多年来,rustc 一直通过插入符号(caret)来回答这个问题,而 ariadne 库只需添加一个依赖项即可实现相同的渲染效果。将其引入命令行工具的想法源于 uutils awk,它已经以这种方式报告 awk 程序中的错误;coreutils 的参数虽然是更小的语言,但它们的解析逻辑是一样的。从 0.11.0 版本开始,coreutils 也采用了这种方式。

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. 当 stderr 是终端时,解析错误会以报告形式打印:参数会作为源代码行回显,插入符号标记出问题所在,如果能提供有用的建议,还会有一行帮助信息来解释语法。


What it looks like

效果展示

Start with tr, whose GNU message assumes you already know what a collating sequence is. 以 tr 为例,其 GNU 原版消息假设你已经知道什么是“校对顺序”(collating sequence)。

Before:

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

A cut list can be long, with a single bad item in it. cut 的列表可能很长,其中包含一个错误的项。

Before:

$ cut -f 1,4-2,9-12 notes.txt
cut: invalid decreasing range
Try 'cut --help' for more information.

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

The caret does not have to cover a whole argument. It can land on one character. 插入符号不必覆盖整个参数,它可以精确指向单个字符。

Before:

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

sort keys are short enough that a stray character is easy to miss. sort 的键通常很短,因此很容易忽略多余的字符。

Before:

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

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. env -S 接收整个命令行并像 shell 那样对其进行分割。旧的错误消息只能将出错的片段原样引用给你。

Before:

$ env -S 'echo ${1FOO}'
env: only ${VARNAME} expansion is supported, error at: ${1FOO}

After:

$ 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. test 通过独立的参数构建表达式。报告会单独回显该表达式(不带前面的 test),并标记出导致错误的参数。

Before:

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

A SIZE is a number followed by a unit. The report says which half was rejected. SIZE 是一个数字后跟一个单位。报告会指出哪一部分被拒绝了。

Before:

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

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. 套件中的所有 SIZE 都由同一个解析器处理,因此同样的报告也会出现在 tail -ctruncate -ssplit -bshred -sod -Nsort -Sdu -Bdf -Bls --block-size 的块大小以及 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. numfmt --format 是一种 printf 风格的格式,仅允许进行一次转换。旧的错误消息只是重申了规则,而现在的注释会指出你实际编写的转换格式。

Before:

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

csplit patterns contain regexes, and the regex engine already knows which character it choked on. We were simply throwing that position away. csplit 的模式包含正则表达式,而正则表达式引擎本身就知道它在哪个字符上卡住了。我们之前只是简单地丢弃了该位置信息。

Before:

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

After:

$ 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], ea