mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
Compare commits
42 Commits
latest-commit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 795d29ab35 | |||
| c619fbeb82 | |||
| a70f5ccd7c | |||
| 5992901805 | |||
| 782744f35c | |||
| c100ac226f | |||
| 5bf236b891 | |||
| 001966f326 | |||
| 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 |
@@ -65,6 +65,29 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
cargo clippy --all-targets -- -D warnings
|
cargo clippy --all-targets -- -D warnings
|
||||||
|
|
||||||
|
wasm:
|
||||||
|
name: cargo build (wasm32-wasip1)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
WASI_SDK_VERSION: "33"
|
||||||
|
WASI_SDK_PATH: ${{ github.workspace }}/wasi-sdk
|
||||||
|
# onig_sys compiles bundled C, so it needs a C compiler with a WASI
|
||||||
|
# sysroot to cross-compile to wasm.
|
||||||
|
CC_wasm32_wasip1: ${{ github.workspace }}/wasi-sdk/bin/clang
|
||||||
|
AR_wasm32_wasip1: ${{ github.workspace }}/wasi-sdk/bin/llvm-ar
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Install wasm target
|
||||||
|
run: rustup target add wasm32-wasip1
|
||||||
|
- name: Install WASI SDK
|
||||||
|
run: |
|
||||||
|
curl -sSfL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" | tar xz
|
||||||
|
mv "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux" "${WASI_SDK_PATH}"
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --target wasm32-wasip1
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy --target wasm32-wasip1 -- -D warnings
|
||||||
|
|
||||||
grcov:
|
grcov:
|
||||||
name: Code coverage
|
name: Code coverage
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -92,7 +115,7 @@ jobs:
|
|||||||
path: lcov.info
|
path: lcov.info
|
||||||
|
|
||||||
- name: Upload coverage to codecov.io
|
- name: Upload coverage to codecov.io
|
||||||
uses: codecov/codecov-action@v6
|
uses: codecov/codecov-action@v7
|
||||||
with:
|
with:
|
||||||
files: lcov.info
|
files: lcov.info
|
||||||
fail_ci_if_error: true
|
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
|
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
|
ZSTD_CLEVEL=19 tar --zstd -caf ../findutils-x86_64-unknown-linux-gnu.tar.zst findutils-x86_64-unknown-linux-gnu
|
||||||
- name: Publish latest commit
|
- 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'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
tag_name: latest-commit
|
tag_name: latest-commit
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ jobs:
|
|||||||
# Write and read notes from a file to avoid quoting breaking things
|
# Write and read notes from a file to avoid quoting breaking things
|
||||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
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:
|
announce:
|
||||||
needs:
|
needs:
|
||||||
|
|||||||
Generated
+722
-92
File diff suppressed because it is too large
Load Diff
+66
-11
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "findutils"
|
name = "findutils"
|
||||||
version = "0.8.0"
|
version = "0.9.0"
|
||||||
homepage = "https://github.com/uutils/findutils"
|
homepage = "https://github.com/uutils/findutils"
|
||||||
repository = "https://github.com/uutils/findutils"
|
repository = "https://github.com/uutils/findutils"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -11,26 +11,55 @@ authors = ["uutils developers"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argmax = "0.4.0"
|
argmax = "0.4.0"
|
||||||
chrono = "0.4.44"
|
chrono = "0.4.45"
|
||||||
clap = "4.6"
|
clap = { version = "4.6", features = ["env"] }
|
||||||
faccess = "0.2.4"
|
faccess = "0.2.4"
|
||||||
nix = { version = "0.31", features = ["fs", "user"] }
|
nix = { version = "0.31", features = ["fs", "user"] }
|
||||||
onig = { version = "6.5", default-features = false }
|
onig = { version = "6.5", default-features = false }
|
||||||
regex = "1.12"
|
regex = "1.12"
|
||||||
uucore = { version = "0.9.0", features = ["entries", "fs", "fsext", "mode"] }
|
uucore = { version = "0.9.0", features = ["entries", "fs", "fsext", "mode"] }
|
||||||
walkdir = "2.5"
|
walkdir = "2.5"
|
||||||
|
itertools = "0.14.0"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
criterion = { version = "4.7.0", package = "codspeed-criterion-compat" }
|
||||||
|
assert_cmd = "2"
|
||||||
ctor = "1.0"
|
ctor = "1.0"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
nix = { version = "0.31", features = ["fs"] }
|
nix = { version = "0.31", features = ["fs"] }
|
||||||
|
rstest = "0.26.1"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
uutests = "0.9.0"
|
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]]
|
[[bin]]
|
||||||
name = "find"
|
name = "find"
|
||||||
path = "src/find/main.rs"
|
path = "src/find/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "locate"
|
||||||
|
path = "src/locate/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "updatedb"
|
||||||
|
path = "src/updatedb/main.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "xargs"
|
name = "xargs"
|
||||||
path = "src/xargs/main.rs"
|
path = "src/xargs/main.rs"
|
||||||
@@ -48,17 +77,43 @@ inherits = "release"
|
|||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
[lints.clippy]
|
[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"
|
multiple_crate_versions = "allow"
|
||||||
cargo_common_metadata = "allow"
|
cargo_common_metadata = "allow"
|
||||||
uninlined_format_args = "allow"
|
uninlined_format_args = "allow"
|
||||||
|
missing_errors_doc = "allow"
|
||||||
missing_panics_doc = "allow"
|
missing_panics_doc = "allow"
|
||||||
|
must_use_candidate = "allow"
|
||||||
use_self = "warn"
|
match_same_arms = "allow"
|
||||||
needless_pass_by_value = "warn"
|
cast_possible_truncation = "allow"
|
||||||
semicolon_if_nothing_returned = "warn"
|
too_many_lines = "allow"
|
||||||
single_char_pattern = "warn"
|
cast_possible_wrap = "allow"
|
||||||
explicit_iter_loop = "warn"
|
cast_sign_loss = "allow"
|
||||||
if_not_else = "warn"
|
struct_excessive_bools = "allow"
|
||||||
manual_let_else = "warn"
|
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
|
# Disable for now, we have a few occurrences
|
||||||
# panic = "warn"
|
# 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 {
|
impl From<&walkdir::Error> for WalkError {
|
||||||
fn from(e: &walkdir::Error) -> Self {
|
fn from(e: &walkdir::Error) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: e.path().map(|p| p.to_owned()),
|
path: e.path().map(std::borrow::ToOwned::to_owned),
|
||||||
depth: Some(e.depth()),
|
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 {
|
impl From<&WalkError> for io::Error {
|
||||||
fn from(e: &WalkError) -> Self {
|
fn from(e: &WalkError) -> Self {
|
||||||
e.raw
|
e.raw
|
||||||
.map(Self::from_raw_os_error)
|
.map_or_else(|| ErrorKind::Other.into(), Self::from_raw_os_error)
|
||||||
.unwrap_or_else(|| ErrorKind::Other.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,8 +278,7 @@ impl WalkEntry {
|
|||||||
// Path::file_name() only works if the last component is normal
|
// Path::file_name() only works if the last component is normal
|
||||||
path.components()
|
path.components()
|
||||||
.next_back()
|
.next_back()
|
||||||
.map(|c| c.as_os_str())
|
.map_or_else(|| path.as_os_str(), std::path::Component::as_os_str)
|
||||||
.unwrap_or_else(|| path.as_os_str())
|
|
||||||
}
|
}
|
||||||
Entry::WalkDir(ent) => ent.file_name(),
|
Entry::WalkDir(ent) => ent.file_name(),
|
||||||
}
|
}
|
||||||
@@ -311,7 +309,7 @@ impl WalkEntry {
|
|||||||
Entry::Explicit(_, _) => Ok(self.get_metadata()?),
|
Entry::Explicit(_, _) => Ok(self.get_metadata()?),
|
||||||
Entry::WalkDir(ent) => Ok(ent.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.
|
/// Get the file type of this entry.
|
||||||
@@ -319,8 +317,7 @@ impl WalkEntry {
|
|||||||
match &self.inner {
|
match &self.inner {
|
||||||
Entry::Explicit(_, _) => self
|
Entry::Explicit(_, _) => self
|
||||||
.metadata()
|
.metadata()
|
||||||
.map(|m| m.file_type().into())
|
.map_or(FileType::Unknown, |m| m.file_type().into()),
|
||||||
.unwrap_or(FileType::Unknown),
|
|
||||||
Entry::WalkDir(ent) => ent.file_type().into(),
|
Entry::WalkDir(ent) => ent.file_type().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-35
@@ -18,8 +18,17 @@ enum Arg {
|
|||||||
LiteralArg(OsString),
|
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 {
|
pub struct SingleExecMatcher {
|
||||||
executable: String,
|
executable: Arg,
|
||||||
args: Vec<Arg>,
|
args: Vec<Arg>,
|
||||||
exec_in_parent_dir: bool,
|
exec_in_parent_dir: bool,
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
@@ -31,7 +40,7 @@ impl SingleExecMatcher {
|
|||||||
args: &[&str],
|
args: &[&str],
|
||||||
exec_in_parent_dir: bool,
|
exec_in_parent_dir: bool,
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> 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(
|
pub fn new_interactive(
|
||||||
@@ -39,7 +48,7 @@ impl SingleExecMatcher {
|
|||||||
args: &[&str],
|
args: &[&str],
|
||||||
exec_in_parent_dir: bool,
|
exec_in_parent_dir: bool,
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> 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(
|
fn new_impl(
|
||||||
@@ -47,26 +56,15 @@ impl SingleExecMatcher {
|
|||||||
args: &[&str],
|
args: &[&str],
|
||||||
exec_in_parent_dir: bool,
|
exec_in_parent_dir: bool,
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
) -> Self {
|
||||||
let transformed_args = args
|
let transformed_args = args.iter().map(|&a| parse_arg(a)).collect();
|
||||||
.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();
|
|
||||||
|
|
||||||
Ok(Self {
|
Self {
|
||||||
executable: executable.to_string(),
|
executable: parse_arg(executable),
|
||||||
args: transformed_args,
|
args: transformed_args,
|
||||||
exec_in_parent_dir,
|
exec_in_parent_dir,
|
||||||
interactive,
|
interactive,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,28 +80,30 @@ impl Matcher for SingleExecMatcher {
|
|||||||
file_info.path().to_path_buf()
|
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 {
|
if self.interactive {
|
||||||
let rendered_args: Vec<String> = self
|
// GNU find prints a fixed, abbreviated prompt of the form
|
||||||
.args
|
// "< executable ... pathname > ? ". It does not render the
|
||||||
.iter()
|
// substituted argument list, and always shows the full path of
|
||||||
.map(|arg| match arg {
|
// the entry being processed (even for -okdir, whose command runs
|
||||||
Arg::LiteralArg(a) => a.to_string_lossy().into_owned(),
|
// with the "./basename" form).
|
||||||
Arg::FileArg(parts) => parts
|
let prompt = format!(
|
||||||
.join(path_to_file.as_os_str())
|
"< {} ... {} > ? ",
|
||||||
.to_string_lossy()
|
resolved_executable.to_string_lossy(),
|
||||||
.into_owned(),
|
file_info.path().to_string_lossy()
|
||||||
})
|
);
|
||||||
.collect();
|
|
||||||
let mut prompt_parts = vec![self.executable.clone()];
|
|
||||||
prompt_parts.extend(rendered_args);
|
|
||||||
let prompt = format!("< {} >? ", prompt_parts.join(" "));
|
|
||||||
|
|
||||||
if !matcher_io.confirm(&prompt) {
|
if !matcher_io.confirm(&prompt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut command = Command::new(&self.executable);
|
let mut command = Command::new(&resolved_executable);
|
||||||
|
|
||||||
for arg in &self.args {
|
for arg in &self.args {
|
||||||
match *arg {
|
match *arg {
|
||||||
Arg::LiteralArg(ref a) => command.arg(a.as_os_str()),
|
Arg::LiteralArg(ref a) => command.arg(a.as_os_str()),
|
||||||
@@ -127,7 +127,13 @@ impl Matcher for SingleExecMatcher {
|
|||||||
match command.status() {
|
match command.status() {
|
||||||
Ok(status) => status.success(),
|
Ok(status) => status.success(),
|
||||||
Err(e) => {
|
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
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-12
@@ -42,8 +42,18 @@ pub fn get_file_system_type(path: &Path, cache: &RefCell<Option<Cache>>) -> URes
|
|||||||
Ok(metadata) => metadata,
|
Ok(metadata) => metadata,
|
||||||
Err(err) => Err(err)?,
|
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 let Some(cache) = cache.borrow().as_ref() {
|
||||||
if cache.dev_id == dev_id {
|
if cache.dev_id == dev_id {
|
||||||
return Ok(cache.fs_type.clone());
|
return Ok(cache.fs_type.clone());
|
||||||
@@ -94,18 +104,34 @@ impl FileSystemMatcher {
|
|||||||
impl Matcher for FileSystemMatcher {
|
impl Matcher for FileSystemMatcher {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
fn matches(&self, file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||||
match get_file_system_type(file_info.path(), &self.cache) {
|
use std::os::unix::fs::MetadataExt;
|
||||||
Ok(result) => result == self.fs_text,
|
|
||||||
Err(_) => {
|
|
||||||
writeln!(
|
|
||||||
&mut stderr(),
|
|
||||||
"Error getting filesystem type for {}",
|
|
||||||
file_info.path().to_string_lossy()
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ fn extract_bracket_expr(pattern: &str) -> Option<(String, &str)> {
|
|||||||
|
|
||||||
if matches!(delim, '.' | '=' | ':') {
|
if matches!(delim, '.' | '=' | ':') {
|
||||||
let rest = chars.as_str();
|
let rest = chars.as_str();
|
||||||
let end = rest.find([delim, ']'])? + 2;
|
// Search for the two-byte closer `<delim>]` (e.g. `:]`);
|
||||||
|
// matching either byte alone let `+ 2` overshoot a char boundary.
|
||||||
|
let closer = format!("{delim}]");
|
||||||
|
let end = rest.find(closer.as_str())? + 2;
|
||||||
expr.push_str(&rest[..end]);
|
expr.push_str(&rest[..end]);
|
||||||
chars = rest[end..].chars();
|
chars = rest[end..].chars();
|
||||||
}
|
}
|
||||||
@@ -222,6 +225,16 @@ mod tests {
|
|||||||
assert_glob_regex(r"foo[bar[!baz", r"foo\[bar\[!baz");
|
assert_glob_regex(r"foo[bar[!baz", r"foo\[bar\[!baz");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_posix_class_with_multibyte_char() {
|
||||||
|
for pat in ["[[:]é", "[[:a]é", "[[.]é", "[[:é]", "[[=]😀"] {
|
||||||
|
assert!(
|
||||||
|
glob_to_regex(pat).is_some(),
|
||||||
|
"panicked or rejected: {pat:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn incomplete_escape() {
|
fn incomplete_escape() {
|
||||||
assert_eq!(glob_to_regex(r"foo\"), None);
|
assert_eq!(glob_to_regex(r"foo\"), None);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ use nix::unistd::Group;
|
|||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
pub struct GroupMatcher {
|
pub struct GroupMatcher {
|
||||||
|
// Only read on Unix; the non-Unix `matches` implementation is a stub.
|
||||||
|
#[cfg_attr(not(unix), allow(dead_code))]
|
||||||
gid: ComparableValue,
|
gid: ComparableValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ impl GroupMatcher {
|
|||||||
Self { gid }
|
Self { gid }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(not(unix))]
|
||||||
pub fn from_group_name(_group: &str) -> Option<Self> {
|
pub fn from_group_name(_group: &str) -> Option<Self> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -46,10 +48,10 @@ impl Matcher for GroupMatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(not(unix))]
|
||||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||||
// The user group acquisition function for Windows systems is not implemented in MetadataExt,
|
// The user group acquisition function for non-Unix systems is not implemented in
|
||||||
// so it is somewhat difficult to implement it. :(
|
// MetadataExt, so it is somewhat difficult to implement it. :(
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +82,7 @@ impl Matcher for NoGroupMatcher {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(not(unix))]
|
||||||
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
fn matches(&self, _file_info: &WalkEntry, _: &mut MatcherIO) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-22
@@ -126,7 +126,6 @@ impl Ls {
|
|||||||
mut out: impl Write,
|
mut out: impl Write,
|
||||||
print_error_message: bool,
|
print_error_message: bool,
|
||||||
) {
|
) {
|
||||||
use nix::unistd::{Gid, Group, Uid, User};
|
|
||||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
|
||||||
let metadata = file_info.metadata().unwrap();
|
let metadata = file_info.metadata().unwrap();
|
||||||
@@ -150,14 +149,13 @@ impl Ls {
|
|||||||
let permission =
|
let permission =
|
||||||
{ format_permissions(metadata.permissions().mode() as uucore::libc::mode_t) };
|
{ format_permissions(metadata.permissions().mode() as uucore::libc::mode_t) };
|
||||||
let hard_links = metadata.nlink();
|
let hard_links = metadata.nlink();
|
||||||
let user = {
|
// Fall back to the numeric id when the uid/gid has no passwd/group entry
|
||||||
let uid = metadata.uid();
|
// (unmapped owner) — matching GNU find, which never crashes. uucore::entries
|
||||||
User::from_uid(Uid::from_raw(uid)).unwrap().unwrap().name
|
// serializes the non-thread-safe getpwuid/getgrgid behind a mutex.
|
||||||
};
|
let user =
|
||||||
let group = {
|
uucore::entries::uid2usr(metadata.uid()).unwrap_or_else(|_| metadata.uid().to_string());
|
||||||
let gid = metadata.gid();
|
let group =
|
||||||
Group::from_gid(Gid::from_raw(gid)).unwrap().unwrap().name
|
uucore::entries::gid2grp(metadata.gid()).unwrap_or_else(|_| metadata.gid().to_string());
|
||||||
};
|
|
||||||
let size = metadata.size();
|
let size = metadata.size();
|
||||||
let last_modified = {
|
let last_modified = {
|
||||||
let system_time = metadata.modified().unwrap();
|
let system_time = metadata.modified().unwrap();
|
||||||
@@ -195,7 +193,7 @@ impl Ls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(not(unix))]
|
||||||
fn print(
|
fn print(
|
||||||
&self,
|
&self,
|
||||||
file_info: &WalkEntry,
|
file_info: &WalkEntry,
|
||||||
@@ -203,16 +201,15 @@ impl Ls {
|
|||||||
mut out: impl Write,
|
mut out: impl Write,
|
||||||
print_error_message: bool,
|
print_error_message: bool,
|
||||||
) {
|
) {
|
||||||
use std::os::windows::fs::MetadataExt;
|
// Non-Unix targets (Windows, wasm, ...) don't expose inode, owner,
|
||||||
|
// group or Unix permission bits, so those columns are left blank.
|
||||||
let metadata = file_info.metadata().unwrap();
|
let metadata = file_info.metadata().unwrap();
|
||||||
|
|
||||||
let inode_number = 0;
|
let inode_number = 0;
|
||||||
|
let size = metadata.len();
|
||||||
let number_of_blocks = {
|
let number_of_blocks = {
|
||||||
let size = metadata.file_size();
|
|
||||||
let number_of_blocks = size / 1024;
|
let number_of_blocks = size / 1024;
|
||||||
let remainder = number_of_blocks % 4;
|
let remainder = number_of_blocks % 4;
|
||||||
|
|
||||||
if remainder == 0 {
|
if remainder == 0 {
|
||||||
if number_of_blocks == 0 {
|
if number_of_blocks == 0 {
|
||||||
4
|
4
|
||||||
@@ -220,14 +217,19 @@ impl Ls {
|
|||||||
number_of_blocks
|
number_of_blocks
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
number_of_blocks + (4 - (remainder))
|
number_of_blocks + (4 - remainder)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let permission = { format_permissions(metadata.file_attributes()) };
|
#[cfg(windows)]
|
||||||
|
let permission = {
|
||||||
|
use std::os::windows::fs::MetadataExt;
|
||||||
|
format_permissions(metadata.file_attributes())
|
||||||
|
};
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let permission = "?---------";
|
||||||
let hard_links = 0;
|
let hard_links = 0;
|
||||||
let user = 0;
|
let user = 0;
|
||||||
let group = 0;
|
let group = 0;
|
||||||
let size = metadata.file_size();
|
|
||||||
let last_modified = {
|
let last_modified = {
|
||||||
let system_time = metadata.modified().unwrap();
|
let system_time = metadata.modified().unwrap();
|
||||||
let now_utc: DateTime<chrono::Utc> = system_time.into();
|
let now_utc: DateTime<chrono::Utc> = system_time.into();
|
||||||
@@ -235,9 +237,9 @@ impl Ls {
|
|||||||
};
|
};
|
||||||
let path = file_info.path().to_string_lossy();
|
let path = file_info.path().to_string_lossy();
|
||||||
|
|
||||||
match write!(
|
match writeln!(
|
||||||
out,
|
out,
|
||||||
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}\n",
|
" {:<4} {:>6} {:<10} {:>3} {:<8} {:<8} {:>8} {} {}",
|
||||||
inode_number,
|
inode_number,
|
||||||
number_of_blocks,
|
number_of_blocks,
|
||||||
permission,
|
permission,
|
||||||
@@ -292,15 +294,15 @@ mod tests {
|
|||||||
fn test_format_permissions() {
|
fn test_format_permissions() {
|
||||||
use super::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--";
|
let expected = "-rw-r--r--";
|
||||||
assert_eq!(format_permissions(mode), expected);
|
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";
|
let expected = "drwxr-xr-x";
|
||||||
assert_eq!(format_permissions(mode), expected);
|
assert_eq!(format_permissions(mode), expected);
|
||||||
|
|
||||||
let mode: uucore::libc::mode_t = 0o100777;
|
let mode: uucore::libc::mode_t = 0o100_777;
|
||||||
let expected = "-rwxrwxrwx";
|
let expected = "-rwxrwxrwx";
|
||||||
assert_eq!(format_permissions(mode), expected);
|
assert_eq!(format_permissions(mode), expected);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ use std::{
|
|||||||
use super::{Config, Dependencies};
|
use super::{Config, Dependencies};
|
||||||
|
|
||||||
pub use entry::{FileType, WalkEntry, WalkError};
|
pub use entry::{FileType, WalkEntry, WalkError};
|
||||||
|
pub use regex::RegexType;
|
||||||
|
|
||||||
/// Symlink following mode.
|
/// Symlink following mode.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[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:
|
/// Additionally, there is support for the -anewer and -cnewer short arguments. as follows:
|
||||||
/// 1. -anewer is equivalent to -neweram
|
/// 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.
|
/// If -newer is used it will be resolved to -newermm.
|
||||||
fn parse_str_to_newer_args(input: &str) -> Option<(String, String)> {
|
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
|
let mut string_segments: Vec<String> = buffer_split
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|s| std::str::from_utf8(s).ok())
|
.filter_map(|s| std::str::from_utf8(s).ok())
|
||||||
.map(|s| s.to_string())
|
.map(std::string::ToString::to_string)
|
||||||
.collect();
|
.collect();
|
||||||
// empty starting point checker
|
// 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");
|
eprintln!("find: invalid zero-length file name");
|
||||||
// remove the empty ones so as to avoid file not found error
|
// remove the empty ones so as to avoid file not found error
|
||||||
string_segments.retain(|s| !s.is_empty());
|
string_segments.retain(|s| !s.is_empty());
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl Matcher for NameMatcher {
|
|||||||
self.pattern.matches(&name)
|
self.pattern.matches(&name)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(not(unix))]
|
||||||
self.pattern.matches(&name)
|
self.pattern.matches(&name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ impl PermMatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
pub fn new(_dummy_pattern: &str) -> Result<PermMatcher, Box<dyn Error>> {
|
pub fn new(_dummy_pattern: &str) -> Result<Self, Box<dyn Error>> {
|
||||||
Err(From::from(
|
Err(From::from(
|
||||||
"Permission matching is not available on this platform",
|
"Permission matching is not available on this platform",
|
||||||
))
|
))
|
||||||
@@ -132,7 +132,7 @@ impl Matcher for PermMatcher {
|
|||||||
"Permission matching not available on this platform!"
|
"Permission matching not available on this platform!"
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
return false;
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+42
-11
@@ -17,6 +17,7 @@ use super::{FileType, Matcher, MatcherIO, WalkEntry, WalkError};
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::prelude::MetadataExt;
|
use std::os::unix::prelude::MetadataExt;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
const STANDARD_BLOCK_SIZE: u64 = 512;
|
const STANDARD_BLOCK_SIZE: u64 = 512;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
@@ -167,16 +168,21 @@ impl FormatStringParser<'_> {
|
|||||||
// Try parsing an octal sequence first.
|
// Try parsing an octal sequence first.
|
||||||
let first = self.front()?;
|
let first = self.front()?;
|
||||||
if first.is_digit(OCTAL_RADIX) {
|
if first.is_digit(OCTAL_RADIX) {
|
||||||
if let Ok(code) = self.peek(OCTAL_LEN).and_then(|octal| {
|
// A GNU octal escape is 1 to 3 octal digits. Consume only the leading
|
||||||
u32::from_str_radix(octal, OCTAL_RADIX).map_err(std::convert::Into::into)
|
// octal digits (which are ASCII), rather than slicing a fixed 3 bytes
|
||||||
}) {
|
// that can land inside a following multibyte char.
|
||||||
// safe to unwrap: .peek() already succeeded above.
|
let octal: String = self
|
||||||
let octal = self.advance_by(OCTAL_LEN).unwrap();
|
.string
|
||||||
return match char::from_u32(code) {
|
.chars()
|
||||||
Some(c) => Ok(FormatComponent::Literal(c.to_string())),
|
.take(OCTAL_LEN)
|
||||||
None => Err(format!("Invalid character value: \\{octal}").into()),
|
.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()?;
|
self.advance_one()?;
|
||||||
@@ -205,7 +211,7 @@ impl FormatStringParser<'_> {
|
|||||||
let start = self.string;
|
let start = self.string;
|
||||||
let mut digits = 0;
|
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;
|
digits += 1;
|
||||||
// safe to unwrap: the front() check already succeeded above.
|
// safe to unwrap: the front() check already succeeded above.
|
||||||
self.advance_one().unwrap();
|
self.advance_one().unwrap();
|
||||||
@@ -688,6 +694,31 @@ mod tests {
|
|||||||
assert!(FormatString::parse("\\").is_err());
|
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]
|
#[test]
|
||||||
fn test_parse_formatting() {
|
fn test_parse_formatting() {
|
||||||
fn unaligned_directive(directive: FormatDirective) -> FormatComponent {
|
fn unaligned_directive(directive: FormatDirective) -> FormatComponent {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user