mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efe6dc05f9 | |||
| 53980035af | |||
| e2d84e98d5 | |||
| 865ab0991e | |||
| 8add685cc6 | |||
| c4afeab1bc | |||
| 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 |
@@ -92,7 +92,7 @@ jobs:
|
||||
path: lcov.info
|
||||
|
||||
- name: Upload coverage to codecov.io
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
files: lcov.info
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -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@v6
|
||||
|
||||
- name: Setup Rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@v1
|
||||
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)> {
|
||||
@@ -974,7 +975,7 @@ fn build_matcher_tree(
|
||||
)
|
||||
}
|
||||
}
|
||||
None => return Err(From::from(format!("Unrecognized flag: '{}'", args[i]))),
|
||||
None => return Err(From::from(format!("unknown predicate '{}'", args[i]))),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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());
|
||||
|
||||
+41
-11
@@ -167,16 +167,21 @@ impl FormatStringParser<'_> {
|
||||
// Try parsing an octal sequence first.
|
||||
let first = self.front()?;
|
||||
if first.is_digit(OCTAL_RADIX) {
|
||||
if let Ok(code) = self.peek(OCTAL_LEN).and_then(|octal| {
|
||||
u32::from_str_radix(octal, OCTAL_RADIX).map_err(std::convert::Into::into)
|
||||
}) {
|
||||
// safe to unwrap: .peek() already succeeded above.
|
||||
let octal = self.advance_by(OCTAL_LEN).unwrap();
|
||||
return match char::from_u32(code) {
|
||||
Some(c) => Ok(FormatComponent::Literal(c.to_string())),
|
||||
None => Err(format!("Invalid character value: \\{octal}").into()),
|
||||
};
|
||||
}
|
||||
// A GNU octal escape is 1 to 3 octal digits. Consume only the leading
|
||||
// octal digits (which are ASCII), rather than slicing a fixed 3 bytes
|
||||
// that can land inside a following multibyte char.
|
||||
let octal: String = self
|
||||
.string
|
||||
.chars()
|
||||
.take(OCTAL_LEN)
|
||||
.take_while(|c| c.is_digit(OCTAL_RADIX))
|
||||
.collect();
|
||||
let code = u32::from_str_radix(&octal, OCTAL_RADIX)?;
|
||||
self.advance_by(octal.len())?;
|
||||
return match char::from_u32(code) {
|
||||
Some(c) => Ok(FormatComponent::Literal(c.to_string())),
|
||||
None => Err(format!("Invalid character value: \\{octal}").into()),
|
||||
};
|
||||
}
|
||||
|
||||
self.advance_one()?;
|
||||
@@ -205,7 +210,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();
|
||||
@@ -688,6 +693,31 @@ mod tests {
|
||||
assert!(FormatString::parse("\\").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_octal_escape_before_multibyte_char() {
|
||||
assert_eq!(
|
||||
FormatString::parse("\\0€").unwrap().components,
|
||||
vec![
|
||||
FormatComponent::Literal("\0".to_owned()),
|
||||
FormatComponent::Literal("€".to_owned()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
FormatString::parse("\\1😀").unwrap().components,
|
||||
vec![
|
||||
FormatComponent::Literal("\u{1}".to_owned()),
|
||||
FormatComponent::Literal("😀".to_owned()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
FormatString::parse("\\00é").unwrap().components,
|
||||
vec![
|
||||
FormatComponent::Literal("\0".to_owned()),
|
||||
FormatComponent::Literal("é".to_owned()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_formatting() {
|
||||
fn unaligned_directive(directive: FormatDirective) -> FormatComponent {
|
||||
|
||||
@@ -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());
|
||||
|
||||
+105
-53
@@ -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.
|
||||
@@ -494,7 +546,7 @@ mod tests {
|
||||
//
|
||||
let result = super::parse_args(&["-asdadsafsfsadcs"]);
|
||||
if let Err(e) = result {
|
||||
assert_eq!(e.to_string(), "Unrecognized flag: '-asdadsafsfsadcs'");
|
||||
assert_eq!(e.to_string(), "unknown predicate '-asdadsafsfsadcs'");
|
||||
} else {
|
||||
panic!("parse_args should have returned an error");
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user