Skip to content

tr: align [:upper:]/[:lower:] the way GNU does, and say so - #14400

Open
Socialpranker wants to merge 1 commit into
uutils:mainfrom
Socialpranker:tr-gnu-messages
Open

Socialpranker wants to merge 1 commit into
uutils:mainfrom
Socialpranker:tr-gnu-messages

Conversation

@Socialpranker

@Socialpranker Socialpranker commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Three related gaps in how SET1/SET2 character classes are validated, found
by diffing against GNU:

$ echo aZ1 | tr '[:alpha:]' '[:upper:]'
tr: misaligned [:upper:] and/or [:lower:] construct        # GNU
tr: when translating with string1 longer than string2,     # uutils, before
the latter string must not end with a character class

$ echo aZ1 | tr '[:digit:]' '[:upper:]'
tr: misaligned [:upper:] and/or [:lower:] construct        # GNU, rc 1
aZ1                                                        # uutils, before, rc 0

$ echo aZ1 | tr -ds '[:alpha:]' '[:upper:]'
1                                                          # GNU, rc 0
tr: when translating with string1 longer than string2,     # uutils, before, rc 1
the latter string must not end with a character class

$ echo aZ1 | tr abc '[:upper:]'
tr: misaligned [:upper:] and/or [:lower:] construct        # GNU
tr: when translating, every 'upper'/'lower' in set2 must   # uutils, before
be matched by a 'upper'/'lower' in the same position in set1

The fix

  1. The alignment loop accepted any class in SET1 as the partner of an
    upper/lower in SET2. Only an upper/lower can be one — anything
    else leaves the mapping undefined, which is why GNU rejects it. With
    [:alpha:]/[:upper:] the bogus match let the input fall through to
    the length check and produce the wrong message; with
    [:digit:]/[:upper:] it produced no error at all.
  2. Both class checks ran regardless of mode. With -d, SET2 is the
    squeeze set and lines up with nothing, so both are now gated on
    translating.
  3. tr-error-class-in-set2-not-matched restated the rule where GNU names
    the fault: misaligned [:upper:] and/or [:lower:] construct.

The -ds missing-operand wording, which was part of this PR at first,
is now in #14873.

The when translating with string1 longer than string2 message is not
removed — GNU has it too, for tr '[:upper:][:lower:]' '[:lower:]', and a
new test pins that it still fires there.

How the GNU behavior was established

By running the installed GNU coreutils 9.11 binary (Homebrew, gtr) as a
black box over 14 SET1/SET2 pairs: classes aligned and misaligned, a class
in SET2 against a literal, a non-upper/lower class in SET1, SET1
longer than SET2, and each of those again under -d, -s, -ds and
-dcs. Exit status and stderr diffed against uutils. I did not read GNU
coreutils source.

Testing

  • New in tests/by-util/test_tr.rs: test_misaligned_upper_lower_construct
    (five pairs, exact message and exit status 1),
    test_misaligned_construct_not_checked_when_deleting,
    test_misaligned_construct_checked_when_squeezing (-s alone still
    translates), test_delete_with_class_in_set2 (plain -d reports the
    extra operand), and test_set1_longer_than_set2_ending_in_class
    guarding the message that must stay.
    Mutation-checked: reverting operation.rs alone fails three of them,
    reverting the .ftl alone fails two.
  • cargo test --no-default-features --features tr --test tests test_tr:
    165 passed, 0 failed.
  • cargo clippy -p uu_tr --all-targets -- -D warnings: clean.
  • cargo fmt --check: clean.
  • Differential A/B against GNU coreutils 9.11 over the 38 tr invocations
    in my harness: mismatches 2 -> 0 (measured before the -ds wording
    moved to tr: word the -ds missing-operand hint the way GNU does #14873).

French translations updated to match.

Disclosure

Prepared with AI assistance (Claude Opus 5, via Claude Code), per the AI
policy in CONTRIBUTING.md. Every GNU behavior quoted above came from
running the installed binary, not from reading GPL source. All testing was
run locally.

@github-actions

github-actions Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/date/resolution (fails in this run but passes in the 'main' branch)
Congrats! The gnu test tests/id/setgid is no longer failing!

Comment thread src/uu/tr/locales/fr-FR.ftl Outdated
tr-error-empty-set2-when-not-truncating = quand on ne tronque pas set1, string2 doit être non-vide
tr-error-class-except-lower-upper-in-set2 = lors de la traduction, les seules classes de caractères qui peuvent apparaître dans set2 sont 'upper' et 'lower'
tr-error-class-in-set2-not-matched = lors de la traduction, chaque 'upper'/'lower' dans set2 doit être associé à un 'upper'/'lower' à la même position dans set1
tr-error-class-in-set2-not-matched = construction [:upper:] et/ou [:lower:] mal alignée

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.

does this match the french translation of gnu in meaning ? Could you post it here ?

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.

GNU's official translation (translationproject.org fr.po):

msgid "misaligned [:upper:] and/or [:lower:] construct"
msgstr "les structures [:upper:] ou [:lower:] sont mal alignées"

Our fr string was close in meaning but not the official wording. Updated fr-FR.ftl to match GNU's translation exactly, in 828738d.

@Socialpranker

Copy link
Copy Markdown
Contributor Author

GNU's official translation (translationproject.org fr.po):

msgid "misaligned [:upper:] and/or [:lower:] construct"
msgstr "les structures [:upper:] ou [:lower:] sont mal alignées"

Our previous fr string was close in meaning but not the official wording. Updated fr-FR.ftl to match GNU's translation exactly, in 828738d.

@anastygnome anastygnome left a comment

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.

You must NOT copy code from GNU for licencing reasons.

Just to be in the clear

Replace
sont mal alignées
By
ne sont pas alignées correctement

@Socialpranker

Copy link
Copy Markdown
Contributor Author

Understood — replaced with your wording in f773c6b: les structures [:upper:] ou [:lower:] ne sont pas alignées correctement.

@anastygnome

Copy link
Copy Markdown
Contributor

please read the AI policy
"Answers to the reviewers should be done by a human, not a agent."

Two strings must be given when translating.
tr-error-missing-operand-deleting-squeezing = missing operand after { $set }
Two strings must be given when deleting and squeezing.
Two strings must be given when both deleting and squeezing repeats.

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.

unrelated to the upper/lower change, a separate pr would have been better

Comment thread src/uu/tr/src/operation.rs Outdated
// Only when translating: with -d, set2 is the squeeze set and lines up with nothing.
for (set2_pos, set2_item) in set2.iter().enumerate() {
if matches!(set2_item, Self::Class(_)) {
if translating && matches!(set2_item, Self::Class(_)) {

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.

could you wrap the whole loop in if translating { ... } instead of checking it on every item?

Comment thread src/uu/tr/src/operation.rs Outdated
let mut class_matches = false;
for (set1_pos, set1_item) in set1.iter().enumerate() {
if matches!(set1_item, Self::Class(_)) {
// Only an 'upper'/'lower' in set1 can line up with one in

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.

could be a one line comment, no? e.g. // only upper/lower can pair with upper/lower

Comment thread tests/by-util/test_tr.rs
/// The alignment rule is about translating only: with -d, SET2 is the squeeze
/// set and lines up with nothing.
#[test]
fn test_misaligned_construct_not_checked_when_deleting() {

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.

please also test -s alone (still translating, so still rejected) and plain -d with a class in set2

@xtqqczze

Copy link
Copy Markdown
Collaborator

@Socialpranker Please test against updated GNU coreutils 9.12.

@codspeed

codspeed Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 8 untouched benchmarks
⏩ 440 skipped benchmarks1


Comparing Socialpranker:tr-gnu-messages (d22dd2c) with main (2b85f6f)

Open in CodSpeed

Footnotes

  1. 440 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@Socialpranker

Copy link
Copy Markdown
Contributor Author

wrapped the loop in if translating, comment is one line, added -s alone and plain -d with a class in set2. checked against 9.12, the upper/lower messages match. you're right about the ftl bit, should i split it out into its own pr?

This branch has not been deployed

No deployments
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.

4 participants