Skip to content

Commit ba373e5

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.
1 parent 7807c8d commit ba373e5

3 files changed

Lines changed: 161 additions & 23 deletions

File tree

src/uu/tr/src/operation.rs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,33 @@ 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+
/// As [`emit_diagnostic`], but prefixed like `show_warning!`.
42+
fn emit_warning(message: impl Display) {
43+
emit_diagnostic(format_args!("warning: {message}"));
44+
}
45+
46+
/// Whether the raw operand ends in an odd-length run of backslashes, i.e. a
47+
/// final backslash that is not itself escaped.
48+
fn has_unescaped_trailing_backslash(input: &[u8]) -> bool {
49+
input.iter().rev().take_while(|&&b| b == b'\\').count() % 2 == 1
50+
}
2851

2952
/// Common trait for operations that can process chunks of data
3053
pub trait ChunkProcessor {
@@ -217,12 +240,18 @@ impl Sequence {
217240
) -> Result<(Vec<u8>, Vec<u8>), BadSequence> {
218241
let is_char_star = |s: &&Self| -> bool { matches!(s, Self::CharStar(_)) };
219242

243+
// Both operands are parsed before either is validated, so that warnings
244+
// and syntax errors from set2 are reported ahead of a semantic
245+
// rejection of a syntactically valid set1, as GNU does. Set1 is still
246+
// parsed first, so a syntax error in it prevents set2 from being
247+
// parsed at all.
220248
let set1 = Self::from_str(set1_str)?;
249+
let mut set2 = Self::from_str(set2_str)?;
250+
221251
if set1.iter().filter(is_char_star).count() != 0 {
222252
return Err(BadSequence::CharRepeatInSet1);
223253
}
224254

225-
let mut set2 = Self::from_str(set2_str)?;
226255
if set2.iter().filter(is_char_star).count() > 1 {
227256
return Err(BadSequence::MultipleCharRepeatInSet2);
228257
}
@@ -350,7 +379,7 @@ impl Sequence {
350379

351380
impl Sequence {
352381
pub fn from_str(input: &[u8]) -> Result<Vec<Self>, BadSequence> {
353-
many0(alt((
382+
let parsed = many0(alt((
354383
Self::parse_char_range,
355384
Self::parse_char_star,
356385
Self::parse_char_repeat,
@@ -363,9 +392,17 @@ impl Sequence {
363392
)))
364393
.parse(input)
365394
.map(|(_, r)| r)
366-
.unwrap()
367-
.into_iter()
368-
.collect::<Result<Vec<_>, _>>()
395+
.unwrap();
396+
397+
// Warn once per operand, after the whole operand has been scanned, so
398+
// that this warning follows any warning emitted while parsing it and
399+
// precedes a syntax error found in the same operand.
400+
if has_unescaped_trailing_backslash(input) {
401+
// This message already carries its own localized "warning: " prefix.
402+
emit_diagnostic(translate!("tr-warning-unescaped-backslash"));
403+
}
404+
405+
parsed.into_iter().collect::<Result<Vec<_>, _>>()
369406
}
370407

371408
fn parse_octal(input: &[u8]) -> IResult<&[u8], u8> {
@@ -409,12 +446,11 @@ impl Sequence {
409446
if let Ok(origin_octal) = std::str::from_utf8(input) {
410447
let actual_octal_tail: &str = std::str::from_utf8(&input[0..2]).unwrap();
411448
let outstand_char: char = char::from_u32(input[2] as u32).unwrap();
412-
show_warning!(
413-
"{}",
449+
emit_warning(
414450
translate!("tr-warning-ambiguous-octal-escape", "origin_octal" => origin_octal, "actual_octal_tail" => actual_octal_tail, "outstand_char" => outstand_char)
415451
);
416452
} else {
417-
show_warning!("{}", translate!("tr-warning-invalid-utf8"));
453+
emit_warning(translate!("tr-warning-invalid-utf8"));
418454
}
419455
}
420456
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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,120 @@ 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_trailing_backslash_warning_write_failure() {
1670+
new_ucmd!()
1671+
.args(&[".", r"\"])
1672+
.pipe_in(".")
1673+
.set_stderr(std::fs::File::create("/dev/full").unwrap())
1674+
.fails_with_code(1)
1675+
.stdout_is("\\");
1676+
}
1677+
1678+
#[cfg(target_os = "linux")]
1679+
#[test]
1680+
fn test_ambiguous_octal_warning_write_failure() {
1681+
new_ucmd!()
1682+
.args(&["-d", r"\501"])
1683+
.pipe_in("(1Ł)")
1684+
.set_stderr(std::fs::File::create("/dev/full").unwrap())
1685+
.fails_with_code(1)
1686+
.stdout_is("Ł)");
1687+
}
1688+
15751689
#[test]
15761690
fn test_multibyte_octal_sequence() {
15771691
new_ucmd!()

0 commit comments

Comments
 (0)