Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --glob and --regex #96

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ clap = "2.26.0"
ansi_term = "0.9"
atty = "0.2"
clap = "2.26.0"
globset = "0.2.0"
ignore = "0.2"
lazy_static = "0.2.9"
num_cpus = "1.6.2"
Expand Down
7 changes: 7 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub fn build_app() -> App<'static, 'static> {
.usage("fd [FLAGS/OPTIONS] [<pattern>] [<path>]")
.setting(AppSettings::ColoredHelp)
.setting(AppSettings::DeriveDisplayOrder)
.arg(arg("use-glob").long("glob").overrides_with("use-regex"))
.arg(arg("use-regex").long("regex").overrides_with("use-glob"))
.arg(arg("hidden").long("hidden").short("H"))
.arg(arg("no-ignore").long("no-ignore").short("I"))
.arg(
Expand Down Expand Up @@ -103,6 +105,11 @@ pub fn build_app() -> App<'static, 'static> {
#[cfg_attr(rustfmt, rustfmt_skip)]
fn usage() -> HashMap<&'static str, Help> {
let mut h = HashMap::new();
doc!(h, "use-glob"
, "Search with a glob pattern (default: use regex pattern)"
, "The pattern is a glob pattern, which will be converted to a regex pattern.");
doc!(h, "use-regex"
, "Search with a regex pattern. This is the default behavior.");
doc!(h, "hidden"
, "Search hidden files and directories"
, "Include hidden directories and files in the search results (default: hidden files \
Expand Down
72 changes: 72 additions & 0 deletions src/glob.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::error::Error;

use globset;
use regex::RegexBuilder;

use internal::error;

// http://pubs.opengroup.org/onlinepubs/9699919799/functions/glob.html
//
// TODO: Custom rules:
// 1. On Windows, all "\" in the path must be replaced with "/" before matching.
// 2. "\" removes special meaning of any single following character, then be discarded.
// 3. No character class expression?
// 4. Do not skip dot-files.
// 5. Ignore system locales.
//
// TODO: Make a new fork of globset? With a simpler rules set?
pub struct GlobBuilder {}

impl GlobBuilder {
pub fn new(pattern: &str, search_full_path: bool) -> RegexBuilder {
#[cfg(windows)]
let pattern = &patch_glob_pattern(pattern, search_full_path);

match globset::GlobBuilder::new(pattern)
.literal_separator(search_full_path)
.build() {
Ok(glob) => {
eprintln!("PATTERN: {} -> {}", glob.glob(), glob.regex());
// NOTE: .replace("(?-u)", "") works with globset 0.2.0
// FIXME: do not escape multi-byte chars with \xHH
RegexBuilder::new(glob.regex().replace("(?-u)", "").as_str())
}
Err(err) => error(err.description()),
}
}
}

#[cfg(windows)]
fn patch_glob_pattern(pattern: &str, search_full_path: bool) -> String {
if search_full_path {
let mut s = String::new();

if pattern.starts_with("/") {
s.push_str(&get_default_root());
} else if pattern.starts_with("*") {
s.push_str(&get_default_root());
if cfg!(windows) {
s.push('/');
}
} // else if start with "[/]"? TODO
s.push_str(pattern);
s
} else {
pattern.to_string()
}
}

#[cfg(windows)]
fn get_default_root() -> String {
use std::env;

if let Ok(cwd) = env::current_dir() {
let mut compos = cwd.components();
compos.next().map_or(String::from(""), |compo| {
// FIXME: escape special chars
compo.as_os_str().to_string_lossy().into()
})
} else {
error("Error: could not get current directory.");
}
}
3 changes: 3 additions & 0 deletions src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub enum PathDisplay {

/// Configuration options for *fd*.
pub struct FdOptions {
/// Whether to search with a glob pattern.
pub use_glob: bool,

/// Whether the search is case-sensitive or case-insensitive.
pub case_sensitive: bool,

Expand Down
11 changes: 10 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ extern crate ansi_term;
extern crate atty;
#[macro_use]
extern crate clap;
extern crate globset;
extern crate ignore;
#[macro_use]
extern crate lazy_static;
Expand All @@ -15,6 +16,7 @@ pub mod fshelper;
pub mod lscolors;
mod app;
mod exec;
mod glob;
mod internal;
mod output;
mod walk;
Expand All @@ -30,6 +32,7 @@ use regex::RegexBuilder;

use exec::TokenizedCommand;
use internal::{error, pattern_has_uppercase_char, FdOptions, PathDisplay};
use glob::GlobBuilder;
use lscolors::LsColors;
use walk::FileType;

Expand Down Expand Up @@ -94,6 +97,7 @@ fn main() {
let command = matches.value_of("exec").map(|x| TokenizedCommand::new(&x));

let config = FdOptions {
use_glob: matches.is_present("use-glob"),
case_sensitive,
search_full_path: matches.is_present("full-path"),
ignore_hidden: !(matches.is_present("hidden") ||
Expand Down Expand Up @@ -131,7 +135,12 @@ fn main() {
command,
};

match RegexBuilder::new(pattern)
let mut builder = if !config.use_glob {
RegexBuilder::new(pattern)
} else {
GlobBuilder::new(pattern, config.search_full_path)
};
match builder
.case_insensitive(!config.case_sensitive)
.dot_matches_new_line(true)
.build() {
Expand Down
11 changes: 9 additions & 2 deletions src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use fshelper;
use internal::{error, FdOptions};
use output;

use std::path::Path;
use std::path::{Path, MAIN_SEPARATOR};
use std::sync::{Arc, Mutex};
use std::sync::mpsc::channel;
use std::thread;
Expand Down Expand Up @@ -181,7 +181,14 @@ pub fn scan(root: &Path, pattern: Arc<Regex>, config: Arc<FdOptions>) {

let search_str_o = if config.search_full_path {
match fshelper::path_absolute_form(&entry_path) {
Ok(path_abs_buf) => Some(path_abs_buf.to_string_lossy().into_owned().into()),
Ok(path_abs_buf) => {
let path_str = path_abs_buf.to_string_lossy();
if cfg!(windows) && config.use_glob && MAIN_SEPARATOR == '\\' {
Some(path_str.replace('\\', "/").into())
} else {
Some(path_str.into_owned().into())
}
}
Err(_) => error("Error: unable to get full path."),
}
} else {
Expand Down
69 changes: 69 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,75 @@ fn test_smart_case() {
te.assert_output(&["\\AC"], "one/two/C.Foo2");
}

/// Glob searches (--glob)
#[test]
fn test_glob_searches() {
let te = TestEnv::new();

te.assert_output(
&["--glob", "*.foo"],
"a.foo
one/b.foo
one/two/c.foo
one/two/three/d.foo",
);

te.assert_output(
&["--glob", "--regex", "[a-c].foo"],
"a.foo
one/b.foo
one/two/c.foo
one/two/C.Foo2",
);

te.assert_output(
&["--regex", "--glob", "[a-c].foo"],
"a.foo
one/b.foo
one/two/c.foo",
);

te.assert_output(&["--full-path", "--glob", "*"], "");

te.assert_output(
&["--full-path", "--glob", "**"],
"a.foo
one
one/b.foo
one/two
one/two/c.foo
one/two/C.Foo2
one/two/three
one/two/three/d.foo
one/two/three/directory_foo
symlink",
);

te.assert_output(
&["--full-path", "--glob", "**/*.foo"],
"a.foo
one/b.foo
one/two/c.foo
one/two/three/d.foo",
);

te.assert_output(
&["--full-path", "--glob", "*/**/*.foo"],
"a.foo
one/b.foo
one/two/c.foo
one/two/three/d.foo",
);

te.assert_output(
&["--full-path", "--glob", "**/**/*.foo"],
"a.foo
one/b.foo
one/two/c.foo
one/two/three/d.foo",
);
}

/// Case sensitivity (--case-sensitive)
#[test]
fn test_case_sensitive() {
Expand Down