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
85 changes: 80 additions & 5 deletions src/xargs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,11 @@ where

let mut result = vec![];
let mut terminated_by_newline = false;
// Whether we're inside an argument. This is *not* the same as
// `!result.is_empty()`: quotes and escapes start an argument without
// necessarily contributing any bytes to it, so `''` is a genuine empty
// argument, while a run of blanks is just a delimiter.
let mut in_argument = false;

let mut pending = vec![];
std::mem::swap(&mut pending, &mut self.pending);
Expand All @@ -606,7 +611,9 @@ where
format!("Unterminated quote: {q}"),
));
}
if i == 0 {
// Anything we skipped over was a delimiter, not an
// argument, so there is nothing left to emit.
if result.is_empty() {
return Ok(None);
}
pending.clear();
Expand All @@ -624,15 +631,24 @@ where
result.push(c);
escape = None;
}
(None, c @ (b'"' | b'\'')) => escape = Some(Escape::Quote(c)),
(None, b'\\') => escape = Some(Escape::Slash),
(None, c @ (b'"' | b'\'')) => {
in_argument = true;
escape = Some(Escape::Quote(c));
}
(None, b'\\') => {
in_argument = true;
escape = Some(Escape::Slash);
}
(None, c) if c.is_ascii_whitespace() => {
if !result.is_empty() {
if in_argument {
terminated_by_newline = c == b'\n';
break;
}
}
(None, c) => result.push(c),
(None, c) => {
in_argument = true;
result.push(c);
}
}

i += 1;
Expand Down Expand Up @@ -1490,6 +1506,65 @@ mod tests {
assert_eq!(reader.next().unwrap(), None);
}

#[test]
fn test_whitespace_delimited_reader_trailing_blanks() {
// A run of blanks before the final newline is a delimiter, not an
// empty argument, no matter where in the input it appears.
for input in [
&b"aaa \nbbb \n"[..],
&b"aaa\nbbb \n"[..],
&b"aaa \nbbb\n"[..],
&b"aaa \nbbb \n \t \n"[..],
] {
let mut reader =
WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![Chunk::Data(input)]));
assert_eq!(reader.next().unwrap().unwrap().arg, "aaa", "{input:?}");
assert_eq!(reader.next().unwrap().unwrap().arg, "bbb", "{input:?}");
assert_eq!(reader.next().unwrap(), None, "{input:?}");
}

// Blanks are only a soft terminator, so the newline that follows them
// does not end the logical line (this matters for -L).
let mut reader =
WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![Chunk::Data(b"aaa \n")]));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("aaa"));
assert_eq!(reader.next().unwrap(), None);

let mut reader = WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![
Chunk::Data(b"aaa "),
Chunk::Error(io::ErrorKind::Interrupted),
Chunk::Data(b" \t "),
]));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("aaa"));
assert_eq!(reader.next().unwrap(), None);

let mut reader =
WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![Chunk::Data(b" \n\t\n")]));
assert_eq!(reader.next().unwrap(), None);
}

#[test]
fn test_whitespace_delimited_reader_quoted_empty_arguments() {
// Quotes start an argument even when they contribute no bytes, so an
// empty quoted string is a real (empty) argument.
let mut reader = WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![
Chunk::Data(b"'' x \"\"\n"),
Chunk::Data(b"y ''\n"),
]));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft(""));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("x"));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard(""));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("y"));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_hard(""));
assert_eq!(reader.next().unwrap(), None);

// ...but an unterminated one at end of input is dropped, as GNU does.
let mut reader =
WhitespaceDelimitedArgumentReader::new(ChunkReader::new(vec![Chunk::Data(b"x ''")]));
assert_eq!(reader.next().unwrap().unwrap(), make_arg_soft("x"));
assert_eq!(reader.next().unwrap(), None);
}

#[test]
fn test_eof_argument_reader() {
let filter = String::from("def");
Expand Down
28 changes: 28 additions & 0 deletions tests/test_xargs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ fn xargs_basics() {
.stdout_only("abc def ghi i j \"k\n");
}

#[test]
fn xargs_trailing_blanks() {
// A blank before the final newline is a delimiter, so it must not produce
// a trailing empty argument (GNU findutils behaves the same way).
for input in [
"aaa \nbbb \n",
"aaa\nbbb \n",
"aaa \nbbb\n",
"aaa \nbbb \n",
] {
ucmd()
.arg("-n1")
.pipe_in(input)
.succeeds()
.stdout_only("aaa\nbbb\n");
}
}

#[test]
fn xargs_quoted_empty_argument() {
// An empty quoted string is a real argument, unlike a run of blanks.
ucmd()
.args(&["-n1"])
.pipe_in("'' aaa \"\"\n")
.succeeds()
.stdout_only("\naaa\n\n");
}

#[test]
fn xargs_null() {
ucmd()
Expand Down
Loading