Skip to content

tr: accept -u for unbuffered output, as GNU, FreeBSD and macOS tr do - #15228

Merged
sylvestre merged 4 commits into
uutils:mainfrom
Hekzory:tr-unbuffered
Oct 10, 2026
Merged

sylvestre merged 4 commits into
uutils:mainfrom
Hekzory:tr-unbuffered

Conversation

@Hekzory

@Hekzory Hekzory commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Closes #15227.

Accept -u in tr, as FreeBSD and macOS do, so that scripts written for them stop failing. The examples are in the issue.

Since #14897, tr on Unix writes every chunk to the file descriptor as soon as it is translated, so -u has nothing to do there. On Windows tr still writes through the line-buffered Stdout, which holds back whatever follows the last newline, so with -u it now flushes after every chunk. The option is hidden from --help and the man page, as in GNU tr, which accepts the same undocumented -u since coreutils/coreutils#374.

Tests added:

  • test_unbuffered_flag: -u is accepted and leaves the output unchanged.
  • test_unbuffered_partial_line: writes abc with no newline and expects it back before sending more input, for translation, -d and -s. It times out on line-buffered output, as tr had in 0.12.0.

Checked on Windows 11 26H2 with rustc 1.99.0 (MSVC): the tr suite passes as on main, the new tests passed in 10 of 10 runs, clippy is clean, and test_unbuffered_partial_line fails if the flush is removed. On Linux the tr suite, fmt and clippy pass as well.

FreeBSD and macOS tr take -u to guarantee that output is unbuffered,
and scripts written for them use it to pass progress output through a
pipeline as it arrives, e.g. `curl ... 2>&1 | tr -u '\r' '\n'`. uutils
tr rejected the option, so such pipelines failed outright.

On Unix every chunk is already written as soon as it is translated, so
-u has nothing left to do there. Windows still writes through the line
buffered Stdout, so flush it after every chunk when -u is given.
@oech3

oech3 commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

See #15227 (comment) .

We should not add this yet.

Comment thread src/uu/tr/src/tr.rs
.overrides_with(options::TRUNCATE_SET1),
)
.arg(
Arg::new(options::UNBUFFERED)

@oech3 oech3 Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mask this from manpage by .hide(true). While silently supporting is useful for existing scripts, we should not encourage usage of it, or many people would write scripts incompatible with GNU tr.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, done in 742af96. -u is gone from --help and the man page but still accepted. I kept the short note in docs/src/extensions.md since Devel08 asked for it in the issue, happy to drop it if maintainers would rather leave it fully undocumented.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop it from extensions.md after GNU supported it.

GNU tr rejects -u, so a script that uses it does not run with GNU tr.
Keep accepting the option for scripts written for FreeBSD and macOS,
but do not advertise it, the same way GNU tr accepts AIX's -A without
listing it.
Comment thread src/uu/tr/src/tr.rs
Arg::new(options::UNBUFFERED)
.short('u')
.hide(true)
.help(translate!("tr-help-unbuffered"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed if it is hidden?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for --help or the man page, but uudoc still lists hidden options in the zsh/fish/PowerShell completions and on the generated docs page, so -u shows up there either way. Without the text it would just be a bare -u with no explanation; with it, someone who runs into -u in a macOS script can see what it does. It also means unhiding it later is only removing .hide(true). more -u and uname -p/-i keep their help text while hidden too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

Comment thread docs/src/extensions.md Outdated
## `tr`

Just like on FreeBSD and macOS, `tr` accepts `-u` to guarantee that any output
is unbuffered: each chunk is written as soon as it has been read and translated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If unbuffered I/O is actually default of GNU tr, we can just say:
Noop. Existing for FreeBSD and macOS compatibility.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I missed comment about Windows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, GNU tr isn't unbuffered by default either, it writes through stdio. With GNU tr 9.12, { printf abc; sleep 2; printf 'def\n'; } | tr a-z A-Z | cat only prints ABC after the 2 seconds, while stdbuf -o0 tr prints it right away. On Unix it's only our tr that writes every chunk immediately, since #14897.

This comment was marked as low quality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. Your doc is correct.

Comment thread src/uu/tr/src/tr.rs
pub const DELETE: &str = "delete";
pub const SQUEEZE: &str = "squeeze-repeats";
pub const TRUNCATE_SET1: &str = "truncate-set1";
pub const UNBUFFERED: &str = "unbuffered";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should drop long option since we need BSD compatibility only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't one, "unbuffered" is only the argument ID (like SETS = "sets"), there's no .long(). tr --unbuffered is rejected with unexpected argument '--unbuffered' found, only -u is accepted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry.

@Hekzory

Hekzory commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

I've asked GNU in coreutils/coreutils#374. If they add -u, the follow-up here would just be matching their interface and dropping the extensions.md note, so maybe this doesn't have to wait until the change actually lands in GNU? Happy to go with whatever the maintainers prefer.

Comment thread src/uu/tr/locales/en-US.ftl Outdated
tr-help-delete = delete characters in SET1, do not translate
tr-help-squeeze = replace each sequence of a repeated character that is listed in the last specified SET, with a single occurrence of that character
tr-help-truncate-set1 = first truncate SET1 to length of SET2
tr-help-unbuffered = guarantee that any output is unbuffered

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tr-help-unbuffered = guarantee that any output is unbuffered
tr-help-unbuffered = do not buffer output

We should be same with GNU, but we don't need to be same with BSD.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be changed in src/uu/tr/locales/fr-FR.ftl as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9853afb, and updated the French string to match.

Say what -u does instead of reusing the BSD man page wording.
GNU tr now accepts -u as an undocumented option as well (coreutils
commit a40b8d1191dd), so it is no longer an extension over GNU.
@Hekzory

Hekzory commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

GNU merged it as an undocumented option: https://git.savannah.gnu.org/cgit/coreutils.git/commit/?id=a40b8d1191ddcb77dad838c64acc1491dd6298dd (coreutils/coreutils#374). So -u now matches GNU, and I dropped the extensions.md note in 4aa88e5.

@Hekzory Hekzory changed the title tr: accept -u for unbuffered output, as on FreeBSD and macOS tr: accept -u for unbuffered output, as GNU, FreeBSD and macOS tr do Oct 10, 2026
@sylvestre
sylvestre enabled auto-merge (rebase) October 10, 2026 20:40
@sylvestre
sylvestre merged commit 04a59e2 into uutils:main Oct 10, 2026
96 of 97 checks passed
@Hekzory
Hekzory deleted the tr-unbuffered branch October 10, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tr: accept -u (unbuffered output) like GNU, FreeBSD and macOS

4 participants