Skip to content

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

Open
Socialpranker wants to merge 2 commits into
uutils:mainfrom
Socialpranker:tr-gnu-messages
Open

tr: align [:upper:]/[:lower:] the way GNU does, and say so#14400
Socialpranker wants to merge 2 commits into
uutils:mainfrom
Socialpranker:tr-gnu-messages

Conversation

@Socialpranker

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.

Plus the second line of the -ds missing-operand message, which GNU
spells Two strings must be given when both deleting and squeezing repeats.

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, and
    test_set1_longer_than_set2_ending_in_class guarding the message that
    must stay. One existing assertion updated for the new -ds wording.
    Mutation-checked: reverting operation.rs alone fails two of them,
    reverting the .ftl alone fails two.
  • cargo test --features tr --test tests test_tr: 253 passed, 0 failed
    (250/0 before).
  • 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.

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.

Three related gaps in the SET1/SET2 class rules:

- Any class in SET1 counted as a match for an 'upper'/'lower' in SET2, so
  'tr [:alpha:] [:upper:]' fell through to the length check and
  'tr [:digit:] [:upper:]' was accepted and translated nothing. Only an
  'upper'/'lower' can line up with one.
- Both class checks ran with -d, where SET2 is the squeeze set and lines
  up with nothing: 'tr -ds [:alpha:] [:upper:]' failed instead of
  deleting.
- The diagnostic named the rule instead of the fault; GNU calls it a
  misaligned construct.

Also the second line of the -ds missing-operand message, which GNU spells
'when both deleting and squeezing repeats'.
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/cut/bounded-memory (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/tail/tail-n0f (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/tail/pipe-f is now passing!

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.

…er/lower

GNU's translationproject.org fr.po translates 'misaligned [:upper:] and/or
[:lower:] construct' as 'les structures [:upper:] ou [:lower:] sont mal
alignées'; match that wording instead of the ad hoc phrasing.
@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

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.

3 participants