First: which implementation is each command?
Ubuntu Rust Coreutils: Check and Switch Back lists which commands are Rust and which are still GNU on your box, and has the apt line that switches the whole set back.
A backup job that checks if [[ $rc -eq 137 ]] to tell a hard kill from a soft timeout reports "timed out" for every kill after the upgrade, and nothing errors. That is the shape of most Rust coreutils breakage on Ubuntu 26.04: the command succeeds or fails as before, and one number or one line of text is different.
What Is Actually Different?
Every row below was run on 2026-10-10 in an ubuntu:26.04 container on this machine: Ubuntu 26.04.1 LTS, uutils coreutils 0.10.0 (rust-coreutils 0.10.0-1ubuntu2~26.04.1) against GNU coreutils 9.7 (gnu-coreutils 9.7-3ubuntu2.1, which 26.04 keeps installed as gnuls, gnusort and so on). Each was run through the script on this page, so both sides used the plain command names.
| Command | GNU 9.7 | uutils 0.10.0 | What breaks |
|---|---|---|---|
timeout -s KILL 2 sleep 10; echo $? | 137 | 124 | checks that tell a kill from a timeout |
mkdir backup (exists) | cannot create directory 'backup': File exists | backup: File exists | grep on stderr |
ln -s a latest (exists) | ... File exists | ... Already exists | grep for File exists |
chmod 999 f | invalid mode: '999' + a Try line | invalid digit found in string | grep on stderr |
wc -m on café with no locale set | 6 (bytes) | 5 (characters) | byte counts in C-locale jobs |
numfmt --from=iec 12Q | error, exit 2 | prints a 32-digit number, exit 0 | input validation |
printf 'aGk=!!' | base64 -d | prints hi, then error, exit 1 | prints nothing, error, exit 1 | partial-output recovery |
printf '%q\n' 'a b' | 'a b' | a\ b | string compares on quoted output |
pr -t -2 file | columns down then across, tabs | columns across, padded to the page | report formatting |
tail -n1 f >> f on a file over 4 KB | stops after one line | never stops | the disk |
What matched exactly, in the same container: sort with -t/-k/-n/-h/-V/-M/-g, uniq, cut (including multibyte), wc -l, head -n -1, tail -n +2, tr, seq, printf number and %b formats, stat -c, du -sh and --apparent-size, df -P, date with -d, +%s, -R and --iso-8601, touch -t, mktemp, install -m, split, join, comm, od, sha256sum -c, cp -a, mv and rm. Every other failure tested exited with the same code on both, even where the message differed.
Two problems reported against earlier uutils releases did not reproduce in 0.10.0: du -smc /var/log /var counting the subdirectory twice (reported on 25.10) and ls -v not sorting by version (uutils issue 14859, closed). A Unix & Linux question about sort -n -k1 -k2 -k3 -t. blamed uutils, but GNU 9.7 gave the same order: -k2 without an end field means "from field 2 to the end of the line", so -k1,1 -k2,2 -k3,3 or sort -V is the fix on both.
The tail Bug That Fills a Disk
Appending a file's last line to itself is an odd thing to do on purpose, and a common thing to do by accident in a log-rotation or "mark the end" script. The uutils tail in 26.04.1 reads the lines it is writing:
Without the timeout 5, that loop runs until the filesystem is full. It is uutils issue 15057, open at the time of writing. Write to a temporary file and mv it into place instead; that pattern is correct on both implementations.
The Script
Save as coreutils-diff.sh on an Ubuntu 26.04 box or in an ubuntu:26.04 container. Give it one quoted command line. It runs that line twice, once with GNU coreutils first in PATH and once with uutils, each in a fresh copy of the current directory, and compares the exit status, stdout, stderr and the files the command left behind.
Prerequisites
Ubuntu 26.04 (or any system with uutils in one directory and GNU coreutils available under a prefix; set UU_DIR and GNU_PREFIX to match), bash 4 or later, and diff. The current directory must be under 50 MB, because it is copied twice. No root.
How Does Each Check Work?
- Two bin directories of symlinks. Each holds a link per command (
ls,sort, ...) to one implementation. Prepending one toPATHmakes the command line, and every script it calls by plain name, use that implementation. Because the links have the plain names, both sides printls:in errors, so a difference in stderr is a real one, not agnuls:prefix. Commands called by absolute path (/usr/bin/sort) bypassPATHand are not switched. - A fresh copy per run.
cp -a .into a temp directory meansrm,mvor a log append in the first run cannot change what the second run sees, and the copies can then be compared withdiff -r. - Four comparisons. Exit status, stdout, stderr and the directory tree. The tree catches commands that write their result to a file instead of stdout.
diff ... || true.diffexits 1 when files differ, which is the case being reported, so it is kept from trippingset -eandpipefail.
What Does the Script Print?
Lines from real scripts, run in a scratch directory holding a small CSV and a file containing café:
The timeout case shows a second difference: with GNU, bash itself reports Killed. GNU timeout sends the signal to its whole process group, itself included, and its man page lists exit 137 for "COMMAND (or timeout itself) is sent the KILL (9) signal". The uutils timeout exits normally with 124, so bash has nothing to report.
The wc -m result depends on the locale. The container had none set (POSIX), where GNU counts bytes for -m; with LC_ALL=C.UTF-8 both printed 5. Set LC_ALL explicitly in any script that counts bytes, and use wc -c when bytes are what you mean.
How Do I Make a Script Safe on Both?
- Exit codes: test for every value that can mean the same thing. For
timeout,case $rc in 124|137) ... ;; esac. The Bash Exit Code Lookup hastimeout's full table. - Error text: branch on exit codes, never on stderr wording. Where you must, match the stable part (
exists, notFile exists). - Locale: set
LC_ALLat the top of any script that counts or sorts bytes. - Pin while you fix:
gnutimeout,gnuprand the othergnu*names run GNU for one call, without switching the whole system. To switch the whole system, see Ubuntu Rust Coreutils: Check and Switch Back.
The rest of what changes in the upgrade, including sudo-rs, is in Ubuntu 26.04: What Breaks in Bash Scripts.
Frequently Asked Questions
Is Rust coreutils compatible with GNU coreutils?
Mostly. uutils aims to be a drop-in replacement and treats differences as bugs, and in tests for this page sort, cut, uniq, stat, date formats, seq, printf, readlink, split, mktemp and most others matched GNU 9.7 exactly. The differences are in edge cases: exit codes (timeout -s KILL), error message wording, locale handling in wc -m, invalid-input handling in numfmt and base64, and pr's column layout.
Why does timeout return 124 instead of 137 on Ubuntu 26.04?
With -s KILL, GNU timeout exits 137 (128 + 9, the signal it sent) and the uutils timeout in Ubuntu 26.04 exits 124, the code both use for an ordinary timeout. Scripts that check for 137 to detect a hard kill now miss it. Check for both values, or call gnutimeout explicitly while the scripts are updated.
How do I test my bash script against Rust coreutils?
Run it through coreutils-diff.sh on an Ubuntu 26.04 machine or container: ./coreutils-diff.sh './myscript.sh'. It builds two bin directories, one with GNU coreutils under their plain names and one with uutils, runs the command line once with each first in PATH, each time in a fresh copy of the current directory, and prints any difference in exit status, stdout, stderr or the files left behind.
Do error messages differ between uutils and GNU coreutils?
Some do. In uutils 0.10.0, mkdir prints mkdir: backup: File exists where GNU printed cannot create directory 'backup': File exists, ln -s says Already exists instead of File exists, chmod 999 says invalid digit found in string, and touch -d says Unable to parse date. Exit codes for those failures matched. Scripts that test exit codes are fine; scripts that grep stderr need updating.
Is there a bug where tail never stops on Ubuntu 26.04?
Yes, in uutils 0.10.0: tail -n1 big.log >> big.log keeps reading what it writes when the file is larger than 4096 bytes, and grew a 2,000-line file to over 2.4 million lines in five seconds in the test on this page. GNU tail stops after one line. It is reported upstream as uutils issue 15057. Write to a temporary file and move it into place instead.
Part of the bash snippets collection
Related Scripts
- Ubuntu Rust Coreutils: Check and Switch Back — which commands are Rust, and the apt line to go back to GNU
- Ubuntu 26.04: What Breaks in Bash Scripts — the full upgrade checklist
- Bash Timeout Command — timeouts, kill-after and the exit codes to check
- Bash Error Handling — branch on exit codes, not on message text