diff --git a/Cargo.lock b/Cargo.lock index e86840b5..9c1e80f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "autocfg" version = "1.1.0" @@ -166,8 +172,10 @@ dependencies = [ "clap", "crossbeam", "ignore", + "indoc", "lscolors", "once_cell", + "strip-ansi-escapes", "tempdir", ] @@ -246,6 +254,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indoc" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2b9d82064e8a0226fddb3547f37f28eaa46d0fc210e275d835f08cf3b76a7" + [[package]] name = "io-lifetimes" version = "1.0.5" @@ -488,6 +502,15 @@ version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -539,12 +562,39 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "walkdir" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index 7934aed9..5d1034f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,6 @@ lscolors = { version = "0.13.0", features = ["ansi_term"] } once_cell = "1.17.0" [dev-dependencies] +indoc = "2.0.0" +strip-ansi-escapes = "0.1.1" tempdir = "0.3" diff --git a/README.md b/README.md index 6b857549..1944d980 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ Options: -n, --num-threads Number of threads to use [default: 4] -o, --order Sort order to display directory content [default: none] [possible values: filename, size, none] -s, --show-hidden Whether to show hidden files; disabled by default + -g, --glob Include or exclude files using glob patterns + --iglob Include or exclude files using glob patterns; case insensitive + --glob-case-insensitive Process all glob patterns case insensitively -h, --help Print help (see more with '--help') -V, --version Print version diff --git a/src/cli.rs b/src/cli.rs index 0e7fbb5c..ec496250 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,8 +1,13 @@ use crate::fs::erdtree::order::Order; use clap::Parser; -use ignore::{WalkBuilder, WalkParallel}; +use ignore::{ + overrides::{Override, OverrideBuilder}, + WalkBuilder, WalkParallel, +}; use std::{ convert::From, + error::Error as StdError, + fmt::{self, Display, Formatter}, path::{Path, PathBuf}, usize, }; @@ -36,6 +41,18 @@ pub struct Clargs { /// Whether to show hidden files; disabled by default #[arg(short, long)] pub show_hidden: bool, + + /// Include or exclude files using glob patterns + #[arg(short, long)] + glob: Vec, + + /// Include or exclude files using glob patterns; case insensitive + #[arg(long)] + iglob: Vec, + + /// Process all glob patterns case insensitively + #[arg(long)] + glob_case_insensitive: bool, } impl Clargs { @@ -58,15 +75,64 @@ impl Clargs { pub fn max_depth(&self) -> Option { self.max_depth } + + /// Ignore file overrides. + pub fn overrides(&self) -> Result { + if self.glob.is_empty() && self.iglob.is_empty() { + return Ok(Override::empty()); + } + + let mut builder = OverrideBuilder::new(self.dir()); + if self.glob_case_insensitive { + builder.case_insensitive(true).unwrap(); + } + + for glob in self.glob.iter() { + builder.add(glob)?; + } + + // all subsequent patterns are case insensitive + builder.case_insensitive(true).unwrap(); + for glob in self.iglob.iter() { + builder.add(glob)?; + } + + builder.build() + } } -impl From<&Clargs> for WalkParallel { - fn from(clargs: &Clargs) -> WalkParallel { - WalkBuilder::new(clargs.dir()) +impl TryFrom<&Clargs> for WalkParallel { + type Error = Error; + + fn try_from(clargs: &Clargs) -> Result { + Ok(WalkBuilder::new(clargs.dir()) .follow_links(false) + .overrides(clargs.overrides()?) .git_ignore(!clargs.ignore_git_ignore) .hidden(!clargs.show_hidden) .threads(clargs.num_threads) - .build_parallel() + .build_parallel()) + } +} + +/// Errors which may occur during command-line argument parsing. +#[derive(Debug)] +pub enum Error { + InvalidGlobPatterns(ignore::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidGlobPatterns(e) => write!(f, "Invalid glob patterns: {e}"), + } + } +} + +impl StdError for Error {} + +impl From for Error { + fn from(value: ignore::Error) -> Self { + Self::InvalidGlobPatterns(value) } } diff --git a/src/main.rs b/src/main.rs index 06c62e4f..f899bd4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::process::ExitCode; + use clap::Parser; use cli::Clargs; use fs::erdtree::{self, tree::Tree}; @@ -9,10 +11,19 @@ mod cli; /// Filesystem operations. mod fs; -fn main() -> Result<(), fs::error::Error> { +fn main() -> ExitCode { + if let Err(e) = run() { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + + ExitCode::SUCCESS +} + +fn run() -> Result<(), Box> { erdtree::init_ls_colors(); let clargs = Clargs::parse(); - let walker = WalkParallel::from(&clargs); + let walker = WalkParallel::try_from(&clargs)?; let tree = Tree::new(walker, clargs.order(), clargs.max_depth())?; println!("{tree}"); diff --git a/tests/data/a.txt b/tests/data/a.txt new file mode 100644 index 00000000..8bd6648e --- /dev/null +++ b/tests/data/a.txt @@ -0,0 +1 @@ +asdf diff --git a/tests/data/b.txt b/tests/data/b.txt new file mode 100644 index 00000000..cde0709e --- /dev/null +++ b/tests/data/b.txt @@ -0,0 +1 @@ +bvcx diff --git a/tests/data/c.md b/tests/data/c.md new file mode 100644 index 00000000..7ac8882e --- /dev/null +++ b/tests/data/c.md @@ -0,0 +1 @@ +# File diff --git a/tests/data/nested/other.md b/tests/data/nested/other.md new file mode 100644 index 00000000..24ebb076 --- /dev/null +++ b/tests/data/nested/other.md @@ -0,0 +1 @@ +# Nested file diff --git a/tests/glob.rs b/tests/glob.rs new file mode 100644 index 00000000..abf9096b --- /dev/null +++ b/tests/glob.rs @@ -0,0 +1,89 @@ +use indoc::indoc; +use std::process::Command; +use std::process::Stdio; +use strip_ansi_escapes::strip as strip_ansi_escapes; + +fn run_cmd(args: &[&str]) -> String { + let mut cmd = Command::new("cargo"); + cmd.arg("run") + .arg("--") + .arg("tests/data") + .arg("--num-threads") + .arg("1") + .arg("--order") + .arg("filename"); + + for arg in args { + cmd.arg(arg); + } + + let output = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + assert!(output.status.success()); + + String::from_utf8(strip_ansi_escapes(output.stdout).unwrap()) + .unwrap() + .trim() + .to_string() +} + +#[test] +fn glob() { + assert_eq!( + run_cmd(&["--glob", "*.txt"]), + indoc!( + " + data (10.00 B) + ├─ a.txt (5.00 B) + ├─ b.txt (5.00 B) + └─ nested" + ) + ) +} + +#[test] +fn glob_negative() { + assert_eq!( + run_cmd(&["--glob", "!*.txt"]), + indoc!( + " + data (21.00 B) + ├─ c.md (7.00 B) + └─ nested (14.00 B) + └─ other.md (14.00 B)" + ) + ) +} + +#[test] +fn glob_case_insensitive() { + assert_eq!( + run_cmd(&["--glob", "*.TXT", "--glob-case-insensitive"]), + indoc!( + " + data (10.00 B) + ├─ a.txt (5.00 B) + ├─ b.txt (5.00 B) + └─ nested" + ) + ) +} + +#[test] +fn iglob() { + assert_eq!( + run_cmd(&["--iglob", "*.TXT"]), + indoc!( + " + data (10.00 B) + ├─ a.txt (5.00 B) + ├─ b.txt (5.00 B) + └─ nested" + ) + ) +}