mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
5269e233a6
-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.
119 lines
3.5 KiB
Rust
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)
|
|
}
|