diff --git a/Cargo.lock b/Cargo.lock index a3003c04..de68692f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -223,7 +223,6 @@ dependencies = [ "chrono", "clap", "filetime", - "glob", "once_cell", "onig", "predicates", diff --git a/Cargo.toml b/Cargo.toml index df9e9c2d..96b72e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ authors = ["uutils developers"] [dependencies] chrono = "0.4" clap = "2.34" -glob = "0.3" walkdir = "2.3" regex = "1.5" once_cell = "1.9" diff --git a/src/find/main.rs b/src/find/main.rs index 1b6b3247..f3e7a3fd 100644 --- a/src/find/main.rs +++ b/src/find/main.rs @@ -4,9 +4,6 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -extern crate findutils; -extern crate glob; - fn main() { let args = std::env::args().collect::>(); let strs: Vec<&str> = args.iter().map(|s| s.as_ref()).collect(); diff --git a/src/find/matchers/glob.rs b/src/find/matchers/glob.rs new file mode 100644 index 00000000..5695d1df --- /dev/null +++ b/src/find/matchers/glob.rs @@ -0,0 +1,238 @@ +// Copyright 2022 Tavian Barnes +// +// 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. + +use onig::{self, Regex, RegexOptions, Syntax}; + +/// Parse a string as a POSIX Basic Regular Expression. +fn parse_bre(expr: &str, options: RegexOptions) -> Result { + let bre = Syntax::posix_basic(); + Regex::with_options(expr, bre.options() | options, bre) +} + +/// Push a literal character onto a regex, escaping it if necessary. +fn regex_push_literal(regex: &mut String, ch: char) { + // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_03 + if matches!(ch, '.' | '[' | '\\' | '*' | '^' | '$') { + regex.push('\\'); + } + regex.push(ch); +} + +/// Extracts a bracket expression from a glob. +fn extract_bracket_expr(pattern: &str) -> Option<(String, &str)> { + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13_01 + // + // If an open bracket introduces a bracket expression as in XBD RE Bracket Expression, + // except that the character ( '!' ) shall replace the + // character ( '^' ) in its role in a non-matching list in the regular expression notation, + // it shall introduce a pattern bracket expression. A bracket expression starting with an + // unquoted character produces unspecified results. Otherwise, '[' shall match + // the character itself. + // + // To check for valid bracket expressions, we scan for the closing bracket and + // attempt to parse that segment as a regex. If that fails, we treat the '[' + // literally. + + let mut expr = "[".to_string(); + + let mut chars = pattern.chars(); + let mut next = chars.next(); + + // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 + // + // 3. A non-matching list expression begins with a ( '^' ) ... + // + // (but in a glob, '!' is used instead of '^') + if next == Some('!') { + expr.push('^'); + next = chars.next(); + } + + // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 + // + // 1. ... The ( ']' ) shall lose its special meaning and represent + // itself in a bracket expression if it occurs first in the list (after an initial + // ( '^' ), if any). + if next == Some(']') { + expr.push(']'); + next = chars.next(); + } + + while let Some(ch) = next { + expr.push(ch); + + match ch { + '[' => { + // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 + // + // 4. A collating symbol is a collating element enclosed within bracket-period + // ( "[." and ".]" ) delimiters. ... + // + // 5. An equivalence class expression shall ... be expressed by enclosing any + // one of the collating elements in the equivalence class within bracket- + // equal ( "[=" and "=]" ) delimiters. + // + // 6. ... A character class expression is expressed as a character class name + // enclosed within bracket- ( "[:" and ":]" ) delimiters. + next = chars.next(); + if let Some(delim) = next { + expr.push(delim); + + if matches!(delim, '.' | '=' | ':') { + let rest = chars.as_str(); + let end = rest.find([delim, ']'])? + 2; + expr.push_str(&rest[..end]); + chars = rest[end..].chars(); + } + } + } + ']' => { + // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 + // + // 1. ... The ( ']' ) shall ... terminate the bracket + // expression, unless it appears in a collating symbol (such as "[.].]" ) or is + // the ending for a collating symbol, equivalence class, + // or character class. + break; + } + _ => {} + } + + next = chars.next(); + } + + if parse_bre(&expr, RegexOptions::REGEX_OPTION_NONE).is_ok() { + Some((expr, chars.as_str())) + } else { + None + } +} + +/// Converts a POSIX glob into a POSIX Basic Regular Expression +fn glob_to_regex(pattern: &str) -> String { + let mut regex = String::new(); + + let mut chars = pattern.chars(); + while let Some(ch) = chars.next() { + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13 + match ch { + '?' => regex.push('.'), + '*' => regex.push_str(".*"), + '\\' => { + if let Some(ch) = chars.next() { + regex_push_literal(&mut regex, ch); + } else { + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html + // + // If pattern ends with an unescaped , fnmatch() shall return a + // non-zero value (indicating either no match or an error). + // + // Most implementations return FNM_NOMATCH in this case, so return a regex that + // never matches. + return "$.".to_string(); + } + } + '[' => { + if let Some((expr, rest)) = extract_bracket_expr(chars.as_str()) { + regex.push_str(&expr); + chars = rest.chars(); + } else { + regex_push_literal(&mut regex, ch); + } + } + _ => regex_push_literal(&mut regex, ch), + } + } + + regex +} + +/// An fnmatch()-style glob matcher. +pub struct Pattern { + regex: Regex, +} + +impl Pattern { + /// Parse an fnmatch()-style glob. + pub fn new(pattern: &str, caseless: bool) -> Self { + let options = if caseless { + RegexOptions::REGEX_OPTION_IGNORECASE + } else { + RegexOptions::REGEX_OPTION_NONE + }; + + // As long as glob_to_regex() is correct, this should never fail + let regex = parse_bre(&glob_to_regex(pattern), options).unwrap(); + Self { regex } + } + + /// Test if this patern matches a string. + pub fn matches(&self, string: &str) -> bool { + self.regex.is_match(string) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn literals() { + assert_eq!(glob_to_regex(r"foo.bar"), r"foo\.bar"); + } + + #[test] + fn regex_special() { + assert_eq!(glob_to_regex(r"^foo.bar$"), r"\^foo\.bar\$"); + } + + #[test] + fn wildcards() { + assert_eq!(glob_to_regex(r"foo?bar*baz"), r"foo.bar.*baz"); + } + + #[test] + fn escapes() { + assert_eq!(glob_to_regex(r"fo\o\?bar\*baz\\"), r"foo?bar\*baz\\"); + } + + #[test] + fn incomplete_escape() { + assert_eq!(glob_to_regex(r"foo\"), r"$.") + } + + #[test] + fn valid_brackets() { + assert_eq!(glob_to_regex(r"foo[bar][!baz]"), r"foo[bar][^baz]"); + } + + #[test] + fn complex_brackets() { + assert_eq!( + glob_to_regex(r"[!]!.*[\[.].][=]=][:space:]-]"), + r"[^]!.*[\[.].][=]=][:space:]-]" + ); + } + + #[test] + fn invalid_brackets() { + assert_eq!(glob_to_regex(r"foo[bar[!baz"), r"foo\[bar\[!baz"); + } + + #[test] + fn pattern_matches() { + assert!(Pattern::new(r"foo*bar", false).matches("foo--bar")); + + assert!(!Pattern::new(r"foo*bar", false).matches("bar--foo")); + } + + #[test] + fn caseless_matches() { + assert!(Pattern::new(r"foo*BAR", true).matches("FOO--bar")); + + assert!(!Pattern::new(r"foo*BAR", true).matches("BAR--foo")); + } +} diff --git a/src/find/matchers/lname.rs b/src/find/matchers/lname.rs new file mode 100644 index 00000000..58c130e4 --- /dev/null +++ b/src/find/matchers/lname.rs @@ -0,0 +1,107 @@ +// Copyright 2017 Google Inc. +// +// 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. + +use std::io::{stderr, Write}; +use std::path::PathBuf; + +use walkdir::DirEntry; + +use super::glob::Pattern; +use super::{Matcher, MatcherIO}; + +fn read_link_target(file_info: &DirEntry) -> Option { + match file_info.path().read_link() { + Ok(target) => Some(target), + Err(err) => { + // If it's not a symlink, then it's not an error that should be + // shown. + if err.kind() != std::io::ErrorKind::InvalidInput { + writeln!( + &mut stderr(), + "Error reading target of {}: {}", + file_info.path().display(), + err + ) + .unwrap(); + } + + None + } + } +} + +/// This matcher makes a comparison of the link target against a shell wildcard +/// pattern. See `glob::Pattern` for details on the exact syntax. +pub struct LinkNameMatcher { + pattern: Pattern, +} + +impl LinkNameMatcher { + pub fn new(pattern_string: &str, caseless: bool) -> LinkNameMatcher { + let pattern = Pattern::new(pattern_string, caseless); + Self { pattern } + } +} + +impl Matcher for LinkNameMatcher { + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + if let Some(target) = read_link_target(file_info) { + self.pattern.matches(&target.to_string_lossy()) + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::find::matchers::tests::get_dir_entry_for; + use crate::find::matchers::Matcher; + use crate::find::tests::FakeDependencies; + + use std::io::ErrorKind; + + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(windows)] + use std::os::windows::fs::symlink_file; + + fn create_file_link() { + #[cfg(unix)] + if let Err(e) = symlink("abbbc", "test_data/links/link-f") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + #[cfg(windows)] + if let Err(e) = symlink_file("abbbc", "test_data/links/link-f") { + if e.kind() != ErrorKind::AlreadyExists { + panic!("Failed to create sym link: {:?}", e); + } + } + } + + #[test] + fn matches_against_link_target() { + create_file_link(); + + let link_f = get_dir_entry_for("test_data/links", "link-f"); + let matcher = LinkNameMatcher::new("ab?bc", false); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); + } + + #[test] + fn caseless_matches_against_link_target() { + create_file_link(); + + let link_f = get_dir_entry_for("test_data/links", "link-f"); + let matcher = LinkNameMatcher::new("AbB?c", true); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); + } +} diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index 687e6e91..ee97e879 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -7,8 +7,11 @@ mod delete; mod empty; pub mod exec; +mod glob; +mod lname; mod logical_matchers; mod name; +mod path; mod perm; mod printer; mod printf; @@ -28,10 +31,12 @@ use walkdir::DirEntry; use self::delete::DeleteMatcher; use self::empty::EmptyMatcher; use self::exec::SingleExecMatcher; +use self::lname::LinkNameMatcher; use self::logical_matchers::{ AndMatcherBuilder, FalseMatcher, ListMatcherBuilder, NotMatcher, TrueMatcher, }; -use self::name::{CaselessNameMatcher, NameMatcher}; +use self::name::NameMatcher; +use self::path::PathMatcher; use self::perm::PermMatcher; use self::printer::{PrintDelimiter, Printer}; use self::printf::Printf; @@ -279,19 +284,26 @@ fn build_matcher_tree( } "-true" => Some(TrueMatcher.into_box()), "-false" => Some(FalseMatcher.into_box()), - "-name" | "-lname" => { + "-lname" | "-ilname" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; - Some(NameMatcher::new(args[i], args[i - 1].starts_with("-l"))?.into_box()) + Some(LinkNameMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) } - "-iname" | "-ilname" => { + "-name" | "-iname" => { if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } i += 1; - Some(CaselessNameMatcher::new(args[i], args[i - 1].starts_with("-il"))?.into_box()) + Some(NameMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) + } + "-path" | "-ipath" | "-wholename" | "-iwholename" => { + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(PathMatcher::new(args[i], args[i - 1].starts_with("-i")).into_box()) } "-regextype" => { if i >= args.len() - 1 { diff --git a/src/find/matchers/name.rs b/src/find/matchers/name.rs index d16ce5f6..94d8eb9c 100644 --- a/src/find/matchers/name.rs +++ b/src/find/matchers/name.rs @@ -4,109 +4,32 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -use std::io::stderr; -use std::io::Write; -use std::path::PathBuf; - -use glob::Pattern; -use glob::PatternError; use walkdir::DirEntry; +use super::glob::Pattern; use super::{Matcher, MatcherIO}; -fn read_link_target(file_info: &DirEntry) -> Option { - match file_info.path().read_link() { - Ok(target) => Some(target), - Err(err) => { - // If it's not a symlink, then it's not an error that should be - // shown. - if err.kind() != std::io::ErrorKind::InvalidInput { - writeln!( - &mut stderr(), - "Error reading target of {}: {}", - file_info.path().display(), - err - ) - .unwrap(); - } - - None - } - } -} - -/// This matcher makes a case-sensitive comparison of the name against a -/// shell wildcard pattern. See `glob::Pattern` for details on the exact -/// syntax. +/// This matcher makes a comparison of the name against a shell wildcard +/// pattern. See `glob::Pattern` for details on the exact syntax. pub struct NameMatcher { pattern: Pattern, - match_link_target: bool, } impl NameMatcher { - pub fn new(pattern_string: &str, match_link_target: bool) -> Result { - let p = Pattern::new(pattern_string)?; - Ok(NameMatcher { - pattern: p, - match_link_target, - }) + pub fn new(pattern_string: &str, caseless: bool) -> Self { + let pattern = Pattern::new(pattern_string, caseless); + Self { pattern } } } impl Matcher for NameMatcher { fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { - if self.match_link_target { - if let Some(target) = read_link_target(file_info) { - self.pattern.matches(&target.to_string_lossy()) - } else { - false - } - } else { - self.pattern - .matches(file_info.file_name().to_string_lossy().as_ref()) - } - } -} - -/// This matcher makes a case-insensitive comparison of the name against a -/// shell wildcard pattern. See `glob::Pattern` for details on the exact -/// syntax. -pub struct CaselessNameMatcher { - pattern: Pattern, - match_link_target: bool, -} - -impl CaselessNameMatcher { - pub fn new( - pattern_string: &str, - match_link_target: bool, - ) -> Result { - let p = Pattern::new(&pattern_string.to_lowercase())?; - Ok(CaselessNameMatcher { - pattern: p, - match_link_target, - }) - } -} - -impl super::Matcher for CaselessNameMatcher { - fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { - if self.match_link_target { - if let Some(target) = read_link_target(file_info) { - self.pattern - .matches(&target.to_string_lossy().to_lowercase()) - } else { - false - } - } else { - self.pattern - .matches(&file_info.file_name().to_string_lossy().to_lowercase()) - } + let name = file_info.file_name().to_string_lossy(); + self.pattern.matches(&name) } } #[cfg(test)] - mod tests { use super::*; use crate::find::matchers::tests::get_dir_entry_for; @@ -119,7 +42,7 @@ mod tests { use std::os::unix::fs::symlink; #[cfg(windows)] - use std::os::windows::fs::{symlink_dir, symlink_file}; + use std::os::windows::fs::symlink_file; fn create_file_link() { #[cfg(unix)] @@ -139,7 +62,7 @@ mod tests { #[test] fn matching_with_wrong_case_returns_false() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); - let matcher = NameMatcher::new(&"A*C".to_string(), false).unwrap(); + let matcher = NameMatcher::new("A*C", false); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } @@ -147,7 +70,7 @@ mod tests { #[test] fn matching_with_right_case_returns_true() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); - let matcher = NameMatcher::new(&"abb?c".to_string(), false).unwrap(); + let matcher = NameMatcher::new("abb?c", false); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } @@ -155,7 +78,7 @@ mod tests { #[test] fn not_matching_returns_false() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); - let matcher = NameMatcher::new(&"shouldn't match".to_string(), false).unwrap(); + let matcher = NameMatcher::new("shouldn't match", false); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } @@ -165,31 +88,15 @@ mod tests { create_file_link(); let link_f = get_dir_entry_for("test_data/links", "link-f"); - let matcher = NameMatcher::new("link?f", false).unwrap(); + let matcher = NameMatcher::new("link?f", false); let deps = FakeDependencies::new(); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); } - #[test] - fn matches_against_link_target_if_requested() { - create_file_link(); - - let link_f = get_dir_entry_for("test_data/links", "link-f"); - let matcher = NameMatcher::new("ab?bc", true).unwrap(); - let deps = FakeDependencies::new(); - assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); - } - - #[test] - fn cant_create_with_invalid_pattern() { - let result = NameMatcher::new(&"a**c".to_string(), false); - assert!(result.is_err()); - } - #[test] fn caseless_matching_with_wrong_case_returns_true() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); - let matcher = CaselessNameMatcher::new(&"A*C".to_string(), false).unwrap(); + let matcher = NameMatcher::new("A*C", true); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } @@ -197,7 +104,7 @@ mod tests { #[test] fn caseless_matching_with_right_case_returns_true() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); - let matcher = CaselessNameMatcher::new(&"abb?c".to_string(), false).unwrap(); + let matcher = NameMatcher::new("abb?c", true); let deps = FakeDependencies::new(); assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); } @@ -205,7 +112,7 @@ mod tests { #[test] fn caseless_not_matching_returns_false() { let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); - let matcher = CaselessNameMatcher::new(&"shouldn't match".to_string(), false).unwrap(); + let matcher = NameMatcher::new("shouldn't match", true); let deps = FakeDependencies::new(); assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); } @@ -215,24 +122,8 @@ mod tests { create_file_link(); let link_f = get_dir_entry_for("test_data/links", "link-f"); - let matcher = CaselessNameMatcher::new("linK?f", false).unwrap(); + let matcher = NameMatcher::new("linK?f", true); let deps = FakeDependencies::new(); assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); } - - #[test] - fn caseless_matches_against_link_target_if_requested() { - create_file_link(); - - let link_f = get_dir_entry_for("test_data/links", "link-f"); - let matcher = CaselessNameMatcher::new("AbB?c", true).unwrap(); - let deps = FakeDependencies::new(); - assert!(matcher.matches(&link_f, &mut deps.new_matcher_io())); - } - - #[test] - fn caseless_cant_create_with_invalid_pattern() { - let result = CaselessNameMatcher::new(&"a**c".to_string(), false); - assert!(result.is_err()); - } } diff --git a/src/find/matchers/path.rs b/src/find/matchers/path.rs new file mode 100644 index 00000000..58e2a970 --- /dev/null +++ b/src/find/matchers/path.rs @@ -0,0 +1,82 @@ +// Copyright 2017 Google Inc. +// +// 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. + +use walkdir::DirEntry; + +use super::glob::Pattern; +use super::{Matcher, MatcherIO}; + +/// This matcher makes a comparison of the path against a shell wildcard +/// pattern. See `glob::Pattern` for details on the exact syntax. +pub struct PathMatcher { + pattern: Pattern, +} + +impl PathMatcher { + pub fn new(pattern_string: &str, caseless: bool) -> Self { + let pattern = Pattern::new(pattern_string, caseless); + Self { pattern } + } +} + +impl Matcher for PathMatcher { + fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool { + let path = file_info.path().to_string_lossy(); + self.pattern.matches(&path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::find::matchers::tests::get_dir_entry_for; + use crate::find::matchers::Matcher; + use crate::find::tests::FakeDependencies; + + // Variants of fix_up_slashes that properly escape the forward slashes for + // being in a glob. + #[cfg(windows)] + fn fix_up_glob_slashes(re: &str) -> String { + re.replace("/", "\\\\") + } + + #[cfg(not(windows))] + fn fix_up_glob_slashes(re: &str) -> String { + re.to_owned() + } + + #[test] + fn matching_against_whole_path() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/a*c"), false); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn not_matching_against_just_name() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = PathMatcher::new("a*c", false); + let deps = FakeDependencies::new(); + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn not_matching_against_wrong_case() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/A*C"), false); + let deps = FakeDependencies::new(); + assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } + + #[test] + fn caseless_matching() { + let abbbc = get_dir_entry_for("test_data/simple", "abbbc"); + let matcher = PathMatcher::new(&fix_up_glob_slashes("test_*/*/A*C"), true); + let deps = FakeDependencies::new(); + assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io())); + } +} diff --git a/src/find/matchers/regex.rs b/src/find/matchers/regex.rs index 0627a6d0..b29e3e57 100644 --- a/src/find/matchers/regex.rs +++ b/src/find/matchers/regex.rs @@ -130,7 +130,7 @@ mod tests { // being in a regex. #[cfg(windows)] fn fix_up_regex_slashes(re: &str) -> String { - re.replace("/", "\\\\") + re.replace("/", r"\\") } #[cfg(not(windows))] diff --git a/src/lib.rs b/src/lib.rs index 7a60d7d9..9f959d0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,5 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -extern crate glob; -extern crate regex; -extern crate walkdir; - -#[cfg(test)] -extern crate tempfile; - pub mod find; pub mod xargs; diff --git a/util/build-bfs.sh b/util/build-bfs.sh index 375e3366..77f96582 100755 --- a/util/build-bfs.sh +++ b/util/build-bfs.sh @@ -26,6 +26,12 @@ LOG_FILE=bfs/tests.log PASS=$(sed -n "s/^tests passed: \(.*\)/\1/p" "$LOG_FILE" | head -n1) SKIP=$(sed -n "s/^tests skipped: \(.*\)/\1/p" "$LOG_FILE" | head -n1) FAIL=$(sed -n "s/^tests failed: \(.*\)/\1/p" "$LOG_FILE" | head -n1) + +# Default any missing numbers to zero (e.g. no tests skipped) +: ${PASS:=0} +: ${SKIP:=0} +: ${FAIL:=0} + TOTAL=$((PASS + SKIP + FAIL)) if (( TOTAL <= 1 )); then echo "Error in the execution, failing early"