Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions src/uu/ptx/src/ptx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@
use uucore::format_usage;
use uucore::translate;

/// GNU's regex engine treats a trailing lone backslash as a literal backslash,
/// while the `regex` crate rejects it as an incomplete escape sequence. Double
/// it so that such patterns keep working instead of erroring out.
fn escape_trailing_backslash(pattern: &str) -> String {
let trailing = pattern.chars().rev().take_while(|&c| c == '\\').count();
if trailing % 2 == 1 {
format!("{pattern}\\")
} else {
pattern.to_owned()
}
}

#[derive(Debug, PartialEq)]
enum OutFormat {
Dumb,
Expand Down Expand Up @@ -149,7 +161,7 @@
matches
.get_one::<String>(options::WORD_REGEXP)
.filter(|v| !v.is_empty())
.map(ToOwned::to_owned)
.map(|v| escape_trailing_backslash(v))
} else {
None
};
Expand Down Expand Up @@ -196,7 +208,10 @@
config.format = OutFormat::Roff;
"[^ \t\n]+".clone_into(&mut config.context_regex);
}
if let Some(regex) = matches.remove_one::<String>(options::SENTENCE_REGEXP) {
if let Some(regex) = matches
.remove_one::<String>(options::SENTENCE_REGEXP)
.map(|r| escape_trailing_backslash(&r))
{
// TODO: The regex crate used here is not fully compatible with GNU's regex implementation.
// For example, it does not support backreferences.
// In the future, we might want to switch to the onig crate (like expr does) for better compatibility.
Expand Down Expand Up @@ -306,17 +321,20 @@
sentence_splitter: Option<&Regex>,
reader: &mut dyn BufRead,
) -> std::io::Result<Vec<String>> {
if let Some(re) = sentence_splitter {
let mut buffer = String::new();
reader.read_to_string(&mut buffer)?;
// GNU ptx works on bytes, so invalid UTF-8 input must not be an error.
// Read everything and replace invalid sequences instead of failing.
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
let buffer = String::from_utf8_lossy(&bytes);

if let Some(re) = sentence_splitter {
Ok(re
.split(&buffer)
.map(|s| s.replace('\n', " ")) // ptx behavior: newlines become spaces inside sentences
.filter(|s| !s.is_empty()) // remove empty sentences
.collect())
} else {
reader.lines().collect()
Ok(buffer.lines().map(ToOwned::to_owned).collect())
}
}

Expand Down Expand Up @@ -358,7 +376,7 @@
continue;
}
let mut word = line[beg..end].to_owned();
if filter.only_specified && !filter.only_set.contains(&word) {

Check warning on line 379 in src/uu/ptx/src/ptx.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

WARNING: `cspell`: Unknown word 'rescanning' (file:'src/uu/ptx/src/ptx.rs', line:379)
continue;
}
if filter.ignore_specified && filter.ignore_set.contains(&word) {
Expand Down
23 changes: 23 additions & 0 deletions tests/by-util/test_ptx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,26 @@ fn test_missing_file_error_contains_filename() {
.fails()
.stderr_is("ptx: 'zxc': No such file or directory\n");
}

#[test]
fn test_sentence_regex_trailing_backslash() {
// GNU treats a trailing lone backslash as a literal one instead of erroring.
new_ucmd!()
.args(&["-S", "paris\\"])
.pipe_in("")
.succeeds()
.no_output();
new_ucmd!()
.args(&["-S", "london\\\\\\"])
.pipe_in("")
.succeeds()
.no_output();
}

#[test]
fn test_invalid_utf8_input_is_not_an_error() {
new_ucmd!()
.pipe_in(b"ab\xFFcd\n".to_vec())
.succeeds()
.no_stderr();
}
Loading