Skip to content

Commit f4c2699

Browse files
tr: handle trailing-backslash warnings for both sets
tr checked only SET1 for an unescaped trailing backslash, so SET2 never produced GNU's portability warning. Warning writes also went through helpers that discard stderr errors, so redirecting a warning to /dev/full still exited successfully. Move terminal-backslash detection from the SET1-only pre-scan in tr.rs to operand parse completion, so SET1 and SET2 are checked in parse order and the warning follows any warning emitted while scanning the same operand. Parse both operands before applying semantic validation, so SET2 warnings and syntax errors surface ahead of a semantic rejection of a syntactically valid SET1. Add a tr-local checked diagnostic writer that records exit status 1 when a warning cannot be written, without returning early, so stdin processing and stdout output continue. Route tr's existing ambiguous-octal and invalid-UTF-8 warnings through the same writer. The shared uucore diagnostic macros are unchanged. Move the localized "warning: " label into the tr-warning-* Fluent messages so the label and its locale-specific punctuation are translated rather than hardcoded in Rust. This changes two existing message keys from warning bodies into complete warnings.
1 parent 7807c8d commit f4c2699

5 files changed

Lines changed: 155 additions & 27 deletions

File tree

src/uu/tr/locales/en-US.ftl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ tr-error-write-error = write error
2222
2323
# Warning messages
2424
tr-warning-unescaped-backslash = warning: an unescaped backslash at end of string is not portable
25-
tr-warning-ambiguous-octal-escape = the ambiguous octal escape \{ $origin_octal } is being interpreted as the 2-byte sequence \0{ $actual_octal_tail }, { $outstand_char }
26-
tr-warning-invalid-utf8 = invalid utf8 sequence
25+
tr-warning-ambiguous-octal-escape = warning: the ambiguous octal escape \{ $origin_octal } is being interpreted as the 2-byte sequence \0{ $actual_octal_tail }, { $outstand_char }
26+
tr-warning-invalid-utf8 = warning: invalid utf8 sequence
2727
2828
# Sequence parsing error messages
2929
tr-error-missing-char-class-name = missing character class name '[::]'

src/uu/tr/locales/fr-FR.ftl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ tr-error-write-error = erreur d'écriture
2222
2323
# Messages d'avertissement
2424
tr-warning-unescaped-backslash = avertissement : une barre oblique inverse non échappée à la fin de la chaîne n'est pas portable
25-
tr-warning-ambiguous-octal-escape = l'échappement octal ambigu \{ $origin_octal } est en cours
25+
tr-warning-ambiguous-octal-escape = avertissement : l'échappement octal ambigu \{ $origin_octal } est en cours
2626
d'interprétation comme la séquence de 2 octets \0{ $actual_octal_tail }, { $outstand_char }
27-
tr-warning-invalid-utf8 = séquence UTF-8 non valide
27+
tr-warning-invalid-utf8 = avertissement : séquence UTF-8 non valide
2828
2929
# Messages d'erreur d'analyse de séquence
3030
tr-error-missing-char-class-name = nom de classe de caractères manquant '[::]'

src/uu/tr/src/operation.rs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,28 @@ use std::{
2121
fmt::{Debug, Display},
2222
io::{BufRead, Write},
2323
};
24-
use uucore::error::{FromIo, UError, UResult};
24+
use uucore::error::{FromIo, UError, UResult, set_exit_code};
2525
use uucore::translate;
2626

27-
use uucore::show_warning;
27+
/// Write a diagnostic to stderr, recording exit status 1 if the write fails.
28+
///
29+
/// Unlike `show_warning!`, this does not discard stderr write errors. The
30+
/// failure is recorded but not propagated, so translation of stdin continues
31+
/// and the translated stdout is preserved, as GNU does. A successful
32+
/// diagnostic leaves the stored status unchanged.
33+
fn emit_diagnostic(message: impl Display) {
34+
let mut stderr = std::io::stderr().lock();
35+
36+
if writeln!(stderr, "{}: {message}", uucore::util_name()).is_err() {
37+
set_exit_code(1);
38+
}
39+
}
40+
41+
/// Whether the raw operand ends in an odd-length run of backslashes, i.e. a
42+
/// final backslash that is not itself escaped.
43+
fn has_unescaped_trailing_backslash(input: &[u8]) -> bool {
44+
input.iter().rev().take_while(|&&b| b == b'\\').count() % 2 == 1
45+
}
2846

2947
/// Common trait for operations that can process chunks of data
3048
pub trait ChunkProcessor {
@@ -217,12 +235,18 @@ impl Sequence {
217235
) -> Result<(Vec<u8>, Vec<u8>), BadSequence> {
218236
let is_char_star = |s: &&Self| -> bool { matches!(s, Self::CharStar(_)) };
219237

238+
// Both operands are parsed before either is validated, so that warnings
239+
// and syntax errors from set2 are reported ahead of a semantic
240+
// rejection of a syntactically valid set1, as GNU does. Set1 is still
241+
// parsed first, so a syntax error in it prevents set2 from being
242+
// parsed at all.
220243
let set1 = Self::from_str(set1_str)?;
244+
let mut set2 = Self::from_str(set2_str)?;
245+
221246
if set1.iter().filter(is_char_star).count() != 0 {
222247
return Err(BadSequence::CharRepeatInSet1);
223248
}
224249

225-
let mut set2 = Self::from_str(set2_str)?;
226250
if set2.iter().filter(is_char_star).count() > 1 {
227251
return Err(BadSequence::MultipleCharRepeatInSet2);
228252
}
@@ -350,7 +374,7 @@ impl Sequence {
350374

351375
impl Sequence {
352376
pub fn from_str(input: &[u8]) -> Result<Vec<Self>, BadSequence> {
353-
many0(alt((
377+
let parsed = many0(alt((
354378
Self::parse_char_range,
355379
Self::parse_char_star,
356380
Self::parse_char_repeat,
@@ -363,9 +387,16 @@ impl Sequence {
363387
)))
364388
.parse(input)
365389
.map(|(_, r)| r)
366-
.unwrap()
367-
.into_iter()
368-
.collect::<Result<Vec<_>, _>>()
390+
.unwrap();
391+
392+
// Warn once per operand, after the whole operand has been scanned, so
393+
// that this warning follows any warning emitted while parsing it and
394+
// precedes a syntax error found in the same operand.
395+
if has_unescaped_trailing_backslash(input) {
396+
emit_diagnostic(translate!("tr-warning-unescaped-backslash"));
397+
}
398+
399+
parsed.into_iter().collect::<Result<Vec<_>, _>>()
369400
}
370401

371402
fn parse_octal(input: &[u8]) -> IResult<&[u8], u8> {
@@ -409,12 +440,11 @@ impl Sequence {
409440
if let Ok(origin_octal) = std::str::from_utf8(input) {
410441
let actual_octal_tail: &str = std::str::from_utf8(&input[0..2]).unwrap();
411442
let outstand_char: char = char::from_u32(input[2] as u32).unwrap();
412-
show_warning!(
413-
"{}",
443+
emit_diagnostic(
414444
translate!("tr-warning-ambiguous-octal-escape", "origin_octal" => origin_octal, "actual_octal_tail" => actual_octal_tail, "outstand_char" => outstand_char)
415445
);
416446
} else {
417-
show_warning!("{}", translate!("tr-warning-invalid-utf8"));
447+
emit_diagnostic(translate!("tr-warning-invalid-utf8"));
418448
}
419449
}
420450
result

src/uu/tr/src/tr.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use uucore::display::Quotable;
1919
use uucore::error::{UResult, USimpleError, UUsageError};
2020
use uucore::fs::is_stdin_directory;
2121
use uucore::translate;
22-
use uucore::{format_usage, os_str_as_bytes, show};
22+
use uucore::{format_usage, os_str_as_bytes};
2323

2424
mod options {
2525
pub const COMPLEMENT: &str = "complement";
@@ -84,18 +84,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
8484
}
8585
}
8686

87-
if let Some(first) = sets.first() {
88-
let slice = os_str_as_bytes(first)?;
89-
let trailing_backslashes = slice.iter().rev().take_while(|&&c| c == b'\\').count();
90-
if trailing_backslashes % 2 == 1 {
91-
// The trailing backslash has a non-backslash character before it.
92-
show!(USimpleError::new(
93-
0,
94-
translate!("tr-warning-unescaped-backslash")
95-
));
96-
}
97-
}
98-
9987
let stdin = stdin();
10088
let mut locked_stdin = stdin.lock();
10189
let mut locked_stdout = stdout().lock();

tests/by-util/test_tr.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,116 @@ fn test_trailing_backslash() {
15721572
.stdout_is("abc");
15731573
}
15741574

1575+
#[test]
1576+
fn test_trailing_backslash_warning_in_set2() {
1577+
new_ucmd!()
1578+
.args(&[".", r"\"])
1579+
.pipe_in(".")
1580+
.succeeds()
1581+
.stderr_is("tr: warning: an unescaped backslash at end of string is not portable\n")
1582+
.stdout_is("\\");
1583+
}
1584+
1585+
#[test]
1586+
fn test_trailing_backslash_warning_in_both_sets() {
1587+
new_ucmd!()
1588+
.args(&[r"\", r"\"])
1589+
.pipe_in("\\")
1590+
.succeeds()
1591+
.stderr_is(concat!(
1592+
"tr: warning: an unescaped backslash at end of string is not portable\n",
1593+
"tr: warning: an unescaped backslash at end of string is not portable\n",
1594+
))
1595+
.stdout_is("\\");
1596+
}
1597+
1598+
#[test]
1599+
fn test_escaped_trailing_backslash_in_set2_does_not_warn() {
1600+
new_ucmd!()
1601+
.args(&[".", r"\\"])
1602+
.pipe_in(".")
1603+
.succeeds()
1604+
.no_stderr()
1605+
.stdout_is("\\");
1606+
}
1607+
1608+
#[test]
1609+
fn test_set1_syntax_error_prevents_set2_warning() {
1610+
new_ucmd!()
1611+
.args(&["z-a", r"\"])
1612+
.pipe_in("")
1613+
.fails()
1614+
.stderr_only("tr: range-endpoints of 'z-a' are in reverse collating sequence order\n");
1615+
}
1616+
1617+
#[test]
1618+
fn test_set2_warning_precedes_set1_semantic_error() {
1619+
new_ucmd!()
1620+
.args(&["[x*]", r"\"])
1621+
.pipe_in("")
1622+
.fails()
1623+
.stderr_is(concat!(
1624+
"tr: warning: an unescaped backslash at end of string is not portable\n",
1625+
"tr: the [c*] repeat construct may not appear in string1\n",
1626+
));
1627+
}
1628+
1629+
#[test]
1630+
fn test_set2_syntax_error_precedes_set1_semantic_error() {
1631+
new_ucmd!()
1632+
.args(&["[x*]", "z-a"])
1633+
.pipe_in("")
1634+
.fails()
1635+
.stderr_only("tr: range-endpoints of 'z-a' are in reverse collating sequence order\n");
1636+
}
1637+
1638+
#[test]
1639+
fn test_set2_trailing_warning_precedes_its_syntax_error() {
1640+
new_ucmd!()
1641+
.args(&["x", r"z-a\"])
1642+
.pipe_in("")
1643+
.fails()
1644+
.stderr_is(concat!(
1645+
"tr: warning: an unescaped backslash at end of string is not portable\n",
1646+
"tr: range-endpoints of 'z-a' are in reverse collating sequence order\n",
1647+
));
1648+
}
1649+
1650+
#[test]
1651+
fn test_parser_warning_precedes_trailing_backslash_warning() {
1652+
// Only the relative warning order is relevant to this regression.
1653+
let result = new_ucmd!()
1654+
.args(&["-d", r"\501\"])
1655+
.pipe_in("(1Ł)")
1656+
.succeeds();
1657+
result.stdout_is("Ł)");
1658+
1659+
let stderr = result.stderr_str();
1660+
let ambiguous_octal = stderr.find("warning: the ambiguous octal escape").unwrap();
1661+
let trailing_backslash = stderr
1662+
.find("warning: an unescaped backslash at end of string is not portable")
1663+
.unwrap();
1664+
assert!(ambiguous_octal < trailing_backslash);
1665+
}
1666+
1667+
#[cfg(target_os = "linux")]
1668+
#[test]
1669+
fn test_warning_write_failures() {
1670+
// A warning that cannot be written sets the exit status without discarding
1671+
// the translated output, for both the trailing-backslash and the parser
1672+
// warning paths.
1673+
for (args, input, expected_stdout) in
1674+
[([".", r"\"], ".", "\\"), (["-d", r"\501"], "(1Ł)", "Ł)")]
1675+
{
1676+
new_ucmd!()
1677+
.args(&args)
1678+
.pipe_in(input)
1679+
.set_stderr(std::fs::File::create("/dev/full").unwrap())
1680+
.fails_with_code(1)
1681+
.stdout_is(expected_stdout);
1682+
}
1683+
}
1684+
15751685
#[test]
15761686
fn test_multibyte_octal_sequence() {
15771687
new_ucmd!()

0 commit comments

Comments
 (0)