mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
find: implement -ok and -okdir (#650)
-ok is like -exec ... ; but prompts the user on stderr before each
invocation and only runs the command if the response is affirmative
(starts with 'y' or 'Y'). -okdir is the corresponding -execdir variant.
Only the ';' terminator is accepted: POSIX does not define -ok ... + and
GNU find rejects it.
Response source: GNU find reads from /dev/tty so that the user's answer
always comes from the real terminal even when stdin is occupied (e.g.
`find -files0-from - -ok rm {} \;` reads paths from stdin, so responses
cannot also come from there). /dev/tty is the POSIX name for a
process's controlling terminal and exists on all Unix-like systems. We
open /dev/tty only when stdin is itself a terminal
(std::io::stdin().is_terminal() == true). When stdin is a pipe or file,
we read from stdin directly — matching BSD find and making scripted use
(and integration tests via pipe_in()) work naturally without any special
environment variable. On Windows, or when /dev/tty cannot be opened, we
likewise fall back to stdin.
Implementation: rather than a separate OkMatcher that duplicated
SingleExecMatcher's fields, constructor, and exec logic, the interactive
prompt is folded into SingleExecMatcher behind an `interactive: bool`
field. `SingleExecMatcher::new_interactive()` constructs the -ok/-okdir
variant; `matches()` adds a guarded block that builds a GNU-find-style
prompt ("< executable arg... >? ") and calls `matcher_io.confirm()`
before executing. If the user declines, the expression is false and the
command is not run. This keeps the arg-parsing, path resolution,
current_dir logic, and error handling in one place so bug fixes apply to
both -exec and -ok.
Dependencies::confirm() trait method: abstracts prompt+read so matchers
remain testable without a real terminal; FakeDependencies uses a
VecDeque of preset responses.
Tests:
- Unit tests (exec_unit_tests.rs): confirmed executes command, declined
skips command and returns false, confirmed but command fails returns
false, -okdir runs in parent dir
- Parser unit tests (matchers/mod.rs): missing-arg errors, missing
semicolon, correct parse, confirm/decline via FakeDependencies
- Integration tests (test_find.rs): pipe_in() makes stdin a pipe so
is_terminal() returns false and responses are read from the pipe;
"y"/"Y"/"yes"/" y" run command, "n" and empty response skip command,
-okdir runs in parent dir, prompt format verified, missing-semicolon
error
Closes https://github.com/uutils/findutils/issues/8.
This commit is contained in:
@@ -22,6 +22,7 @@ pub struct SingleExecMatcher {
|
||||
executable: String,
|
||||
args: Vec<Arg>,
|
||||
exec_in_parent_dir: bool,
|
||||
interactive: bool,
|
||||
}
|
||||
|
||||
impl SingleExecMatcher {
|
||||
@@ -29,6 +30,23 @@ impl SingleExecMatcher {
|
||||
executable: &str,
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Self::new_impl(executable, args, exec_in_parent_dir, false)
|
||||
}
|
||||
|
||||
pub fn new_interactive(
|
||||
executable: &str,
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Self::new_impl(executable, args, exec_in_parent_dir, true)
|
||||
}
|
||||
|
||||
fn new_impl(
|
||||
executable: &str,
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
interactive: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let transformed_args = args
|
||||
.iter()
|
||||
@@ -47,13 +65,13 @@ impl SingleExecMatcher {
|
||||
executable: executable.to_string(),
|
||||
args: transformed_args,
|
||||
exec_in_parent_dir,
|
||||
interactive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher for SingleExecMatcher {
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
let mut command = Command::new(&self.executable);
|
||||
fn matches(&self, file_info: &WalkEntry, matcher_io: &mut MatcherIO) -> bool {
|
||||
let path_to_file = if self.exec_in_parent_dir {
|
||||
if let Some(f) = file_info.path().file_name() {
|
||||
Path::new(".").join(f)
|
||||
@@ -64,6 +82,28 @@ impl Matcher for SingleExecMatcher {
|
||||
file_info.path().to_path_buf()
|
||||
};
|
||||
|
||||
if self.interactive {
|
||||
let rendered_args: Vec<String> = self
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| match arg {
|
||||
Arg::LiteralArg(a) => a.to_string_lossy().into_owned(),
|
||||
Arg::FileArg(parts) => parts
|
||||
.join(path_to_file.as_os_str())
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
})
|
||||
.collect();
|
||||
let mut prompt_parts = vec![self.executable.clone()];
|
||||
prompt_parts.extend(rendered_args);
|
||||
let prompt = format!("< {} >? ", prompt_parts.join(" "));
|
||||
|
||||
if !matcher_io.confirm(&prompt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let mut command = Command::new(&self.executable);
|
||||
for arg in &self.args {
|
||||
match *arg {
|
||||
Arg::LiteralArg(ref a) => command.arg(a.as_os_str()),
|
||||
|
||||
@@ -187,6 +187,14 @@ impl MatcherIO<'_> {
|
||||
pub fn now(&self) -> SystemTime {
|
||||
self.deps.now()
|
||||
}
|
||||
|
||||
/// Prompt the user and return whether they confirmed. Delegates to
|
||||
/// `Dependencies::confirm` so that matchers stay testable without a real
|
||||
/// terminal — unit tests inject preset responses via `FakeDependencies`.
|
||||
#[must_use]
|
||||
pub fn confirm(&self, prompt: &str) -> bool {
|
||||
self.deps.confirm(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
/// A basic interface that can be used to determine whether a directory entry
|
||||
@@ -668,6 +676,31 @@ fn build_matcher_tree(
|
||||
_ => unreachable!("Encountered unexpected value {}", args[arg_index]),
|
||||
}
|
||||
}
|
||||
"-ok" | "-okdir" => {
|
||||
// -ok is like -exec ... ; but prompts before each invocation.
|
||||
// Only ';' is accepted: POSIX does not define -ok ... + and
|
||||
// GNU find rejects it (batch mode makes no sense with prompts).
|
||||
let mut arg_index = i + 1;
|
||||
while arg_index < args.len() && args[arg_index] != ";" {
|
||||
arg_index += 1;
|
||||
}
|
||||
if arg_index < i + 2 || arg_index == args.len() {
|
||||
// Need at least the executable and the terminating ';'.
|
||||
return Err(From::from(format!("missing argument to {}", args[i])));
|
||||
}
|
||||
let expression = args[i];
|
||||
let executable = args[i + 1];
|
||||
let exec_args = &args[i + 2..arg_index];
|
||||
i = arg_index;
|
||||
Some(
|
||||
SingleExecMatcher::new_interactive(
|
||||
executable,
|
||||
exec_args,
|
||||
expression == "-okdir",
|
||||
)?
|
||||
.into_box(),
|
||||
)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
"-inum" => {
|
||||
if i >= args.len() - 1 {
|
||||
@@ -1620,6 +1653,67 @@ mod tests {
|
||||
.expect("only {} + should be considered a multi-exec");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_top_level_ok_not_enough_args() {
|
||||
// -ok follows the same validation rules as -exec for missing arguments.
|
||||
let mut config = Config::default();
|
||||
if let Err(e) = build_top_level_matcher(&["-ok"], &mut config) {
|
||||
assert!(e.to_string().contains("missing argument"));
|
||||
} else {
|
||||
panic!("parsing -ok with no executable or semicolon should fail");
|
||||
}
|
||||
|
||||
if let Err(e) = build_top_level_matcher(&["-ok", ";"], &mut config) {
|
||||
assert!(e.to_string().contains("missing argument"));
|
||||
} else {
|
||||
panic!("parsing -ok with no executable should fail");
|
||||
}
|
||||
|
||||
if let Err(e) = build_top_level_matcher(&["-ok", "foo"], &mut config) {
|
||||
assert!(e.to_string().contains("missing argument"));
|
||||
} else {
|
||||
panic!("parsing -ok without terminating ';' should fail");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_top_level_ok_missing_semicolon() {
|
||||
let mut config = Config::default();
|
||||
build_top_level_matcher(&["-ok", "echo", "{}"], &mut config)
|
||||
.err()
|
||||
.expect("parsing -ok without ';' should fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_top_level_ok_parses_correctly() {
|
||||
let mut config = Config::default();
|
||||
build_top_level_matcher(&["-ok", "echo", "{}", ";"], &mut config)
|
||||
.expect("-ok with executable, {} and ';' should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn build_top_level_ok_matches_with_confirmation() {
|
||||
// When the user confirms, -ok should match (run the command and return
|
||||
// its exit status). When the user declines, -ok is false and the
|
||||
// command is not run.
|
||||
let abbbc = get_dir_entry_for("./test_data/simple", "abbbc");
|
||||
let mut config = Config::default();
|
||||
|
||||
// Confirmed: -ok behaves like -exec (prints nothing; has_side_effects
|
||||
// suppresses the default print, same as -exec).
|
||||
let matcher = build_top_level_matcher(&["-ok", "true", ";"], &mut config).unwrap();
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(true);
|
||||
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
|
||||
|
||||
// Declined: -ok is false, so the default print fires.
|
||||
let matcher = build_top_level_matcher(&["-ok", "true", ";"], &mut config).unwrap();
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(false);
|
||||
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_top_level_multi_exec_too_many_holders() {
|
||||
let mut config = Config::default();
|
||||
|
||||
+68
-1
@@ -9,7 +9,7 @@ pub mod matchers;
|
||||
use matchers::{Follow, WalkEntry};
|
||||
use std::cell::RefCell;
|
||||
use std::error::Error;
|
||||
use std::io::{stderr, stdout, Write};
|
||||
use std::io::{stderr, stdout, BufRead, BufReader, IsTerminal, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::time::SystemTime;
|
||||
@@ -57,20 +57,44 @@ impl Default for Config {
|
||||
pub trait Dependencies {
|
||||
fn get_output(&self) -> &RefCell<dyn Write>;
|
||||
fn now(&self) -> SystemTime;
|
||||
/// Write `prompt` to stderr and return whether the user's response is
|
||||
/// affirmative (starts with 'y' or 'Y').
|
||||
///
|
||||
/// POSIX specifies that -ok writes a prompt to stderr but leaves the input
|
||||
/// source implementation-defined. GNU find reads from /dev/tty so that
|
||||
/// the answer comes from the real terminal even when stdin is redirected;
|
||||
/// BSD find reads from stdin unconditionally.
|
||||
fn confirm(&self, prompt: &str) -> bool;
|
||||
}
|
||||
|
||||
/// Struct that holds the dependencies we use when run as the real executable.
|
||||
pub struct StandardDependencies {
|
||||
output: Rc<RefCell<dyn Write>>,
|
||||
now: SystemTime,
|
||||
/// Open handle to /dev/tty for reading -ok responses, or None when stdin
|
||||
/// is not a terminal (pipe/file) or we are on Windows. Opened once at
|
||||
/// construction so we don't re-open it for every matched file.
|
||||
tty: Option<RefCell<BufReader<std::fs::File>>>,
|
||||
}
|
||||
|
||||
impl StandardDependencies {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
#[cfg(unix)]
|
||||
let tty = if std::io::stdin().is_terminal() {
|
||||
std::fs::File::open("/dev/tty")
|
||||
.ok()
|
||||
.map(|f| RefCell::new(BufReader::new(f)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let tty = None;
|
||||
|
||||
Self {
|
||||
output: Rc::new(RefCell::new(stdout())),
|
||||
now: SystemTime::now(),
|
||||
tty,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +113,31 @@ impl Dependencies for StandardDependencies {
|
||||
fn now(&self) -> SystemTime {
|
||||
self.now
|
||||
}
|
||||
|
||||
fn confirm(&self, prompt: &str) -> bool {
|
||||
// POSIX requires the prompt on stderr.
|
||||
eprint!("{}", prompt);
|
||||
let _ = stderr().flush();
|
||||
|
||||
// self.tty is Some when stdin was a terminal at startup: read from the
|
||||
// controlling terminal so responses come from the keyboard even when
|
||||
// stdin is occupied (e.g. `find -files0-from - -ok rm {} \;`).
|
||||
// Otherwise fall back to stdin — BSD find's behaviour, and what we
|
||||
// want when stdin is a pipe supplying scripted responses.
|
||||
// EOF and errors both yield None/Err, which unwrap_or_default turns
|
||||
// into an empty string — treated as "declined", matching GNU find.
|
||||
let response = if let Some(tty) = &self.tty {
|
||||
// Deref through RefMut to get &mut BufReader so lines() can take
|
||||
// it by value without moving out of the RefCell.
|
||||
(&mut *tty.borrow_mut()).lines().next()
|
||||
} else {
|
||||
std::io::stdin().lock().lines().next()
|
||||
}
|
||||
.and_then(Result::ok)
|
||||
.unwrap_or_default();
|
||||
|
||||
response.trim_start().starts_with(['y', 'Y'])
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of parsing the command-line arguments into useful forms.
|
||||
@@ -298,6 +347,7 @@ Early alpha implementation. Currently the only expressions supported are
|
||||
-perm [-/]{{octal|u=rwx,go=w}}
|
||||
-newer path_to_file
|
||||
-exec[dir] executable [args] [{{}}] [more args] ;
|
||||
-ok[dir] executable [args] [{{}}] [more args] ;
|
||||
-sorted
|
||||
a non-standard extension that sorts directory contents by name before
|
||||
processing them. Less efficient, but allows for deterministic output.
|
||||
@@ -360,6 +410,8 @@ mod tests {
|
||||
pub struct FakeDependencies {
|
||||
pub output: RefCell<Cursor<Vec<u8>>>,
|
||||
now: SystemTime,
|
||||
/// Preset responses for confirm(), consumed front-to-back.
|
||||
confirm_responses: RefCell<std::collections::VecDeque<bool>>,
|
||||
}
|
||||
|
||||
impl<'a> FakeDependencies {
|
||||
@@ -367,6 +419,7 @@ mod tests {
|
||||
Self {
|
||||
output: RefCell::new(Cursor::new(Vec::<u8>::new())),
|
||||
now: SystemTime::now(),
|
||||
confirm_responses: RefCell::new(std::collections::VecDeque::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,6 +438,11 @@ mod tests {
|
||||
cursor.read_to_string(&mut contents).unwrap();
|
||||
contents
|
||||
}
|
||||
|
||||
/// Queue a response to be returned by the next call to confirm().
|
||||
pub fn push_confirm_response(&self, response: bool) {
|
||||
self.confirm_responses.borrow_mut().push_back(response);
|
||||
}
|
||||
}
|
||||
|
||||
impl Dependencies for FakeDependencies {
|
||||
@@ -395,6 +453,15 @@ mod tests {
|
||||
fn now(&self) -> SystemTime {
|
||||
self.now
|
||||
}
|
||||
|
||||
fn confirm(&self, _prompt: &str) -> bool {
|
||||
// Return the next preset response; default to false (decline) so
|
||||
// that a test that forgets to queue a response fails safely.
|
||||
self.confirm_responses
|
||||
.borrow_mut()
|
||||
.pop_front()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file_link() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::env;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::path::Path;
|
||||
@@ -19,6 +20,8 @@ use findutils::find::Dependencies;
|
||||
pub struct FakeDependencies {
|
||||
pub output: RefCell<Cursor<Vec<u8>>>,
|
||||
now: SystemTime,
|
||||
/// Preset responses for confirm(), consumed front-to-back.
|
||||
confirm_responses: RefCell<VecDeque<bool>>,
|
||||
}
|
||||
|
||||
impl FakeDependencies {
|
||||
@@ -26,6 +29,7 @@ impl FakeDependencies {
|
||||
Self {
|
||||
output: RefCell::new(Cursor::new(Vec::<u8>::new())),
|
||||
now: SystemTime::now(),
|
||||
confirm_responses: RefCell::new(VecDeque::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +44,11 @@ impl FakeDependencies {
|
||||
cursor.read_to_string(&mut contents).unwrap();
|
||||
contents
|
||||
}
|
||||
|
||||
/// Queue a response to be returned by the next call to confirm().
|
||||
pub fn push_confirm_response(&self, response: bool) {
|
||||
self.confirm_responses.borrow_mut().push_back(response);
|
||||
}
|
||||
}
|
||||
|
||||
impl Dependencies for FakeDependencies {
|
||||
@@ -50,6 +59,15 @@ impl Dependencies for FakeDependencies {
|
||||
fn now(&self) -> SystemTime {
|
||||
self.now
|
||||
}
|
||||
|
||||
fn confirm(&self, _prompt: &str) -> bool {
|
||||
// Return the next preset response; default to false (decline) so
|
||||
// that a test that forgets to queue a response fails safely.
|
||||
self.confirm_responses
|
||||
.borrow_mut()
|
||||
.pop_front()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_to_testing_commandline() -> String {
|
||||
|
||||
@@ -336,3 +336,137 @@ fn multi_set_exit_code_if_command_fails() {
|
||||
matcher.finished_dir(Path::new("test_data/simple"), &mut matcher_io);
|
||||
assert!(matcher_io.exit_code() == 1);
|
||||
}
|
||||
|
||||
// -ok / -okdir tests
|
||||
//
|
||||
// These use FakeDependencies, which answers confirm() from a preset queue
|
||||
// rather than a real terminal. That tests both the "confirmed" and
|
||||
// "declined" paths without needing a TTY. The integration tests in
|
||||
// test_find.rs cover the stdin-fallback path (no controlling terminal).
|
||||
|
||||
#[test]
|
||||
fn ok_executes_when_confirmed() {
|
||||
// When the user confirms, -ok runs the command and returns true on success.
|
||||
let temp_dir = Builder::new()
|
||||
.prefix("ok_executes_when_confirmed")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let temp_dir_path = temp_dir.path().to_string_lossy();
|
||||
|
||||
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
|
||||
let matcher = SingleExecMatcher::new_interactive(
|
||||
&path_to_testing_commandline(),
|
||||
&[temp_dir_path.as_ref(), "abc", "{}", "xyz"],
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create matcher");
|
||||
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(true);
|
||||
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
|
||||
|
||||
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
|
||||
let mut s = String::new();
|
||||
f.read_to_string(&mut s)
|
||||
.expect("failed to read output file");
|
||||
assert_eq!(
|
||||
s,
|
||||
fix_up_slashes(&format!(
|
||||
"cwd={}\nargs=\nabc\ntest_data/simple/abbbc\nxyz\n",
|
||||
env::current_dir().unwrap().to_string_lossy()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ok_skips_when_declined() {
|
||||
// When the user declines, -ok returns false without running the command.
|
||||
let temp_dir = Builder::new()
|
||||
.prefix("ok_skips_when_declined")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let temp_dir_path = temp_dir.path().to_string_lossy();
|
||||
|
||||
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
|
||||
let matcher = SingleExecMatcher::new_interactive(
|
||||
&path_to_testing_commandline(),
|
||||
&[temp_dir_path.as_ref(), "abc", "{}", "xyz"],
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create matcher");
|
||||
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(false);
|
||||
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
|
||||
|
||||
// The command was not run, so no output file should exist.
|
||||
assert!(
|
||||
!temp_dir.path().join("1.txt").exists(),
|
||||
"command should not have run when user declined"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn okdir_executes_in_parent_dir_when_confirmed() {
|
||||
// -okdir runs the command in the file's parent directory, same as -execdir.
|
||||
let temp_dir = Builder::new()
|
||||
.prefix("okdir_executes_when_confirmed")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let temp_dir_path = temp_dir.path().to_string_lossy();
|
||||
|
||||
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
|
||||
let matcher = SingleExecMatcher::new_interactive(
|
||||
&path_to_testing_commandline(),
|
||||
&[temp_dir_path.as_ref(), "abc", "{}", "xyz"],
|
||||
true, // exec_in_parent_dir = true → -okdir behaviour
|
||||
)
|
||||
.expect("Failed to create matcher");
|
||||
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(true);
|
||||
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
|
||||
|
||||
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
|
||||
let mut s = String::new();
|
||||
f.read_to_string(&mut s)
|
||||
.expect("failed to read output file");
|
||||
assert_eq!(
|
||||
s,
|
||||
fix_up_slashes(&format!(
|
||||
"cwd={}/test_data/simple\nargs=\nabc\n./abbbc\nxyz\n",
|
||||
env::current_dir().unwrap().to_string_lossy()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ok_returns_false_when_command_fails() {
|
||||
// When the user confirms but the command exits non-zero, -ok returns false.
|
||||
let temp_dir = Builder::new()
|
||||
.prefix("ok_returns_false_when_command_fails")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let temp_dir_path = temp_dir.path().to_string_lossy();
|
||||
|
||||
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
|
||||
let matcher = SingleExecMatcher::new_interactive(
|
||||
&path_to_testing_commandline(),
|
||||
&[
|
||||
temp_dir_path.as_ref(),
|
||||
"--exit_with_failure",
|
||||
"abc",
|
||||
"{}",
|
||||
"xyz",
|
||||
],
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create matcher");
|
||||
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(true);
|
||||
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
|
||||
|
||||
// The command did run (output file exists), but it failed.
|
||||
assert!(temp_dir.path().join("1.txt").exists());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,84 @@ use common::test_helpers::{fix_up_slashes, path_to_testing_commandline, FakeDepe
|
||||
use findutils::find::find_main;
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn find_ok_confirmed() {
|
||||
let temp_dir = Builder::new()
|
||||
.prefix("find_ok_confirmed")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let temp_dir_path = temp_dir.path().to_string_lossy();
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(true);
|
||||
|
||||
let rc = find_main(
|
||||
&[
|
||||
"find",
|
||||
&fix_up_slashes("./test_data/simple"),
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-ok",
|
||||
&path_to_testing_commandline(),
|
||||
temp_dir_path.as_ref(),
|
||||
"abc",
|
||||
"{}",
|
||||
"xyz",
|
||||
";",
|
||||
],
|
||||
&deps,
|
||||
);
|
||||
|
||||
assert_eq!(rc, 0);
|
||||
assert_eq!(deps.get_output_as_string(), "");
|
||||
|
||||
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
|
||||
let mut s = String::new();
|
||||
f.read_to_string(&mut s)
|
||||
.expect("failed to read output file");
|
||||
assert_eq!(
|
||||
s,
|
||||
fix_up_slashes(&format!(
|
||||
"cwd={}\nargs=\nabc\n./test_data/simple/abbbc\nxyz\n",
|
||||
env::current_dir().unwrap().to_string_lossy()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ok_declined() {
|
||||
let temp_dir = Builder::new().prefix("find_ok_declined").tempdir().unwrap();
|
||||
let temp_dir_path = temp_dir.path().to_string_lossy();
|
||||
let deps = FakeDependencies::new();
|
||||
deps.push_confirm_response(false);
|
||||
|
||||
let rc = find_main(
|
||||
&[
|
||||
"find",
|
||||
&fix_up_slashes("./test_data/simple"),
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-ok",
|
||||
&path_to_testing_commandline(),
|
||||
temp_dir_path.as_ref(),
|
||||
"abc",
|
||||
"{}",
|
||||
"xyz",
|
||||
";",
|
||||
],
|
||||
&deps,
|
||||
);
|
||||
|
||||
assert_eq!(rc, 0);
|
||||
// -ok is false when declined, so no default print either.
|
||||
assert_eq!(deps.get_output_as_string(), "");
|
||||
// Command did not run.
|
||||
assert!(!temp_dir.path().join("1.txt").exists());
|
||||
}
|
||||
#[test]
|
||||
fn find_exec() {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
|
||||
@@ -1001,3 +1001,153 @@ fn find_slashes() {
|
||||
.succeeds()
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
// -ok / -okdir integration tests
|
||||
//
|
||||
// These tests use pipe_in() to supply the user's response. Because pipe_in()
|
||||
// makes stdin a pipe, std::io::stdin().is_terminal() returns false inside the
|
||||
// find subprocess, so StandardDependencies::confirm reads from stdin directly
|
||||
// instead of opening /dev/tty. No special environment variable is needed.
|
||||
|
||||
#[test]
|
||||
fn find_ok_yes_runs_command() {
|
||||
// When the user answers "y", -ok should run the command and print output.
|
||||
ucmd()
|
||||
.args(&[
|
||||
"test_data/simple",
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-ok",
|
||||
"echo",
|
||||
"{}",
|
||||
";",
|
||||
])
|
||||
// Pipe the affirmative response for the single file found.
|
||||
.pipe_in("y\n")
|
||||
.succeeds()
|
||||
.stderr_contains("< echo") // prompt appeared
|
||||
.stdout_contains("abbbc"); // echo ran
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ok_no_skips_command() {
|
||||
// When the user answers "n", -ok should not run the command.
|
||||
// The expression is false so no output is produced, but find exits 0.
|
||||
ucmd()
|
||||
.args(&[
|
||||
"test_data/simple",
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-ok",
|
||||
"echo",
|
||||
"{}",
|
||||
";",
|
||||
])
|
||||
.pipe_in("n\n")
|
||||
.succeeds()
|
||||
.stderr_contains("< echo") // prompt still appeared
|
||||
.no_stdout(); // but echo was not run
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ok_prompt_format() {
|
||||
// The prompt should follow GNU find's "< executable args... >? " format.
|
||||
ucmd()
|
||||
.args(&[
|
||||
"test_data/simple",
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-ok",
|
||||
"echo",
|
||||
"{}",
|
||||
";",
|
||||
])
|
||||
.pipe_in("n\n")
|
||||
.succeeds()
|
||||
.stderr_contains(format!(
|
||||
"< echo {} >? ",
|
||||
Path::new("test_data/simple")
|
||||
.join("abbbc")
|
||||
.to_string_lossy()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ok_empty_response_declines() {
|
||||
// An empty line (just Enter) should be treated as decline.
|
||||
ucmd()
|
||||
.args(&[
|
||||
"test_data/simple",
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-ok",
|
||||
"echo",
|
||||
"{}",
|
||||
";",
|
||||
])
|
||||
.pipe_in("\n")
|
||||
.succeeds()
|
||||
.no_stdout();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ok_accepts_y_variants() {
|
||||
// "Y", "yes", and " y" (leading whitespace) should all be accepted.
|
||||
for response in &["Y\n", "yes\n", " y\n"] {
|
||||
ucmd()
|
||||
.args(&[
|
||||
"test_data/simple",
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-ok",
|
||||
"echo",
|
||||
"{}",
|
||||
";",
|
||||
])
|
||||
.pipe_in(*response)
|
||||
.succeeds()
|
||||
.stdout_contains("abbbc");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_okdir_yes_runs_command() {
|
||||
// -okdir should run the command in the file's parent directory.
|
||||
ucmd()
|
||||
.args(&[
|
||||
"test_data/simple",
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-name",
|
||||
"abbbc",
|
||||
"-okdir",
|
||||
"echo",
|
||||
"{}",
|
||||
";",
|
||||
])
|
||||
.pipe_in("y\n")
|
||||
.succeeds()
|
||||
.stderr_contains("< echo")
|
||||
.stdout_contains(fix_up_slashes("./abbbc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ok_missing_semicolon() {
|
||||
// -ok without a closing ';' should be an error (just like -exec).
|
||||
ucmd()
|
||||
.args(&["test_data/simple", "-ok", "echo", "{}"])
|
||||
.pipe_in("")
|
||||
.fails()
|
||||
.stderr_contains("missing argument to -ok")
|
||||
.no_stdout();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user