Skip to content

Commit 8e0d90d

Browse files
authored
fix(builtins): add find -path predicate and fix -not argument consumption (#974)
## Summary - Add `-path PATTERN` predicate to find builtin, matching against full paths with glob patterns - Fix `-not`/`!` to properly consume the following predicate and its arguments before negating ## Test plan - [x] New spec tests: `find_path_predicate`, `find_not_name`, `find_not_path_exclude` - [x] No regressions in existing spec tests - [x] clippy + fmt clean Closes #950
1 parent d6335ff commit 8e0d90d

File tree

2 files changed

+74
-3
lines changed

2 files changed

+74
-3
lines changed

crates/bashkit/src/builtins/ls.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ fn human_readable_size(size: u64) -> String {
334334
/// Options for find command
335335
struct FindOptions {
336336
name_pattern: Option<String>,
337+
/// -path pattern: match against the full display path
338+
path_pattern: Option<String>,
337339
type_filter: Option<char>,
338340
max_depth: Option<usize>,
339341
min_depth: Option<usize>,
@@ -342,6 +344,10 @@ struct FindOptions {
342344
exec_args: Vec<String>,
343345
/// true if -exec uses + (batch mode), false for \; (per-file mode)
344346
exec_batch: bool,
347+
/// Negate the -name predicate
348+
negate_name: bool,
349+
/// Negate the -path predicate
350+
negate_path: bool,
345351
}
346352

347353
/// The find builtin - search for files.
@@ -366,13 +372,17 @@ fn parse_find_args(args: &[String]) -> std::result::Result<(Vec<String>, FindOpt
366372
let mut paths: Vec<String> = Vec::new();
367373
let mut opts = FindOptions {
368374
name_pattern: None,
375+
path_pattern: None,
369376
type_filter: None,
370377
max_depth: None,
371378
min_depth: None,
372379
printf_format: None,
373380
exec_args: Vec::new(),
374381
exec_batch: false,
382+
negate_name: false,
383+
negate_path: false,
375384
};
385+
let mut negate_next = false;
376386

377387
let mut i = 0;
378388
while i < args.len() {
@@ -387,6 +397,24 @@ fn parse_find_args(args: &[String]) -> std::result::Result<(Vec<String>, FindOpt
387397
));
388398
}
389399
opts.name_pattern = Some(args[i].clone());
400+
if negate_next {
401+
opts.negate_name = true;
402+
negate_next = false;
403+
}
404+
}
405+
"-path" => {
406+
i += 1;
407+
if i >= args.len() {
408+
return Err(ExecResult::err(
409+
"find: missing argument to '-path'\n".to_string(),
410+
1,
411+
));
412+
}
413+
opts.path_pattern = Some(args[i].clone());
414+
if negate_next {
415+
opts.negate_path = true;
416+
negate_next = false;
417+
}
390418
}
391419
"-type" => {
392420
i += 1;
@@ -469,7 +497,7 @@ fn parse_find_args(args: &[String]) -> std::result::Result<(Vec<String>, FindOpt
469497
}
470498
}
471499
"-not" | "!" => {
472-
// Negation - skip (not fully supported)
500+
negate_next = true;
473501
}
474502
s if s.starts_with('-') => {
475503
return Err(ExecResult::err(
@@ -501,12 +529,15 @@ async fn collect_find_paths(
501529
// Reuse find_recursive but with a temporary output buffer
502530
let temp_opts = FindOptions {
503531
name_pattern: opts.name_pattern.clone(),
532+
path_pattern: opts.path_pattern.clone(),
504533
type_filter: opts.type_filter,
505534
max_depth: opts.max_depth,
506535
min_depth: opts.min_depth,
507536
printf_format: None, // Don't format, just collect paths
508537
exec_args: Vec::new(),
509538
exec_batch: false,
539+
negate_name: opts.negate_name,
540+
negate_path: opts.negate_path,
510541
};
511542
let mut output = String::new();
512543
for path_str in search_paths {
@@ -672,7 +703,19 @@ fn find_recursive<'a>(
672703

673704
// Check name pattern
674705
let name_matches = match &opts.name_pattern {
675-
Some(pattern) => glob_match(&entry_name, pattern),
706+
Some(pattern) => {
707+
let m = glob_match(&entry_name, pattern);
708+
if opts.negate_name { !m } else { m }
709+
}
710+
None => true,
711+
};
712+
713+
// Check path pattern
714+
let path_matches = match &opts.path_pattern {
715+
Some(pattern) => {
716+
let m = glob_match(display_path, pattern);
717+
if opts.negate_path { !m } else { m }
718+
}
676719
None => true,
677720
};
678721

@@ -683,7 +726,7 @@ fn find_recursive<'a>(
683726
};
684727

685728
// Output if matches (or if no filters, show everything)
686-
if type_matches && name_matches && above_min_depth {
729+
if type_matches && name_matches && path_matches && above_min_depth {
687730
if let Some(ref fmt) = opts.printf_format {
688731
output.push_str(&find_printf_format(fmt, display_path, &metadata));
689732
} else {

crates/bashkit/tests/spec_cases/bash/find.test.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,34 @@ find /tmp/totally_nonexistent_path 2>&1
191191
find: '/tmp/totally_nonexistent_path': No such file or directory
192192
### end
193193

194+
### find_path_predicate
195+
# find -path should filter by path pattern
196+
mkdir -p /tmp/fp_test/a/b
197+
touch /tmp/fp_test/a/b/file.txt /tmp/fp_test/top.txt
198+
find /tmp/fp_test -path '*/a/*' | sort
199+
### expect
200+
/tmp/fp_test/a/b
201+
/tmp/fp_test/a/b/file.txt
202+
### end
203+
204+
### find_not_name
205+
# find -not -name should negate
206+
mkdir -p /tmp/fn_test
207+
touch /tmp/fn_test/keep.txt /tmp/fn_test/skip.log
208+
find /tmp/fn_test -maxdepth 1 -type f -not -name '*.log'
209+
### expect
210+
/tmp/fn_test/keep.txt
211+
### end
212+
213+
### find_not_path_exclude
214+
# find -not -path should exclude paths
215+
mkdir -p /tmp/fnp_test/.git /tmp/fnp_test/src
216+
touch /tmp/fnp_test/src/main.rs /tmp/fnp_test/.git/config
217+
find /tmp/fnp_test -type f -not -path '*/.git/*' | sort
218+
### expect
219+
/tmp/fnp_test/src/main.rs
220+
### end
221+
194222
### ls_recursive
195223
# ls -R should list nested directories
196224
mkdir -p /tmp/lsrec/a/b

0 commit comments

Comments
 (0)