Files
findutils/tests/find_exec_tests.rs
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

289 lines
8.2 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.
/// ! This file contains what would be normally be unit tests for `find::find_main`
/// ! related to -exec[dir] and ok[dir] clauses.
/// ! But as the tests require running an external executable, they need to be run
/// ! as integration tests so we can ensure that our testing-commandline binary
/// ! has been built.
use std::env;
use std::fs::File;
use std::io::Read;
use tempfile::Builder;
use common::test_helpers::{fix_up_slashes, path_to_testing_commandline, FakeDependencies};
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()
.prefix("find_exec")
.tempdir()
.unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let deps = FakeDependencies::new();
let rc = find_main(
&[
"find",
&fix_up_slashes("./test_data/simple/subdir"),
"-type",
"f",
"-exec",
&path_to_testing_commandline(),
temp_dir_path.as_ref(),
"(",
"{}",
"-o",
";",
],
&deps,
);
assert_eq!(rc, 0);
// exec has side effects, so we won't output anything unless -print is
// explicitly passed in.
assert_eq!(deps.get_output_as_string(), "");
// check the executable ran as expected
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=\n(\n./test_data/simple/subdir/ABBBC\n-o\n",
env::current_dir().unwrap().to_string_lossy()
))
);
}
#[test]
fn find_execdir() {
let temp_dir = Builder::new().prefix("example").tempdir().unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let deps = FakeDependencies::new();
// only look at files because the "size" of a directory is a system (and filesystem)
// dependent thing and we want these tests to be universal.
let rc = find_main(
&[
"find",
&fix_up_slashes("./test_data/simple/subdir"),
"-type",
"f",
"-execdir",
&path_to_testing_commandline(),
temp_dir_path.as_ref(),
")",
"{}",
",",
";",
],
&deps,
);
assert_eq!(rc, 0);
// exec has side effects, so we won't output anything unless -print is
// explicitly passed in.
assert_eq!(deps.get_output_as_string(), "");
// check the executable ran as expected
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/subdir\nargs=\n)\n./ABBBC\n,\n",
env::current_dir().unwrap().to_string_lossy()
))
);
}
#[test]
fn find_exec_multi() {
let temp_dir = tempfile::Builder::new()
.prefix("find_exec_multi")
.tempdir()
.unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let deps = FakeDependencies::new();
let rc = find_main(
&[
"find",
&fix_up_slashes("./test_data/simple"),
"-type",
"f",
"-exec",
&path_to_testing_commandline(),
temp_dir_path.as_ref(),
"--sort",
"(",
"-o",
"{}",
"+",
],
&deps,
);
assert_eq!(rc, 0);
// exec has side effects, so we won't output anything unless -print is
// explicitly passed in.
assert_eq!(deps.get_output_as_string(), "");
// check the executable ran as expected
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=\n(\n--sort\n-o\n./test_data/simple/abbbc\n./test_data/simple/subdir/ABBBC\n",
env::current_dir().unwrap().to_string_lossy()
))
);
}
#[test]
fn find_execdir_multi() {
let temp_dir = Builder::new()
.prefix("find_execdir_multi")
.tempdir()
.unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let deps = FakeDependencies::new();
// only look at files because the "size" of a directory is a system (and filesystem)
// dependent thing and we want these tests to be universal.
let rc = find_main(
&[
"find",
&fix_up_slashes("./test_data/simple"),
"-maxdepth",
"1",
"-execdir",
&path_to_testing_commandline(),
temp_dir_path.as_ref(),
"--sort",
")",
"{}",
"+",
],
&deps,
);
assert_eq!(rc, 0);
// exec has side effects, so we won't output anything unless -print is
// explicitly passed in.
assert_eq!(deps.get_output_as_string(), "");
// check the executable ran as expected
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\nargs=\n)\n--sort\n./simple\n",
env::current_dir().unwrap().to_string_lossy()
))
);
let mut f = File::open(temp_dir.path().join("2.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=\n)\n--sort\n./abbbc\n./subdir\n",
env::current_dir().unwrap().to_string_lossy()
))
);
}