mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62deaf6e60 | |||
| 1be5541c74 | |||
| 5d0e7add34 | |||
| 42f8a9af36 | |||
| 664a9c23e6 | |||
| fd4bd6acfa | |||
| fa4f90befb | |||
| 7deeee6fdf | |||
| 51a50022c2 | |||
| 600f3af140 | |||
| a63482b8cf | |||
| 47796303ca | |||
| baa6ce5d92 | |||
| 9f33c14fc9 | |||
| a80620c88f | |||
| a2c75f94ca | |||
| 44fbc59263 | |||
| fb9f8df047 | |||
| 2044d1e31d | |||
| 0494ba1be4 | |||
| fc446aba0b | |||
| 33703b79d8 | |||
| c5ff1bbb6b | |||
| e7c34ab12f | |||
| 1a77a83432 | |||
| a65160f724 | |||
| 435f00d491 | |||
| db941e7fba | |||
| 5bef01f72f |
@@ -0,0 +1,37 @@
|
||||
name: CodSpeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
# `workflow_dispatch` allows CodSpeed to trigger backtest
|
||||
# performance analysis in order to generate initial data.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
codspeed:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@v0
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
install --strip -Dm755 target/release/{find,xargs} -t findutils-x86_64-unknown-linux-gnu
|
||||
ZSTD_CLEVEL=19 tar --zstd -caf ../findutils-x86_64-unknown-linux-gnu.tar.zst findutils-x86_64-unknown-linux-gnu
|
||||
- name: Publish latest commit
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
tag_name: latest-commit
|
||||
|
||||
@@ -272,7 +272,7 @@ jobs:
|
||||
# Write and read notes from a file to avoid quoting breaking things
|
||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --draft --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
|
||||
announce:
|
||||
needs:
|
||||
|
||||
Generated
+646
-23
File diff suppressed because it is too large
Load Diff
+66
-11
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "findutils"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
homepage = "https://github.com/uutils/findutils"
|
||||
repository = "https://github.com/uutils/findutils"
|
||||
edition = "2021"
|
||||
@@ -11,26 +11,55 @@ authors = ["uutils developers"]
|
||||
|
||||
[dependencies]
|
||||
argmax = "0.4.0"
|
||||
chrono = "0.4.44"
|
||||
clap = "4.6"
|
||||
chrono = "0.4.45"
|
||||
clap = { version = "4.6", features = ["env"] }
|
||||
faccess = "0.2.4"
|
||||
nix = { version = "0.31", features = ["fs", "user"] }
|
||||
onig = { version = "6.5", default-features = false }
|
||||
regex = "1.12"
|
||||
uucore = { version = "0.9.0", features = ["entries", "fs", "fsext", "mode"] }
|
||||
walkdir = "2.5"
|
||||
itertools = "0.14.0"
|
||||
thiserror = "2.0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "4.7.0", package = "codspeed-criterion-compat" }
|
||||
assert_cmd = "2"
|
||||
ctor = "1.0"
|
||||
filetime = "0.2"
|
||||
nix = { version = "0.31", features = ["fs"] }
|
||||
rstest = "0.26.1"
|
||||
tempfile = "3"
|
||||
uutests = "0.9.0"
|
||||
|
||||
[[bench]]
|
||||
name = "find_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "xargs_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "updatedb_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "locate_bench"
|
||||
harness = false
|
||||
|
||||
[[bin]]
|
||||
name = "find"
|
||||
path = "src/find/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "locate"
|
||||
path = "src/locate/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "updatedb"
|
||||
path = "src/updatedb/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "xargs"
|
||||
path = "src/xargs/main.rs"
|
||||
@@ -48,17 +77,43 @@ inherits = "release"
|
||||
panic = "abort"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
use_self = "warn" # nursery lint
|
||||
|
||||
# Group-level allows for lints that are too noisy or not worth enforcing for now.
|
||||
multiple_crate_versions = "allow"
|
||||
cargo_common_metadata = "allow"
|
||||
uninlined_format_args = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
|
||||
use_self = "warn"
|
||||
needless_pass_by_value = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
single_char_pattern = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
if_not_else = "warn"
|
||||
manual_let_else = "warn"
|
||||
must_use_candidate = "allow"
|
||||
match_same_arms = "allow"
|
||||
cast_possible_truncation = "allow"
|
||||
too_many_lines = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
struct_excessive_bools = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_lossless = "allow"
|
||||
ignored_unit_patterns = "allow"
|
||||
similar_names = "allow"
|
||||
float_cmp = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
inline_always = "allow"
|
||||
fn_params_excessive_bools = "allow"
|
||||
used_underscore_items = "allow"
|
||||
should_panic_without_expect = "allow"
|
||||
doc_markdown = "allow"
|
||||
unused_self = "allow"
|
||||
enum_glob_use = "allow"
|
||||
unnested_or_patterns = "allow"
|
||||
implicit_hasher = "allow"
|
||||
doc_link_with_quotes = "allow"
|
||||
format_push_string = "allow"
|
||||
flat_map_option = "allow"
|
||||
from_iter_instead_of_collect = "allow"
|
||||
large_types_passed_by_value = "allow"
|
||||
# Disable for now, we have a few occurrences
|
||||
# panic = "warn"
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// Copyright 2024 the uutils developers
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! End-to-end benchmarks for `find`, run through the real `find_main` entry
|
||||
//! point so they exercise argument parsing, the matcher tree and the directory
|
||||
//! walk together. Output is sent to a sink so the benchmarks measure the work
|
||||
//! `find` does rather than terminal I/O.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use findutils::find::{find_main, Dependencies};
|
||||
|
||||
/// `Dependencies` implementation that throws output away. A fixed `now` keeps
|
||||
/// time-based matchers (`-newer`, `-mtime`, …) deterministic across runs.
|
||||
struct SinkDependencies {
|
||||
output: RefCell<io::Sink>,
|
||||
now: SystemTime,
|
||||
}
|
||||
|
||||
impl SinkDependencies {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
output: RefCell::new(io::sink()),
|
||||
now: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dependencies for SinkDependencies {
|
||||
fn get_output(&self) -> &RefCell<dyn Write> {
|
||||
&self.output
|
||||
}
|
||||
|
||||
fn now(&self) -> SystemTime {
|
||||
self.now
|
||||
}
|
||||
|
||||
fn confirm(&self, _prompt: &str) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `find` end-to-end. `args` are the arguments after the program name
|
||||
/// (flags, paths, expression). The exit status is ignored — we only care about
|
||||
/// the work performed.
|
||||
fn run(args: &[&str]) {
|
||||
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
|
||||
argv.push("find");
|
||||
argv.extend_from_slice(args);
|
||||
let deps = SinkDependencies::new();
|
||||
let _ = find_main(&argv, &deps);
|
||||
}
|
||||
|
||||
/// Build a moderately deep directory tree to walk. `depth` levels, each holding
|
||||
/// `dirs` sub-directories and `files` files. File names vary so name/regex
|
||||
/// matchers have something to discriminate on, and sizes vary so `-size` does
|
||||
/// real work. Returns the root directory.
|
||||
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
|
||||
let root = std::env::temp_dir().join(format!("uu_find_bench_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
populate(&root, depth, dirs, files, 0);
|
||||
root
|
||||
}
|
||||
|
||||
fn populate(dir: &Path, depth: u32, dirs: u32, files: u32, seed: u32) {
|
||||
for f in 0..files {
|
||||
let n = seed.wrapping_add(f);
|
||||
// A mix of extensions and a sprinkling of a rare marker name.
|
||||
let name = match n % 5 {
|
||||
0 => format!("module_{n}.rs"),
|
||||
1 => format!("data_{n}.txt"),
|
||||
2 => format!("image_{n}.png"),
|
||||
3 => format!("README_{n}.md"),
|
||||
_ if n % 500 == 0 => format!("RAREHIT_{n}.log"),
|
||||
_ => format!("file_{n}.log"),
|
||||
};
|
||||
// Sizes from a few bytes up to ~8 KiB so -size buckets differ.
|
||||
let size = (n as usize % 8192) + 1;
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, vec![b'x'; size]).unwrap();
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for d in 0..dirs {
|
||||
let sub = dir.join(format!("dir_{d}"));
|
||||
std::fs::create_dir_all(&sub).unwrap();
|
||||
populate(
|
||||
&sub,
|
||||
depth - 1,
|
||||
dirs,
|
||||
files,
|
||||
seed.wrapping_add((d + 1) * 31),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_e2e(c: &mut Criterion) {
|
||||
// depth 4, 4 dirs/level, 25 files/dir → a few thousand entries.
|
||||
let root = build_tree(4, 4, 25);
|
||||
let dir = root.to_str().unwrap();
|
||||
|
||||
let mut group = c.benchmark_group("find");
|
||||
|
||||
// Plain full walk with the implicit -print.
|
||||
group.bench_function("walk_all", |b| {
|
||||
b.iter(|| run(black_box(&[dir])));
|
||||
});
|
||||
// Filter by file type only.
|
||||
group.bench_function("type_f", |b| {
|
||||
b.iter(|| run(black_box(&[dir, "-type", "f"])));
|
||||
});
|
||||
// Glob name match — a common invocation.
|
||||
group.bench_function("name_glob", |b| {
|
||||
b.iter(|| run(black_box(&[dir, "-name", "*.rs"])));
|
||||
});
|
||||
// Case-insensitive name match.
|
||||
group.bench_function("iname_glob", |b| {
|
||||
b.iter(|| run(black_box(&[dir, "-iname", "*.RS"])));
|
||||
});
|
||||
// Regex over the whole path.
|
||||
group.bench_function("regex_path", |b| {
|
||||
b.iter(|| run(black_box(&[dir, "-regex", r".*/module_[0-9]+\.rs"])));
|
||||
});
|
||||
// Size predicate forces a stat per entry.
|
||||
group.bench_function("size_gt", |b| {
|
||||
b.iter(|| run(black_box(&[dir, "-type", "f", "-size", "+4k"])));
|
||||
});
|
||||
// Combined predicate with AND/OR and grouping.
|
||||
group.bench_function("combined_expr", |b| {
|
||||
b.iter(|| {
|
||||
run(black_box(&[
|
||||
dir, "-type", "f", "(", "-name", "*.rs", "-o", "-name", "*.md", ")",
|
||||
]));
|
||||
});
|
||||
});
|
||||
// Prune whole subtrees, then print the rest.
|
||||
group.bench_function("prune", |b| {
|
||||
b.iter(|| {
|
||||
run(black_box(&[
|
||||
dir, "-name", "dir_0", "-prune", "-o", "-type", "f", "-print",
|
||||
]));
|
||||
});
|
||||
});
|
||||
// -printf with several directives exercises the formatter.
|
||||
group.bench_function("printf", |b| {
|
||||
b.iter(|| run(black_box(&[dir, "-type", "f", "-printf", "%p %s %y\\n"])));
|
||||
});
|
||||
|
||||
group.finish();
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_e2e);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2024 the uutils developers
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! End-to-end benchmarks for `locate`, run through the real `locate_main` entry
|
||||
//! point. A LOCATE02 database is built once (via `updatedb`) from a generated
|
||||
//! tree, then each case scans it in `-c`/count mode so the database decoding
|
||||
//! and pattern matching dominate rather than terminal output.
|
||||
//!
|
||||
//! `locate` is Unix-only (it relies on `OsStr`/`CStr` byte APIs), so the
|
||||
//! benchmark body is compiled only there.
|
||||
|
||||
#[cfg(unix)]
|
||||
mod bench {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use criterion::{black_box, criterion_group, Criterion};
|
||||
use findutils::locate::locate_main;
|
||||
use findutils::updatedb::updatedb_main;
|
||||
|
||||
/// Run `locate` end-to-end. `args` are the arguments after the program name.
|
||||
fn run(args: &[&str]) {
|
||||
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
|
||||
argv.push("locate");
|
||||
argv.extend_from_slice(args);
|
||||
let _ = locate_main(&argv);
|
||||
}
|
||||
|
||||
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
|
||||
let root = std::env::temp_dir().join(format!("uu_locate_bench_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
populate(&root, depth, dirs, files, 0);
|
||||
root
|
||||
}
|
||||
|
||||
fn populate(dir: &Path, depth: u32, dirs: u32, files: u32, seed: u32) {
|
||||
for f in 0..files {
|
||||
let n = seed.wrapping_add(f);
|
||||
let name = match n % 4 {
|
||||
0 => format!("module_{n}.rs"),
|
||||
1 => format!("data_{n}.txt"),
|
||||
2 => format!("image_{n}.png"),
|
||||
_ => format!("file_{n}.log"),
|
||||
};
|
||||
std::fs::write(dir.join(name), b"x").unwrap();
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for d in 0..dirs {
|
||||
let sub = dir.join(format!("dir_{d}"));
|
||||
std::fs::create_dir_all(&sub).unwrap();
|
||||
populate(
|
||||
&sub,
|
||||
depth - 1,
|
||||
dirs,
|
||||
files,
|
||||
seed.wrapping_add((d + 1) * 31),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bench_e2e(c: &mut Criterion) {
|
||||
// ~8.5k paths, then front-coded into a LOCATE02 database once.
|
||||
let root = build_tree(4, 4, 25);
|
||||
let dir = root.to_str().unwrap();
|
||||
let db = std::env::temp_dir().join(format!("uu_locate_bench_{}.db", std::process::id()));
|
||||
let db_path = db.to_str().unwrap();
|
||||
|
||||
let _ = updatedb_main(&[
|
||||
"updatedb",
|
||||
&format!("--localpaths={dir}"),
|
||||
&format!("--output={db_path}"),
|
||||
"--prunepaths=",
|
||||
"--prunefs=",
|
||||
]);
|
||||
|
||||
let mut group = c.benchmark_group("locate");
|
||||
|
||||
// Pattern matching nothing: forces a full decode + match of every entry.
|
||||
group.bench_function("count_no_match", |b| {
|
||||
b.iter(|| run(black_box(&["-d", db_path, "-c", "NONEXISTENT_TOKEN_XYZ"])));
|
||||
});
|
||||
// Substring matching many entries (count mode → no per-match output).
|
||||
group.bench_function("count_many", |b| {
|
||||
b.iter(|| run(black_box(&["-d", db_path, "-c", ".log"])));
|
||||
});
|
||||
// Basename matching (-b) restricts the comparison to the file name.
|
||||
group.bench_function("basename", |b| {
|
||||
b.iter(|| run(black_box(&["-d", db_path, "-c", "-b", "module"])));
|
||||
});
|
||||
// Case-insensitive matching (-i).
|
||||
group.bench_function("ignore_case", |b| {
|
||||
b.iter(|| run(black_box(&["-d", db_path, "-c", "-i", "MODULE"])));
|
||||
});
|
||||
// Regex matching (-r) over the whole path.
|
||||
group.bench_function("regex", |b| {
|
||||
b.iter(|| {
|
||||
run(black_box(&[
|
||||
"-d",
|
||||
db_path,
|
||||
"-c",
|
||||
"-r",
|
||||
r"module_[0-9]+\.rs",
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let _ = std::fs::remove_file(&db);
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_e2e);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
criterion::criterion_main!(bench::benches);
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2024 the uutils developers
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! End-to-end benchmarks for `updatedb`, run through the real `updatedb_main`
|
||||
//! entry point. Each case walks a generated directory tree (via the internal
|
||||
//! `find`), encodes it into the LOCATE02 database format and writes the result
|
||||
//! to a temp file, so the walk + front-coding + write dominate the measurement.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use findutils::updatedb::updatedb_main;
|
||||
|
||||
/// Run `updatedb` end-to-end. `args` are the arguments after the program name.
|
||||
fn run(args: &[&str]) {
|
||||
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
|
||||
argv.push("updatedb");
|
||||
argv.extend_from_slice(args);
|
||||
let _ = updatedb_main(&argv);
|
||||
}
|
||||
|
||||
/// Build a directory tree to index. `depth` levels, each holding `dirs`
|
||||
/// sub-directories and `files` files with varied names. Returns the root.
|
||||
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
|
||||
let root = std::env::temp_dir().join(format!("uu_updatedb_bench_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
populate(&root, depth, dirs, files, 0);
|
||||
root
|
||||
}
|
||||
|
||||
fn populate(dir: &Path, depth: u32, dirs: u32, files: u32, seed: u32) {
|
||||
for f in 0..files {
|
||||
let n = seed.wrapping_add(f);
|
||||
let name = match n % 4 {
|
||||
0 => format!("module_{n}.rs"),
|
||||
1 => format!("data_{n}.txt"),
|
||||
2 => format!("image_{n}.png"),
|
||||
_ => format!("file_{n}.log"),
|
||||
};
|
||||
// Tiny files: updatedb only records names, so content size is irrelevant.
|
||||
std::fs::write(dir.join(name), b"x").unwrap();
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for d in 0..dirs {
|
||||
let sub = dir.join(format!("dir_{d}"));
|
||||
std::fs::create_dir_all(&sub).unwrap();
|
||||
populate(
|
||||
&sub,
|
||||
depth - 1,
|
||||
dirs,
|
||||
files,
|
||||
seed.wrapping_add((d + 1) * 31),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_e2e(c: &mut Criterion) {
|
||||
// depth 4, 4 dirs/level, 25 files/dir → ~8.5k indexed paths.
|
||||
let root = build_tree(4, 4, 25);
|
||||
let dir = root.to_str().unwrap();
|
||||
let db = std::env::temp_dir().join(format!("uu_updatedb_bench_{}.db", std::process::id()));
|
||||
let db_path = db.to_str().unwrap();
|
||||
|
||||
let localpaths = format!("--localpaths={dir}");
|
||||
let output = format!("--output={db_path}");
|
||||
|
||||
let mut group = c.benchmark_group("updatedb");
|
||||
|
||||
// Full build: walk, front-code and write the whole tree. Empty prune
|
||||
// options keep the run deterministic regardless of where temp_dir lives.
|
||||
group.bench_function("build_full", |b| {
|
||||
b.iter(|| {
|
||||
run(black_box(&[
|
||||
&localpaths,
|
||||
&output,
|
||||
"--prunepaths=",
|
||||
"--prunefs=",
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
// Build with a prune clause that drops every `dir_0` subtree, exercising the
|
||||
// -regex/-prune path updatedb hands to find.
|
||||
group.bench_function("build_pruned", |b| {
|
||||
b.iter(|| {
|
||||
run(black_box(&[
|
||||
&localpaths,
|
||||
&output,
|
||||
"--prunepaths=.*/dir_0",
|
||||
"--prunefs=",
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let _ = std::fs::remove_file(&db);
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_e2e);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2024 the uutils developers
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! End-to-end benchmarks for `xargs`, run through the real `xargs_main` entry
|
||||
//! point. Input is supplied via `-a <file>` (rather than stdin) so the harness
|
||||
//! can feed a fixed corpus without touching the process's stdin, and the
|
||||
//! command is `true` so the measurement is dominated by `xargs`'s own work
|
||||
//! (reading, splitting and batching arguments) rather than the child.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use findutils::xargs::xargs_main;
|
||||
|
||||
/// Run `xargs` end-to-end. `args` are the arguments after the program name.
|
||||
/// The exit status is ignored — we only care about the work performed.
|
||||
fn run(args: &[&str]) {
|
||||
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
|
||||
argv.push("xargs");
|
||||
argv.extend_from_slice(args);
|
||||
let _ = xargs_main(&argv);
|
||||
}
|
||||
|
||||
/// Write `count` short tokens to a temp file, joined by `sep`. Returns the path.
|
||||
fn build_input(count: u32, sep: &str, tag: &str) -> PathBuf {
|
||||
let mut content = String::new();
|
||||
for i in 0..count {
|
||||
if i > 0 {
|
||||
content.push_str(sep);
|
||||
}
|
||||
content.push_str(&format!("item_{i}"));
|
||||
}
|
||||
let path = std::env::temp_dir().join(format!("uu_xargs_bench_{}_{tag}", std::process::id()));
|
||||
std::fs::write(&path, content).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn bench_e2e(c: &mut Criterion) {
|
||||
// Whitespace- and newline-separated corpora, plus a NUL-separated one for
|
||||
// the `-0` path. A few thousand short tokens keeps parsing dominant while
|
||||
// `true` batches stay to a handful of (untraced) spawns.
|
||||
let ws = build_input(4000, " ", "ws");
|
||||
let nul = build_input(4000, "\0", "nul");
|
||||
let ws_path = ws.to_str().unwrap();
|
||||
let nul_path = nul.to_str().unwrap();
|
||||
|
||||
let mut group = c.benchmark_group("xargs");
|
||||
|
||||
// Default whitespace splitting, single batch (fits one command line).
|
||||
group.bench_function("split_whitespace", |b| {
|
||||
b.iter(|| run(black_box(&["-a", ws_path, "true"])));
|
||||
});
|
||||
// NUL-delimited input (`find -print0 | xargs -0` shape).
|
||||
group.bench_function("split_null", |b| {
|
||||
b.iter(|| run(black_box(&["-0", "-a", nul_path, "true"])));
|
||||
});
|
||||
// Cap arguments per command with -n, exercising the batching path across
|
||||
// several (untraced) invocations.
|
||||
group.bench_function("batched_n", |b| {
|
||||
b.iter(|| run(black_box(&["-a", ws_path, "-n", "1000", "true"])));
|
||||
});
|
||||
// Bound each command line by size with -s, another batching trigger.
|
||||
group.bench_function("batched_size", |b| {
|
||||
b.iter(|| run(black_box(&["-a", ws_path, "-s", "4096", "true"])));
|
||||
});
|
||||
|
||||
group.finish();
|
||||
|
||||
let _ = std::fs::remove_file(&ws);
|
||||
let _ = std::fs::remove_file(&nul);
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_e2e);
|
||||
criterion_main!(benches);
|
||||
@@ -174,9 +174,9 @@ impl From<walkdir::Error> for WalkError {
|
||||
impl From<&walkdir::Error> for WalkError {
|
||||
fn from(e: &walkdir::Error) -> Self {
|
||||
Self {
|
||||
path: e.path().map(|p| p.to_owned()),
|
||||
path: e.path().map(std::borrow::ToOwned::to_owned),
|
||||
depth: Some(e.depth()),
|
||||
raw: e.io_error().and_then(|e| e.raw_os_error()),
|
||||
raw: e.io_error().and_then(std::io::Error::raw_os_error),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,8 +190,7 @@ impl From<WalkError> for io::Error {
|
||||
impl From<&WalkError> for io::Error {
|
||||
fn from(e: &WalkError) -> Self {
|
||||
e.raw
|
||||
.map(Self::from_raw_os_error)
|
||||
.unwrap_or_else(|| ErrorKind::Other.into())
|
||||
.map_or_else(|| ErrorKind::Other.into(), Self::from_raw_os_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,8 +278,7 @@ impl WalkEntry {
|
||||
// Path::file_name() only works if the last component is normal
|
||||
path.components()
|
||||
.next_back()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| path.as_os_str())
|
||||
.map_or_else(|| path.as_os_str(), std::path::Component::as_os_str)
|
||||
}
|
||||
Entry::WalkDir(ent) => ent.file_name(),
|
||||
}
|
||||
@@ -311,7 +309,7 @@ impl WalkEntry {
|
||||
Entry::Explicit(_, _) => Ok(self.get_metadata()?),
|
||||
Entry::WalkDir(ent) => Ok(ent.metadata()?),
|
||||
});
|
||||
result.as_ref().map_err(|e| e.clone())
|
||||
result.as_ref().map_err(std::clone::Clone::clone)
|
||||
}
|
||||
|
||||
/// Get the file type of this entry.
|
||||
@@ -319,8 +317,7 @@ impl WalkEntry {
|
||||
match &self.inner {
|
||||
Entry::Explicit(_, _) => self
|
||||
.metadata()
|
||||
.map(|m| m.file_type().into())
|
||||
.unwrap_or(FileType::Unknown),
|
||||
.map_or(FileType::Unknown, |m| m.file_type().into()),
|
||||
Entry::WalkDir(ent) => ent.file_type().into(),
|
||||
}
|
||||
}
|
||||
|
||||
+41
-35
@@ -18,8 +18,17 @@ enum Arg {
|
||||
LiteralArg(OsString),
|
||||
}
|
||||
|
||||
fn parse_arg(s: &str) -> Arg {
|
||||
let parts = s.split("{}").collect::<Vec<_>>();
|
||||
if parts.len() == 1 {
|
||||
Arg::LiteralArg(OsString::from(s))
|
||||
} else {
|
||||
Arg::FileArg(parts.iter().map(OsString::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SingleExecMatcher {
|
||||
executable: String,
|
||||
executable: Arg,
|
||||
args: Vec<Arg>,
|
||||
exec_in_parent_dir: bool,
|
||||
interactive: bool,
|
||||
@@ -31,7 +40,7 @@ impl SingleExecMatcher {
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Self::new_impl(executable, args, exec_in_parent_dir, false)
|
||||
Ok(Self::new_impl(executable, args, exec_in_parent_dir, false))
|
||||
}
|
||||
|
||||
pub fn new_interactive(
|
||||
@@ -39,7 +48,7 @@ impl SingleExecMatcher {
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Self::new_impl(executable, args, exec_in_parent_dir, true)
|
||||
Ok(Self::new_impl(executable, args, exec_in_parent_dir, true))
|
||||
}
|
||||
|
||||
fn new_impl(
|
||||
@@ -47,26 +56,15 @@ impl SingleExecMatcher {
|
||||
args: &[&str],
|
||||
exec_in_parent_dir: bool,
|
||||
interactive: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let transformed_args = args
|
||||
.iter()
|
||||
.map(|&a| {
|
||||
let parts = a.split("{}").collect::<Vec<_>>();
|
||||
if parts.len() == 1 {
|
||||
// No {} present
|
||||
Arg::LiteralArg(OsString::from(a))
|
||||
} else {
|
||||
Arg::FileArg(parts.iter().map(OsString::from).collect())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
) -> Self {
|
||||
let transformed_args = args.iter().map(|&a| parse_arg(a)).collect();
|
||||
|
||||
Ok(Self {
|
||||
executable: executable.to_string(),
|
||||
Self {
|
||||
executable: parse_arg(executable),
|
||||
args: transformed_args,
|
||||
exec_in_parent_dir,
|
||||
interactive,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,28 +80,30 @@ impl Matcher for SingleExecMatcher {
|
||||
file_info.path().to_path_buf()
|
||||
};
|
||||
|
||||
let resolved_executable = match self.executable {
|
||||
Arg::LiteralArg(ref a) => a.clone(),
|
||||
Arg::FileArg(ref parts) => parts.join(path_to_file.as_os_str()),
|
||||
};
|
||||
|
||||
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(" "));
|
||||
// GNU find prints a fixed, abbreviated prompt of the form
|
||||
// "< executable ... pathname > ? ". It does not render the
|
||||
// substituted argument list, and always shows the full path of
|
||||
// the entry being processed (even for -okdir, whose command runs
|
||||
// with the "./basename" form).
|
||||
let prompt = format!(
|
||||
"< {} ... {} > ? ",
|
||||
resolved_executable.to_string_lossy(),
|
||||
file_info.path().to_string_lossy()
|
||||
);
|
||||
|
||||
if !matcher_io.confirm(&prompt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let mut command = Command::new(&self.executable);
|
||||
let mut command = Command::new(&resolved_executable);
|
||||
|
||||
for arg in &self.args {
|
||||
match *arg {
|
||||
Arg::LiteralArg(ref a) => command.arg(a.as_os_str()),
|
||||
@@ -127,7 +127,13 @@ impl Matcher for SingleExecMatcher {
|
||||
match command.status() {
|
||||
Ok(status) => status.success(),
|
||||
Err(e) => {
|
||||
writeln!(&mut stderr(), "Failed to run {}: {}", self.executable, e).unwrap();
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Failed to run {}: {}",
|
||||
resolved_executable.to_string_lossy(),
|
||||
e
|
||||
)
|
||||
.unwrap();
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
+38
-12
@@ -42,8 +42,18 @@ pub fn get_file_system_type(path: &Path, cache: &RefCell<Option<Cache>>) -> URes
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => Err(err)?,
|
||||
};
|
||||
let dev_id = metadata.dev().to_string();
|
||||
|
||||
fs_type_for_dev(metadata.dev().to_string(), cache)
|
||||
}
|
||||
|
||||
/// Resolve a device id to its filesystem type, consulting (and updating) `cache`.
|
||||
///
|
||||
/// Callers are expected to obtain `dev_id` from metadata they already hold (e.g. the cached
|
||||
/// [`WalkEntry::metadata`]), so that no extra `stat`/`statx` syscall is issued per entry.
|
||||
///
|
||||
/// This is only supported on Unix.
|
||||
#[cfg(unix)]
|
||||
pub fn fs_type_for_dev(dev_id: String, cache: &RefCell<Option<Cache>>) -> UResult<String> {
|
||||
if let Some(cache) = cache.borrow().as_ref() {
|
||||
if cache.dev_id == dev_id {
|
||||
return Ok(cache.fs_type.clone());
|
||||
@@ -94,18 +104,34 @@ impl FileSystemMatcher {
|
||||
impl Matcher for FileSystemMatcher {
|
||||
#[cfg(unix)]
|
||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||
match get_file_system_type(file_info.path(), &self.cache) {
|
||||
Ok(result) => result == self.fs_text,
|
||||
Err(_) => {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error getting filesystem type for {}",
|
||||
file_info.path().to_string_lossy()
|
||||
)
|
||||
.unwrap();
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
false
|
||||
}
|
||||
// Reuse the metadata already cached on the entry (a single shared `statx` per entry)
|
||||
// rather than issuing a fresh `lstat`/`statx` here. With several `-fstype` clauses (as
|
||||
// `updatedb` builds) this turns N stats per file into one.
|
||||
let Ok(metadata) = file_info.metadata() else {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error getting filesystem type for {}",
|
||||
file_info.path().to_string_lossy()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
return false;
|
||||
};
|
||||
let dev_id = metadata.dev().to_string();
|
||||
|
||||
if let Ok(result) = fs_type_for_dev(dev_id, &self.cache) {
|
||||
result == self.fs_text
|
||||
} else {
|
||||
writeln!(
|
||||
&mut stderr(),
|
||||
"Error getting filesystem type for {}",
|
||||
file_info.path().to_string_lossy()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -292,15 +292,15 @@ mod tests {
|
||||
fn test_format_permissions() {
|
||||
use super::format_permissions;
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o100644;
|
||||
let mode: uucore::libc::mode_t = 0o100_644;
|
||||
let expected = "-rw-r--r--";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o040755;
|
||||
let mode: uucore::libc::mode_t = 0o040_755;
|
||||
let expected = "drwxr-xr-x";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
|
||||
let mode: uucore::libc::mode_t = 0o100777;
|
||||
let mode: uucore::libc::mode_t = 0o100_777;
|
||||
let expected = "-rwxrwxrwx";
|
||||
assert_eq!(format_permissions(mode), expected);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ use std::{
|
||||
use super::{Config, Dependencies};
|
||||
|
||||
pub use entry::{FileType, WalkEntry, WalkError};
|
||||
pub use regex::RegexType;
|
||||
|
||||
/// Symlink following mode.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -399,7 +400,7 @@ fn parse_date_str_to_timestamps(date_str: &str) -> Option<i64> {
|
||||
///
|
||||
/// Additionally, there is support for the -anewer and -cnewer short arguments. as follows:
|
||||
/// 1. -anewer is equivalent to -neweram
|
||||
/// 2. -cnewer is equivalent to - newercm
|
||||
/// 2. -cnewer is equivalent to -newercm
|
||||
///
|
||||
/// If -newer is used it will be resolved to -newermm.
|
||||
fn parse_str_to_newer_args(input: &str) -> Option<(String, String)> {
|
||||
@@ -1031,10 +1032,10 @@ fn parse_files0_args(config: &mut Config) -> Result<(), Box<dyn Error>> {
|
||||
let mut string_segments: Vec<String> = buffer_split
|
||||
.iter()
|
||||
.filter_map(|s| std::str::from_utf8(s).ok())
|
||||
.map(|s| s.to_string())
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
// empty starting point checker
|
||||
if string_segments.iter().any(|s| s.is_empty()) {
|
||||
if string_segments.iter().any(std::string::String::is_empty) {
|
||||
eprintln!("find: invalid zero-length file name");
|
||||
// remove the empty ones so as to avoid file not found error
|
||||
string_segments.retain(|s| !s.is_empty());
|
||||
|
||||
@@ -205,7 +205,7 @@ impl FormatStringParser<'_> {
|
||||
let start = self.string;
|
||||
let mut digits = 0;
|
||||
|
||||
while self.front().map(|c| c.is_ascii_digit()).unwrap_or(false) {
|
||||
while self.front().is_ok_and(|c| c.is_ascii_digit()) {
|
||||
digits += 1;
|
||||
// safe to unwrap: the front() check already succeeded above.
|
||||
self.advance_one().unwrap();
|
||||
|
||||
@@ -842,13 +842,11 @@ mod tests {
|
||||
// - find test_data/simple -cmin +1
|
||||
// - find test_data/simple -mmin +1
|
||||
// Means to find files accessed / modified more than 1 minute ago.
|
||||
[
|
||||
for time_type in &[
|
||||
FileTimeType::Accessed,
|
||||
FileTimeType::Changed,
|
||||
FileTimeType::Modified,
|
||||
]
|
||||
.iter()
|
||||
.for_each(|time_type| {
|
||||
] {
|
||||
let more_matcher =
|
||||
FileAgeRangeMatcher::new(*time_type, ComparableValue::MoreThan(1), true);
|
||||
assert!(
|
||||
@@ -863,7 +861,7 @@ mod tests {
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// less test
|
||||
// mocks:
|
||||
@@ -871,14 +869,12 @@ mod tests {
|
||||
// - find test_data/simple -cmin -1
|
||||
// - find test_data/simple -mmin -1
|
||||
// Means to find files accessed / modified less than 1 minute ago.
|
||||
[
|
||||
for time_type in &[
|
||||
FileTimeType::Accessed,
|
||||
#[cfg(unix)]
|
||||
FileTimeType::Changed,
|
||||
FileTimeType::Modified,
|
||||
]
|
||||
.iter()
|
||||
.for_each(|time_type| {
|
||||
] {
|
||||
let less_matcher =
|
||||
FileAgeRangeMatcher::new(*time_type, ComparableValue::LessThan(1), true);
|
||||
assert!(
|
||||
@@ -893,7 +889,7 @@ mod tests {
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// catch file error
|
||||
let _ = fs::remove_file(&*new_file.path().to_string_lossy());
|
||||
|
||||
+104
-52
@@ -9,7 +9,7 @@ pub mod matchers;
|
||||
use matchers::{Follow, WalkEntry};
|
||||
use std::cell::RefCell;
|
||||
use std::error::Error;
|
||||
use std::io::{stderr, stdout, BufRead, BufReader, IsTerminal, Write};
|
||||
use std::io::{self, stderr, stdout, BufRead, BufReader, IsTerminal, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::time::SystemTime;
|
||||
@@ -187,7 +187,7 @@ fn parse_args(args: &[&str]) -> Result<ParsedInfo, Box<dyn Error>> {
|
||||
let matcher = matchers::build_top_level_matcher(&args[i..], &mut config)?;
|
||||
if let Some(new_paths) = &config.new_paths {
|
||||
if paths.len() == 1 && paths[0] == "." {
|
||||
paths = new_paths.to_vec();
|
||||
paths.clone_from(new_paths);
|
||||
} else {
|
||||
return Err(From::from(format!(
|
||||
"extra operand '{}'\nfile operands cannot be combined with -files0-from",
|
||||
@@ -237,7 +237,7 @@ fn process_dir(
|
||||
Ok(entry) => {
|
||||
let mut matcher_io = matchers::MatcherIO::new(deps);
|
||||
|
||||
let new_dir = entry.path().parent().map(|x| x.to_path_buf());
|
||||
let new_dir = entry.path().parent().map(std::path::Path::to_path_buf);
|
||||
if new_dir != current_dir {
|
||||
if let Some(dir) = current_dir.take() {
|
||||
matcher.finished_dir(dir.as_path(), &mut matcher_io);
|
||||
@@ -278,11 +278,11 @@ fn process_dir(
|
||||
fn do_find(args: &[&str], deps: &dyn Dependencies) -> Result<i32, Box<dyn Error>> {
|
||||
let paths_and_matcher = parse_args(args)?;
|
||||
if paths_and_matcher.config.help_requested {
|
||||
print_help();
|
||||
print_help(deps)?;
|
||||
return Ok(0);
|
||||
}
|
||||
if paths_and_matcher.config.version_requested {
|
||||
print_version();
|
||||
print_version(deps)?;
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
@@ -307,56 +307,108 @@ fn do_find(args: &[&str], deps: &dyn Dependencies) -> Result<i32, Box<dyn Error>
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
fn print_help(deps: &dyn Dependencies) -> Result<(), io::Error> {
|
||||
writeln!(
|
||||
&mut deps.get_output().borrow_mut(),
|
||||
r"Usage: find [path...] [expression]
|
||||
|
||||
If no path is supplied then the current working directory is used by default.
|
||||
|
||||
Early alpha implementation. Currently the only expressions supported are
|
||||
-print
|
||||
-print0
|
||||
-printf
|
||||
-name case-sensitive_filename_pattern
|
||||
-lname case-sensitive_filename_pattern
|
||||
-iname case-insensitive_filename_pattern
|
||||
-ilname case-insensitive_filename_pattern
|
||||
-regextype type
|
||||
-files0-from
|
||||
-regex pattern
|
||||
-iregex pattern
|
||||
-type type_char
|
||||
currently type_char can only be f (for file) or d (for directory)
|
||||
-size [+-]N[bcwkMG]
|
||||
-delete
|
||||
-prune
|
||||
-not
|
||||
-a
|
||||
-o[r]
|
||||
,
|
||||
()
|
||||
-true
|
||||
-false
|
||||
-maxdepth N
|
||||
-mindepth N
|
||||
-d[epth]
|
||||
-xdev
|
||||
-ctime [+-]N
|
||||
-atime [+-]N
|
||||
-mtime [+-]N
|
||||
-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.
|
||||
Options (before paths):
|
||||
-P Never follow symbolic links (default)
|
||||
-L Follow symbolic links
|
||||
-H Follow symbolic links on the command line only
|
||||
-Olevel Optimization level (0-3, currently ignored)
|
||||
|
||||
Operators:
|
||||
( EXPR ) Force precedence
|
||||
! EXPR / -not EXPR Negate expression
|
||||
EXPR -a / -and EXPR Logical AND (default)
|
||||
EXPR -o / -or EXPR Logical OR
|
||||
EXPR , EXPR List (both evaluated, result of last used)
|
||||
|
||||
Positional options:
|
||||
-daystart Measure times from start of today
|
||||
-follow Dereference symbolic links
|
||||
-maxdepth N Descend at most N levels
|
||||
-mindepth N Do not apply tests at levels less than N
|
||||
-d / -depth Process directory contents before directory itself
|
||||
-mount / -xdev Do not descend into other file systems
|
||||
-noleaf Do not optimize by assuming 2+ hard links
|
||||
-sorted Sort directory contents by name (non-standard)
|
||||
-regextype type Set regex syntax (default: emacs)
|
||||
-files0-from file Read starting points from file, NUL-separated
|
||||
|
||||
Tests:
|
||||
-name pattern Base name matches shell pattern
|
||||
-iname pattern Like -name but case-insensitive
|
||||
-path pattern Full path matches shell pattern
|
||||
-ipath pattern Like -path but case-insensitive
|
||||
-wholename pattern Same as -path
|
||||
-iwholename pattern Same as -ipath
|
||||
-lname pattern Symlink target matches pattern
|
||||
-ilname pattern Like -lname but case-insensitive
|
||||
-regex pattern Full path matches regular expression
|
||||
-iregex pattern Like -regex but case-insensitive
|
||||
-type [bcdflps] File is of given type
|
||||
-xtype [bcdflps] Like -type but check symlink target
|
||||
-fstype type File is on filesystem of given type
|
||||
-size [+-]N[bcwkMG] File uses N units of space
|
||||
-empty File is empty (regular file or directory)
|
||||
-newer file Modified more recently than file
|
||||
-anewer file Accessed more recently than file was modified
|
||||
-cnewer file Status changed more recently than file was modified
|
||||
-newerXY ref File's X time is newer than ref's Y time (X,Y: aBcmt)
|
||||
-ctime [+-]N Status changed N*24 hours ago
|
||||
-atime [+-]N Accessed N*24 hours ago
|
||||
-mtime [+-]N Modified N*24 hours ago
|
||||
-cmin [+-]N Status changed N minutes ago
|
||||
-amin [+-]N Accessed N minutes ago
|
||||
-mmin [+-]N Modified N minutes ago
|
||||
-perm [-/]mode Permission bits match mode
|
||||
-samefile file Same inode as file
|
||||
-readable Readable by current user
|
||||
-writable Writable by current user
|
||||
-executable Executable by current user
|
||||
-user name/uid Owned by user
|
||||
-nouser No user corresponds to file's UID
|
||||
-uid [+-]N Numeric user ID matches
|
||||
-group name/gid Owned by group
|
||||
-nogroup No group corresponds to file's GID
|
||||
-gid [+-]N Numeric group ID matches
|
||||
-inum [+-]N Inode number matches (Unix only)
|
||||
-links [+-]N Hard link count matches (Unix only)
|
||||
-true Always true
|
||||
-false Always false
|
||||
|
||||
Actions:
|
||||
-print Print path followed by newline (default)
|
||||
-print0 Print path followed by NUL
|
||||
-printf format Print formatted string
|
||||
-fprint file Like -print but write to file
|
||||
-fprint0 file Like -print0 but write to file
|
||||
-fprintf file format Like -printf but write to file
|
||||
-ls List in ls -dils format
|
||||
-fls file Like -ls but write to file
|
||||
-exec command ; Execute command for each file
|
||||
-exec command {{}} + Execute command with multiple files
|
||||
-execdir command ; Like -exec but in file's directory
|
||||
-execdir command {{}} + Like -exec {{}} + but in file's directory
|
||||
-ok command ; Like -exec but prompt user first
|
||||
-okdir command ; Like -execdir but prompt user first
|
||||
-delete Delete files (implies -depth)
|
||||
-prune Do not descend into directory
|
||||
-quit Exit immediately
|
||||
"
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
fn print_version() {
|
||||
println!("find (Rust) {}", env!("CARGO_PKG_VERSION"));
|
||||
fn print_version(deps: &dyn Dependencies) -> Result<(), io::Error> {
|
||||
writeln!(
|
||||
&mut deps.get_output().borrow_mut(),
|
||||
"find (Rust) {}",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)
|
||||
}
|
||||
|
||||
/// Does all the work for find.
|
||||
@@ -1271,7 +1323,7 @@ mod tests {
|
||||
assert_eq!(rc, 1);
|
||||
|
||||
// test empty user name
|
||||
["-user", "-nouser"].iter().for_each(|&arg| {
|
||||
for &arg in &["-user", "-nouser"] {
|
||||
let deps = FakeDependencies::new();
|
||||
let rc = find_main(&["find", "./test_data/simple/subdir", arg, ""], &deps);
|
||||
|
||||
@@ -1281,7 +1333,7 @@ mod tests {
|
||||
let rc = find_main(&["find", "./test_data/simple/subdir", arg, " "], &deps);
|
||||
|
||||
assert_eq!(rc, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1359,7 +1411,7 @@ mod tests {
|
||||
assert_eq!(rc, 1);
|
||||
|
||||
// test empty user name and group name
|
||||
["-group", "-nogroup"].iter().for_each(|&arg| {
|
||||
for &arg in &["-group", "-nogroup"] {
|
||||
let deps = FakeDependencies::new();
|
||||
let rc = find_main(&["find", "./test_data/simple/subdir", arg, ""], &deps);
|
||||
|
||||
@@ -1369,7 +1421,7 @@ mod tests {
|
||||
let rc = find_main(&["find", "./test_data/simple/subdir", arg, " "], &deps);
|
||||
|
||||
assert_eq!(rc, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,4 +5,6 @@
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
pub mod find;
|
||||
pub mod locate;
|
||||
pub mod updatedb;
|
||||
pub mod xargs;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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>>();
|
||||
let strs: Vec<&str> = args.iter().map(std::convert::AsRef::as_ref).collect();
|
||||
std::process::exit(findutils::locate::locate_main(strs.as_slice()));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user