From 58711ec7373cf4bb73e140b7f3b1028f47d4a9c2 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 2 Apr 2026 05:54:09 +0000 Subject: [PATCH] fix(builtins): add find -path predicate and fix -not argument consumption Add -path predicate that matches against full paths using glob patterns. Fix -not/! to properly consume the following predicate and its arguments before negating, rather than only skipping itself. Closes #950 --- crates/bashkit/src/builtins/ls.rs | 49 +++++++++++++++++-- .../tests/spec_cases/bash/find.test.sh | 28 +++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/bashkit/src/builtins/ls.rs b/crates/bashkit/src/builtins/ls.rs index 12470b80..69cea049 100644 --- a/crates/bashkit/src/builtins/ls.rs +++ b/crates/bashkit/src/builtins/ls.rs @@ -334,6 +334,8 @@ fn human_readable_size(size: u64) -> String { /// Options for find command struct FindOptions { name_pattern: Option, + /// -path pattern: match against the full display path + path_pattern: Option, type_filter: Option, max_depth: Option, min_depth: Option, @@ -342,6 +344,10 @@ struct FindOptions { exec_args: Vec, /// true if -exec uses + (batch mode), false for \; (per-file mode) exec_batch: bool, + /// Negate the -name predicate + negate_name: bool, + /// Negate the -path predicate + negate_path: bool, } /// The find builtin - search for files. @@ -366,13 +372,17 @@ fn parse_find_args(args: &[String]) -> std::result::Result<(Vec, FindOpt let mut paths: Vec = Vec::new(); let mut opts = FindOptions { name_pattern: None, + path_pattern: None, type_filter: None, max_depth: None, min_depth: None, printf_format: None, exec_args: Vec::new(), exec_batch: false, + negate_name: false, + negate_path: false, }; + let mut negate_next = false; let mut i = 0; while i < args.len() { @@ -387,6 +397,24 @@ fn parse_find_args(args: &[String]) -> std::result::Result<(Vec, FindOpt )); } opts.name_pattern = Some(args[i].clone()); + if negate_next { + opts.negate_name = true; + negate_next = false; + } + } + "-path" => { + i += 1; + if i >= args.len() { + return Err(ExecResult::err( + "find: missing argument to '-path'\n".to_string(), + 1, + )); + } + opts.path_pattern = Some(args[i].clone()); + if negate_next { + opts.negate_path = true; + negate_next = false; + } } "-type" => { i += 1; @@ -469,7 +497,7 @@ fn parse_find_args(args: &[String]) -> std::result::Result<(Vec, FindOpt } } "-not" | "!" => { - // Negation - skip (not fully supported) + negate_next = true; } s if s.starts_with('-') => { return Err(ExecResult::err( @@ -501,12 +529,15 @@ async fn collect_find_paths( // Reuse find_recursive but with a temporary output buffer let temp_opts = FindOptions { name_pattern: opts.name_pattern.clone(), + path_pattern: opts.path_pattern.clone(), type_filter: opts.type_filter, max_depth: opts.max_depth, min_depth: opts.min_depth, printf_format: None, // Don't format, just collect paths exec_args: Vec::new(), exec_batch: false, + negate_name: opts.negate_name, + negate_path: opts.negate_path, }; let mut output = String::new(); for path_str in search_paths { @@ -672,7 +703,19 @@ fn find_recursive<'a>( // Check name pattern let name_matches = match &opts.name_pattern { - Some(pattern) => glob_match(&entry_name, pattern), + Some(pattern) => { + let m = glob_match(&entry_name, pattern); + if opts.negate_name { !m } else { m } + } + None => true, + }; + + // Check path pattern + let path_matches = match &opts.path_pattern { + Some(pattern) => { + let m = glob_match(display_path, pattern); + if opts.negate_path { !m } else { m } + } None => true, }; @@ -683,7 +726,7 @@ fn find_recursive<'a>( }; // Output if matches (or if no filters, show everything) - if type_matches && name_matches && above_min_depth { + if type_matches && name_matches && path_matches && above_min_depth { if let Some(ref fmt) = opts.printf_format { output.push_str(&find_printf_format(fmt, display_path, &metadata)); } else { diff --git a/crates/bashkit/tests/spec_cases/bash/find.test.sh b/crates/bashkit/tests/spec_cases/bash/find.test.sh index f8179e39..a2e04b77 100644 --- a/crates/bashkit/tests/spec_cases/bash/find.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/find.test.sh @@ -191,6 +191,34 @@ find /tmp/totally_nonexistent_path 2>&1 find: '/tmp/totally_nonexistent_path': No such file or directory ### end +### find_path_predicate +# find -path should filter by path pattern +mkdir -p /tmp/fp_test/a/b +touch /tmp/fp_test/a/b/file.txt /tmp/fp_test/top.txt +find /tmp/fp_test -path '*/a/*' | sort +### expect +/tmp/fp_test/a/b +/tmp/fp_test/a/b/file.txt +### end + +### find_not_name +# find -not -name should negate +mkdir -p /tmp/fn_test +touch /tmp/fn_test/keep.txt /tmp/fn_test/skip.log +find /tmp/fn_test -maxdepth 1 -type f -not -name '*.log' +### expect +/tmp/fn_test/keep.txt +### end + +### find_not_path_exclude +# find -not -path should exclude paths +mkdir -p /tmp/fnp_test/.git /tmp/fnp_test/src +touch /tmp/fnp_test/src/main.rs /tmp/fnp_test/.git/config +find /tmp/fnp_test -type f -not -path '*/.git/*' | sort +### expect +/tmp/fnp_test/src/main.rs +### end + ### ls_recursive # ls -R should list nested directories mkdir -p /tmp/lsrec/a/b