Files
Jesse Rosenstock 5269e233a6 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.
2026-06-04 20:31:54 +02:00

119 lines
3.5 KiB
Rust

// Copyright 2017 Google Inc.
//
// 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.
use std::cell::RefCell;
use std::collections::VecDeque;
use std::env;
use std::io::{Cursor, Read, Write};
use std::path::Path;
use std::time::SystemTime;
use findutils::find::matchers::{Follow, MatcherIO, WalkEntry};
use findutils::find::Dependencies;
/// A copy of `find::tests::FakeDependencies`.
/// TODO: find out how to share #[cfg(test)] functions/structs between unit
/// and integration tests.
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 {
pub fn new() -> Self {
Self {
output: RefCell::new(Cursor::new(Vec::<u8>::new())),
now: SystemTime::now(),
confirm_responses: RefCell::new(VecDeque::new()),
}
}
pub fn new_matcher_io(&self) -> MatcherIO<'_> {
MatcherIO::new(self)
}
pub fn get_output_as_string(&self) -> String {
let mut cursor = self.output.borrow_mut();
cursor.set_position(0);
let mut contents = String::new();
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 {
fn get_output(&self) -> &RefCell<dyn Write> {
&self.output
}
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 {
let mut path_to_use = env::current_exe()
// this will be something along the lines of /my/homedir/findutils/target/debug/deps/findutils-5532804878869ef1
.expect("can't find path of this executable")
.parent()
.expect("can't find parent directory of this executable")
.to_path_buf();
// and we want /my/homedir/findutils/target/debug/testing-commandline
if path_to_use.ends_with("deps") {
path_to_use.pop();
}
path_to_use = path_to_use.join("testing-commandline");
path_to_use.to_string_lossy().to_string()
}
#[cfg(windows)]
/// A copy of find::tests::fix_up_slashes.
/// TODO: find out how to share #[cfg(test)] functions/structs between unit
/// and integration tests.
pub fn fix_up_slashes(path: &str) -> String {
path.replace("/", "\\")
}
#[cfg(not(windows))]
pub fn fix_up_slashes(path: &str) -> String {
path.to_string()
}
/// A copy of `find::matchers::tests::get_dir_entry_for`.
/// TODO: find out how to share #[cfg(test)] functions/structs between unit
/// and integration tests.
pub fn get_dir_entry_for(root: &str, path: &str) -> WalkEntry {
let root = fix_up_slashes(root);
let root = Path::new(&root);
let path = fix_up_slashes(path);
let path = if path.is_empty() {
root.to_owned()
} else {
root.join(path)
};
let depth = path.components().count() - root.components().count();
WalkEntry::new(path, depth, Follow::Never)
}