Add an initial implementation of xargs

This includes much of the core xargs functionality, with the following
notable exceptions:

- Parallel execution (`-P`): This option currently just does
  nothing, that way anything that passes -P can at least run without a
  notable behavior shift (other than simply being slower).
- Replacement strings (`-I`): This can easily be worked around via an
  intermediate shell invocation (e.g. `xargs -L1 sh -c 'do-things-with
  $@' --`).
- EOF strings (`-E`): I've honestly never seen this actually used,
  though it would not be particularly difficult to implement given the
  current architecture.

Closes #37

Signed-off-by: Ryan Gonzalez <ryan.gonzalez@collabora.com>
This commit is contained in:
Ryan Gonzalez
2022-01-22 20:22:01 -06:00
parent 8fe924cb68
commit 3a1c42123f
8 changed files with 1618 additions and 20 deletions
Generated
+1
View File
@@ -221,6 +221,7 @@ version = "0.2.0"
dependencies = [
"assert_cmd",
"chrono",
"clap",
"filetime",
"glob",
"once_cell",
+5
View File
@@ -11,6 +11,7 @@ authors = ["uutils developers"]
[dependencies]
chrono = "0.4"
clap = "2.34"
glob = "0.3"
walkdir = "2.3"
tempfile = "3"
@@ -29,6 +30,10 @@ serial_test = "0.5"
name = "find"
path = "src/find/main.rs"
[[bin]]
name = "xargs"
path = "src/xargs/main.rs"
[[bin]]
name = "testing-commandline"
path = "src/testing/commandline/main.rs"
+1
View File
@@ -12,3 +12,4 @@ extern crate walkdir;
extern crate tempfile;
pub mod find;
pub mod xargs;
+69 -20
View File
@@ -6,7 +6,7 @@
use std::env;
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::io::{stdin, stdout, Read, Write};
use std::path::PathBuf;
fn usage() -> ! {
@@ -14,10 +14,19 @@ fn usage() -> ! {
std::process::exit(2);
}
enum ExitWith {
Failure,
UrgentFailure,
#[cfg(unix)]
Signal,
}
#[derive(Default)]
struct Config {
exit_with_failure: bool,
destination_dir: String,
exit_with: Option<ExitWith>,
print_stdin: bool,
no_print_cwd: bool,
destination_dir: Option<String>,
}
fn open_file(destination_dir: &str) -> File {
@@ -39,20 +48,61 @@ fn open_file(destination_dir: &str) -> File {
}
}
fn write_content(mut f: impl Write, config: &Config, args: &[String]) {
if !config.no_print_cwd {
writeln!(f, "cwd={}", env::current_dir().unwrap().to_string_lossy())
.expect("failed to write to file");
}
if config.print_stdin {
let mut s = String::new();
stdin()
.read_to_string(&mut s)
.expect("failed to read from stdin");
writeln!(f, "stdin={}", s.trim()).expect("failed to write to file");
}
writeln!(f, "args=").expect("failed to write to file");
// first two args are going to be the path to this executable and
// the destination_dir we want to write to. Don't write either of those
// as they'll be non-deterministic.
for arg in &args[2..] {
writeln!(f, "{}", arg).expect("failed to write to file");
}
}
fn main() {
let args = env::args().collect::<Vec<String>>();
if args.len() < 2 || args[1] == "-h" || args[1] == "--help" {
usage();
}
let mut config = Config {
destination_dir: args[1].clone(),
destination_dir: if args[1] != "-" {
Some(args[1].clone())
} else {
None
},
..Default::default()
};
for arg in &args[2..] {
if arg.starts_with("--") {
match arg.as_ref() {
"--exit_with_failure" => {
config.exit_with_failure = true;
config.exit_with = Some(ExitWith::Failure);
}
"--exit_with_urgent_failure" => {
config.exit_with = Some(ExitWith::UrgentFailure);
}
#[cfg(unix)]
"--exit_with_signal" => {
config.exit_with = Some(ExitWith::Signal);
}
"--no_print_cwd" => {
config.no_print_cwd = true;
}
"--print_stdin" => {
config.print_stdin = true;
}
_ => {
usage();
@@ -61,20 +111,19 @@ fn main() {
}
}
{
let mut f = open_file(&config.destination_dir);
// first two args are going to be the path to this executable and
// the destination_dir we want to write to. Don't write either of those
// as they'll be non-deterministic.
f.write_fmt(format_args!(
"cwd={}\nargs=\n",
env::current_dir().unwrap().to_string_lossy()
))
.expect("failed to write to file");
for arg in &args[2..] {
f.write_fmt(format_args!("{}\n", arg))
.expect("failed to write to file");
}
if let Some(destination_dir) = &config.destination_dir {
write_content(open_file(destination_dir), &config, &args);
} else {
write_content(stdout(), &config, &args);
}
match config.exit_with {
None => std::process::exit(0),
Some(ExitWith::Failure) => std::process::exit(2),
Some(ExitWith::UrgentFailure) => std::process::exit(255),
#[cfg(unix)]
Some(ExitWith::Signal) => unsafe {
uucore::libc::raise(uucore::libc::SIGINT);
},
}
std::process::exit(if config.exit_with_failure { 2 } else { 0 });
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2021 Collabora, Ltd.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
fn main() {
let args = std::env::args().collect::<Vec<String>>();
std::process::exit(findutils::xargs::xargs_main(
&args.iter().map(|s| s.as_ref()).collect::<Vec<&str>>(),
))
}
+1202
View File
File diff suppressed because it is too large Load Diff
+326
View File
@@ -0,0 +1,326 @@
// Copyright 2021 Collabora, Ltd.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
/// ! This file contains integration tests for xargs, separate from the unit
/// ! tests so that testing-commandline can be built first.
extern crate findutils;
extern crate tempfile;
use std::io::{Seek, SeekFrom, Write};
use assert_cmd::Command;
use predicates::prelude::*;
use common::test_helpers::*;
mod common;
#[test]
fn xargs_basics() {
Command::cargo_bin("xargs")
.expect("found binary")
.write_stdin("abc\ndef g\\hi 'i j \"k'")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("abc def ghi i j \"k\n"));
}
#[test]
fn xargs_null() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(&["-0n1"])
.write_stdin("ab c\0d\tef\0")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab c\nd\tef\n"));
}
#[test]
fn xargs_delim() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(&["-d1"])
.write_stdin("ab1cd1ef")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab cd ef\n"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(&["-d\\t", "-n1"])
.write_stdin("a\nb\td e\tfg")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("a\nb\nd e\nfg\n"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(&["-dabc"])
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("Invalid"))
.stdout(predicate::str::is_empty());
}
#[test]
fn xargs_null_conflict() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(&["-d\t", "-0n1"])
.write_stdin("ab c\0d\tef\0")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab c\nd\tef\n"));
}
#[test]
fn xargs_if_empty() {
Command::cargo_bin("xargs")
.expect("found binary")
.assert()
.success()
.stderr(predicate::str::is_empty())
// Should echo at least once still.
.stdout(predicate::eq("\n"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(&["--no-run-if-empty"])
.assert()
.success()
.stderr(predicate::str::is_empty())
// Should never echo.
.stdout(predicate::str::is_empty());
}
#[test]
fn xargs_max_args() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-n2"])
.write_stdin("ab cd ef\ngh i")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab cd\nef gh\ni\n"));
}
#[test]
fn xargs_max_lines() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-L2"])
.write_stdin("ab cd\nef\ngh i\n\njkl\n")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab cd ef\ngh i jkl\n"));
}
#[test]
fn xargs_max_args_lines_conflict() {
Command::cargo_bin("xargs")
.expect("found binary")
// -n2 is last, so it should be given priority.
.args(["-L2", "-n2"])
.write_stdin("ab cd ef\ngh i")
.assert()
.success()
.stderr(predicate::str::contains("WARNING"))
.stdout(predicate::str::diff("ab cd\nef gh\ni\n"));
Command::cargo_bin("xargs")
.expect("found binary")
// -L2 is last, so it should be given priority.
.args(["-n2", "-L2"])
.write_stdin("ab cd\nef\ngh i\n\njkl\n")
.assert()
.success()
.stderr(predicate::str::contains("WARNING"))
.stdout(predicate::str::diff("ab cd ef\ngh i jkl\n"));
}
#[test]
fn xargs_max_chars() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-s11"])
.write_stdin("ab cd efg")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab cd\nefg\n"));
// Behavior should be the same with -x, which only takes effect with -L or
// -n.
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-xs11"])
.write_stdin("ab cd efg")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab cd\nefg\n"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-s10"])
.write_stdin("abcdefghijkl ab")
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("Error:"))
.stdout(predicate::str::is_empty());
}
#[test]
fn xargs_exit_on_large() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-xs11", "-n2"])
.write_stdin("ab cd efg h i")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff("ab cd\nefg h\ni\n"));
Command::cargo_bin("xargs")
.expect("found binary")
.args(["-xs11", "-n2"])
.write_stdin("abcdefg hijklmn")
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("Error:"))
.stdout(predicate::str::is_empty());
}
#[test]
fn xargs_exec() {
Command::cargo_bin("xargs")
.expect("found binary")
.args([
"-n2",
&path_to_testing_commandline(),
"-",
"--print_stdin",
"--no_print_cwd",
])
.write_stdin("a b c\nd")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff(
"stdin=\nargs=\n--print_stdin\n--no_print_cwd\na\nb\n\
stdin=\nargs=\n--print_stdin\n--no_print_cwd\nc\nd\n",
));
}
#[test]
fn xargs_exec_stdin_open() {
let mut temp_file = tempfile::NamedTempFile::new().unwrap();
write!(temp_file, "a b c").unwrap();
temp_file.seek(SeekFrom::Start(0)).unwrap();
Command::cargo_bin("xargs")
.expect("found binary")
.args([
"-a",
&temp_file.path().to_string_lossy(),
&path_to_testing_commandline(),
"-",
"--print_stdin",
"--no_print_cwd",
])
.write_stdin("test")
.assert()
.success()
.stderr(predicate::str::is_empty())
.stdout(predicate::str::diff(
"stdin=test\nargs=\n--print_stdin\n--no_print_cwd\na\nb\nc\n",
));
}
#[test]
fn xargs_exec_failure() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(&[
"-n1",
&path_to_testing_commandline(),
"-",
"--no_print_cwd",
"--exit_with_failure",
])
.write_stdin("a b")
.assert()
.failure()
.code(123)
.stderr(predicate::str::is_empty())
.stdout(
"args=\n--no_print_cwd\n--exit_with_failure\na\n\
args=\n--no_print_cwd\n--exit_with_failure\nb\n",
);
}
#[test]
fn xargs_exec_urgent_failure() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(&[
"-n1",
&path_to_testing_commandline(),
"-",
"--no_print_cwd",
"--exit_with_urgent_failure",
])
.write_stdin("a b")
.assert()
.failure()
.code(124)
.stderr(predicate::str::contains("Error:"))
.stdout("args=\n--no_print_cwd\n--exit_with_urgent_failure\na\n");
}
#[test]
#[cfg(unix)]
fn xargs_exec_with_signal() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(&[
"-n1",
&path_to_testing_commandline(),
"-",
"--no_print_cwd",
"--exit_with_signal",
])
.write_stdin("a b")
.assert()
.failure()
.code(125)
.stderr(predicate::str::contains("Error:"))
.stdout("args=\n--no_print_cwd\n--exit_with_signal\na\n");
}
#[test]
fn xargs_exec_not_found() {
Command::cargo_bin("xargs")
.expect("found binary")
.args(&["this-file-does-not-exist"])
.assert()
.failure()
.code(127)
.stderr(predicate::str::contains("Error:"))
.stdout(predicate::str::is_empty());
}
+2
View File
@@ -10,6 +10,7 @@ fi
# build the rust implementation
cargo build --release
cp target/release/find ../findutils.gnu/find.rust
cp target/release/xargs ../findutils.gnu/xargs.rust
# Clone and build upstream repo
cd ../findutils.gnu
@@ -21,6 +22,7 @@ fi
# overwrite the GNU version with the rust impl
cp find.rust find/find
cp xargs.rust xargs/xargs
if test -n "$1"; then
# if set, run only the test passed