From de03d5dc7582f709a89f73371c1ce7e22ccba118 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Mon, 11 Sep 2023 20:34:22 +0200 Subject: [PATCH 01/47] feat(dependencies): Add Clap as a dependency to the project --- .gitignore | 3 ++ Cargo.lock | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 ++- 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7ba731c44..0873c36c1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ stage # VHS testing stuff out.gif tests/tmp + +#vscode +.vscode diff --git a/Cargo.lock b/Cargo.lock index 87ff7af46..3b835a381 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,54 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -44,6 +92,52 @@ dependencies = [ "jobserver", ] +[[package]] +name = "clap" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "datetime" version = "0.5.2" @@ -83,6 +177,7 @@ name = "eza" version = "0.11.0" dependencies = [ "ansi_term", + "clap", "datetime", "git2", "glob", @@ -133,6 +228,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.2" @@ -407,6 +508,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "2.0.29" @@ -500,6 +607,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uzers" version = "0.11.2" diff --git a/Cargo.toml b/Cargo.toml index 084604238..fef42475b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ name = "eza" [dependencies] ansi_term = "0.12" +clap = { version = "4.4.2", features = ["derive", "string"] } glob = "0.3" lazy_static = "1.3" libc = "0.2" @@ -75,8 +76,8 @@ version = "0.5.2" default-features = false [features] -default = [ "git" ] -git = [ "git2" ] +default = ["git"] +git = ["git2"] vendored-openssl = ["git2/vendored-openssl"] From f438e6cbc5cf7fa1a8f2bdd73042b71c0a6be0da Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Mon, 11 Sep 2023 20:38:08 +0200 Subject: [PATCH 02/47] feat(Parser): reimplement parser with clap Added a new parser using clap and modified the deducing of options --- src/main.rs | 73 ++- src/options/dir_action.rs | 167 ++++--- src/options/error.rs | 73 +-- src/options/file_name.rs | 74 ++- src/options/filter.rs | 277 +++++------ src/options/mod.rs | 162 ++----- src/options/parser.rs | 955 +++++++++----------------------------- src/options/theme.rs | 163 +------ src/options/view.rs | 595 +++++++++++------------- 9 files changed, 895 insertions(+), 1644 deletions(-) diff --git a/src/main.rs b/src/main.rs index a33cf15fa..57ca1d91f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ use std::env; use std::ffi::{OsStr, OsString}; use std::io::{self, Write, ErrorKind}; use std::path::{Component, PathBuf}; +use clap::Parser; use ansi_term::{ANSIStrings, Style}; @@ -34,9 +35,10 @@ use log::*; use crate::fs::{Dir, File}; use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; -use crate::options::{Options, Vars, vars, OptionsResult}; +use crate::options::{Options, Vars, vars}; use crate::output::{escape, lines, grid, grid_details, details, View, Mode}; use crate::theme::Theme; +use crate::options::parser::Opts; mod fs; mod info; @@ -46,6 +48,7 @@ mod output; mod theme; + fn main() { use std::process::exit; @@ -61,56 +64,40 @@ fn main() { warn!("Failed to enable ANSI support: {}", e); } - let args: Vec<_> = env::args_os().skip(1).collect(); - match Options::parse(args.iter().map(std::convert::AsRef::as_ref), &LiveVars) { - OptionsResult::Ok(options, mut input_paths) => { - - // List the current directory by default. - // (This has to be done here, otherwise git_options won’t see it.) - if input_paths.is_empty() { - input_paths = vec![ OsStr::new(".") ]; - } - - let git = git_options(&options, &input_paths); - let writer = io::stdout(); - - let console_width = options.view.width.actual_terminal_width(); - let theme = options.theme.to_theme(terminal_size::terminal_size().is_some()); - let exa = Exa { options, writer, input_paths, theme, console_width, git }; + let cli = Opts::parse(); - match exa.run() { - Ok(exit_status) => { - exit(exit_status); - } + let mut input_paths: Vec<&OsStr> = cli.paths.iter().map(OsString::as_os_str).collect(); + if input_paths.is_empty() { + input_paths.push(OsStr::new(".")); + } + let options = match Options::deduce(&cli, &LiveVars) { + Ok(o) => {o}, + Err(e) => { + eprintln!("{e}"); + exit(exits::OPTIONS_ERROR); + } + }; - Err(e) if e.kind() == ErrorKind::BrokenPipe => { - warn!("Broken pipe error: {e}"); - exit(exits::SUCCESS); - } + let git = git_options(&options, &input_paths); + let writer = io::stdout(); - Err(e) => { - eprintln!("{e}"); - exit(exits::RUNTIME_ERROR); - } - } - } + let console_width = options.view.width.actual_terminal_width(); + let theme = options.theme.to_theme(terminal_size::terminal_size().is_some()); + let exa = Exa { options, writer, input_paths, theme, console_width, git }; - OptionsResult::Help(help_text) => { - print!("{help_text}"); + match exa.run() { + Ok(exit_status) => { + exit(exit_status); } - OptionsResult::Version(version_str) => { - print!("{version_str}"); + Err(e) if e.kind() == ErrorKind::BrokenPipe => { + warn!("Broken pipe error: {e}"); + exit(exits::SUCCESS); } - OptionsResult::InvalidOptions(error) => { - eprintln!("eza: {error}"); - - if let Some(s) = error.suggestion() { - eprintln!("{s}"); - } - - exit(exits::OPTIONS_ERROR); + Err(e) => { + eprintln!("{e}"); + exit(exits::RUNTIME_ERROR); } } } diff --git a/src/options/dir_action.rs b/src/options/dir_action.rs index e729b86c7..5de736959 100644 --- a/src/options/dir_action.rs +++ b/src/options/dir_action.rs @@ -1,9 +1,9 @@ //! Parsing the options for `DirAction`. -use crate::options::parser::MatchedFlags; -use crate::options::{flags, OptionsError, NumberSource}; +use crate::options::OptionsError; use crate::fs::dir_action::{DirAction, RecurseOptions}; +use crate::options::parser::Opts; impl DirAction { @@ -12,31 +12,31 @@ impl DirAction { /// There are three possible actions, and they overlap somewhat: the /// `--tree` flag is another form of recursion, so those two are allowed /// to both be present, but the `--list-dirs` flag is used separately. - pub fn deduce(matches: &MatchedFlags<'_>, can_tree: bool) -> Result { - let recurse = matches.has(&flags::RECURSE)?; - let as_file = matches.has(&flags::LIST_DIRS)?; - let tree = matches.has(&flags::TREE)?; + pub fn deduce(matches: &Opts, can_tree: bool, strictness: bool) -> Result { + let recurse = matches.recurse > 0; + let as_file = matches.list_dirs >0; + let tree = matches.tree > 0; - if matches.is_strict() { + if strictness { // Early check for --level when it wouldn’t do anything - if ! recurse && ! tree && matches.count(&flags::LEVEL) > 0 { - return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)); + if ! recurse && ! tree && matches.level.is_some() { + return Err(OptionsError::Useless2("--level".to_string(), "--recurce".to_string(), "--tree".to_string())); } else if recurse && as_file { - return Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS)); + return Err(OptionsError::Conflict("--recurce".to_string(), "--list-dirs".to_string())); } else if tree && as_file { - return Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS)); + return Err(OptionsError::Conflict("--tree".to_string(), "--list-dirs".to_string())); } } if tree && can_tree { // Tree is only appropriate in details mode, so this has to // examine the View, which should have already been deduced by now - Ok(Self::Recurse(RecurseOptions::deduce(matches, true)?)) + Ok(Self::Recurse(RecurseOptions::deduce(matches, true))) } else if recurse { - Ok(Self::Recurse(RecurseOptions::deduce(matches, false)?)) + Ok(Self::Recurse(RecurseOptions::deduce(matches, false))) } else if as_file { Ok(Self::AsFile) @@ -54,79 +54,104 @@ impl RecurseOptions { /// flag’s value, and whether the `--tree` flag was passed, which was /// determined earlier. The maximum level should be a number, and this /// will fail with an `Err` if it isn’t. - pub fn deduce(matches: &MatchedFlags<'_>, tree: bool) -> Result { - if let Some(level) = matches.get(&flags::LEVEL)? { - let arg_str = level.to_string_lossy(); - match arg_str.parse() { - Ok(l) => { - Ok(Self { tree, max_depth: Some(l) }) - } - Err(e) => { - let source = NumberSource::Arg(&flags::LEVEL); - Err(OptionsError::FailedParse(arg_str.to_string(), source, e)) - } - } - } - else { - Ok(Self { tree, max_depth: None }) + pub fn deduce(matches: &Opts, tree: bool) -> Self { + Self { + tree, + max_depth: matches.level } } } - #[cfg(test)] mod test { use super::*; - use crate::options::flags; - use crate::options::parser::Flag; - - macro_rules! test { - ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => { - #[test] - fn $name() { - use crate::options::parser::Arg; - use crate::options::test::parse_for_test; - use crate::options::test::Strictnesses::*; - - static TEST_ARGS: &[&Arg] = &[&flags::RECURSE, &flags::LIST_DIRS, &flags::TREE, &flags::LEVEL ]; - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, true)) { - assert_eq!(result, $result); - } - } + + #[test] + fn deduces_list() { + let matches = Opts { + ..Opts::default() + }; + + assert_eq!(DirAction::deduce(&matches, false, false).unwrap(), DirAction::List); + } + + #[test] + fn deduce_recurse() { + let matches = Opts { + recurse: 1, + ..Opts::default() + }; + assert_eq!(DirAction::deduce(&matches, false, false).unwrap(), DirAction::Recurse(RecurseOptions { + tree: false, + max_depth: None, + })); + } + + #[test] + fn deduce_recurse_tree() { + let matches = Opts { + tree: 1, + ..Opts::default() + }; + assert_eq!(DirAction::deduce(&matches, true, false).unwrap(), DirAction::Recurse(RecurseOptions { + tree: true, + max_depth: None, + })); + } + + #[test] + fn deduce_as_file() { + let matches = Opts { + list_dirs: 1, + ..Opts::default() }; + assert_eq!(DirAction::deduce(&matches, false, false).unwrap(), DirAction::AsFile); } + #[test] + fn deduce_strict_unuseful_level() { + let matches = Opts { + level: Some(2), + ..Opts::default() + }; - // Default behaviour - test!(empty: DirAction <- []; Both => Ok(DirAction::List)); + assert!(DirAction::deduce(&matches, false, true).is_err()); + } - // Listing files as directories - test!(dirs_short: DirAction <- ["-d"]; Both => Ok(DirAction::AsFile)); - test!(dirs_long: DirAction <- ["--list-dirs"]; Both => Ok(DirAction::AsFile)); + #[test] + fn deduce_strict_recurse_and_as_file_option() { + let matches = Opts { + recurse: 1, + list_dirs: 1, + ..Opts::default() + }; - // Recursing - use self::DirAction::Recurse; - test!(rec_short: DirAction <- ["-R"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: None }))); - test!(rec_long: DirAction <- ["--recurse"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: None }))); - test!(rec_lim_short: DirAction <- ["-RL4"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(4) }))); - test!(rec_lim_short_2: DirAction <- ["-RL=5"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(5) }))); - test!(rec_lim_long: DirAction <- ["--recurse", "--level", "666"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(666) }))); - test!(rec_lim_long_2: DirAction <- ["--recurse", "--level=0118"]; Both => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(118) }))); - test!(tree: DirAction <- ["--tree"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None }))); - test!(rec_tree: DirAction <- ["--recurse", "--tree"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None }))); - test!(rec_short_tree: DirAction <- ["-TR"]; Both => Ok(Recurse(RecurseOptions { tree: true, max_depth: None }))); + assert!(DirAction::deduce(&matches, false, true).is_err()); + } - // Overriding --list-dirs, --recurse, and --tree - test!(dirs_recurse: DirAction <- ["--list-dirs", "--recurse"]; Last => Ok(Recurse(RecurseOptions { tree: false, max_depth: None }))); - test!(dirs_tree: DirAction <- ["--list-dirs", "--tree"]; Last => Ok(Recurse(RecurseOptions { tree: true, max_depth: None }))); - test!(just_level: DirAction <- ["--level=4"]; Last => Ok(DirAction::List)); + #[test] + fn deduce_strict_tree_and_as_file_option() { + let matches = Opts { + tree: 1, + list_dirs: 1, + ..Opts::default() + }; - test!(dirs_recurse_2: DirAction <- ["--list-dirs", "--recurse"]; Complain => Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS))); - test!(dirs_tree_2: DirAction <- ["--list-dirs", "--tree"]; Complain => Err(OptionsError::Conflict(&flags::TREE, &flags::LIST_DIRS))); - test!(just_level_2: DirAction <- ["--level=4"]; Complain => Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE))); + assert!(DirAction::deduce(&matches, false, true).is_err()); + } + #[test] + fn deduce_recurse_options() { + let matches = Opts { + recurse: 1, + tree: 1, + level: Some(2), + ..Opts::default() + }; - // Overriding levels - test!(overriding_1: DirAction <- ["-RL=6", "-L=7"]; Last => Ok(Recurse(RecurseOptions { tree: false, max_depth: Some(7) }))); - test!(overriding_2: DirAction <- ["-RL=6", "-L=7"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'L'), Flag::Short(b'L')))); + assert_eq!(DirAction::deduce(&matches, true, false).unwrap(), DirAction::Recurse(RecurseOptions { + tree: true, + max_depth: Some(2), + })); + } } diff --git a/src/options/error.rs b/src/options/error.rs index 420ff3fa5..622febabc 100644 --- a/src/options/error.rs +++ b/src/options/error.rs @@ -1,37 +1,30 @@ -use std::ffi::OsString; use std::fmt; use std::num::ParseIntError; -use crate::options::flags; -use crate::options::parser::{Arg, Flag, ParseError}; - /// Something wrong with the combination of options the user has picked. #[derive(PartialEq, Eq, Debug)] pub enum OptionsError { - /// There was an error (from `getopts`) parsing the arguments. - Parse(ParseError), - /// The user supplied an illegal choice to an Argument. - BadArgument(&'static Arg, OsString), + BadArgument(String, String), /// The user supplied a set of options that are unsupported Unsupported(String), /// An option was given twice or more in strict mode. - Duplicate(Flag, Flag), + Duplicate(String, String), /// Two options were given that conflict with one another. - Conflict(&'static Arg, &'static Arg), + Conflict(String, String), /// An option was given that does nothing when another one either is or /// isn’t present. - Useless(&'static Arg, bool, &'static Arg), + Useless(String, bool, String), /// An option was given that does nothing when either of two other options /// are not present. - Useless2(&'static Arg, &'static Arg, &'static Arg), + Useless2(String, String, String), /// A very specific edge case where --tree can’t be used with --all twice. TreeAllAll, @@ -43,15 +36,19 @@ pub enum OptionsError { FailedGlobPattern(String), } -/// The source of a string that failed to be parsed as a number. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Clone)] pub enum NumberSource { + Var(String), + Arg(String), +} - /// It came... from a command-line argument! - Arg(&'static Arg), - - /// It came... from the environment! - Env(&'static str), +impl fmt::Display for NumberSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Var(s) => write!(f, "variable {s}"), + Self::Arg(s) => write!(f, "argument {s}"), + } + } } impl From for OptionsError { @@ -60,29 +57,13 @@ impl From for OptionsError { } } -impl fmt::Display for NumberSource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Arg(arg) => write!(f, "option {arg}"), - Self::Env(env) => write!(f, "environment variable {env}"), - } - } -} impl fmt::Display for OptionsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use crate::options::parser::TakesValue; - match self { Self::BadArgument(arg, attempt) => { - if let TakesValue::Necessary(Some(values)) = arg.takes_value { - write!(f, "Option {} has no {:?} setting ({})", arg, attempt, Choices(values)) - } - else { - write!(f, "Option {arg} has no {attempt:?} setting") - } + write!(f, "Argument {arg} doesn’t take {attempt}") } - Self::Parse(e) => write!(f, "{e}"), Self::Unsupported(e) => write!(f, "{e}"), Self::Conflict(a, b) => write!(f, "Option {a} conflicts with option {b}"), Self::Duplicate(a, b) if a == b => write!(f, "Flag {a} was given twice"), @@ -97,26 +78,6 @@ impl fmt::Display for OptionsError { } } -impl OptionsError { - - /// Try to second-guess what the user was trying to do, depending on what - /// went wrong. - pub fn suggestion(&self) -> Option<&'static str> { - // ‘ls -lt’ and ‘ls -ltr’ are common combinations - match self { - Self::BadArgument(time, r) if *time == &flags::TIME && r == "r" => { - Some("To sort oldest files last, try \"--sort oldest\", or just \"-sold\"") - } - Self::Parse(ParseError::NeedsValue { ref flag, .. }) if *flag == Flag::Short(b't') => { - Some("To sort newest files last, try \"--sort newest\", or just \"-snew\"") - } - _ => { - None - } - } - } -} - /// A list of legal choices for an argument-taking option. #[derive(PartialEq, Eq, Debug)] diff --git a/src/options/file_name.rs b/src/options/file_name.rs index f1ffbb000..7908168d5 100644 --- a/src/options/file_name.rs +++ b/src/options/file_name.rs @@ -1,32 +1,32 @@ -use crate::options::{flags, OptionsError, NumberSource}; -use crate::options::parser::MatchedFlags; +use crate::options::{OptionsError, NumberSource}; use crate::options::vars::{self, Vars}; use crate::output::file_name::{Options, Classify, ShowIcons, EmbedHyperlinks}; +use crate::options::parser::Opts; impl Options { - pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { - let classify = Classify::deduce(matches)?; + pub fn deduce(matches: &Opts, vars: &V) -> Result { + let classify = Classify::deduce(matches); let show_icons = ShowIcons::deduce(matches, vars)?; - let embed_hyperlinks = EmbedHyperlinks::deduce(matches)?; + let embed_hyperlinks = EmbedHyperlinks::deduce(matches); Ok(Self { classify, show_icons, embed_hyperlinks }) } } impl Classify { - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let flagged = matches.has(&flags::CLASSIFY)?; - - if flagged { Ok(Self::AddFileIndicators) } - else { Ok(Self::JustFilenames) } + fn deduce(matches: &Opts) -> Self { + if matches.classify > 0 { + return Self::AddFileIndicators; + } + Self::JustFilenames } } impl ShowIcons { - pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { - if matches.has(&flags::NO_ICONS)? || !matches.has(&flags::ICONS)? { + pub fn deduce(matches: &Opts, vars: &V) -> Result { + if matches.no_icons > 0 || matches.icons == 0 { Ok(Self::Off) } else if let Some(columns) = vars.get(vars::EXA_ICON_SPACING).and_then(|s| s.into_string().ok()) { @@ -35,7 +35,7 @@ impl ShowIcons { Ok(Self::On(width)) } Err(e) => { - let source = NumberSource::Env(vars::EXA_ICON_SPACING); + let source = NumberSource::Var(vars::EXA_ICON_SPACING.to_string()); Err(OptionsError::FailedParse(columns, source, e)) } } @@ -47,10 +47,50 @@ impl ShowIcons { } impl EmbedHyperlinks { - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let flagged = matches.has(&flags::HYPERLINK)?; + fn deduce(matches: &Opts) -> Self { + if matches.hyperlink > 0 { + return Self::On; + } + Self::Off + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn deduce_hyperlinks() { + assert_eq!(EmbedHyperlinks::deduce(&Opts::default()), EmbedHyperlinks::Off); + } + + #[test] + fn deduce_hyperlinks_on() { + + let matches = Opts { + hyperlink: 1, + ..Opts::default() + }; + + assert_eq!(EmbedHyperlinks::deduce(&matches), EmbedHyperlinks::On); + } + + #[test] + fn deduce_classify() { + let matches = Opts { + classify: 1, + ..Opts::default() + }; + + assert_eq!(Classify::deduce(&matches), Classify::AddFileIndicators); + } + + #[test] + fn deduce_classify_no_classify() { + let matches = Opts { + ..Opts::default() + }; - if flagged { Ok(Self::On) } - else { Ok(Self::Off) } + assert_eq!(Classify::deduce(&matches), Classify::JustFilenames); } } diff --git a/src/options/filter.rs b/src/options/filter.rs index 9828767b8..540a56f79 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -1,22 +1,25 @@ //! Parsing the options for `FileFilter`. +use clap::Error; + use crate::fs::DotFilter; use crate::fs::filter::{FileFilter, SortField, SortCase, IgnorePatterns, GitIgnore}; -use crate::options::{flags, OptionsError}; -use crate::options::parser::MatchedFlags; +use crate::options::OptionsError; + +use super::parser::Opts; impl FileFilter { /// Determines which of all the file filter options to use. - pub fn deduce(matches: &MatchedFlags<'_>) -> Result { + pub fn deduce(matches: &Opts, strictness: bool) -> Result { Ok(Self { - list_dirs_first: matches.has(&flags::DIRS_FIRST)?, - reverse: matches.has(&flags::REVERSE)?, - only_dirs: matches.has(&flags::ONLY_DIRS)?, + list_dirs_first: matches.dirs_first > 0, + reverse: matches.reverse > 0, + only_dirs: matches.only_dirs > 0, sort_field: SortField::deduce(matches)?, - dot_filter: DotFilter::deduce(matches)?, + dot_filter: DotFilter::deduce(matches, strictness)?, ignore_patterns: IgnorePatterns::deduce(matches)?, git_ignore: GitIgnore::deduce(matches)?, }) @@ -29,11 +32,11 @@ impl SortField { /// This argument’s value can be one of several flags, listed above. /// Returns the default sort field if none is given, or `Err` if the /// value doesn’t correspond to a sort field we know about. - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let Some(word) = matches.get(&flags::SORT)? else { return Ok(Self::default()) }; + fn deduce(matches: &Opts) -> Result { + let Some(ref word) = matches.sort else { return Ok(Self::default()) }; // Get String because we can’t match an OsStr - let Some(word) = word.to_str() else { return Err(OptionsError::BadArgument(&flags::SORT, word.into())) }; + let Some(word) = word.to_str() else { return Err(OptionsError::BadArgument("SORT".to_string(), word.to_string_lossy().to_string())) }; let field = match word { "name" | "filename" => { @@ -93,7 +96,7 @@ impl SortField { Self::Unsorted } _ => { - return Err(OptionsError::BadArgument(&flags::SORT, word.into())); + return Err(OptionsError::BadArgument("SORT".to_string(), word.into())); } }; @@ -153,20 +156,20 @@ impl DotFilter { /// /// `--almost-all` binds stronger than multiple `--all` as we currently do not take the order /// of arguments into account and it is the safer option (does not clash with `--tree`) - pub fn deduce(matches: &MatchedFlags<'_>) -> Result { - let all_count = matches.count(&flags::ALL); - let has_almost_all = matches.has(&flags::ALMOST_ALL)?; + pub fn deduce(matches: &Opts, strictness: bool) -> Result { + let all_count = matches.all; + let has_almost_all = matches.almost_all; match (all_count, has_almost_all) { - (0, false) => Ok(Self::JustFiles), + (0, 0) => Ok(Self::JustFiles), // either a single --all or at least one --almost-all is given - (1, _) | (0, true) => Ok(Self::Dotfiles), + (1 | 0, _) => Ok(Self::Dotfiles), // more than one --all - (c, _) => if matches.count(&flags::TREE) > 0 { + (_, _) => if matches.tree > 0 { Err(OptionsError::TreeAllAll) - } else if matches.is_strict() && c > 2 { - Err(OptionsError::Conflict(&flags::ALL, &flags::ALL)) + } else if strictness { + Err(OptionsError::Conflict("ALL".to_string(), "ALL".to_string())) } else { Ok(Self::DotfilesAndDots) }, @@ -180,11 +183,11 @@ impl IgnorePatterns { /// Determines the set of glob patterns to use based on the /// `--ignore-glob` argument’s value. This is a list of strings /// separated by pipe (`|`) characters, given in any order. - pub fn deduce(matches: &MatchedFlags<'_>) -> Result { + pub fn deduce(matches: &Opts) -> Result { // If there are no inputs, we return a set of patterns that doesn’t // match anything, rather than, say, `None`. - let Some(inputs) = matches.get(&flags::IGNORE_GLOB)? else { return Ok(Self::empty()) }; + let Some(ref inputs) = matches.ignore_glob else { return Ok(Self::empty()) }; // Awkwardly, though, a glob pattern can be invalid, and we need to // deal with invalid patterns somehow. @@ -201,124 +204,124 @@ impl IgnorePatterns { impl GitIgnore { - pub fn deduce(matches: &MatchedFlags<'_>) -> Result { - if matches.has(&flags::GIT_IGNORE)? { - Ok(Self::CheckAndIgnore) - } - else { - Ok(Self::Off) + pub fn deduce(matches: &Opts) -> Result { + if matches.git_ignore > 0 && matches.no_git == 0 { + return Ok(Self::CheckAndIgnore); + } else if matches.git_ignore > 0 && matches.no_git > 0 { + return Err(OptionsError::Conflict("GIT_IGNORE".to_string(), "NO_GIT".to_string())); } + Ok(Self::Off) } } -#[cfg(test)] -mod test { - use super::*; - use std::ffi::OsString; - use crate::options::flags; - use crate::options::parser::Flag; - - macro_rules! test { - ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => { - #[test] - fn $name() { - use crate::options::parser::Arg; - use crate::options::test::parse_for_test; - use crate::options::test::Strictnesses::*; - - static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::ALMOST_ALL, &flags::TREE, &flags::IGNORE_GLOB, &flags::GIT_IGNORE ]; - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { - assert_eq!(result, $result); - } - } - }; - } - - mod sort_fields { - use super::*; - - // Default behaviour - test!(empty: SortField <- []; Both => Ok(SortField::default())); - - // Sort field arguments - test!(one_arg: SortField <- ["--sort=mod"]; Both => Ok(SortField::ModifiedDate)); - test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size)); - test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate)); - test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc))); - test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc))); - test!(old: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedDate)); - test!(oldest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedDate)); - test!(new: SortField <- ["--sort", "old"]; Both => Ok(SortField::ModifiedAge)); - test!(newest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedAge)); - test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge)); - - test!(mix_hidden_lowercase: SortField <- ["--sort", ".name"]; Both => Ok(SortField::NameMixHidden(SortCase::AaBbCc))); - test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc))); - - // Errors - test!(error: SortField <- ["--sort=colour"]; Both => Err(OptionsError::BadArgument(&flags::SORT, OsString::from("colour")))); - - // Overriding - test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate)); - test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc))); - test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); - test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); - } - - - mod dot_filters { - use super::*; - - // Default behaviour - test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles)); - - // --all - test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles)); - test!(all_all: DotFilter <- ["--all", "-a"]; Both => Ok(DotFilter::DotfilesAndDots)); - test!(all_all_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots)); - - test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots)); - test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))); - - // --all and --tree - test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles)); - test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(OptionsError::TreeAllAll)); - test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(OptionsError::TreeAllAll)); - - // --almost-all - test!(almost_all: DotFilter <- ["--almost-all"]; Both => Ok(DotFilter::Dotfiles)); - test!(almost_all_all: DotFilter <- ["-Aa"]; Both => Ok(DotFilter::Dotfiles)); - test!(almost_all_all_2: DotFilter <- ["-Aaa"]; Both => Ok(DotFilter::DotfilesAndDots)); - } - - - mod ignore_patterns { - use super::*; - use std::iter::FromIterator; - - fn pat(string: &'static str) -> glob::Pattern { - glob::Pattern::new(string).unwrap() - } - - // Various numbers of globs - test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty())); - test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ]))); - test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ]))); - test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ]))); - - // Overriding - test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ]))); - test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ]))); - test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); - test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); - } - - - mod git_ignores { - use super::*; - - test!(off: GitIgnore <- []; Both => Ok(GitIgnore::Off)); - test!(on: GitIgnore <- ["--git-ignore"]; Both => Ok(GitIgnore::CheckAndIgnore)); - } -} +// #[cfg(test)] +// mod test { +// use super::*; +// use std::ffi::OsString; +// use crate::options::flags; +// use crate::options::parser::Flag; + +// macro_rules! test { +// ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => { +// #[test] +// fn $name() { +// use crate::options::parser::Arg; +// use crate::options::test::parse_for_test; +// use crate::options::test::Strictnesses::*; + +// static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::ALMOST_ALL, &flags::TREE, &flags::IGNORE_GLOB, &flags::GIT_IGNORE ]; +// for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { +// assert_eq!(result, $result); +// } +// } +// }; +// } + +// mod sort_fields { +// use super::*; + +// // Default behaviour +// test!(empty: SortField <- []; Both => Ok(SortField::default())); + +// // Sort field arguments +// test!(one_arg: SortField <- ["--sort=mod"]; Both => Ok(SortField::ModifiedDate)); +// test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size)); +// test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate)); +// test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc))); +// test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc))); +// test!(old: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedDate)); +// test!(oldest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedDate)); +// test!(new: SortField <- ["--sort", "old"]; Both => Ok(SortField::ModifiedAge)); +// test!(newest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedAge)); +// test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge)); + +// test!(mix_hidden_lowercase: SortField <- ["--sort", ".name"]; Both => Ok(SortField::NameMixHidden(SortCase::AaBbCc))); +// test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc))); + +// // Errors +// test!(error: SortField <- ["--sort=colour"]; Both => Err(OptionsError::BadArgument(&flags::SORT, OsString::from("colour")))); + +// // Overriding +// test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate)); +// test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc))); +// test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); +// test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); +// } + + +// mod dot_filters { +// use super::*; + +// // Default behaviour +// test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles)); + +// // --all +// test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles)); +// test!(all_all: DotFilter <- ["--all", "-a"]; Both => Ok(DotFilter::DotfilesAndDots)); +// test!(all_all_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots)); + +// test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots)); +// test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))); + +// // --all and --tree +// test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles)); +// test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(OptionsError::TreeAllAll)); +// test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(OptionsError::TreeAllAll)); + +// // --almost-all +// test!(almost_all: DotFilter <- ["--almost-all"]; Both => Ok(DotFilter::Dotfiles)); +// test!(almost_all_all: DotFilter <- ["-Aa"]; Both => Ok(DotFilter::Dotfiles)); +// test!(almost_all_all_2: DotFilter <- ["-Aaa"]; Both => Ok(DotFilter::DotfilesAndDots)); +// } + + +// mod ignore_patterns { +// use super::*; +// use std::iter::FromIterator; + +// fn pat(string: &'static str) -> glob::Pattern { +// glob::Pattern::new(string).unwrap() +// } + +// // Various numbers of globs +// test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty())); +// test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ]))); +// test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ]))); +// test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ]))); + +// // Overriding +// test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ]))); +// test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ]))); +// test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); +// test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); +// } + + +// mod git_ignores { +// use super::*; + +// test!(off: GitIgnore <- []; Both => Ok(GitIgnore::Off)); +// test!(on: GitIgnore <- ["--git-ignore"]; Both => Ok(GitIgnore::CheckAndIgnore)); +// } +// } diff --git a/src/options/mod.rs b/src/options/mod.rs index c20317629..bc4c9a3b4 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -69,36 +69,25 @@ //! it’s clear what the user wants. -use std::ffi::OsStr; - use crate::fs::dir_action::DirAction; use crate::fs::filter::{FileFilter, GitIgnore}; use crate::output::{View, Mode, details, grid_details}; use crate::theme::Options as ThemeOptions; +use crate::options::parser::Opts; mod dir_action; mod file_name; mod filter; -mod flags; mod theme; mod view; +pub(crate) mod parser; mod error; pub use self::error::{OptionsError, NumberSource}; -mod help; -use self::help::HelpString; - -mod parser; -use self::parser::MatchedFlags; - pub mod vars; pub use self::vars::Vars; -mod version; -use self::version::VersionString; - - /// These **options** represent a parsed, error-checked versions of the /// user’s command-line options. #[derive(Debug)] @@ -123,41 +112,6 @@ pub struct Options { impl Options { - /// Parse the given iterator of command-line strings into an Options - /// struct and a list of free filenames, using the environment variables - /// for extra options. - #[allow(unused_results)] - pub fn parse<'args, I, V>(args: I, vars: &V) -> OptionsResult<'args> - where I: IntoIterator, - V: Vars, - { - use crate::options::parser::{Matches, Strictness}; - - let strictness = match vars.get(vars::EXA_STRICT) { - None => Strictness::UseLastArguments, - Some(ref t) if t.is_empty() => Strictness::UseLastArguments, - Some(_) => Strictness::ComplainAboutRedundantArguments, - }; - - let Matches { flags, frees } = match flags::ALL_ARGS.parse(args, strictness) { - Ok(m) => m, - Err(pe) => return OptionsResult::InvalidOptions(OptionsError::Parse(pe)), - }; - - if let Some(help) = HelpString::deduce(&flags) { - return OptionsResult::Help(help); - } - - if let Some(version) = VersionString::deduce(&flags) { - return OptionsResult::Version(version); - } - - match Self::deduce(&flags, vars) { - Ok(options) => OptionsResult::Ok(options, frees), - Err(oe) => OptionsResult::InvalidOptions(oe), - } - } - /// Whether the View specified in this set of options includes a Git /// status column. It’s only worth trying to discover a repository if the /// results will end up being displayed. @@ -175,17 +129,22 @@ impl Options { /// Determines the complete set of options based on the given command-line /// arguments, after they’ve been parsed. - fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + pub fn deduce(matches: &Opts, vars: &V) -> Result { if cfg!(not(feature = "git")) && - matches.has_where_any(|f| f.matches(&flags::GIT) || f.matches(&flags::GIT_IGNORE)).is_some() { + (matches.git > 0 || matches.git_ignore > 0) { return Err(OptionsError::Unsupported(String::from( "Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa" ))); } - let view = View::deduce(matches, vars)?; - let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)))?; - let filter = FileFilter::deduce(matches)?; + let strictness = match vars.get(vars::EXA_STRICT) { + None => false, + Some(s) => ! s.is_empty() + }; + + let view = View::deduce(matches, vars, strictness)?; + let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)), strictness)?; + let filter = FileFilter::deduce(matches, strictness)?; let theme = ThemeOptions::deduce(matches, vars)?; Ok(Self { dir_action, filter, view, theme }) @@ -193,61 +152,42 @@ impl Options { } -/// The result of the `Options::getopts` function. -#[derive(Debug)] -pub enum OptionsResult<'args> { - - /// The options were parsed successfully. - Ok(Options, Vec<&'args OsStr>), - - /// There was an error parsing the arguments. - InvalidOptions(OptionsError), - - /// One of the arguments was `--help`, so display help. - Help(HelpString), - - /// One of the arguments was `--version`, so display the version number. - Version(VersionString), -} - - -#[cfg(test)] -pub mod test { - use crate::options::parser::{Arg, MatchedFlags}; - use std::ffi::OsStr; - - #[derive(PartialEq, Eq, Debug)] - pub enum Strictnesses { - Last, - Complain, - Both, - } - - /// This function gets used by the other testing modules. - /// It can run with one or both strictness values: if told to run with - /// both, then both should resolve to the same result. - /// - /// It returns a vector with one or two elements in. - /// These elements can then be tested with `assert_eq` or what have you. - pub fn parse_for_test(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec - where F: Fn(&MatchedFlags<'_>) -> T - { - use self::Strictnesses::*; - use crate::options::parser::{Args, Strictness}; - - let bits = inputs.iter().map(OsStr::new).collect::>(); - let mut result = Vec::new(); - - if strictnesses == Last || strictnesses == Both { - let results = Args(args).parse(bits.clone(), Strictness::UseLastArguments); - result.push(get(&results.unwrap().flags)); - } - - if strictnesses == Complain || strictnesses == Both { - let results = Args(args).parse(bits, Strictness::ComplainAboutRedundantArguments); - result.push(get(&results.unwrap().flags)); - } - - result - } -} +// #[cfg(test)] +// pub mod test { +// use crate::options::parser::{Arg, MatchedFlags}; +// use std::ffi::OsStr; + +// #[derive(PartialEq, Eq, Debug)] +// pub enum Strictnesses { +// Last, +// Complain, +// Both, +// } + +// /// This function gets used by the other testing modules. +// /// It can run with one or both strictness values: if told to run with +// /// both, then both should resolve to the same result. +// /// +// /// It returns a vector with one or two elements in. +// /// These elements can then be tested with `assert_eq` or what have you. +// pub fn parse_for_test(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec +// where F: Fn(&MatchedFlags<'_>) -> T +// { +// use self::Strictnesses::*; + +// let bits = inputs.iter().map(OsStr::new).collect::>(); +// let mut result = Vec::new(); + +// if strictnesses == Last || strictnesses == Both { +// let results = Args(args).parse(bits.clone(), Strictness::UseLastArguments); +// result.push(get(&results.unwrap().flags)); +// } + +// if strictnesses == Complain || strictnesses == Both { +// let results = Args(args).parse(bits, Strictness::ComplainAboutRedundantArguments); +// result.push(get(&results.unwrap().flags)); +// } + +// result +// } +// } diff --git a/src/options/parser.rs b/src/options/parser.rs index e7722014a..d6e845caf 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -1,748 +1,217 @@ -//! A general parser for command-line options. -//! -//! exa uses its own hand-rolled parser for command-line options. It supports -//! the following syntax: -//! -//! - Long options: `--inode`, `--grid` -//! - Long options with values: `--sort size`, `--level=4` -//! - Short options: `-i`, `-G` -//! - Short options with values: `-ssize`, `-L=4` -//! -//! These values can be mixed and matched: `exa -lssize --grid`. If you’ve used -//! other command-line programs, then hopefully it’ll work much like them. -//! -//! Because exa already has its own files for the help text, shell completions, -//! man page, and readme, so it can get away with having the options parser do -//! very little: all it really needs to do is parse a slice of strings. -//! -//! -//! ## UTF-8 and `OsStr` -//! -//! The parser uses `OsStr` as its string type. This is necessary for exa to -//! list files that have invalid UTF-8 in their names: by treating file paths -//! as bytes with no encoding, a file can be specified on the command-line and -//! be looked up without having to be encoded into a `str` first. -//! -//! It also avoids the overhead of checking for invalid UTF-8 when parsing -//! command-line options, as all the options and their values (such as -//! `--sort size`) are guaranteed to just be 8-bit ASCII. - - -use std::ffi::{OsStr, OsString}; -use std::fmt; - -use crate::options::error::{OptionsError, Choices}; - - -/// A **short argument** is a single ASCII character. -pub type ShortArg = u8; - -/// A **long argument** is a string. This can be a UTF-8 string, even though -/// the arguments will all be unchecked `OsString` values, because we don’t -/// actually store the user’s input after it’s been matched to a flag, we just -/// store which flag it was. -pub type LongArg = &'static str; - -/// A **list of values** that an option can have, to be displayed when the -/// user enters an invalid one or skips it. -/// -/// This is literally just help text, and won’t be used to validate a value to -/// see if it’s correct. -pub type Values = &'static [&'static str]; - -/// A **flag** is either of the two argument types, because they have to -/// be in the same array together. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum Flag { - Short(ShortArg), - Long(LongArg), -} - -impl Flag { - pub fn matches(&self, arg: &Arg) -> bool { - match self { - Self::Short(short) => arg.short == Some(*short), - Self::Long(long) => arg.long == *long, - } - } -} - -impl fmt::Display for Flag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self { - Self::Short(short) => write!(f, "-{}", *short as char), - Self::Long(long) => write!(f, "--{long}"), - } - } -} - -/// Whether redundant arguments should be considered a problem. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum Strictness { - - /// Throw an error when an argument doesn’t do anything, either because - /// it requires another argument to be specified, or because two conflict. - ComplainAboutRedundantArguments, - - /// Search the arguments list back-to-front, giving ones specified later - /// in the list priority over earlier ones. - UseLastArguments, -} - -/// Whether a flag takes a value. This is applicable to both long and short -/// arguments. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub enum TakesValue { - - /// This flag has to be followed by a value. - /// If there’s a fixed set of possible values, they can be printed out - /// with the error text. - Necessary(Option), - - /// This flag will throw an error if there’s a value after it. - Forbidden, - - /// This flag may be followed by a value to override its defaults - Optional(Option), -} - - -/// An **argument** can be matched by one of the user’s input strings. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub struct Arg { - - /// The short argument that matches it, if any. - pub short: Option, - - /// The long argument that matches it. This is non-optional; all flags - /// should at least have a descriptive long name. - pub long: LongArg, - - /// Whether this flag takes a value or not. - pub takes_value: TakesValue, -} - -impl fmt::Display for Arg { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "--{}", self.long)?; - - if let Some(short) = self.short { - write!(f, " (-{})", short as char)?; - } - - Ok(()) - } -} - - -/// Literally just several args. -#[derive(PartialEq, Eq, Debug)] -pub struct Args(pub &'static [&'static Arg]); - -impl Args { - - /// Iterates over the given list of command-line arguments and parses - /// them into a list of matched flags and free strings. - pub fn parse<'args, I>(&self, inputs: I, strictness: Strictness) -> Result, ParseError> - where I: IntoIterator - { - let mut parsing = true; - - // The results that get built up. - let mut result_flags = Vec::new(); - let mut frees: Vec<&OsStr> = Vec::new(); - - // Iterate over the inputs with “while let” because we need to advance - // the iterator manually whenever an argument that takes a value - // doesn’t have one in its string so it needs the next one. - let mut inputs = inputs.into_iter(); - while let Some(arg) = inputs.next() { - let bytes = os_str_to_bytes(arg); - - // Stop parsing if one of the arguments is the literal string “--”. - // This allows a file named “--arg” to be specified by passing in - // the pair “-- --arg”, without it getting matched as a flag that - // doesn’t exist. - if ! parsing { - frees.push(arg); - } - else if arg == "--" { - parsing = false; - } - - // If the string starts with *two* dashes then it’s a long argument. - else if bytes.starts_with(b"--") { - let long_arg_name = bytes_to_os_str(&bytes[2..]); - - // If there’s an equals in it, then the string before the - // equals will be the flag’s name, and the string after it - // will be its value. - if let Some((before, after)) = split_on_equals(long_arg_name) { - let arg = self.lookup_long(before)?; - let flag = Flag::Long(arg.long); - match arg.takes_value { - TakesValue::Necessary(_) | - TakesValue::Optional(_) => result_flags.push((flag, Some(after))), - TakesValue::Forbidden => return Err(ParseError::ForbiddenValue { flag }), - } - } - - // If there’s no equals, then the entire string (apart from - // the dashes) is the argument name. - else { - let arg = self.lookup_long(long_arg_name)?; - let flag = Flag::Long(arg.long); - match arg.takes_value { - TakesValue::Forbidden => { - result_flags.push((flag, None)); - } - TakesValue::Necessary(values) => { - if let Some(next_arg) = inputs.next() { - result_flags.push((flag, Some(next_arg))); - } - else { - return Err(ParseError::NeedsValue { flag, values }) - } - } - TakesValue::Optional(_) => { - if let Some(next_arg) = inputs.next() { - result_flags.push((flag, Some(next_arg))); - } - else { - result_flags.push((flag, None)); - } - } - } - } - } - - // If the string starts with *one* dash then it’s one or more - // short arguments. - else if bytes.starts_with(b"-") && arg != "-" { - let short_arg = bytes_to_os_str(&bytes[1..]); - - // If there’s an equals in it, then the argument immediately - // before the equals was the one that has the value, with the - // others (if any) as value-less short ones. - // - // -x=abc => ‘x=abc’ - // -abcdx=fgh => ‘a’, ‘b’, ‘c’, ‘d’, ‘x=fgh’ - // -x= => error - // -abcdx= => error - // - // There’s no way to give two values in a cluster like this: - // it’s an error if any of the first set of arguments actually - // takes a value. - if let Some((before, after)) = split_on_equals(short_arg) { - let (arg_with_value, other_args) = os_str_to_bytes(before).split_last().unwrap(); - - // Process the characters immediately following the dash... - for byte in other_args { - let arg = self.lookup_short(*byte)?; - let flag = Flag::Short(*byte); - match arg.takes_value { - TakesValue::Forbidden | - TakesValue::Optional(_) => { - result_flags.push((flag, None)); - } - TakesValue::Necessary(values) => { - return Err(ParseError::NeedsValue { flag, values }); - } - } - } - - // ...then the last one and the value after the equals. - let arg = self.lookup_short(*arg_with_value)?; - let flag = Flag::Short(arg.short.unwrap()); - match arg.takes_value { - TakesValue::Necessary(_) | - TakesValue::Optional(_) => { - result_flags.push((flag, Some(after))); - } - TakesValue::Forbidden => { - return Err(ParseError::ForbiddenValue { flag }); - } - } - } - - // If there’s no equals, then every character is parsed as - // its own short argument. However, if any of the arguments - // takes a value, then the *rest* of the string is used as - // its value, and if there’s no rest of the string, then it - // uses the next one in the iterator. - // - // -a => ‘a’ - // -abc => ‘a’, ‘b’, ‘c’ - // -abxdef => ‘a’, ‘b’, ‘x=def’ - // -abx def => ‘a’, ‘b’, ‘x=def’ - // -abx => error - // - else { - for (index, byte) in bytes.iter().enumerate().skip(1) { - let arg = self.lookup_short(*byte)?; - let flag = Flag::Short(*byte); - match arg.takes_value { - TakesValue::Forbidden => { - result_flags.push((flag, None)); - } - TakesValue::Necessary(values) | - TakesValue::Optional(values) => { - if index < bytes.len() - 1 { - let remnants = &bytes[index+1 ..]; - result_flags.push((flag, Some(bytes_to_os_str(remnants)))); - break; - } - else if let Some(next_arg) = inputs.next() { - result_flags.push((flag, Some(next_arg))); - } - else { - match arg.takes_value { - TakesValue::Forbidden => { - unreachable!() - } - TakesValue::Necessary(_) => { - return Err(ParseError::NeedsValue { flag, values }); - } - TakesValue::Optional(_) => { - result_flags.push((flag, None)); - } - } - } - } - } - } - } - } - - // Otherwise, it’s a free string, usually a file name. - else { - frees.push(arg); - } - } - - Ok(Matches { frees, flags: MatchedFlags { flags: result_flags, strictness } }) - } - - fn lookup_short(&self, short: ShortArg) -> Result<&Arg, ParseError> { - match self.0.iter().find(|arg| arg.short == Some(short)) { - Some(arg) => Ok(arg), - None => Err(ParseError::UnknownShortArgument { attempt: short }) - } - } - - fn lookup_long(&self, long: &OsStr) -> Result<&Arg, ParseError> { - match self.0.iter().find(|arg| arg.long == long) { - Some(arg) => Ok(arg), - None => Err(ParseError::UnknownArgument { attempt: long.to_os_string() }) - } - } -} - - -/// The **matches** are the result of parsing the user’s command-line strings. -#[derive(PartialEq, Eq, Debug)] -pub struct Matches<'args> { - - /// The flags that were parsed from the user’s input. - pub flags: MatchedFlags<'args>, - - /// All the strings that weren’t matched as arguments, as well as anything - /// after the special “--” string. - pub frees: Vec<&'args OsStr>, -} - -#[derive(PartialEq, Eq, Debug)] -pub struct MatchedFlags<'args> { - - /// The individual flags from the user’s input, in the order they were - /// originally given. - /// - /// Long and short arguments need to be kept in the same vector because - /// we usually want the one nearest the end to count, and to know this, - /// we need to know where they are in relation to one another. - flags: Vec<(Flag, Option<&'args OsStr>)>, - - /// Whether to check for duplicate or redundant arguments. - strictness: Strictness, -} - -impl<'a> MatchedFlags<'a> { - - /// Whether the given argument was specified. - /// Returns `true` if it was, `false` if it wasn’t, and an error in - /// strict mode if it was specified more than once. - pub fn has(&self, arg: &'static Arg) -> Result { - self.has_where(|flag| flag.matches(arg)) - .map(|flag| flag.is_some()) - } - - /// Returns the first found argument that satisfies the predicate, or - /// nothing if none is found, or an error in strict mode if multiple - /// argument satisfy the predicate. - /// - /// You’ll have to test the resulting flag to see which argument it was. - pub fn has_where

(&self, predicate: P) -> Result, OptionsError> - where P: Fn(&Flag) -> bool { - if self.is_strict() { - let all = self.flags.iter() - .filter(|tuple| tuple.1.is_none() && predicate(&tuple.0)) - .collect::>(); - - if all.len() < 2 { Ok(all.first().map(|t| &t.0)) } - else { Err(OptionsError::Duplicate(all[0].0, all[1].0)) } - } - else { - Ok(self.has_where_any(predicate)) - } - } - - /// Returns the first found argument that satisfies the predicate, or - /// nothing if none is found, with strict mode having no effect. +pub use clap::Parser; +use std::ffi::OsString; + +#[derive(Parser, Default)] +#[command(author, version, about, long_about = None)] // Read from `Cargo.toml` +#[clap(disable_help_flag = true)] +pub struct Opts { + pub paths: Vec, + /// Show hidden files. + #[arg(short, long, action = clap::ArgAction::Count)] + pub all: u8, + /// display extended file metadata as a table. + #[arg(short, long, action = clap::ArgAction::Count)] + pub long: u8, + /// list each file's Git status, if tracked or ignored. + #[arg(long, action = clap::ArgAction::Count)] + pub git: u8, + /// Display one entry per line. + #[arg(short = '1', long, action = clap::ArgAction::Count)] + pub oneline: u8, + ///recurse into directories as a tree. + #[arg(short = 'T', long, action = clap::ArgAction::Count)] + pub tree: u8, + /// display entries as a grid (default). + #[arg(short = 'G', long, action = clap::ArgAction::Count)] + pub grid: u8, + /// sort the grid across, rather than downwards. + #[arg(short = 'x', long, action = clap::ArgAction::Count)] + pub across: u8, + /// recurse into directories. + #[arg(short = 'R', long, action = clap::ArgAction::Count)] + pub recurse: u8, + /// display type indicator by file names. + #[arg(short = 'F', long, action = clap::ArgAction::Count)] + pub classify: u8, + /// + #[arg(short = 'X', long, action = clap::ArgAction::Count)] + pub dereference: u8, + /// set screen width in columns. + #[arg(short = 'w', long)] + pub width: Option, + /// when to use terminal colours (always, auto, never). + #[arg(long)] + pub color: Option, + /// highlight levels of file sizes distinctly. + #[arg(long, action = clap::ArgAction::Count)] + pub color_scale: u8, /// - /// You’ll have to test the resulting flag to see which argument it was. - pub fn has_where_any

(&self, predicate: P) -> Option<&Flag> - where P: Fn(&Flag) -> bool { - self.flags.iter().rev() - .find(|tuple| tuple.1.is_none() && predicate(&tuple.0)) - .map(|tuple| &tuple.0) - } - - // This code could probably be better. - // Both ‘has’ and ‘get’ immediately begin with a conditional, which makes - // me think the functionality could be moved to inside Strictness. - - /// Returns the value of the given argument if it was specified, nothing - /// if it wasn’t, and an error in strict mode if it was specified more - /// than once. - pub fn get(&self, arg: &'static Arg) -> Result, OptionsError> { - self.get_where(|flag| flag.matches(arg)) - } - - /// Returns the value of the argument that matches the predicate if it - /// was specified, nothing if it wasn’t, and an error in strict mode if - /// multiple arguments matched the predicate. + #[arg(short = 'A', long, action = clap::ArgAction::Count)] + pub almost_all: u8, + /// list directories as files; don't list their contents. + #[arg(short = 'd', long, action = clap::ArgAction::Count)] + pub list_dirs: u8, + /// limit the depth of recursion. + #[arg(short = 'L', long)] + pub level: Option, + /// reverse the sort order. + #[arg(short = 'r', long, action = clap::ArgAction::Count)] + pub reverse: u8, + /// which field to sort by. + #[arg(short = 's', long)] + pub sort: Option, + /// glob patterns (pipe-separated) of files to ignore. + #[arg(short = 'I', long)] + pub ignore_glob: Option, + /// ignore files mentioned in '.gitignore'. + #[arg(long = "git-ignore", action = clap::ArgAction::Count)] + pub git_ignore: u8, + /// list directories before other files. + #[arg(long = "group-directories-first", action = clap::ArgAction::Count)] + pub dirs_first: u8, + /// list only directories. + #[arg(short = 'D', long = "only-dirs", action = clap::ArgAction::Count)] + pub only_dirs: u8, + /// list file sizes with binary prefixes. + #[arg(short = 'b', long, action = clap::ArgAction::Count)] + pub binary: u8, + /// list file sizes in bytes, without any prefixes. + #[arg(short = 'B', long, action = clap::ArgAction::Count)] + pub bytes: u8, + /// list each file's group. + #[arg(short = 'g', long, action = clap::ArgAction::Count)] + pub group: u8, + /// list numeric user and group IDs. + #[arg(short = 'n', long, action = clap::ArgAction::Count)] + pub numeric: u8, + /// add a header row to each column. + #[arg(short = 'h', long, action = clap::ArgAction::Count)] + pub header: u8, + /// display icons + #[arg(long, action = clap::ArgAction::Count)] + pub icons: u8, + /// list each file's inode number. + #[arg(short = 'i', long, action = clap::ArgAction::Count)] + pub inode: u8, + /// list each file's number of hard links. + #[arg(short = 'H', long, action = clap::ArgAction::Count)] + pub links: u8, + /// use the modified timestamp field. + #[arg(short = 'm', long, action = clap::ArgAction::Count)] + pub modified: u8, + /// use the changed timestamp field. + #[arg(long, action = clap::ArgAction::Count)] + pub changed: u8, + /// show size of allocated file system blocks. + #[arg(short = 'S', long, action = clap::ArgAction::Count)] + pub blocksize: u8, + /// which timestamp field to list (modified, accessed, created). + #[arg(short = 't', long)] + pub time: Option, + /// use the accessed timestamp field. + #[arg(short = 'u', long, action = clap::ArgAction::Count)] + pub accessed: u8, + /// use the created timestamp field. + #[arg(short = 'U', long, action = clap::ArgAction::Count)] + pub created: u8, + /// how to format timestamps (default, iso, long-iso, full-iso, relative). + #[arg(long = "time-style")] + pub time_style: Option, + /// display entries as hyperlinks. + #[arg(long, action = clap::ArgAction::Count)] + pub hyperlink: u8, + /// supress the permissions field. + #[arg(long = "no-permissions", action = clap::ArgAction::Count)] + pub no_permissions: u8, + /// suppress the filesize field. + #[arg(long = "no-filesize", action = clap::ArgAction::Count)] + pub no_filesize: u8, + /// suppress the user field. + #[arg(long = "no-user", action = clap::ArgAction::Count)] + pub no_user: u8, + /// suppress the time field. + #[arg(long = "no-time", action = clap::ArgAction::Count)] + pub no_time: u8, + /// don't display icons (always override --icons). + #[arg(long = "no-icons", action = clap::ArgAction::Count)] + pub no_icons: u8, + /// supress git. + #[arg(long = "no-git", action = clap::ArgAction::Count)] + pub no_git: u8, + /// list root of git-tree status. + #[arg(long = "git-repos", action = clap::ArgAction::Count)] + pub git_repos: u8, /// - /// It’s not possible to tell which flag the value belonged to from this. - pub fn get_where

(&self, predicate: P) -> Result, OptionsError> - where P: Fn(&Flag) -> bool { - if self.is_strict() { - let those = self.flags.iter() - .filter(|tuple| tuple.1.is_some() && predicate(&tuple.0)) - .collect::>(); - - if those.len() < 2 { Ok(those.first().copied().map(|t| t.1.unwrap())) } - else { Err(OptionsError::Duplicate(those[0].0, those[1].0)) } - } - else { - let found = self.flags.iter().rev() - .find(|tuple| tuple.1.is_some() && predicate(&tuple.0)) - .map(|tuple| tuple.1.unwrap()); - Ok(found) + #[arg(long = "git-repos-no-status", action = clap::ArgAction::Count)] + pub git_repos_no_status: u8, + /// list each file's permission in octal format. + #[arg(short = 'o', long, action = clap::ArgAction::Count)] + pub octal: u8, + /// Display the number of hard links to file. + #[arg(short = 'Z', long = "context", action = clap::ArgAction::Count)] + pub security_context: u8, + /// Show extended attributes. + #[arg(short = '@', long, action = clap::ArgAction::Count)] + pub extended: u8, + /// show list of command-line options. + #[arg(short ='?', long, action = clap::ArgAction::Help)] + pub help: Option, +} + +impl Opts { + pub fn default() -> Opts { + Opts { + paths: vec![], + all: 0, + long: 0, + git: 0, + oneline: 0, + recurse: 0, + list_dirs: 0, + tree: 0, + level: None, + reverse: 0, + sort: None, + ignore_glob: None, + git_ignore: 0, + dirs_first: 0, + only_dirs: 0, + binary: 0, + bytes: 0, + group: 0, + numeric: 0, + grid: 0, + across: 0, + classify: 0, + dereference: 0, + width: None, + color: None, + color_scale: 0, + almost_all: 0, + header: 0, + icons: 0, + inode: 0, + git_repos: 0, + git_repos_no_status: 0, + links: 0, + modified: 0, + created: 0, + accessed: 0, + changed: 0, + blocksize: 0, + time: None, + time_style: None, + no_filesize: 0, + no_icons: 0, + no_permissions: 0, + no_time: 0, + no_user: 0, + extended: 0, + hyperlink: 0, + octal: 0, + security_context: 0, + help: Some(false), + no_git: 0, } } - - // It’s annoying that ‘has’ and ‘get’ won’t work when accidentally given - // flags that do/don’t take values, but this should be caught by tests. - - /// Counts the number of occurrences of the given argument, even in - /// strict mode. - pub fn count(&self, arg: &Arg) -> usize { - self.flags.iter() - .filter(|tuple| tuple.0.matches(arg)) - .count() - } - - /// Checks whether strict mode is on. This is usually done from within - /// ‘has’ and ‘get’, but it’s available in an emergency. - pub fn is_strict(&self) -> bool { - self.strictness == Strictness::ComplainAboutRedundantArguments - } -} - - -/// A problem with the user’s input that meant it couldn’t be parsed into a -/// coherent list of arguments. -#[derive(PartialEq, Eq, Debug)] -pub enum ParseError { - - /// A flag that has to take a value was not given one. - NeedsValue { flag: Flag, values: Option }, - - /// A flag that can’t take a value *was* given one. - ForbiddenValue { flag: Flag }, - - /// A short argument, either alone or in a cluster, was not - /// recognised by the program. - UnknownShortArgument { attempt: ShortArg }, - - /// A long argument was not recognised by the program. - /// We don’t have a known &str version of the flag, so - /// this may not be valid UTF-8. - UnknownArgument { attempt: OsString }, -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NeedsValue { flag, values: None } => write!(f, "Flag {flag} needs a value"), - Self::NeedsValue { flag, values: Some(cs) } => write!(f, "Flag {flag} needs a value ({})", Choices(cs)), - Self::ForbiddenValue { flag } => write!(f, "Flag {flag} cannot take a value"), - Self::UnknownShortArgument { attempt } => write!(f, "Unknown argument -{}", *attempt as char), - Self::UnknownArgument { attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()), - } - } -} - -#[cfg(unix)] -fn os_str_to_bytes(s: &OsStr) -> &[u8]{ - use std::os::unix::ffi::OsStrExt; - - return s.as_bytes() -} - -#[cfg(unix)] -fn bytes_to_os_str(b: &[u8]) -> &OsStr{ - use std::os::unix::ffi::OsStrExt; - - return OsStr::from_bytes(b); -} - -#[cfg(windows)] -fn os_str_to_bytes(s: &OsStr) -> &[u8]{ - return s.to_str().unwrap().as_bytes() -} - -#[cfg(windows)] -fn bytes_to_os_str(b: &[u8]) -> &OsStr{ - use std::str; - - return OsStr::new(str::from_utf8(b).unwrap()); -} - -/// Splits a string on its `=` character, returning the two substrings on -/// either side. Returns `None` if there’s no equals or a string is missing. -fn split_on_equals(input: &OsStr) -> Option<(&OsStr, &OsStr)> { - if let Some(index) = os_str_to_bytes(input).iter().position(|elem| *elem == b'=') { - let (before, after) = os_str_to_bytes(input).split_at(index); - - // The after string contains the = that we need to remove. - if ! before.is_empty() && after.len() >= 2 { - return Some((bytes_to_os_str(before), - bytes_to_os_str(&after[1..]))) - } - } - - None -} - - -#[cfg(test)] -mod split_test { - use super::split_on_equals; - use std::ffi::{OsStr, OsString}; - - macro_rules! test_split { - ($name:ident: $input:expr => None) => { - #[test] - fn $name() { - assert_eq!(split_on_equals(&OsString::from($input)), - None); - } - }; - - ($name:ident: $input:expr => $before:expr, $after:expr) => { - #[test] - fn $name() { - assert_eq!(split_on_equals(&OsString::from($input)), - Some((OsStr::new($before), OsStr::new($after)))); - } - }; - } - - test_split!(empty: "" => None); - test_split!(letter: "a" => None); - - test_split!(just: "=" => None); - test_split!(intro: "=bbb" => None); - test_split!(denou: "aaa=" => None); - test_split!(equals: "aaa=bbb" => "aaa", "bbb"); - - test_split!(sort: "--sort=size" => "--sort", "size"); - test_split!(more: "this=that=other" => "this", "that=other"); -} - - -#[cfg(test)] -mod parse_test { - use super::*; - - macro_rules! test { - ($name:ident: $inputs:expr => frees: $frees:expr, flags: $flags:expr) => { - #[test] - fn $name() { - - let inputs: &[&'static str] = $inputs.as_ref(); - let inputs = inputs.iter().map(OsStr::new); - - let frees: &[&'static str] = $frees.as_ref(); - let frees = frees.iter().map(OsStr::new).collect(); - - let flags = <[_]>::into_vec(Box::new($flags)); - - let strictness = Strictness::UseLastArguments; // this isn’t even used - let got = Args(TEST_ARGS).parse(inputs, strictness); - let flags = MatchedFlags { flags, strictness }; - - let expected = Ok(Matches { frees, flags }); - assert_eq!(got, expected); - } - }; - - ($name:ident: $inputs:expr => error $error:expr) => { - #[test] - fn $name() { - use self::ParseError::*; - - let inputs = $inputs.iter().map(OsStr::new); - - let strictness = Strictness::UseLastArguments; // this isn’t even used - let got = Args(TEST_ARGS).parse(inputs, strictness); - assert_eq!(got, Err($error)); - } - }; - } - - const SUGGESTIONS: Values = &[ "example" ]; - - static TEST_ARGS: &[&Arg] = &[ - &Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }, - &Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden }, - &Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) }, - &Arg { short: Some(b't'), long: "type", takes_value: TakesValue::Necessary(Some(SUGGESTIONS)) } - ]; - - - // Just filenames - test!(empty: [] => frees: [], flags: []); - test!(one_arg: ["exa"] => frees: [ "exa" ], flags: []); - - // Dashes and double dashes - test!(one_dash: ["-"] => frees: [ "-" ], flags: []); - test!(two_dashes: ["--"] => frees: [], flags: []); - test!(two_file: ["--", "file"] => frees: [ "file" ], flags: []); - test!(two_arg_l: ["--", "--long"] => frees: [ "--long" ], flags: []); - test!(two_arg_s: ["--", "-l"] => frees: [ "-l" ], flags: []); - - - // Long args - test!(long: ["--long"] => frees: [], flags: [ (Flag::Long("long"), None) ]); - test!(long_then: ["--long", "4"] => frees: [ "4" ], flags: [ (Flag::Long("long"), None) ]); - test!(long_two: ["--long", "--verbose"] => frees: [], flags: [ (Flag::Long("long"), None), (Flag::Long("verbose"), None) ]); - - // Long args with values - test!(bad_equals: ["--long=equals"] => error ForbiddenValue { flag: Flag::Long("long") }); - test!(no_arg: ["--count"] => error NeedsValue { flag: Flag::Long("count"), values: None }); - test!(arg_equals: ["--count=4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]); - test!(arg_then: ["--count", "4"] => frees: [], flags: [ (Flag::Long("count"), Some(OsStr::new("4"))) ]); - - // Long args with values and suggestions - test!(no_arg_s: ["--type"] => error NeedsValue { flag: Flag::Long("type"), values: Some(SUGGESTIONS) }); - test!(arg_equals_s: ["--type=exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]); - test!(arg_then_s: ["--type", "exa"] => frees: [], flags: [ (Flag::Long("type"), Some(OsStr::new("exa"))) ]); - - - // Short args - test!(short: ["-l"] => frees: [], flags: [ (Flag::Short(b'l'), None) ]); - test!(short_then: ["-l", "4"] => frees: [ "4" ], flags: [ (Flag::Short(b'l'), None) ]); - test!(short_two: ["-lv"] => frees: [], flags: [ (Flag::Short(b'l'), None), (Flag::Short(b'v'), None) ]); - test!(mixed: ["-v", "--long"] => frees: [], flags: [ (Flag::Short(b'v'), None), (Flag::Long("long"), None) ]); - - // Short args with values - test!(bad_short: ["-l=equals"] => error ForbiddenValue { flag: Flag::Short(b'l') }); - test!(short_none: ["-c"] => error NeedsValue { flag: Flag::Short(b'c'), values: None }); - test!(short_arg_eq: ["-c=4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]); - test!(short_arg_then: ["-c", "4"] => frees: [], flags: [(Flag::Short(b'c'), Some(OsStr::new("4"))) ]); - test!(short_two_together: ["-lctwo"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]); - test!(short_two_equals: ["-lc=two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]); - test!(short_two_next: ["-lc", "two"] => frees: [], flags: [(Flag::Short(b'l'), None), (Flag::Short(b'c'), Some(OsStr::new("two"))) ]); - - // Short args with values and suggestions - test!(short_none_s: ["-t"] => error NeedsValue { flag: Flag::Short(b't'), values: Some(SUGGESTIONS) }); - test!(short_two_together_s: ["-texa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]); - test!(short_two_equals_s: ["-t=exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]); - test!(short_two_next_s: ["-t", "exa"] => frees: [], flags: [(Flag::Short(b't'), Some(OsStr::new("exa"))) ]); - - - // Unknown args - test!(unknown_long: ["--quiet"] => error UnknownArgument { attempt: OsString::from("quiet") }); - test!(unknown_long_eq: ["--quiet=shhh"] => error UnknownArgument { attempt: OsString::from("quiet") }); - test!(unknown_short: ["-q"] => error UnknownShortArgument { attempt: b'q' }); - test!(unknown_short_2nd: ["-lq"] => error UnknownShortArgument { attempt: b'q' }); - test!(unknown_short_eq: ["-q=shhh"] => error UnknownShortArgument { attempt: b'q' }); - test!(unknown_short_2nd_eq: ["-lq=shhh"] => error UnknownShortArgument { attempt: b'q' }); -} - - -#[cfg(test)] -mod matches_test { - use super::*; - - macro_rules! test { - ($name:ident: $input:expr, has $param:expr => $result:expr) => { - #[test] - fn $name() { - let flags = MatchedFlags { - flags: $input.to_vec(), - strictness: Strictness::UseLastArguments, - }; - - assert_eq!(flags.has(&$param), Ok($result)); - } - }; - } - - static VERBOSE: Arg = Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden }; - static COUNT: Arg = Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) }; - - - test!(short_never: [], has VERBOSE => false); - test!(short_once: [(Flag::Short(b'v'), None)], has VERBOSE => true); - test!(short_twice: [(Flag::Short(b'v'), None), (Flag::Short(b'v'), None)], has VERBOSE => true); - test!(long_once: [(Flag::Long("verbose"), None)], has VERBOSE => true); - test!(long_twice: [(Flag::Long("verbose"), None), (Flag::Long("verbose"), None)], has VERBOSE => true); - test!(long_mixed: [(Flag::Long("verbose"), None), (Flag::Short(b'v'), None)], has VERBOSE => true); - - - #[test] - fn only_count() { - let everything = OsString::from("everything"); - - let flags = MatchedFlags { - flags: vec![ (Flag::Short(b'c'), Some(&*everything)) ], - strictness: Strictness::UseLastArguments, - }; - - assert_eq!(flags.get(&COUNT), Ok(Some(&*everything))); - } - - #[test] - fn rightmost_count() { - let everything = OsString::from("everything"); - let nothing = OsString::from("nothing"); - - let flags = MatchedFlags { - flags: vec![ (Flag::Short(b'c'), Some(&*everything)), - (Flag::Short(b'c'), Some(&*nothing)) ], - strictness: Strictness::UseLastArguments, - }; - - assert_eq!(flags.get(&COUNT), Ok(Some(&*nothing))); - } - - #[test] - fn no_count() { - let flags = MatchedFlags { flags: Vec::new(), strictness: Strictness::UseLastArguments }; - - assert!(!flags.has(&COUNT).unwrap()); - } } diff --git a/src/options/theme.rs b/src/options/theme.rs index 4aae7ba1d..c77cdec10 100644 --- a/src/options/theme.rs +++ b/src/options/theme.rs @@ -1,12 +1,13 @@ -use crate::options::{flags, vars, Vars, OptionsError}; -use crate::options::parser::MatchedFlags; +use crate::options::{vars, Vars, OptionsError}; use crate::theme::{Options, UseColours, ColourScale, Definitions}; +use super::parser::Opts; + impl Options { - pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + pub fn deduce(matches: &Opts, vars: &V) -> Result { let use_colours = UseColours::deduce(matches, vars)?; - let colour_scale = ColourScale::deduce(matches)?; + let colour_scale = ColourScale::deduce(matches); let definitions = if use_colours == UseColours::Never { Definitions::default() @@ -21,13 +22,13 @@ impl Options { impl UseColours { - fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + fn deduce(matches: &Opts, vars: &V) -> Result { let default_value = match vars.get(vars::NO_COLOR) { Some(_) => Self::Never, None => Self::Automatic, }; - let Some(word) = matches.get_where(|f| f.matches(&flags::COLOR) || f.matches(&flags::COLOUR))? else { return Ok(default_value) }; + let Some(ref word) = matches.color else { return Ok(default_value) }; if word == "always" { Ok(Self::Always) @@ -39,20 +40,18 @@ impl UseColours { Ok(Self::Never) } else { - Err(OptionsError::BadArgument(&flags::COLOR, word.into())) + Err(OptionsError::BadArgument("--color".to_string(), word.to_string_lossy().to_string())) } } } impl ColourScale { - fn deduce(matches: &MatchedFlags<'_>) -> Result { - if matches.has_where(|f| f.matches(&flags::COLOR_SCALE) || f.matches(&flags::COLOUR_SCALE))?.is_some() { - Ok(Self::Gradient) - } - else { - Ok(Self::Fixed) + fn deduce(matches: &Opts) -> Self { + if matches.color_scale > 0 { + return Self::Gradient; } + Self::Fixed } } @@ -67,137 +66,25 @@ impl Definitions { #[cfg(test)] -mod terminal_test { +mod tests { use super::*; - use std::ffi::OsString; - use crate::options::flags; - use crate::options::parser::{Flag, Arg}; - - use crate::options::test::parse_for_test; - use crate::options::test::Strictnesses::*; - - static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR, - &flags::COLOR_SCALE, &flags::COLOUR_SCALE, ]; - - macro_rules! test { - ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => { - #[test] - fn $name() { - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { - assert_eq!(result, $result); - } - } - }; - - ($name:ident: $type:ident <- $inputs:expr, $env:expr; $stricts:expr => $result:expr) => { - #[test] - fn $name() { - let env = $env; - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &env)) { - assert_eq!(result, $result); - } - } - }; - - ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => { - #[test] - fn $name() { - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { - assert_eq!(result.unwrap_err(), $result); - } - } - }; - ($name:ident: $type:ident <- $inputs:expr, $env:expr; $stricts:expr => err $result:expr) => { - #[test] - fn $name() { - let env = $env; - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &env)) { - assert_eq!(result.unwrap_err(), $result); - } - } + #[test] + fn deduce_colour_scale() { + let matches = Opts { + ..Opts::default() }; - } - struct MockVars { - ls: &'static str, - exa: &'static str, - no_color: &'static str, + assert_eq!(ColourScale::deduce(&matches), ColourScale::Fixed); } - impl MockVars { - fn empty() -> MockVars { - MockVars { - ls: "", - exa: "", - no_color: "", - } - } - fn with_no_color() -> MockVars { - MockVars { - ls: "", - exa: "", - no_color: "true", - } - } - } + #[test] + fn deduce_colour_scale_on() { + let matches = Opts { + color_scale: 1, + ..Opts::default() + }; - // Test impl that just returns the value it has. - impl Vars for MockVars { - fn get(&self, name: &'static str) -> Option { - if name == vars::LS_COLORS && ! self.ls.is_empty() { - Some(OsString::from(self.ls)) - } - else if name == vars::EXA_COLORS && ! self.exa.is_empty() { - Some(OsString::from(self.exa)) - } - else if name == vars::NO_COLOR && ! self.no_color.is_empty() { - Some(OsString::from(self.no_color)) - } - else { - None - } - } + assert_eq!(ColourScale::deduce(&matches), ColourScale::Gradient); } - - - - // Default - test!(empty: UseColours <- [], MockVars::empty(); Both => Ok(UseColours::Automatic)); - test!(empty_with_no_color: UseColours <- [], MockVars::with_no_color(); Both => Ok(UseColours::Never)); - - // --colour - test!(u_always: UseColours <- ["--colour=always"], MockVars::empty(); Both => Ok(UseColours::Always)); - test!(u_auto: UseColours <- ["--colour", "auto"], MockVars::empty(); Both => Ok(UseColours::Automatic)); - test!(u_never: UseColours <- ["--colour=never"], MockVars::empty(); Both => Ok(UseColours::Never)); - - // --color - test!(no_u_always: UseColours <- ["--color", "always"], MockVars::empty(); Both => Ok(UseColours::Always)); - test!(no_u_auto: UseColours <- ["--color=auto"], MockVars::empty(); Both => Ok(UseColours::Automatic)); - test!(no_u_never: UseColours <- ["--color", "never"], MockVars::empty(); Both => Ok(UseColours::Never)); - - // Errors - test!(no_u_error: UseColours <- ["--color=upstream"], MockVars::empty(); Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color - test!(u_error: UseColours <- ["--colour=lovers"], MockVars::empty(); Both => err OptionsError::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one! - - // Overriding - test!(overridden_1: UseColours <- ["--colour=auto", "--colour=never"], MockVars::empty(); Last => Ok(UseColours::Never)); - test!(overridden_2: UseColours <- ["--color=auto", "--colour=never"], MockVars::empty(); Last => Ok(UseColours::Never)); - test!(overridden_3: UseColours <- ["--colour=auto", "--color=never"], MockVars::empty(); Last => Ok(UseColours::Never)); - test!(overridden_4: UseColours <- ["--color=auto", "--color=never"], MockVars::empty(); Last => Ok(UseColours::Never)); - - test!(overridden_5: UseColours <- ["--colour=auto", "--colour=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("colour"))); - test!(overridden_6: UseColours <- ["--color=auto", "--colour=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("colour"))); - test!(overridden_7: UseColours <- ["--colour=auto", "--color=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("colour"), Flag::Long("color"))); - test!(overridden_8: UseColours <- ["--color=auto", "--color=never"], MockVars::empty(); Complain => err OptionsError::Duplicate(Flag::Long("color"), Flag::Long("color"))); - - test!(scale_1: ColourScale <- ["--color-scale", "--colour-scale"]; Last => Ok(ColourScale::Gradient)); - test!(scale_2: ColourScale <- ["--color-scale", ]; Last => Ok(ColourScale::Gradient)); - test!(scale_3: ColourScale <- [ "--colour-scale"]; Last => Ok(ColourScale::Gradient)); - test!(scale_4: ColourScale <- [ ]; Last => Ok(ColourScale::Fixed)); - - test!(scale_5: ColourScale <- ["--color-scale", "--colour-scale"]; Complain => err OptionsError::Duplicate(Flag::Long("color-scale"), Flag::Long("colour-scale"))); - test!(scale_6: ColourScale <- ["--color-scale", ]; Complain => Ok(ColourScale::Gradient)); - test!(scale_7: ColourScale <- [ "--colour-scale"]; Complain => Ok(ColourScale::Gradient)); - test!(scale_8: ColourScale <- [ ]; Complain => Ok(ColourScale::Fixed)); } diff --git a/src/options/view.rs b/src/options/view.rs index eba0fb7b5..a535d45f2 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -1,19 +1,19 @@ use crate::fs::feature::xattr; -use crate::options::{flags, OptionsError, NumberSource, Vars}; -use crate::options::parser::MatchedFlags; +use crate::options::{OptionsError, NumberSource, Vars}; use crate::output::{View, Mode, TerminalWidth, grid, details}; use crate::output::grid_details::{self, RowThreshold}; use crate::output::file_name::Options as FileStyle; use crate::output::table::{TimeTypes, SizeFormat, UserFormat, Columns, Options as TableOptions}; use crate::output::time::TimeFormat; +use crate::options::parser::Opts; impl View { - pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { - let mode = Mode::deduce(matches, vars)?; + pub fn deduce(matches: &Opts, vars: &V, strictness: bool) -> Result { + let mode = Mode::deduce(matches, vars, strictness)?; let width = TerminalWidth::deduce(matches, vars)?; let file_style = FileStyle::deduce(matches, vars)?; - let deref_links = matches.has(&flags::DEREF_LINKS)?; + let deref_links = matches.dereference > 0; Ok(Self { mode, width, file_style, deref_links }) } } @@ -29,28 +29,23 @@ impl Mode { /// /// This is complicated a little by the fact that `--grid` and `--tree` /// can also combine with `--long`, so care has to be taken to use the - pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { - let flag = matches.has_where_any(|f| f.matches(&flags::LONG) || f.matches(&flags::ONE_LINE) - || f.matches(&flags::GRID) || f.matches(&flags::TREE)); + pub fn deduce(matches: &Opts, vars: &V, strictness: bool) -> Result { + let flag = matches.long > 0 || matches.oneline > 0 || matches.grid > 0 || matches.tree > 0; - let Some(flag) = flag else { - Self::strict_check_long_flags(matches)?; - let grid = grid::Options::deduce(matches)?; + if ! flag { + Self::strict_check_long_flags(matches, strictness)?; + let grid = grid::Options::deduce(matches); return Ok(Self::Grid(grid)); }; - if flag.matches(&flags::LONG) - || (flag.matches(&flags::TREE) && matches.has(&flags::LONG)?) - || (flag.matches(&flags::GRID) && matches.has(&flags::LONG)?) + if matches.long > 0 { - let _ = matches.has(&flags::LONG)?; - let details = details::Options::deduce_long(matches, vars)?; + let details = details::Options::deduce_long(matches, vars, strictness)?; - let flag = matches.has_where_any(|f| f.matches(&flags::GRID) || f.matches(&flags::TREE)); + let flags = matches.grid > 0 || (matches.tree > 0); - if flag.is_some() && flag.unwrap().matches(&flags::GRID) { - let _ = matches.has(&flags::GRID)?; - let grid = grid::Options::deduce(matches)?; + if flags && matches.grid > 0 { + let grid = grid::Options::deduce(matches); let row_threshold = RowThreshold::deduce(vars)?; let grid_details = grid_details::Options { grid, details, row_threshold }; return Ok(Self::GridDetails(grid_details)); @@ -60,109 +55,99 @@ impl Mode { return Ok(Self::Details(details)); } - Self::strict_check_long_flags(matches)?; + Self::strict_check_long_flags(matches, strictness)?; - if flag.matches(&flags::TREE) { - let _ = matches.has(&flags::TREE)?; - let details = details::Options::deduce_tree(matches)?; + if matches.tree > 0 { + let details = details::Options::deduce_tree(matches); return Ok(Self::Details(details)); } - if flag.matches(&flags::ONE_LINE) { - let _ = matches.has(&flags::ONE_LINE)?; + if matches.oneline > 0 { return Ok(Self::Lines); } - let grid = grid::Options::deduce(matches)?; + let grid = grid::Options::deduce(matches); Ok(Self::Grid(grid)) } - fn strict_check_long_flags(matches: &MatchedFlags<'_>) -> Result<(), OptionsError> { + fn strict_check_long_flags(matches: &Opts, strictness: bool) -> Result<(), OptionsError> { // If --long hasn’t been passed, then check if we need to warn the // user about flags that won’t have any effect. - if matches.is_strict() { - for option in &[ &flags::BINARY, &flags::BYTES, &flags::INODE, &flags::LINKS, - &flags::HEADER, &flags::BLOCKSIZE, &flags::TIME, &flags::GROUP, &flags::NUMERIC ] { - if matches.has(option)? { - return Err(OptionsError::Useless(option, false, &flags::LONG)); - } - } - - if matches.has(&flags::GIT)? && !matches.has(&flags::NO_GIT)? { - return Err(OptionsError::Useless(&flags::GIT, false, &flags::LONG)); - } - else if matches.has(&flags::LEVEL)? && ! matches.has(&flags::RECURSE)? && ! matches.has(&flags::TREE)? { - return Err(OptionsError::Useless2(&flags::LEVEL, &flags::RECURSE, &flags::TREE)); + // TODO strict handling + if strictness && matches.long == 0 { + if matches.tree > 0 { + return Err(OptionsError::Useless("--tree".to_string(), false, "--long".to_string())); + } else if matches.binary > 0 { + return Err(OptionsError::Useless("--binary".to_string(), false, "--long".to_string())); + } else if matches.bytes > 0 { + return Err(OptionsError::Useless("--bytes".to_string(), false, "--long".to_string())); + } else if matches.inode > 0 { + return Err(OptionsError::Useless("--inode".to_string(), false, "--long".to_string())); + } else if matches.links > 0 { + return Err(OptionsError::Useless("--links".to_string(), false, "--long".to_string())); + } else if matches.header > 0 { + return Err(OptionsError::Useless("--header".to_string(), false, "--long".to_string())); + } else if matches.blocksize > 0 { + return Err(OptionsError::Useless("--blocksize".to_string(), false, "--long".to_string())); + } else if matches.time.is_some() { + return Err(OptionsError::Useless("--time".to_string(), false, "--long".to_string())); + } else if matches.group > 0 { + return Err(OptionsError::Useless("--group".to_string(), false, "--long".to_string())); + } else if matches.numeric > 0 { + return Err(OptionsError::Useless("--numeric".to_string(), false, "--long".to_string())); } } - Ok(()) } } impl grid::Options { - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let grid = grid::Options { - across: matches.has(&flags::ACROSS)?, - }; - - Ok(grid) + fn deduce(matches: &Opts) -> Self { + grid::Options{across: matches.across > 0} } } impl details::Options { - fn deduce_tree(matches: &MatchedFlags<'_>) -> Result { - let details = details::Options { + fn deduce_tree(matches: &Opts) -> Self { + details::Options { table: None, header: false, - xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?, - secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?, - }; - - Ok(details) + xattr: xattr::ENABLED && matches.extended > 0, + secattr: xattr::ENABLED && matches.security_context > 0, + } } - fn deduce_long(matches: &MatchedFlags<'_>, vars: &V) -> Result { - if matches.is_strict() { - if matches.has(&flags::ACROSS)? && ! matches.has(&flags::GRID)? { - return Err(OptionsError::Useless(&flags::ACROSS, true, &flags::LONG)); + fn deduce_long(matches: &Opts, vars: &V, strictness: bool) -> Result { + if strictness { + if matches.across > 0 && ! matches.grid > 0 { + return Err(OptionsError::Useless("--accros".to_string(), true, "--long".to_string())); } - else if matches.has(&flags::ONE_LINE)? { - return Err(OptionsError::Useless(&flags::ONE_LINE, true, &flags::LONG)); + else if matches.oneline > 0 { + return Err(OptionsError::Useless("--oneline".to_string(), true, "--long".to_string())); } } Ok(details::Options { table: Some(TableOptions::deduce(matches, vars)?), - header: matches.has(&flags::HEADER)?, - xattr: xattr::ENABLED && matches.has(&flags::EXTENDED)?, - secattr: xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?, + header: matches.header > 0, + xattr: xattr::ENABLED && matches.extended > 0, + secattr: xattr::ENABLED && matches.security_context > 0, }) } } impl TerminalWidth { - fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + fn deduce(matches: &Opts, vars: &V) -> Result { use crate::options::vars; - if let Some(width) = matches.get(&flags::WIDTH)? { - let arg_str = width.to_string_lossy(); - match arg_str.parse() { - Ok(w) => { - if w >= 1 { - Ok(Self::Set(w)) - } else { - Ok(Self::Automatic) - } - } - Err(e) => { - let source = NumberSource::Arg(&flags::WIDTH); - Err(OptionsError::FailedParse(arg_str.to_string(), source, e)) - } + if let Some(width) = matches.width { + if width == 0 { + return Ok(Self::Automatic); } + Ok(Self::Set(width)) } else if let Some(columns) = vars.get(vars::COLUMNS).and_then(|s| s.into_string().ok()) { match columns.parse() { @@ -170,7 +155,7 @@ impl TerminalWidth { Ok(Self::Set(width)) } Err(e) => { - let source = NumberSource::Env(vars::COLUMNS); + let source = NumberSource::Var(vars::COLUMNS.to_string()); Err(OptionsError::FailedParse(columns, source, e)) } } @@ -192,7 +177,7 @@ impl RowThreshold { Ok(Self::MinimumRows(rows)) } Err(e) => { - let source = NumberSource::Env(vars::EXA_GRID_ROWS); + let source = NumberSource::Var(vars::EXA_GRID_ROWS.to_string()); Err(OptionsError::FailedParse(columns, source, e)) } } @@ -205,10 +190,10 @@ impl RowThreshold { impl TableOptions { - fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + fn deduce(matches: &Opts, vars: &V) -> Result { let time_format = TimeFormat::deduce(matches, vars)?; - let size_format = SizeFormat::deduce(matches)?; - let user_format = UserFormat::deduce(matches)?; + let size_format = SizeFormat::deduce(matches); + let user_format = UserFormat::deduce(matches); let columns = Columns::deduce(matches)?; Ok(Self { size_format, time_format, user_format, columns }) } @@ -216,23 +201,23 @@ impl TableOptions { impl Columns { - fn deduce(matches: &MatchedFlags<'_>) -> Result { + fn deduce(matches: &Opts) -> Result { let time_types = TimeTypes::deduce(matches)?; - let git = matches.has(&flags::GIT)? && !matches.has(&flags::NO_GIT)?; - let subdir_git_repos = matches.has(&flags::GIT_REPOS)? && !matches.has(&flags::NO_GIT)?; - let subdir_git_repos_no_stat = !subdir_git_repos && matches.has(&flags::GIT_REPOS_NO_STAT)? && !matches.has(&flags::NO_GIT)?; + let git = matches.git > 0 && matches.no_git == 0; + let subdir_git_repos = matches.git_repos > 0 && matches.no_git == 0; + let subdir_git_repos_no_stat = subdir_git_repos && matches.git_repos_no_status > 0 && matches.no_git == 0; - let blocksize = matches.has(&flags::BLOCKSIZE)?; - let group = matches.has(&flags::GROUP)?; - let inode = matches.has(&flags::INODE)?; - let links = matches.has(&flags::LINKS)?; - let octal = matches.has(&flags::OCTAL)?; - let security_context = xattr::ENABLED && matches.has(&flags::SECURITY_CONTEXT)?; + let blocksize = matches.blocksize > 0; + let group = matches.group > 0; + let inode = matches.inode > 0; + let links = matches.links > 0; + let octal = matches.octal > 0; + let security_context = xattr::ENABLED && matches.security_context > 0; - let permissions = ! matches.has(&flags::NO_PERMISSIONS)?; - let filesize = ! matches.has(&flags::NO_FILESIZE)?; - let user = ! matches.has(&flags::NO_USER)?; + let permissions = matches.no_permissions == 0; + let filesize = matches.no_filesize == 0; + let user = matches.no_user == 0; Ok(Self { time_types, inode, links, blocksize, group, git, subdir_git_repos, subdir_git_repos_no_stat, octal, security_context, permissions, filesize, user }) } @@ -249,14 +234,20 @@ impl SizeFormat { /// strings of digits in your head. Changing the format to anything else /// involves the `--binary` or `--bytes` flags, and these conflict with /// each other. - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let flag = matches.has_where(|f| f.matches(&flags::BINARY) || f.matches(&flags::BYTES))?; - - Ok(match flag { - Some(f) if f.matches(&flags::BINARY) => Self::BinaryBytes, - Some(f) if f.matches(&flags::BYTES) => Self::JustBytes, - _ => Self::DecimalBytes, - }) + fn deduce(matches: &Opts) -> Self { + let flag = matches.binary > 0 || matches.bytes > 0; + + if flag { + if matches.binary > 0 { + Self::BinaryBytes + } else if matches.bytes > 0 { + Self::JustBytes + } else { + Self::DecimalBytes + } + } else { + Self::DecimalBytes + } } } @@ -264,10 +255,10 @@ impl SizeFormat { impl TimeFormat { /// Determine how time should be formatted in timestamp columns. - fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + fn deduce(matches: &Opts, vars: &V) -> Result { let word = - if let Some(w) = matches.get(&flags::TIME_STYLE)? { - w.to_os_string() + if let Some(ref w) = matches.time_style { + w.clone() } else { use crate::options::vars; @@ -283,16 +274,16 @@ impl TimeFormat { "iso" => Ok(Self::ISOFormat), "long-iso" => Ok(Self::LongISO), "full-iso" => Ok(Self::FullISO), - _ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word)) + _ => Err(OptionsError::BadArgument("--time-style".to_string(), word.to_string_lossy().to_string())) } } } impl UserFormat { - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let flag = matches.has(&flags::NUMERIC)?; - Ok(if flag { Self::Numeric } else { Self::Name }) + fn deduce(matches: &Opts) -> Self { + let flag = matches.numeric > 0 ; + if flag { Self::Numeric } else { Self::Name } } } @@ -309,29 +300,29 @@ impl TimeTypes { /// It’s valid to show more than one column by passing in more than one /// option, but passing *no* options means that the user just wants to /// see the default set. - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let possible_word = matches.get(&flags::TIME)?; - let modified = matches.has(&flags::MODIFIED)?; - let changed = matches.has(&flags::CHANGED)?; - let accessed = matches.has(&flags::ACCESSED)?; - let created = matches.has(&flags::CREATED)?; + fn deduce(matches: &Opts) -> Result { + let possible_word = &matches.time; + let modified = matches.modified > 0; + let changed = matches.changed > 0; + let accessed = matches.accessed > 0; + let created = matches.created > 0; - let no_time = matches.has(&flags::NO_TIME)?; + let no_time = matches.no_time > 0; let time_types = if no_time { Self { modified: false, changed: false, accessed: false, created: false } } else if let Some(word) = possible_word { if modified { - return Err(OptionsError::Useless(&flags::MODIFIED, true, &flags::TIME)); + return Err(OptionsError::Useless("--modified".to_string(), true, "--time".to_string())); } else if changed { - return Err(OptionsError::Useless(&flags::CHANGED, true, &flags::TIME)); + return Err(OptionsError::Useless("--changed".to_string(), true, "--time".to_string())); } else if accessed { - return Err(OptionsError::Useless(&flags::ACCESSED, true, &flags::TIME)); + return Err(OptionsError::Useless("--accessed".to_string(), true, "--time".to_string())); } else if created { - return Err(OptionsError::Useless(&flags::CREATED, true, &flags::TIME)); + return Err(OptionsError::Useless("--created".to_string(), true, "--time".to_string())); } else if word == "mod" || word == "modified" { Self { modified: true, changed: false, accessed: false, created: false } @@ -346,7 +337,7 @@ impl TimeTypes { Self { modified: false, changed: false, accessed: false, created: true } } else { - return Err(OptionsError::BadArgument(&flags::TIME, word.into())); + return Err(OptionsError::BadArgument("--time".to_string(), word.to_string_lossy().to_string())); } } else if modified || changed || accessed || created { @@ -360,257 +351,205 @@ impl TimeTypes { } } - #[cfg(test)] mod test { + use super::*; use std::ffi::OsString; - use crate::options::flags; - use crate::options::parser::{Flag, Arg}; - - use crate::options::test::parse_for_test; - use crate::options::test::Strictnesses::*; - - static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE, - &flags::TIME, &flags::MODIFIED, &flags::CHANGED, - &flags::CREATED, &flags::ACCESSED, - &flags::HEADER, &flags::GROUP, &flags::INODE, &flags::GIT, - &flags::LINKS, &flags::BLOCKSIZE, &flags::LONG, &flags::LEVEL, - &flags::GRID, &flags::ACROSS, &flags::ONE_LINE, &flags::TREE, - &flags::NUMERIC ]; - - macro_rules! test { - - ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => { - /// Macro that writes a test. - /// If testing both strictnesses, they’ll both be done in the same function. - #[test] - fn $name() { - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { - assert_eq!(result, $result); - } - } - }; - ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => err $result:expr) => { - /// Special macro for testing Err results. - /// This is needed because sometimes the Ok type doesn’t implement `PartialEq`. - #[test] - fn $name() { - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { - assert_eq!(result.unwrap_err(), $result); - } - } - }; - - ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => like $pat:pat) => { - /// More general macro for testing against a pattern. - /// Instead of using `PartialEq`, this just tests if it matches a pat. - #[test] - fn $name() { - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { - println!("Testing {:?}", result); - match result { - $pat => assert!(true), - _ => assert!(false), - } - } - } + #[test] + fn deduce_time_type() { + let matches = Opts { + ..Opts::default() }; + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes::default()); + } - ($name:ident: $type:ident <- $inputs:expr, $vars:expr; $stricts:expr => err $result:expr) => { - /// Like above, but with $vars. - #[test] - fn $name() { - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) { - assert_eq!(result.unwrap_err(), $result); - } - } + #[test] + fn deduce_time_type_modified() { + let matches = Opts { + modified: 1, + ..Opts::default() }; - ($name:ident: $type:ident <- $inputs:expr, $vars:expr; $stricts:expr => like $pat:pat) => { - /// Like further above, but with $vars. - #[test] - fn $name() { - for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf, &$vars)) { - println!("Testing {:?}", result); - match result { - $pat => assert!(true), - _ => assert!(false), - } - } - } - }; + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { modified: true, ..TimeTypes::default() }); } + #[test] + fn deduce_time_type_changed() { + let matches = Opts { + changed: 1, + ..Opts::default() + }; - mod size_formats { - use super::*; - - // Default behaviour - test!(empty: SizeFormat <- []; Both => Ok(SizeFormat::DecimalBytes)); - - // Individual flags - test!(binary: SizeFormat <- ["--binary"]; Both => Ok(SizeFormat::BinaryBytes)); - test!(bytes: SizeFormat <- ["--bytes"]; Both => Ok(SizeFormat::JustBytes)); - - // Overriding - test!(both_1: SizeFormat <- ["--binary", "--binary"]; Last => Ok(SizeFormat::BinaryBytes)); - test!(both_2: SizeFormat <- ["--bytes", "--binary"]; Last => Ok(SizeFormat::BinaryBytes)); - test!(both_3: SizeFormat <- ["--binary", "--bytes"]; Last => Ok(SizeFormat::JustBytes)); - test!(both_4: SizeFormat <- ["--bytes", "--bytes"]; Last => Ok(SizeFormat::JustBytes)); - - test!(both_5: SizeFormat <- ["--binary", "--binary"]; Complain => err OptionsError::Duplicate(Flag::Long("binary"), Flag::Long("binary"))); - test!(both_6: SizeFormat <- ["--bytes", "--binary"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("binary"))); - test!(both_7: SizeFormat <- ["--binary", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("binary"), Flag::Long("bytes"))); - test!(both_8: SizeFormat <- ["--bytes", "--bytes"]; Complain => err OptionsError::Duplicate(Flag::Long("bytes"), Flag::Long("bytes"))); + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { changed: true, modified: false, ..TimeTypes::default() }); } + #[test] + fn deduce_time_type_accessed() { + let matches = Opts { + accessed: 1, + ..Opts::default() + }; - mod time_formats { - use super::*; - - // These tests use pattern matching because TimeFormat doesn’t - // implement PartialEq. - - // Default behaviour - test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat)); + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { accessed: true, modified: false,..TimeTypes::default() }); + } - // Individual settings - test!(default: TimeFormat <- ["--time-style=default"], None; Both => like Ok(TimeFormat::DefaultFormat)); - test!(iso: TimeFormat <- ["--time-style", "iso"], None; Both => like Ok(TimeFormat::ISOFormat)); - test!(relative: TimeFormat <- ["--time-style", "relative"], None; Both => like Ok(TimeFormat::Relative)); - test!(long_iso: TimeFormat <- ["--time-style=long-iso"], None; Both => like Ok(TimeFormat::LongISO)); - test!(full_iso: TimeFormat <- ["--time-style", "full-iso"], None; Both => like Ok(TimeFormat::FullISO)); + #[test] + fn deduce_time_type_created() { + let matches = Opts { + created: 1, + ..Opts::default() + }; - // Overriding - test!(actually: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Last => like Ok(TimeFormat::ISOFormat)); - test!(actual_2: TimeFormat <- ["--time-style=default", "--time-style", "iso"], None; Complain => err OptionsError::Duplicate(Flag::Long("time-style"), Flag::Long("time-style"))); + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { created: true, modified: false, ..TimeTypes::default() }); + } - test!(nevermind: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Last => like Ok(TimeFormat::FullISO)); - test!(nevermore: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"], None; Complain => err OptionsError::Duplicate(Flag::Long("time-style"), Flag::Long("time-style"))); + #[test] + fn deduce_time_type_mod_string() { + let matches = Opts { + time: Some(OsString::from("mod")), + ..Opts::default() + }; - // Errors - test!(daily: TimeFormat <- ["--time-style=24-hour"], None; Both => err OptionsError::BadArgument(&flags::TIME_STYLE, OsString::from("24-hour"))); + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { modified: true, ..TimeTypes::default() }); + } - // `TIME_STYLE` environment variable is defined. - // If the time-style argument is not given, `TIME_STYLE` is used. - test!(use_env: TimeFormat <- [], Some("long-iso".into()); Both => like Ok(TimeFormat::LongISO)); + #[test] + fn deduce_time_type_ch_string() { + let matches = Opts { + time: Some(OsString::from("ch")), + ..Opts::default() + }; - // If the time-style argument is given, `TIME_STYLE` is overriding. - test!(override_env: TimeFormat <- ["--time-style=full-iso"], Some("long-iso".into()); Both => like Ok(TimeFormat::FullISO)); + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { changed: true, modified: false, ..TimeTypes::default() }); } + #[test] + fn deduce_time_type_acc_string() { + let matches = Opts { + time: Some(OsString::from("acc")), + ..Opts::default() + }; - mod time_types { - use super::*; - - // Default behaviour - test!(empty: TimeTypes <- []; Both => Ok(TimeTypes::default())); + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { accessed: true, modified: false, ..TimeTypes::default() }); + } - // Modified - test!(modified: TimeTypes <- ["--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); - test!(m: TimeTypes <- ["-m"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); - test!(time_mod: TimeTypes <- ["--time=modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); - test!(t_m: TimeTypes <- ["-tmod"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); + #[test] + fn deduce_time_type_cr_string() { + let matches = Opts { + time: Some(OsString::from("cr")), + ..Opts::default() + }; - // Changed - #[cfg(target_family = "unix")] - test!(changed: TimeTypes <- ["--changed"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false })); - #[cfg(target_family = "unix")] - test!(time_ch: TimeTypes <- ["--time=changed"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false })); - #[cfg(target_family = "unix")] - test!(t_ch: TimeTypes <- ["-t", "ch"]; Both => Ok(TimeTypes { modified: false, changed: true, accessed: false, created: false })); + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes { created: true, modified: false, ..TimeTypes::default() }); + } - // Accessed - test!(acc: TimeTypes <- ["--accessed"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); - test!(a: TimeTypes <- ["-u"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); - test!(time_acc: TimeTypes <- ["--time", "accessed"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); - test!(time_a: TimeTypes <- ["-t", "acc"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: true, created: false })); + #[test] + fn deduce_time_type_useless_mod() { + let matches = Opts { + time: Some(OsString::from("mod")), + modified: 1, + ..Opts::default() + }; - // Created - test!(cr: TimeTypes <- ["--created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); - test!(c: TimeTypes <- ["-U"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); - test!(time_cr: TimeTypes <- ["--time=created"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); - test!(t_cr: TimeTypes <- ["-tcr"]; Both => Ok(TimeTypes { modified: false, changed: false, accessed: false, created: true })); + assert!(TimeTypes::deduce(&matches).is_err()); + } - // Multiples - test!(time_uu: TimeTypes <- ["-u", "--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: true, created: false })); + #[test] + fn deduce_time_type_useless_ch() { + let matches = Opts { + time: Some(OsString::from("ch")), + changed: 1, + ..Opts::default() + }; + assert!(TimeTypes::deduce(&matches).is_err()); + } - // Errors - test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("tea"))); - test!(t_ea: TimeTypes <- ["-tea"]; Both => err OptionsError::BadArgument(&flags::TIME, OsString::from("ea"))); + #[test] + fn deduce_time_type_useless_acc() { + let matches = Opts { + time: Some(OsString::from("acc")), + accessed: 1, + ..Opts::default() + }; - // Overriding - test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { modified: true, changed: false, accessed: false, created: false })); - test!(overridden_2: TimeTypes <- ["-tcr", "-tmod"]; Complain => err OptionsError::Duplicate(Flag::Short(b't'), Flag::Short(b't'))); + assert!(TimeTypes::deduce(&matches).is_err()); } + #[test] + fn deduce_time_type_useless_cr() { + let matches = Opts { + time: Some(OsString::from("cr")), + created: 1, + ..Opts::default() + }; - mod views { - use super::*; + assert!(TimeTypes::deduce(&matches).is_err()); + } - use crate::output::grid::Options as GridOptions; + #[test] + fn deduce_user_format() { + let matches = Opts { + ..Opts::default() + }; + assert_eq!(UserFormat::deduce(&matches), UserFormat::Name); + } - // Default - test!(empty: Mode <- [], None; Both => like Ok(Mode::Grid(_))); + #[test] + fn deduce_user_format_numeric() { + let matches = Opts { + numeric: 1, + ..Opts::default() + }; - // Grid views - test!(original_g: Mode <- ["-G"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, .. }))); - test!(grid: Mode <- ["--grid"], None; Both => like Ok(Mode::Grid(GridOptions { across: false, .. }))); - test!(across: Mode <- ["--across"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, .. }))); - test!(gracross: Mode <- ["-xG"], None; Both => like Ok(Mode::Grid(GridOptions { across: true, .. }))); + assert_eq!(UserFormat::deduce(&matches), UserFormat::Numeric); + } - // Lines views - test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines)); - test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines)); + #[test] + fn deduce_size_format() { + let matches = Opts { + ..Opts::default() + }; - // Details views - test!(long: Mode <- ["--long"], None; Both => like Ok(Mode::Details(_))); - test!(ell: Mode <- ["-l"], None; Both => like Ok(Mode::Details(_))); + assert_eq!(SizeFormat::deduce(&matches), SizeFormat::DecimalBytes); + } - // Grid-details views - test!(lid: Mode <- ["--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_))); - test!(leg: Mode <- ["-lG"], None; Both => like Ok(Mode::GridDetails(_))); + #[test] + fn deduce_size_format_binary() { + let matches = Opts { + binary: 1, + ..Opts::default() + }; - // Options that do nothing with --long - test!(long_across: Mode <- ["--long", "--across"], None; Last => like Ok(Mode::Details(_))); + assert_eq!(SizeFormat::deduce(&matches), SizeFormat::BinaryBytes); + } - // Options that do nothing without --long - test!(just_header: Mode <- ["--header"], None; Last => like Ok(Mode::Grid(_))); - test!(just_group: Mode <- ["--group"], None; Last => like Ok(Mode::Grid(_))); - test!(just_inode: Mode <- ["--inode"], None; Last => like Ok(Mode::Grid(_))); - test!(just_links: Mode <- ["--links"], None; Last => like Ok(Mode::Grid(_))); - test!(just_blocks: Mode <- ["--blocksize"], None; Last => like Ok(Mode::Grid(_))); - test!(just_binary: Mode <- ["--binary"], None; Last => like Ok(Mode::Grid(_))); - test!(just_bytes: Mode <- ["--bytes"], None; Last => like Ok(Mode::Grid(_))); - test!(just_numeric: Mode <- ["--numeric"], None; Last => like Ok(Mode::Grid(_))); + #[test] + fn deduce_size_format_bytes() { + let matches = Opts { + bytes: 1, + ..Opts::default() + }; - #[cfg(feature = "git")] - test!(just_git: Mode <- ["--git"], None; Last => like Ok(Mode::Grid(_))); + assert_eq!(SizeFormat::deduce(&matches), SizeFormat::JustBytes); + } - test!(just_header_2: Mode <- ["--header"], None; Complain => err OptionsError::Useless(&flags::HEADER, false, &flags::LONG)); - test!(just_group_2: Mode <- ["--group"], None; Complain => err OptionsError::Useless(&flags::GROUP, false, &flags::LONG)); - test!(just_inode_2: Mode <- ["--inode"], None; Complain => err OptionsError::Useless(&flags::INODE, false, &flags::LONG)); - test!(just_links_2: Mode <- ["--links"], None; Complain => err OptionsError::Useless(&flags::LINKS, false, &flags::LONG)); - test!(just_blocks_2: Mode <- ["--blocksize"], None; Complain => err OptionsError::Useless(&flags::BLOCKSIZE, false, &flags::LONG)); - test!(just_binary_2: Mode <- ["--binary"], None; Complain => err OptionsError::Useless(&flags::BINARY, false, &flags::LONG)); - test!(just_bytes_2: Mode <- ["--bytes"], None; Complain => err OptionsError::Useless(&flags::BYTES, false, &flags::LONG)); - test!(just_numeric2: Mode <- ["--numeric"], None; Complain => err OptionsError::Useless(&flags::NUMERIC, false, &flags::LONG)); + #[test] + fn deduce_dtails_tree() { + let matches = Opts { + tree: 1, + ..Opts::default() + }; - #[cfg(feature = "git")] - test!(just_git_2: Mode <- ["--git"], None; Complain => err OptionsError::Useless(&flags::GIT, false, &flags::LONG)); - - // Contradictions and combinations - test!(lgo: Mode <- ["--long", "--grid", "--oneline"], None; Both => like Ok(Mode::Lines)); - test!(lgt: Mode <- ["--long", "--grid", "--tree"], None; Both => like Ok(Mode::Details(_))); - test!(tgl: Mode <- ["--tree", "--grid", "--long"], None; Both => like Ok(Mode::GridDetails(_))); - test!(tlg: Mode <- ["--tree", "--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_))); - test!(ot: Mode <- ["--oneline", "--tree"], None; Both => like Ok(Mode::Details(_))); - test!(og: Mode <- ["--oneline", "--grid"], None; Both => like Ok(Mode::Grid(_))); - test!(tg: Mode <- ["--tree", "--grid"], None; Both => like Ok(Mode::Grid(_))); + assert_eq!(details::Options::deduce_tree(&matches), details::Options { + table: None, + header: false, + xattr: xattr::ENABLED && matches.extended > 0, + secattr: xattr::ENABLED && matches.security_context > 0, + }); } } From 424bfea10f2b0cafa27c2e4b36b1464494dad063 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Mon, 11 Sep 2023 20:38:30 +0200 Subject: [PATCH 03/47] refactor(Parser): cleaned unuseful files --- src/options/flags.rs | 93 ------------------------- src/options/help.rs | 155 ----------------------------------------- src/options/version.rs | 57 --------------- 3 files changed, 305 deletions(-) delete mode 100644 src/options/flags.rs delete mode 100644 src/options/help.rs delete mode 100644 src/options/version.rs diff --git a/src/options/flags.rs b/src/options/flags.rs deleted file mode 100644 index cbc672e2e..000000000 --- a/src/options/flags.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::options::parser::{Arg, Args, TakesValue, Values}; - - -// exa options -pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value: TakesValue::Forbidden }; -pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden }; - -// display options -pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden }; -pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }; -pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden }; -pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden }; -pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden }; -pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden }; -pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden }; -pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden }; -pub static WIDTH: Arg = Arg { short: Some(b'w'), long: "width", takes_value: TakesValue::Necessary(None) }; - -pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary(Some(COLOURS)) }; -pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary(Some(COLOURS)) }; -const COLOURS: &[&str] = &["always", "auto", "never"]; - -pub static COLOR_SCALE: Arg = Arg { short: None, long: "color-scale", takes_value: TakesValue::Forbidden }; -pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden }; - -// filtering and sorting options -pub static ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden }; -pub static ALMOST_ALL: Arg = Arg { short: Some(b'A'), long: "almost-all", takes_value: TakesValue::Forbidden }; -pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden }; -pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary(None) }; -pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden }; -pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary(Some(SORTS)) }; -pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary(None) }; -pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", takes_value: TakesValue::Forbidden }; -pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden }; -pub static ONLY_DIRS: Arg = Arg { short: Some(b'D'), long: "only-dirs", takes_value: TakesValue::Forbidden }; -const SORTS: Values = &[ "name", "Name", "size", "extension", - "Extension", "modified", "changed", "accessed", - "created", "inode", "type", "none" ]; - -// display options -pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden }; -pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden }; -pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden }; -pub static NUMERIC: Arg = Arg { short: Some(b'n'), long: "numeric", takes_value: TakesValue::Forbidden }; -pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden }; -pub static ICONS: Arg = Arg { short: None, long: "icons", takes_value: TakesValue::Forbidden }; -pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden }; -pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden }; -pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden }; -pub static CHANGED: Arg = Arg { short: None, long: "changed", takes_value: TakesValue::Forbidden }; -pub static BLOCKSIZE: Arg = Arg { short: Some(b'S'), long: "blocksize", takes_value: TakesValue::Forbidden }; -pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary(Some(TIMES)) }; -pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_value: TakesValue::Forbidden }; -pub static CREATED: Arg = Arg { short: Some(b'U'), long: "created", takes_value: TakesValue::Forbidden }; -pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) }; -pub static HYPERLINK: Arg = Arg { short: None, long: "hyperlink", takes_value: TakesValue::Forbidden}; -const TIMES: Values = &["modified", "changed", "accessed", "created"]; -const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso", "relative"]; - -// suppressing columns -pub static NO_PERMISSIONS: Arg = Arg { short: None, long: "no-permissions", takes_value: TakesValue::Forbidden }; -pub static NO_FILESIZE: Arg = Arg { short: None, long: "no-filesize", takes_value: TakesValue::Forbidden }; -pub static NO_USER: Arg = Arg { short: None, long: "no-user", takes_value: TakesValue::Forbidden }; -pub static NO_TIME: Arg = Arg { short: None, long: "no-time", takes_value: TakesValue::Forbidden }; -pub static NO_ICONS: Arg = Arg { short: None, long: "no-icons", takes_value: TakesValue::Forbidden }; - -// optional feature options -pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden }; -pub static NO_GIT: Arg = Arg { short: None, long: "no-git", takes_value: TakesValue::Forbidden }; -pub static GIT_REPOS: Arg = Arg { short: None, long: "git-repos", takes_value: TakesValue::Forbidden }; -pub static GIT_REPOS_NO_STAT: Arg = Arg { short: None, long: "git-repos-no-status", takes_value: TakesValue::Forbidden }; -pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden }; -pub static OCTAL: Arg = Arg { short: Some(b'o'), long: "octal-permissions", takes_value: TakesValue::Forbidden }; -pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", takes_value: TakesValue::Forbidden }; - - -pub static ALL_ARGS: Args = Args(&[ - &VERSION, &HELP, - - &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, &DEREF_LINKS, - &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, &WIDTH, - - &ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST, - &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, - - &BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED, - &BLOCKSIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, - &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS, - - &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, - &EXTENDED, &OCTAL, &SECURITY_CONTEXT -]); diff --git a/src/options/help.rs b/src/options/help.rs deleted file mode 100644 index 99a1deac1..000000000 --- a/src/options/help.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::fmt; - -use crate::fs::feature::xattr; -use crate::options::flags; -use crate::options::parser::MatchedFlags; - - -static USAGE_PART1: &str = "Usage: - eza [options] [files...] - -META OPTIONS - -?, --help show list of command-line options - -v, --version show version of eza - -DISPLAY OPTIONS - -1, --oneline display one entry per line - -l, --long display extended file metadata as a table - -G, --grid display entries as a grid (default) - -x, --across sort the grid across, rather than downwards - -R, --recurse recurse into directories - -T, --tree recurse into directories as a tree - -F, --classify display type indicator by file names - --colo[u]r=WHEN when to use terminal colours (always, auto, never) - --colo[u]r-scale highlight levels of file sizes distinctly - --icons display icons - --no-icons don't display icons (always overrides --icons) - --hyperlink display entries as hyperlinks - -w, --width COLS set screen width in columns - -FILTERING AND SORTING OPTIONS - -a, --all show hidden and 'dot' files - -d, --list-dirs list directories as files; don't list their contents - -L, --level DEPTH limit the depth of recursion - -r, --reverse reverse the sort order - -s, --sort SORT_FIELD which field to sort by - --group-directories-first list directories before other files - -D, --only-dirs list only directories - -I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore"; - -static GIT_FILTER_HELP: &str = " \ - --git-ignore ignore files mentioned in '.gitignore'"; - -static USAGE_PART2: &str = " \ - Valid sort fields: name, Name, extension, Extension, size, type, - modified, accessed, created, inode, and none. - date, time, old, and new all refer to modified. - -LONG VIEW OPTIONS - -b, --binary list file sizes with binary prefixes - -B, --bytes list file sizes in bytes, without any prefixes - -g, --group list each file's group - -h, --header add a header row to each column - -H, --links list each file's number of hard links - -i, --inode list each file's inode number - -m, --modified use the modified timestamp field - -n, --numeric list numeric user and group IDs - -S, --blocksize show size of allocated file system blocks - -t, --time FIELD which timestamp field to list (modified, accessed, created) - -u, --accessed use the accessed timestamp field - -U, --created use the created timestamp field - --changed use the changed timestamp field - --time-style how to format timestamps (default, iso, long-iso, full-iso, relative) - --no-permissions suppress the permissions field - -o, --octal-permissions list each file's permission in octal format - --no-filesize suppress the filesize field - --no-user suppress the user field - --no-time suppress the time field"; - -static GIT_VIEW_HELP: &str = " \ - --git list each file's Git status, if tracked or ignored - --no-git suppress Git status (always overrides --git, --git-repos, --git-repos-no-status) - --git-repos list root of git-tree status"; -static EXTENDED_HELP: &str = " \ - -@, --extended list each file's extended attributes and sizes"; -static SECATTR_HELP: &str = " \ - -Z, --context list each file's security context"; - -/// All the information needed to display the help text, which depends -/// on which features are enabled and whether the user only wants to -/// see one section’s help. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub struct HelpString; - -impl HelpString { - - /// Determines how to show help, if at all, based on the user’s - /// command-line arguments. This one works backwards from the other - /// ‘deduce’ functions, returning Err if help needs to be shown. - /// - /// We don’t do any strict-mode error checking here: it’s OK to give - /// the --help or --long flags more than once. Actually checking for - /// errors when the user wants help is kind of petty! - pub fn deduce(matches: &MatchedFlags<'_>) -> Option { - if matches.count(&flags::HELP) > 0 { - Some(Self) - } - else { - None - } - } -} - -impl fmt::Display for HelpString { - - /// Format this help options into an actual string of help - /// text to be displayed to the user. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{USAGE_PART1}")?; - - if cfg!(feature = "git") { - write!(f, "\n{GIT_FILTER_HELP}")?; - } - - write!(f, "\n{USAGE_PART2}")?; - - if cfg!(feature = "git") { - write!(f, "\n{GIT_VIEW_HELP}")?; - } - - if xattr::ENABLED { - write!(f, "\n{EXTENDED_HELP}")?; - write!(f, "\n{SECATTR_HELP}")?; - } - - writeln!(f) - } -} - - -#[cfg(test)] -mod test { - use crate::options::{Options, OptionsResult}; - use std::ffi::OsStr; - - #[test] - fn help() { - let args = vec![ OsStr::new("--help") ]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Help(_))); - } - - #[test] - fn help_with_file() { - let args = vec![ OsStr::new("--help"), OsStr::new("me") ]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Help(_))); - } - - #[test] - fn unhelpful() { - let args = vec![]; - let opts = Options::parse(args, &None); - assert!(! matches!(opts, OptionsResult::Help(_))) // no help when --help isn’t passed - } -} diff --git a/src/options/version.rs b/src/options/version.rs deleted file mode 100644 index 3b78c3a4c..000000000 --- a/src/options/version.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Printing the version string. -//! -//! The code that works out which string to print is done in `build.rs`. - -use std::fmt; - -use crate::options::flags; -use crate::options::parser::MatchedFlags; - - -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub struct VersionString; -// There were options here once, but there aren’t anymore! - -impl VersionString { - - /// Determines how to show the version, if at all, based on the user’s - /// command-line arguments. This one works backwards from the other - /// ‘deduce’ functions, returning Err if help needs to be shown. - /// - /// Like --help, this doesn’t check for errors. - pub fn deduce(matches: &MatchedFlags<'_>) -> Option { - if matches.count(&flags::VERSION) > 0 { - Some(Self) - } - else { - None - } - } -} - -impl fmt::Display for VersionString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{}", include_str!(concat!(env!("OUT_DIR"), "/version_string.txt"))) - } -} - - -#[cfg(test)] -mod test { - use crate::options::{Options, OptionsResult}; - use std::ffi::OsStr; - - #[test] - fn version() { - let args = vec![ OsStr::new("--version") ]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Version(_))); - } - - #[test] - fn version_with_file() { - let args = vec![ OsStr::new("--version"), OsStr::new("me") ]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Version(_))); - } -} From c4d6c66fe5f82dfc1e5a31f06b57da3a8ea2d172 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Tue, 12 Sep 2023 00:47:04 +0200 Subject: [PATCH 04/47] feat(parser): Reimplemented the colo[u]r argument It is now back in, but the help message is not as before cause u can't have it like that in clap --- src/main.rs | 1 - src/options/parser.rs | 8 +++++++- src/options/theme.rs | 22 ++++++++++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7752b0cc5..cd6d8f9c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -105,7 +105,6 @@ fn main() { } let cli = Opts::parse(); - let mut input_paths: Vec<&OsStr> = cli.paths.iter().map(OsString::as_os_str).collect(); if input_paths.is_empty() { input_paths.push(OsStr::new(".")); diff --git a/src/options/parser.rs b/src/options/parser.rs index 81bb77731..cb288ca36 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -42,9 +42,13 @@ pub struct Opts { /// when to use terminal colours (always, auto, never). #[arg(long)] pub color: Option, + #[arg(long)] + pub colour: Option, /// highlight levels of file sizes distinctly. #[arg(long, action = clap::ArgAction::Count)] pub color_scale: u8, + #[arg(long, action = clap::ArgAction::Count)] + pub colour_scale: u8, /// #[arg(short = 'A', long, action = clap::ArgAction::Count)] pub almost_all: u8, @@ -157,7 +161,7 @@ pub struct Opts { #[arg(short ='?', long, action = clap::ArgAction::Help)] pub help: Option, /// show mount details (Linux only) - #[arg(short = 'm', long, action = clap::ArgAction::Count)] + #[arg(short = 'M', long, action = clap::ArgAction::Count)] pub mount: u8, } @@ -216,6 +220,8 @@ impl Opts { help: Some(false), no_git: 0, mount: 0, + colour: None, + colour_scale: 0, } } } diff --git a/src/options/theme.rs b/src/options/theme.rs index c77cdec10..e46cb5108 100644 --- a/src/options/theme.rs +++ b/src/options/theme.rs @@ -20,7 +20,6 @@ impl Options { } } - impl UseColours { fn deduce(matches: &Opts, vars: &V) -> Result { let default_value = match vars.get(vars::NO_COLOR) { @@ -28,8 +27,23 @@ impl UseColours { None => Self::Automatic, }; - let Some(ref word) = matches.color else { return Ok(default_value) }; + let color = match matches.color { + Some(ref w) => Some(w), + None => None, + }; + let colour = match matches.colour { + Some(ref w) => Some(w), + None => None, + }; + match (color, colour) { + (Some(ref w), None) => self::UseColours::get_color(w.to_string_lossy().to_string()), + (None, Some(ref w)) => self::UseColours::get_color(w.to_string_lossy().to_string()), + (None, None) => Ok(default_value), + (Some(_), Some(_)) => Err(OptionsError::BadArgument("--color".to_string(), "--colour".to_string())), + } + } + fn get_color(word: String) -> Result { if word == "always" { Ok(Self::Always) } @@ -40,7 +54,7 @@ impl UseColours { Ok(Self::Never) } else { - Err(OptionsError::BadArgument("--color".to_string(), word.to_string_lossy().to_string())) + Err(OptionsError::BadArgument("--color".to_string(), word)) } } } @@ -48,7 +62,7 @@ impl UseColours { impl ColourScale { fn deduce(matches: &Opts) -> Self { - if matches.color_scale > 0 { + if matches.color_scale > 0 || matches.colour_scale > 0{ return Self::Gradient; } Self::Fixed From 8ab0ee9821fd69a98f7fdb3993be983edd40cef2 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Tue, 12 Sep 2023 01:09:22 +0200 Subject: [PATCH 05/47] docs(parser): added dev documentation for the new parser --- src/options/README.md | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/options/README.md diff --git a/src/options/README.md b/src/options/README.md new file mode 100644 index 000000000..213bfa359 --- /dev/null +++ b/src/options/README.md @@ -0,0 +1,45 @@ +# Options Parser - Dev Documentation + +## How to add an argument to the parser + +### Different types of arguments + +If your argument does not take a value please use this syntax: +```rust +///Description of the option +#[arg(short ='', long, action = clap::ArgAction::Count)] +pub : u8; +``` + +If Your argument takes a value please use this syntax instead: +```rust +///Description of the option +#[arg(short ='', long)] +pub : Option; +``` + +Please also change the different [completions](../../completions/) to add your argument too + +For any other more complex usage please refer to [Clap documentation on arguments](https://docs.rs/clap/latest/clap/struct.Arg.html#) (please remeber that anything shown in this can be use even tho we using the derive version) + +### Creating an argument verification function + +If you are adding to an exesting type find the corresponding `deduce` impl + +If you are creating your type, first describe it in the right other directory, +then you need to create a deduce impl in the options following those two cases: + +First case no environnement var needed: +```rust +pub fn deduce(matches: &Opts) -> Result +``` + +Second case environnement var needed: +```rust +pub fn deduce(matches: &Opts, vars: V) -> Result +``` + +Please remeber to write test in the bottom of the file and to handle the strict mode if there is a necessity to it, for that just add `strictness: bool` to your function prototype + + +Then all you need to do is call your new deduce at the right place From 4bc90cc416454244d7d74d23e2f12992570fde9d Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Tue, 12 Sep 2023 10:15:43 +0200 Subject: [PATCH 06/47] chore(gitignore): renamed vscode to Visual Studio Code --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0b79cef98..13f2c0c9a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,5 @@ stage out.gif tests/tmp -#vscode +# Visual Studio Code .vscode From 85cdf34e0a74c9052fb0d36a566f5fcfa03331e0 Mon Sep 17 00:00:00 2001 From: MartinFillon <114775771+MartinFillon@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:17:51 +0200 Subject: [PATCH 07/47] docs(parser): fixed typos and syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Christina Sørensen Signed-off-by: MartinFillon <114775771+MartinFillon@users.noreply.github.com> --- src/options/README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/options/README.md b/src/options/README.md index 213bfa359..eecdbd9fd 100644 --- a/src/options/README.md +++ b/src/options/README.md @@ -1,45 +1,48 @@ # Options Parser - Dev Documentation -## How to add an argument to the parser +## How to Add an Argument to the Parser -### Different types of arguments +### Different Types of Arguments If your argument does not take a value please use this syntax: + ```rust -///Description of the option +/// Description of the option #[arg(short ='', long, action = clap::ArgAction::Count)] pub : u8; ``` -If Your argument takes a value please use this syntax instead: +If your argument takes a value please use this syntax instead: + ```rust -///Description of the option +/// Description of the option #[arg(short ='', long)] pub : Option; ``` Please also change the different [completions](../../completions/) to add your argument too -For any other more complex usage please refer to [Clap documentation on arguments](https://docs.rs/clap/latest/clap/struct.Arg.html#) (please remeber that anything shown in this can be use even tho we using the derive version) +For any other more complex usage please refer to [Clap documentation on arguments](https://docs.rs/clap/latest/clap/struct.Arg.html#) (please remember that anything shown in this can be use even tho we using the derive version) -### Creating an argument verification function +### Creating an Argument Verification Function -If you are adding to an exesting type find the corresponding `deduce` impl +If you are adding to an existing type find the corresponding `deduce` impl If you are creating your type, first describe it in the right other directory, then you need to create a deduce impl in the options following those two cases: -First case no environnement var needed: +First case no environment var needed: + ```rust pub fn deduce(matches: &Opts) -> Result ``` -Second case environnement var needed: +Second case environment var needed: + ```rust pub fn deduce(matches: &Opts, vars: V) -> Result ``` Please remeber to write test in the bottom of the file and to handle the strict mode if there is a necessity to it, for that just add `strictness: bool` to your function prototype - Then all you need to do is call your new deduce at the right place From 28f02c7ede54022765dbfc23b43edb0e9b24c85a Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Tue, 12 Sep 2023 10:24:02 +0200 Subject: [PATCH 08/47] chore: removed unuseful code --- src/main.rs | 1 - src/options/mod.rs | 41 ----------------------------------------- src/options/parser.rs | 2 -- 3 files changed, 44 deletions(-) diff --git a/src/main.rs b/src/main.rs index cd6d8f9c5..df01b8574 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,7 +88,6 @@ lazy_static! { }; } - fn main() { use std::process::exit; diff --git a/src/options/mod.rs b/src/options/mod.rs index bc4c9a3b4..6dbbcab08 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -150,44 +150,3 @@ impl Options { Ok(Self { dir_action, filter, view, theme }) } } - - -// #[cfg(test)] -// pub mod test { -// use crate::options::parser::{Arg, MatchedFlags}; -// use std::ffi::OsStr; - -// #[derive(PartialEq, Eq, Debug)] -// pub enum Strictnesses { -// Last, -// Complain, -// Both, -// } - -// /// This function gets used by the other testing modules. -// /// It can run with one or both strictness values: if told to run with -// /// both, then both should resolve to the same result. -// /// -// /// It returns a vector with one or two elements in. -// /// These elements can then be tested with `assert_eq` or what have you. -// pub fn parse_for_test(inputs: &[&str], args: &'static [&'static Arg], strictnesses: Strictnesses, get: F) -> Vec -// where F: Fn(&MatchedFlags<'_>) -> T -// { -// use self::Strictnesses::*; - -// let bits = inputs.iter().map(OsStr::new).collect::>(); -// let mut result = Vec::new(); - -// if strictnesses == Last || strictnesses == Both { -// let results = Args(args).parse(bits.clone(), Strictness::UseLastArguments); -// result.push(get(&results.unwrap().flags)); -// } - -// if strictnesses == Complain || strictnesses == Both { -// let results = Args(args).parse(bits, Strictness::ComplainAboutRedundantArguments); -// result.push(get(&results.unwrap().flags)); -// } - -// result -// } -// } diff --git a/src/options/parser.rs b/src/options/parser.rs index cb288ca36..c62d58067 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -33,7 +33,6 @@ pub struct Opts { /// display type indicator by file names. #[arg(short = 'F', long, action = clap::ArgAction::Count)] pub classify: u8, - /// #[arg(short = 'X', long, action = clap::ArgAction::Count)] pub dereference: u8, /// set screen width in columns. @@ -49,7 +48,6 @@ pub struct Opts { pub color_scale: u8, #[arg(long, action = clap::ArgAction::Count)] pub colour_scale: u8, - /// #[arg(short = 'A', long, action = clap::ArgAction::Count)] pub almost_all: u8, /// list directories as files; don't list their contents. From 65bb49454f756e7e5cb7ea9b8ea76503d1769d9e Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Tue, 12 Sep 2023 11:56:29 +0200 Subject: [PATCH 09/47] chore(parser): linting with clippy --- src/options/theme.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/options/theme.rs b/src/options/theme.rs index e46cb5108..6f78a8574 100644 --- a/src/options/theme.rs +++ b/src/options/theme.rs @@ -27,17 +27,11 @@ impl UseColours { None => Self::Automatic, }; - let color = match matches.color { - Some(ref w) => Some(w), - None => None, - }; - let colour = match matches.colour { - Some(ref w) => Some(w), - None => None, - }; - match (color, colour) { - (Some(ref w), None) => self::UseColours::get_color(w.to_string_lossy().to_string()), - (None, Some(ref w)) => self::UseColours::get_color(w.to_string_lossy().to_string()), + let color_us = matches.color.as_ref(); + let colour_en = matches.colour.as_ref(); + match (color_us, colour_en) { + (Some(w), None) => self::UseColours::get_color(w.to_string_lossy().to_string()), + (None, Some(w)) => self::UseColours::get_color(w.to_string_lossy().to_string()), (None, None) => Ok(default_value), (Some(_), Some(_)) => Err(OptionsError::BadArgument("--color".to_string(), "--colour".to_string())), } From 30c55817eb0fe1eef42a0ebf95fa634a24fd2677 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Wed, 13 Sep 2023 20:01:18 +0200 Subject: [PATCH 10/47] test(filter): add tests for the filter options --- src/options/filter.rs | 303 +++++++++++++++++++++++++++--------------- 1 file changed, 193 insertions(+), 110 deletions(-) diff --git a/src/options/filter.rs b/src/options/filter.rs index b32dcc7ff..5ac4f695f 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -212,114 +212,197 @@ impl GitIgnore { } } +#[cfg(test)] +mod test{ + use super::*; + + #[test] + fn deduce_git_ignore_ok() { + let opts = Opts { + git_ignore: 1, + ..Opts::default() + }; + assert_eq!(GitIgnore::deduce(&opts).unwrap(), GitIgnore::CheckAndIgnore); + } + + #[test] + fn deduce_git_ignore_conflict() { + let opts = Opts { + git_ignore: 1, + no_git: 1, + ..Opts::default() + }; + assert_eq!(GitIgnore::deduce(&opts).unwrap_err(), OptionsError::Conflict("GIT_IGNORE".to_string(), "NO_GIT".to_string())); + } + + #[test] + fn deduce_ignore_patterns() { + let opts = Opts { + ..Opts::default() + }; + assert_eq!(IgnorePatterns::deduce(&opts).unwrap(), IgnorePatterns::empty()); + } + + #[test] + fn deduce_dot_filter_just_files() { + let opts = Opts { + ..Opts::default() + }; + assert_eq!(DotFilter::deduce(&opts, false).unwrap(), DotFilter::JustFiles); + } + + #[test] + fn deduce_dot_filter_dotfiles() { + let opts = Opts { + all: 1, + ..Opts::default() + }; + assert_eq!(DotFilter::deduce(&opts, false).unwrap(), DotFilter::Dotfiles); + } + + #[test] + fn deduce_dot_filter_dotfiles_and_dots() { + let opts = Opts { + all: 2, + ..Opts::default() + }; + assert_eq!(DotFilter::deduce(&opts, false).unwrap(), DotFilter::DotfilesAndDots); + } + + #[test] + fn deduce_dot_filter_tree_all_all() { + let opts = Opts { + all: 2, + tree: 1, + ..Opts::default() + }; + assert_eq!(DotFilter::deduce(&opts, false).unwrap_err(), OptionsError::TreeAllAll); + } + + #[test] + fn deduce_dot_filter_all_all() { + let opts = Opts { + all: 2, + ..Opts::default() + }; + assert_eq!(DotFilter::deduce(&opts, true).unwrap_err(), OptionsError::Conflict("ALL".to_string(), "ALL".to_string())); + } + + #[test] + fn deduce_sort_field_name() { + let opts = Opts { + sort: Some("name".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::Name(SortCase::AaBbCc)); + } + + #[test] + fn deduce_sort_field_name_mix_hidden() { + let opts = Opts { + sort: Some(".name".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::NameMixHidden(SortCase::AaBbCc)); + } + + #[test] + fn deduce_sort_field_size() { + let opts = Opts { + sort: Some("size".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::Size); + } -// #[cfg(test)] -// mod test { -// use super::*; -// use std::ffi::OsString; -// use crate::options::flags; -// use crate::options::parser::Flag; - -// macro_rules! test { -// ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => { -// #[test] -// fn $name() { -// use crate::options::parser::Arg; -// use crate::options::test::parse_for_test; -// use crate::options::test::Strictnesses::*; - -// static TEST_ARGS: &[&Arg] = &[ &flags::SORT, &flags::ALL, &flags::ALMOST_ALL, &flags::TREE, &flags::IGNORE_GLOB, &flags::GIT_IGNORE ]; -// for result in parse_for_test($inputs.as_ref(), TEST_ARGS, $stricts, |mf| $type::deduce(mf)) { -// assert_eq!(result, $result); -// } -// } -// }; -// } - -// mod sort_fields { -// use super::*; - -// // Default behaviour -// test!(empty: SortField <- []; Both => Ok(SortField::default())); - -// // Sort field arguments -// test!(one_arg: SortField <- ["--sort=mod"]; Both => Ok(SortField::ModifiedDate)); -// test!(one_long: SortField <- ["--sort=size"]; Both => Ok(SortField::Size)); -// test!(one_short: SortField <- ["-saccessed"]; Both => Ok(SortField::AccessedDate)); -// test!(lowercase: SortField <- ["--sort", "name"]; Both => Ok(SortField::Name(SortCase::AaBbCc))); -// test!(uppercase: SortField <- ["--sort", "Name"]; Both => Ok(SortField::Name(SortCase::ABCabc))); -// test!(old: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedDate)); -// test!(oldest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedDate)); -// test!(new: SortField <- ["--sort", "old"]; Both => Ok(SortField::ModifiedAge)); -// test!(newest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedAge)); -// test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge)); - -// test!(mix_hidden_lowercase: SortField <- ["--sort", ".name"]; Both => Ok(SortField::NameMixHidden(SortCase::AaBbCc))); -// test!(mix_hidden_uppercase: SortField <- ["--sort", ".Name"]; Both => Ok(SortField::NameMixHidden(SortCase::ABCabc))); - -// // Errors -// test!(error: SortField <- ["--sort=colour"]; Both => Err(OptionsError::BadArgument(&flags::SORT, OsString::from("colour")))); - -// // Overriding -// test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate)); -// test!(overridden_2: SortField <- ["--sort", "none", "--sort=Extension"]; Last => Ok(SortField::Extension(SortCase::ABCabc))); -// test!(overridden_3: SortField <- ["--sort=cr", "--sort", "mod"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); -// test!(overridden_4: SortField <- ["--sort", "none", "--sort=Extension"]; Complain => Err(OptionsError::Duplicate(Flag::Long("sort"), Flag::Long("sort")))); -// } - - -// mod dot_filters { -// use super::*; - -// // Default behaviour -// test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles)); - -// // --all -// test!(all: DotFilter <- ["--all"]; Both => Ok(DotFilter::Dotfiles)); -// test!(all_all: DotFilter <- ["--all", "-a"]; Both => Ok(DotFilter::DotfilesAndDots)); -// test!(all_all_2: DotFilter <- ["-aa"]; Both => Ok(DotFilter::DotfilesAndDots)); - -// test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots)); -// test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))); - -// // --all and --tree -// test!(tree_a: DotFilter <- ["-Ta"]; Both => Ok(DotFilter::Dotfiles)); -// test!(tree_aa: DotFilter <- ["-Taa"]; Both => Err(OptionsError::TreeAllAll)); -// test!(tree_aaa: DotFilter <- ["-Taaa"]; Both => Err(OptionsError::TreeAllAll)); - -// // --almost-all -// test!(almost_all: DotFilter <- ["--almost-all"]; Both => Ok(DotFilter::Dotfiles)); -// test!(almost_all_all: DotFilter <- ["-Aa"]; Both => Ok(DotFilter::Dotfiles)); -// test!(almost_all_all_2: DotFilter <- ["-Aaa"]; Both => Ok(DotFilter::DotfilesAndDots)); -// } - - -// mod ignore_patterns { -// use super::*; -// use std::iter::FromIterator; - -// fn pat(string: &'static str) -> glob::Pattern { -// glob::Pattern::new(string).unwrap() -// } - -// // Various numbers of globs -// test!(none: IgnorePatterns <- []; Both => Ok(IgnorePatterns::empty())); -// test!(one: IgnorePatterns <- ["--ignore-glob", "*.ogg"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg") ]))); -// test!(two: IgnorePatterns <- ["--ignore-glob=*.ogg|*.MP3"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*.ogg"), pat("*.MP3") ]))); -// test!(loads: IgnorePatterns <- ["-I*|?|.|*"]; Both => Ok(IgnorePatterns::from_iter(vec![ pat("*"), pat("?"), pat("."), pat("*") ]))); - -// // Overriding -// test!(overridden: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.mp3") ]))); -// test!(overridden_2: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Last => Ok(IgnorePatterns::from_iter(vec![ pat("*.MP3") ]))); -// test!(overridden_3: IgnorePatterns <- ["-I=*.ogg", "-I", "*.mp3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); -// test!(overridden_4: IgnorePatterns <- ["-I", "*.OGG", "-I*.MP3"]; Complain => Err(OptionsError::Duplicate(Flag::Short(b'I'), Flag::Short(b'I')))); -// } - - -// mod git_ignores { -// use super::*; - -// test!(off: GitIgnore <- []; Both => Ok(GitIgnore::Off)); -// test!(on: GitIgnore <- ["--git-ignore"]; Both => Ok(GitIgnore::CheckAndIgnore)); -// } -// } + #[test] + fn deduce_sort_field_extension() { + let opts = Opts { + sort: Some("ext".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::Extension(SortCase::AaBbCc)); + } + + #[test] + fn deduce_sort_field_modified_date() { + let opts = Opts { + sort: Some("date".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::ModifiedDate); + } + + #[test] + fn deduce_sort_field_modified_age() { + let opts = Opts { + sort: Some("age".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::ModifiedAge); + } + + #[test] + fn deduce_sort_field_changed_date() { + let opts = Opts { + sort: Some("ch".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::ChangedDate); + } + + #[test] + fn deduce_sort_field_accessed_date() { + let opts = Opts { + sort: Some("acc".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::AccessedDate); + } + + #[test] + fn deduce_sort_field_created_date() { + let opts = Opts { + sort: Some("cr".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::CreatedDate); + } + + #[cfg(unix)] + #[test] + fn deduce_sort_field_inode() { + let opts = Opts { + sort: Some("inode".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::FileInode); + } + + #[test] + fn deduce_sort_field_file_type() { + let opts = Opts { + sort: Some("type".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::FileType); + } + + #[test] + fn deduce_sort_field_unsorted() { + let opts = Opts { + sort: Some("none".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::Unsorted); + } + + #[test] + fn deduce_sort_field_bad_argument() { + let opts = Opts { + sort: Some("bad".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap_err(), OptionsError::BadArgument("SORT".to_string(), "bad".to_string())); + } +} From 0a67ab8aa3ffd6ec48a13220a1f6dd93f97daf9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Thu, 14 Sep 2023 11:17:14 +0200 Subject: [PATCH 11/47] fix(octal): ensure --octal-permissions backwards compatability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- src/options/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options/parser.rs b/src/options/parser.rs index c62d58067..c9746bee5 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -147,7 +147,7 @@ pub struct Opts { #[arg(long = "git-repos-no-status", action = clap::ArgAction::Count)] pub git_repos_no_status: u8, /// list each file's permission in octal format. - #[arg(short = 'o', long, action = clap::ArgAction::Count)] + #[arg(short = 'o', long, alias = "octal-permission", alias = "octal-permissions", action = clap::ArgAction::Count)] pub octal: u8, /// Display the number of hard links to file. #[arg(short = 'Z', long = "context", action = clap::ArgAction::Count)] From 2412b50039d72efd5ab014094856d10898b0c8c2 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Sat, 16 Sep 2023 20:32:14 +0200 Subject: [PATCH 12/47] chore: fixed speeling issues --- src/options/dir_action.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/options/dir_action.rs b/src/options/dir_action.rs index 5de736959..f0b355a85 100644 --- a/src/options/dir_action.rs +++ b/src/options/dir_action.rs @@ -20,10 +20,10 @@ impl DirAction { if strictness { // Early check for --level when it wouldn’t do anything if ! recurse && ! tree && matches.level.is_some() { - return Err(OptionsError::Useless2("--level".to_string(), "--recurce".to_string(), "--tree".to_string())); + return Err(OptionsError::Useless2("--level".to_string(), "--recurse".to_string(), "--tree".to_string())); } else if recurse && as_file { - return Err(OptionsError::Conflict("--recurce".to_string(), "--list-dirs".to_string())); + return Err(OptionsError::Conflict("--recurse".to_string(), "--list-dirs".to_string())); } else if tree && as_file { return Err(OptionsError::Conflict("--tree".to_string(), "--list-dirs".to_string())); From 71ea205972a356d0bc7597ed62445d8f1f681fac Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Mon, 25 Sep 2023 11:47:49 +0200 Subject: [PATCH 13/47] refactor: applied scalafmt and updated the branch Applied scalafmt as per the pr that was merged. Added only-file that was missing from the new parser. --- src/main.rs | 26 +++++-- src/options/dir_action.rs | 83 +++++++++++++-------- src/options/error.rs | 28 ++++--- src/options/file_name.rs | 16 ++-- src/options/filter.rs | 117 +++++++++++++++++++---------- src/options/flags.rs | 94 ----------------------- src/options/help.rs | 152 -------------------------------------- src/options/mod.rs | 19 +++-- src/options/parser.rs | 8 +- src/options/theme.rs | 19 +++-- src/options/version.rs | 57 -------------- src/options/view.rs | 7 +- 12 files changed, 199 insertions(+), 427 deletions(-) delete mode 100644 src/options/flags.rs delete mode 100644 src/options/help.rs delete mode 100644 src/options/version.rs diff --git a/src/main.rs b/src/main.rs index 28cf30595..5e425fe28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,11 +21,11 @@ #![allow(clippy::upper_case_acronyms)] #![allow(clippy::wildcard_imports)] +use clap::Parser; use std::env; use std::ffi::{OsStr, OsString}; use std::io::{self, ErrorKind, Write}; use std::path::{Component, PathBuf}; -use clap::Parser; use std::process::exit; use ansiterm::{ANSIStrings, Style}; @@ -34,10 +34,11 @@ use log::*; use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; -use crate::options::{Options, Vars, vars}; -use crate::output::{escape, lines, grid, grid_details, details, View, Mode}; -use crate::theme::Theme; +use crate::fs::{Dir, File}; use crate::options::parser::Opts; +use crate::options::{vars, Options, Vars}; +use crate::output::{details, escape, grid, grid_details, lines, Mode, View}; +use crate::theme::Theme; mod fs; mod info; @@ -62,10 +63,10 @@ fn main() { let cli = Opts::parse(); let mut input_paths: Vec<&OsStr> = cli.paths.iter().map(OsString::as_os_str).collect(); if input_paths.is_empty() { - input_paths.push(OsStr::new(".")); + input_paths.push(OsStr::new(".")); } let options = match Options::deduce(&cli, &LiveVars) { - Ok(o) => {o}, + Ok(o) => o, Err(e) => { eprintln!("{e}"); exit(exits::OPTIONS_ERROR); @@ -76,8 +77,17 @@ fn main() { let writer = io::stdout(); let console_width = options.view.width.actual_terminal_width(); - let theme = options.theme.to_theme(terminal_size::terminal_size().is_some()); - let exa = Exa { options, writer, input_paths, theme, console_width, git }; + let theme = options + .theme + .to_theme(terminal_size::terminal_size().is_some()); + let exa = Exa { + options, + writer, + input_paths, + theme, + console_width, + git, + }; match exa.run() { Ok(exit_status) => { diff --git a/src/options/dir_action.rs b/src/options/dir_action.rs index a780d68cf..edd97e1b1 100644 --- a/src/options/dir_action.rs +++ b/src/options/dir_action.rs @@ -12,19 +12,27 @@ impl DirAction { /// to both be present, but the `--list-dirs` flag is used separately. pub fn deduce(matches: &Opts, can_tree: bool, strictness: bool) -> Result { let recurse = matches.recurse > 0; - let as_file = matches.list_dirs >0; - let tree = matches.tree > 0; + let as_file = matches.list_dirs > 0; + let tree = matches.tree > 0; if strictness { // Early check for --level when it wouldn’t do anything - if ! recurse && ! tree && matches.level.is_some() { - return Err(OptionsError::Useless2("--level".to_string(), "--recurse".to_string(), "--tree".to_string())); - } - else if recurse && as_file { - return Err(OptionsError::Conflict("--recurse".to_string(), "--list-dirs".to_string())); - } - else if tree && as_file { - return Err(OptionsError::Conflict("--tree".to_string(), "--list-dirs".to_string())); + if !recurse && !tree && matches.level.is_some() { + return Err(OptionsError::Useless2( + "--level".to_string(), + "--recurse".to_string(), + "--tree".to_string(), + )); + } else if recurse && as_file { + return Err(OptionsError::Conflict( + "--recurse".to_string(), + "--list-dirs".to_string(), + )); + } else if tree && as_file { + return Err(OptionsError::Conflict( + "--tree".to_string(), + "--list-dirs".to_string(), + )); } } @@ -32,11 +40,9 @@ impl DirAction { // Tree is only appropriate in details mode, so this has to // examine the View, which should have already been deduced by now Ok(Self::Recurse(RecurseOptions::deduce(matches, true))) - } - else if recurse { + } else if recurse { Ok(Self::Recurse(RecurseOptions::deduce(matches, false))) - } - else if as_file { + } else if as_file { Ok(Self::AsFile) } else { Ok(Self::List) @@ -52,7 +58,7 @@ impl RecurseOptions { pub fn deduce(matches: &Opts, tree: bool) -> Self { Self { tree, - max_depth: matches.level + max_depth: matches.level, } } } @@ -63,11 +69,12 @@ mod test { #[test] fn deduces_list() { - let matches = Opts { - ..Opts::default() - }; + let matches = Opts { ..Opts::default() }; - assert_eq!(DirAction::deduce(&matches, false, false).unwrap(), DirAction::List); + assert_eq!( + DirAction::deduce(&matches, false, false).unwrap(), + DirAction::List + ); } #[test] @@ -76,10 +83,13 @@ mod test { recurse: 1, ..Opts::default() }; - assert_eq!(DirAction::deduce(&matches, false, false).unwrap(), DirAction::Recurse(RecurseOptions { - tree: false, - max_depth: None, - })); + assert_eq!( + DirAction::deduce(&matches, false, false).unwrap(), + DirAction::Recurse(RecurseOptions { + tree: false, + max_depth: None, + }) + ); } #[test] @@ -88,10 +98,13 @@ mod test { tree: 1, ..Opts::default() }; - assert_eq!(DirAction::deduce(&matches, true, false).unwrap(), DirAction::Recurse(RecurseOptions { - tree: true, - max_depth: None, - })); + assert_eq!( + DirAction::deduce(&matches, true, false).unwrap(), + DirAction::Recurse(RecurseOptions { + tree: true, + max_depth: None, + }) + ); } #[test] @@ -100,7 +113,10 @@ mod test { list_dirs: 1, ..Opts::default() }; - assert_eq!(DirAction::deduce(&matches, false, false).unwrap(), DirAction::AsFile); + assert_eq!( + DirAction::deduce(&matches, false, false).unwrap(), + DirAction::AsFile + ); } #[test] @@ -144,9 +160,12 @@ mod test { ..Opts::default() }; - assert_eq!(DirAction::deduce(&matches, true, false).unwrap(), DirAction::Recurse(RecurseOptions { - tree: true, - max_depth: Some(2), - })); + assert_eq!( + DirAction::deduce(&matches, true, false).unwrap(), + DirAction::Recurse(RecurseOptions { + tree: true, + max_depth: Some(2), + }) + ); } } diff --git a/src/options/error.rs b/src/options/error.rs index d5079ccfe..83656f755 100644 --- a/src/options/error.rs +++ b/src/options/error.rs @@ -1,11 +1,9 @@ use std::fmt; use std::num::ParseIntError; - /// Something wrong with the combination of options the user has picked. #[derive(PartialEq, Eq, Debug)] pub enum OptionsError { - /// The user supplied an illegal choice to an Argument. BadArgument(String, String), @@ -57,28 +55,28 @@ impl From for OptionsError { } } - impl fmt::Display for OptionsError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BadArgument(arg, attempt) => { write!(f, "Argument {arg} doesn’t take {attempt}") } - Self::Unsupported(e) => write!(f, "{e}"), - Self::Conflict(a, b) => write!(f, "Option {a} conflicts with option {b}"), - Self::Duplicate(a, b) if a == b => write!(f, "Flag {a} was given twice"), - Self::Duplicate(a, b) => write!(f, "Flag {a} conflicts with flag {b}"), - Self::Useless(a, false, b) => write!(f, "Option {a} is useless without option {b}"), - Self::Useless(a, true, b) => write!(f, "Option {a} is useless given option {b}"), - Self::Useless2(a, b1, b2) => write!(f, "Option {a} is useless without options {b1} or {b2}"), - Self::TreeAllAll => write!(f, "Option --tree is useless given --all --all"), - Self::FailedParse(s, n, e) => write!(f, "Value {s:?} not valid for {n}: {e}"), - Self::FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {e}"), - }; + Self::Unsupported(e) => write!(f, "{e}"), + Self::Conflict(a, b) => write!(f, "Option {a} conflicts with option {b}"), + Self::Duplicate(a, b) if a == b => write!(f, "Flag {a} was given twice"), + Self::Duplicate(a, b) => write!(f, "Flag {a} conflicts with flag {b}"), + Self::Useless(a, false, b) => write!(f, "Option {a} is useless without option {b}"), + Self::Useless(a, true, b) => write!(f, "Option {a} is useless given option {b}"), + Self::Useless2(a, b1, b2) => { + write!(f, "Option {a} is useless without options {b1} or {b2}") + } + Self::TreeAllAll => write!(f, "Option --tree is useless given --all --all"), + Self::FailedParse(s, n, e) => write!(f, "Value {s:?} not valid for {n}: {e}"), + Self::FailedGlobPattern(ref e) => write!(f, "Failed to parse glob pattern: {e}"), + } } } - /// A list of legal choices for an argument-taking option. #[derive(PartialEq, Eq, Debug)] pub struct Choices(pub &'static [&'static str]); diff --git a/src/options/file_name.rs b/src/options/file_name.rs index 08b5a2eb9..e7407183e 100644 --- a/src/options/file_name.rs +++ b/src/options/file_name.rs @@ -1,10 +1,8 @@ -use crate::options::{OptionsError, NumberSource}; use crate::options::vars::{self, Vars}; -use crate::options::{flags, NumberSource, OptionsError}; +use crate::options::{NumberSource, OptionsError}; -use crate::output::file_name::{Options, Classify, ShowIcons, EmbedHyperlinks}; use crate::options::parser::Opts; - +use crate::output::file_name::{Classify, EmbedHyperlinks, Options, ShowIcons}; impl Options { pub fn deduce(matches: &Opts, vars: &V) -> Result { @@ -65,12 +63,14 @@ mod test { #[test] fn deduce_hyperlinks() { - assert_eq!(EmbedHyperlinks::deduce(&Opts::default()), EmbedHyperlinks::Off); + assert_eq!( + EmbedHyperlinks::deduce(&Opts::default()), + EmbedHyperlinks::Off + ); } #[test] fn deduce_hyperlinks_on() { - let matches = Opts { hyperlink: 1, ..Opts::default() @@ -91,9 +91,7 @@ mod test { #[test] fn deduce_classify_no_classify() { - let matches = Opts { - ..Opts::default() - }; + let matches = Opts { ..Opts::default() }; assert_eq!(Classify::deduce(&matches), Classify::JustFilenames); } diff --git a/src/options/filter.rs b/src/options/filter.rs index 588656fd5..66f515236 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -9,19 +9,29 @@ use crate::options::OptionsError; use super::parser::Opts; - impl FileFilter { /// Determines which of all the file filter options to use. pub fn deduce(matches: &Opts, strictness: bool) -> Result { + let mut filter_flags: Vec = vec![]; + + for (has, flags) in &[ + (matches.only_dirs > 0, FileFilterFlags::OnlyDirs), + (matches.only_files > 0, FileFilterFlags::OnlyFiles), + (matches.reverse > 0, FileFilterFlags::Reverse), + ] { + if *has { + filter_flags.push(flags.clone()); + } + } + Ok(Self { - list_dirs_first: matches.dirs_first > 0, - reverse: matches.reverse > 0, - only_dirs: matches.only_dirs > 0, - sort_field: SortField::deduce(matches)?, - dot_filter: DotFilter::deduce(matches, strictness)?, - ignore_patterns: IgnorePatterns::deduce(matches)?, - git_ignore: GitIgnore::deduce(matches)?, - }); + list_dirs_first: matches.dirs_first > 0, + flags: filter_flags, + sort_field: SortField::deduce(matches)?, + dot_filter: DotFilter::deduce(matches, strictness)?, + ignore_patterns: IgnorePatterns::deduce(matches)?, + git_ignore: GitIgnore::deduce(matches)?, + }) } } @@ -131,13 +141,15 @@ impl DotFilter { // either a single --all or at least one --almost-all is given (1 | 0, _) => Ok(Self::Dotfiles), // more than one --all - (_, _) => if matches.tree > 0 { - Err(OptionsError::TreeAllAll) - } else if strictness { - Err(OptionsError::Conflict("ALL".to_string(), "ALL".to_string())) - } else { - Ok(Self::DotfilesAndDots) - }, + (_, _) => { + if matches.tree > 0 { + Err(OptionsError::TreeAllAll) + } else if strictness { + Err(OptionsError::Conflict("ALL".to_string(), "ALL".to_string())) + } else { + Ok(Self::DotfilesAndDots) + } + } } } } @@ -147,7 +159,6 @@ impl IgnorePatterns { /// `--ignore-glob` argument’s value. This is a list of strings /// separated by pipe (`|`) characters, given in any order. pub fn deduce(matches: &Opts) -> Result { - // If there are no inputs, we return a set of patterns that doesn’t // match anything, rather than, say, `None`. let Some(ref inputs) = matches.ignore_glob else { return Ok(Self::empty()) }; @@ -170,14 +181,17 @@ impl GitIgnore { if matches.git_ignore > 0 && matches.no_git == 0 { return Ok(Self::CheckAndIgnore); } else if matches.git_ignore > 0 && matches.no_git > 0 { - return Err(OptionsError::Conflict("GIT_IGNORE".to_string(), "NO_GIT".to_string())); + return Err(OptionsError::Conflict( + "GIT_IGNORE".to_string(), + "NO_GIT".to_string(), + )); } Ok(Self::Off) } } #[cfg(test)] -mod test{ +mod test { use super::*; #[test] @@ -196,23 +210,28 @@ mod test{ no_git: 1, ..Opts::default() }; - assert_eq!(GitIgnore::deduce(&opts).unwrap_err(), OptionsError::Conflict("GIT_IGNORE".to_string(), "NO_GIT".to_string())); + assert_eq!( + GitIgnore::deduce(&opts).unwrap_err(), + OptionsError::Conflict("GIT_IGNORE".to_string(), "NO_GIT".to_string()) + ); } #[test] fn deduce_ignore_patterns() { - let opts = Opts { - ..Opts::default() - }; - assert_eq!(IgnorePatterns::deduce(&opts).unwrap(), IgnorePatterns::empty()); + let opts = Opts { ..Opts::default() }; + assert_eq!( + IgnorePatterns::deduce(&opts).unwrap(), + IgnorePatterns::empty() + ); } #[test] fn deduce_dot_filter_just_files() { - let opts = Opts { - ..Opts::default() - }; - assert_eq!(DotFilter::deduce(&opts, false).unwrap(), DotFilter::JustFiles); + let opts = Opts { ..Opts::default() }; + assert_eq!( + DotFilter::deduce(&opts, false).unwrap(), + DotFilter::JustFiles + ); } #[test] @@ -221,7 +240,10 @@ mod test{ all: 1, ..Opts::default() }; - assert_eq!(DotFilter::deduce(&opts, false).unwrap(), DotFilter::Dotfiles); + assert_eq!( + DotFilter::deduce(&opts, false).unwrap(), + DotFilter::Dotfiles + ); } #[test] @@ -230,7 +252,10 @@ mod test{ all: 2, ..Opts::default() }; - assert_eq!(DotFilter::deduce(&opts, false).unwrap(), DotFilter::DotfilesAndDots); + assert_eq!( + DotFilter::deduce(&opts, false).unwrap(), + DotFilter::DotfilesAndDots + ); } #[test] @@ -240,7 +265,10 @@ mod test{ tree: 1, ..Opts::default() }; - assert_eq!(DotFilter::deduce(&opts, false).unwrap_err(), OptionsError::TreeAllAll); + assert_eq!( + DotFilter::deduce(&opts, false).unwrap_err(), + OptionsError::TreeAllAll + ); } #[test] @@ -249,16 +277,22 @@ mod test{ all: 2, ..Opts::default() }; - assert_eq!(DotFilter::deduce(&opts, true).unwrap_err(), OptionsError::Conflict("ALL".to_string(), "ALL".to_string())); + assert_eq!( + DotFilter::deduce(&opts, true).unwrap_err(), + OptionsError::Conflict("ALL".to_string(), "ALL".to_string()) + ); } - + #[test] fn deduce_sort_field_name() { let opts = Opts { sort: Some("name".into()), ..Opts::default() }; - assert_eq!(SortField::deduce(&opts).unwrap(), SortField::Name(SortCase::AaBbCc)); + assert_eq!( + SortField::deduce(&opts).unwrap(), + SortField::Name(SortCase::AaBbCc) + ); } #[test] @@ -267,7 +301,10 @@ mod test{ sort: Some(".name".into()), ..Opts::default() }; - assert_eq!(SortField::deduce(&opts).unwrap(), SortField::NameMixHidden(SortCase::AaBbCc)); + assert_eq!( + SortField::deduce(&opts).unwrap(), + SortField::NameMixHidden(SortCase::AaBbCc) + ); } #[test] @@ -285,7 +322,10 @@ mod test{ sort: Some("ext".into()), ..Opts::default() }; - assert_eq!(SortField::deduce(&opts).unwrap(), SortField::Extension(SortCase::AaBbCc)); + assert_eq!( + SortField::deduce(&opts).unwrap(), + SortField::Extension(SortCase::AaBbCc) + ); } #[test] @@ -367,6 +407,9 @@ mod test{ sort: Some("bad".into()), ..Opts::default() }; - assert_eq!(SortField::deduce(&opts).unwrap_err(), OptionsError::BadArgument("SORT".to_string(), "bad".to_string())); + assert_eq!( + SortField::deduce(&opts).unwrap_err(), + OptionsError::BadArgument("SORT".to_string(), "bad".to_string()) + ); } } diff --git a/src/options/flags.rs b/src/options/flags.rs deleted file mode 100644 index 74f18098d..000000000 --- a/src/options/flags.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::options::parser::{Arg, Args, TakesValue, Values}; - -// exa options -pub static VERSION: Arg = Arg { short: Some(b'v'), long: "version", takes_value: TakesValue::Forbidden }; -pub static HELP: Arg = Arg { short: Some(b'?'), long: "help", takes_value: TakesValue::Forbidden }; - -// display options -pub static ONE_LINE: Arg = Arg { short: Some(b'1'), long: "oneline", takes_value: TakesValue::Forbidden }; -pub static LONG: Arg = Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }; -pub static GRID: Arg = Arg { short: Some(b'G'), long: "grid", takes_value: TakesValue::Forbidden }; -pub static ACROSS: Arg = Arg { short: Some(b'x'), long: "across", takes_value: TakesValue::Forbidden }; -pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_value: TakesValue::Forbidden }; -pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", takes_value: TakesValue::Forbidden }; -pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden }; -pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden }; -pub static WIDTH: Arg = Arg { short: Some(b'w'), long: "width", takes_value: TakesValue::Necessary(None) }; - -pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary(Some(COLOURS)) }; -pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary(Some(COLOURS)) }; -const COLOURS: &[&str] = &["always", "auto", "never"]; - -pub static COLOR_SCALE: Arg = Arg { short: None, long: "color-scale", takes_value: TakesValue::Forbidden }; -pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_value: TakesValue::Forbidden }; - -// filtering and sorting options -pub static ALL: Arg = Arg { short: Some(b'a'), long: "all", takes_value: TakesValue::Forbidden }; -pub static ALMOST_ALL: Arg = Arg { short: Some(b'A'), long: "almost-all", takes_value: TakesValue::Forbidden }; -pub static LIST_DIRS: Arg = Arg { short: Some(b'd'), long: "list-dirs", takes_value: TakesValue::Forbidden }; -pub static LEVEL: Arg = Arg { short: Some(b'L'), long: "level", takes_value: TakesValue::Necessary(None) }; -pub static REVERSE: Arg = Arg { short: Some(b'r'), long: "reverse", takes_value: TakesValue::Forbidden }; -pub static SORT: Arg = Arg { short: Some(b's'), long: "sort", takes_value: TakesValue::Necessary(Some(SORTS)) }; -pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary(None) }; -pub static GIT_IGNORE: Arg = Arg { short: None, long: "git-ignore", takes_value: TakesValue::Forbidden }; -pub static DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden }; -pub static ONLY_DIRS: Arg = Arg { short: Some(b'D'), long: "only-dirs", takes_value: TakesValue::Forbidden }; -pub static ONLY_FILES: Arg = Arg { short: Some(b'f'), long: "only-files", takes_value: TakesValue::Forbidden }; -const SORTS: Values = &[ "name", "Name", "size", "extension", - "Extension", "modified", "changed", "accessed", - "created", "inode", "type", "none" ]; - -// display options -pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden }; -pub static BYTES: Arg = Arg { short: Some(b'B'), long: "bytes", takes_value: TakesValue::Forbidden }; -pub static GROUP: Arg = Arg { short: Some(b'g'), long: "group", takes_value: TakesValue::Forbidden }; -pub static NUMERIC: Arg = Arg { short: Some(b'n'), long: "numeric", takes_value: TakesValue::Forbidden }; -pub static HEADER: Arg = Arg { short: Some(b'h'), long: "header", takes_value: TakesValue::Forbidden }; -pub static ICONS: Arg = Arg { short: None, long: "icons", takes_value: TakesValue::Forbidden }; -pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_value: TakesValue::Forbidden }; -pub static LINKS: Arg = Arg { short: Some(b'H'), long: "links", takes_value: TakesValue::Forbidden }; -pub static MODIFIED: Arg = Arg { short: Some(b'm'), long: "modified", takes_value: TakesValue::Forbidden }; -pub static CHANGED: Arg = Arg { short: None, long: "changed", takes_value: TakesValue::Forbidden }; -pub static BLOCKSIZE: Arg = Arg { short: Some(b'S'), long: "blocksize", takes_value: TakesValue::Forbidden }; -pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary(Some(TIMES)) }; -pub static ACCESSED: Arg = Arg { short: Some(b'u'), long: "accessed", takes_value: TakesValue::Forbidden }; -pub static CREATED: Arg = Arg { short: Some(b'U'), long: "created", takes_value: TakesValue::Forbidden }; -pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) }; -pub static HYPERLINK: Arg = Arg { short: None, long: "hyperlink", takes_value: TakesValue::Forbidden }; -pub static MOUNTS: Arg = Arg { short: Some(b'M'), long: "mounts", takes_value: TakesValue::Forbidden }; -const TIMES: Values = &["modified", "changed", "accessed", "created"]; -const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso", "relative"]; - -// suppressing columns -pub static NO_PERMISSIONS: Arg = Arg { short: None, long: "no-permissions", takes_value: TakesValue::Forbidden }; -pub static NO_FILESIZE: Arg = Arg { short: None, long: "no-filesize", takes_value: TakesValue::Forbidden }; -pub static NO_USER: Arg = Arg { short: None, long: "no-user", takes_value: TakesValue::Forbidden }; -pub static NO_TIME: Arg = Arg { short: None, long: "no-time", takes_value: TakesValue::Forbidden }; -pub static NO_ICONS: Arg = Arg { short: None, long: "no-icons", takes_value: TakesValue::Forbidden }; - -// optional feature options -pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden }; -pub static NO_GIT: Arg = Arg { short: None, long: "no-git", takes_value: TakesValue::Forbidden }; -pub static GIT_REPOS: Arg = Arg { short: None, long: "git-repos", takes_value: TakesValue::Forbidden }; -pub static GIT_REPOS_NO_STAT: Arg = Arg { short: None, long: "git-repos-no-status", takes_value: TakesValue::Forbidden }; -pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", takes_value: TakesValue::Forbidden }; -pub static OCTAL: Arg = Arg { short: Some(b'o'), long: "octal-permissions", takes_value: TakesValue::Forbidden }; -pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", takes_value: TakesValue::Forbidden }; - - -pub static ALL_ARGS: Args = Args(&[ - &VERSION, &HELP, - - &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, &DEREF_LINKS, - &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, &WIDTH, - - &ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST, - &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, &ONLY_FILES, - - &BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED, - &BLOCKSIZE, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK, &MOUNTS, - &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS, - - &GIT, &NO_GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, - &EXTENDED, &OCTAL, &SECURITY_CONTEXT -]); diff --git a/src/options/help.rs b/src/options/help.rs deleted file mode 100644 index a511db067..000000000 --- a/src/options/help.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::fmt; - -use crate::fs::feature::xattr; -use crate::options::flags; -use crate::options::parser::MatchedFlags; - -static USAGE_PART1: &str = "Usage: - eza [options] [files...] - -META OPTIONS - -?, --help show list of command-line options - -v, --version show version of eza - -DISPLAY OPTIONS - -1, --oneline display one entry per line - -l, --long display extended file metadata as a table - -G, --grid display entries as a grid (default) - -x, --across sort the grid across, rather than downwards - -R, --recurse recurse into directories - -T, --tree recurse into directories as a tree - -F, --classify display type indicator by file names - --colo[u]r=WHEN when to use terminal colours (always, auto, never) - --colo[u]r-scale highlight levels of file sizes distinctly - --icons display icons - --no-icons don't display icons (always overrides --icons) - --hyperlink display entries as hyperlinks - -w, --width COLS set screen width in columns - -FILTERING AND SORTING OPTIONS - -a, --all show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories - -d, --list-dirs list directories as files; don't list their contents - -L, --level DEPTH limit the depth of recursion - -r, --reverse reverse the sort order - -s, --sort SORT_FIELD which field to sort by - --group-directories-first list directories before other files - -D, --only-dirs list only directories - -f, --only-files list only files - -I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore"; - -static GIT_FILTER_HELP: &str = " \ - --git-ignore ignore files mentioned in '.gitignore'"; - -static USAGE_PART2: &str = " \ - Valid sort fields: name, Name, extension, Extension, size, type, - modified, accessed, created, inode, and none. - date, time, old, and new all refer to modified. - -LONG VIEW OPTIONS - -b, --binary list file sizes with binary prefixes - -B, --bytes list file sizes in bytes, without any prefixes - -g, --group list each file's group - -h, --header add a header row to each column - -H, --links list each file's number of hard links - -i, --inode list each file's inode number - -m, --modified use the modified timestamp field - -M, --mounts show mount details (Linux and MacOS only) - -n, --numeric list numeric user and group IDs - -S, --blocksize show size of allocated file system blocks - -t, --time FIELD which timestamp field to list (modified, accessed, created) - -u, --accessed use the accessed timestamp field - -U, --created use the created timestamp field - --changed use the changed timestamp field - --time-style how to format timestamps (default, iso, long-iso, full-iso, relative) - --no-permissions suppress the permissions field - -o, --octal-permissions list each file's permission in octal format - --no-filesize suppress the filesize field - --no-user suppress the user field - --no-time suppress the time field"; - -static GIT_VIEW_HELP: &str = " \ - --git list each file's Git status, if tracked or ignored - --no-git suppress Git status (always overrides --git, --git-repos, --git-repos-no-status) - --git-repos list root of git-tree status"; -static EXTENDED_HELP: &str = " \ - -@, --extended list each file's extended attributes and sizes"; -static SECATTR_HELP: &str = " \ - -Z, --context list each file's security context"; - -/// All the information needed to display the help text, which depends -/// on which features are enabled and whether the user only wants to -/// see one section’s help. -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub struct HelpString; - -impl HelpString { - /// Determines how to show help, if at all, based on the user’s - /// command-line arguments. This one works backwards from the other - /// ‘deduce’ functions, returning Err if help needs to be shown. - /// - /// We don’t do any strict-mode error checking here: it’s OK to give - /// the --help or --long flags more than once. Actually checking for - /// errors when the user wants help is kind of petty! - pub fn deduce(matches: &MatchedFlags<'_>) -> Option { - if matches.count(&flags::HELP) > 0 { - Some(Self) - } else { - None - } - } -} - -impl fmt::Display for HelpString { - /// Format this help options into an actual string of help - /// text to be displayed to the user. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{USAGE_PART1}")?; - - if cfg!(feature = "git") { - write!(f, "\n{GIT_FILTER_HELP}")?; - } - - write!(f, "\n{USAGE_PART2}")?; - - if cfg!(feature = "git") { - write!(f, "\n{GIT_VIEW_HELP}")?; - } - - if xattr::ENABLED { - write!(f, "\n{EXTENDED_HELP}")?; - write!(f, "\n{SECATTR_HELP}")?; - } - - writeln!(f) - } -} - -#[cfg(test)] -mod test { - use crate::options::{Options, OptionsResult}; - use std::ffi::OsStr; - - #[test] - fn help() { - let args = vec![OsStr::new("--help")]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Help(_))); - } - - #[test] - fn help_with_file() { - let args = vec![OsStr::new("--help"), OsStr::new("me")]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Help(_))); - } - - #[test] - fn unhelpful() { - let args = vec![]; - let opts = Options::parse(args, &None); - assert!(!matches!(opts, OptionsResult::Help(_))) // no help when --help isn’t passed - } -} diff --git a/src/options/mod.rs b/src/options/mod.rs index 4e6362d91..a37f918cf 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -68,19 +68,18 @@ //! --grid --long` shouldn’t complain about `--long` being given twice when //! it’s clear what the user wants. - use crate::fs::dir_action::DirAction; use crate::fs::filter::{FileFilter, GitIgnore}; +use crate::options::parser::Opts; use crate::output::{details, grid_details, Mode, View}; use crate::theme::Options as ThemeOptions; -use crate::options::parser::Opts; mod dir_action; mod file_name; mod filter; +pub(crate) mod parser; mod theme; mod view; -pub(crate) mod parser; mod error; pub use self::error::{NumberSource, OptionsError}; @@ -110,7 +109,6 @@ pub struct Options { } impl Options { - /// Whether the View specified in this set of options includes a Git /// status column. It’s only worth trying to discover a repository if the /// results will end up being displayed. @@ -139,20 +137,21 @@ impl Options { /// Determines the complete set of options based on the given command-line /// arguments, after they’ve been parsed. pub fn deduce(matches: &Opts, vars: &V) -> Result { - if cfg!(not(feature = "git")) && - (matches.git > 0 || matches.git_ignore > 0) { + if cfg!(not(feature = "git")) && (matches.git > 0 || matches.git_ignore > 0) { return Err(OptionsError::Unsupported(String::from( "Options --git and --git-ignore can't be used because `git` feature was disabled in this build of exa" ))); } - let strictness = match vars.get(vars::EXA_STRICT) { - None => false, - Some(s) => ! s.is_empty() + let strictness = match (vars.get(vars::EXA_STRICT), vars.get(vars::EZA_STRICT)) { + (None, None) => false, + (Some(s), _) => !s.is_empty(), + (_, Some(s)) => !s.is_empty(), }; let view = View::deduce(matches, vars, strictness)?; - let dir_action = DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)), strictness)?; + let dir_action = + DirAction::deduce(matches, matches!(view.mode, Mode::Details(_)), strictness)?; let filter = FileFilter::deduce(matches, strictness)?; let theme = ThemeOptions::deduce(matches, vars)?; diff --git a/src/options/parser.rs b/src/options/parser.rs index c9746bee5..98038587e 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -155,12 +155,15 @@ pub struct Opts { /// Show extended attributes. #[arg(short = '@', long, action = clap::ArgAction::Count)] pub extended: u8, - /// show list of command-line options. + /// Show list of command-line options. #[arg(short ='?', long, action = clap::ArgAction::Help)] pub help: Option, - /// show mount details (Linux only) + /// Show mount details (Linux only) #[arg(short = 'M', long, action = clap::ArgAction::Count)] pub mount: u8, + /// Show only files + #[arg(short = 'f', long = "only-files", action = clap::ArgAction::Count)] + pub only_files: u8, } impl Opts { @@ -220,6 +223,7 @@ impl Opts { mount: 0, colour: None, colour_scale: 0, + only_files: 0, } } } diff --git a/src/options/theme.rs b/src/options/theme.rs index 2f6a12285..76b6e767a 100644 --- a/src/options/theme.rs +++ b/src/options/theme.rs @@ -1,9 +1,8 @@ -use crate::options::{vars, Vars, OptionsError}; -use crate::theme::{Options, UseColours, ColourScale, Definitions}; +use crate::options::{vars, OptionsError, Vars}; +use crate::theme::{ColourScale, Definitions, Options, UseColours}; use super::parser::Opts; - impl Options { pub fn deduce(matches: &Opts, vars: &V) -> Result { let use_colours = UseColours::deduce(matches, vars)?; @@ -36,7 +35,10 @@ impl UseColours { (Some(w), None) => self::UseColours::get_color(w.to_string_lossy().to_string()), (None, Some(w)) => self::UseColours::get_color(w.to_string_lossy().to_string()), (None, None) => Ok(default_value), - (Some(_), Some(_)) => Err(OptionsError::BadArgument("--color".to_string(), "--colour".to_string())), + (Some(_), Some(_)) => Err(OptionsError::BadArgument( + "--color".to_string(), + "--colour".to_string(), + )), } } @@ -47,8 +49,7 @@ impl UseColours { Ok(Self::Automatic) } else if word == "never" { Ok(Self::Never) - } - else { + } else { Err(OptionsError::BadArgument("--color".to_string(), word)) } } @@ -56,7 +57,7 @@ impl UseColours { impl ColourScale { fn deduce(matches: &Opts) -> Self { - if matches.color_scale > 0 || matches.colour_scale > 0{ + if matches.color_scale > 0 || matches.colour_scale > 0 { return Self::Gradient; } Self::Fixed @@ -81,9 +82,7 @@ mod tests { #[test] fn deduce_colour_scale() { - let matches = Opts { - ..Opts::default() - }; + let matches = Opts { ..Opts::default() }; assert_eq!(ColourScale::deduce(&matches), ColourScale::Fixed); } diff --git a/src/options/version.rs b/src/options/version.rs deleted file mode 100644 index 9c7a7bcc1..000000000 --- a/src/options/version.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Printing the version string. -//! -//! The code that works out which string to print is done in `build.rs`. - -use std::fmt; - -use crate::options::flags; -use crate::options::parser::MatchedFlags; - -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -pub struct VersionString; -// There were options here once, but there aren’t anymore! - -impl VersionString { - /// Determines how to show the version, if at all, based on the user’s - /// command-line arguments. This one works backwards from the other - /// ‘deduce’ functions, returning Err if help needs to be shown. - /// - /// Like --help, this doesn’t check for errors. - pub fn deduce(matches: &MatchedFlags<'_>) -> Option { - if matches.count(&flags::VERSION) > 0 { - Some(Self) - } else { - None - } - } -} - -impl fmt::Display for VersionString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!( - f, - "{}", - include_str!(concat!(env!("OUT_DIR"), "/version_string.txt")) - ) - } -} - -#[cfg(test)] -mod test { - use crate::options::{Options, OptionsResult}; - use std::ffi::OsStr; - - #[test] - fn version() { - let args = vec![OsStr::new("--version")]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Version(_))); - } - - #[test] - fn version_with_file() { - let args = vec![OsStr::new("--version"), OsStr::new("me")]; - let opts = Options::parse(args, &None); - assert!(matches!(opts, OptionsResult::Version(_))); - } -} diff --git a/src/options/view.rs b/src/options/view.rs index 4cffe27fb..092b84b01 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -3,7 +3,6 @@ use crate::options::parser::Opts; use crate::options::{NumberSource, OptionsError, Vars}; use crate::output::file_name::Options as FileStyle; use crate::output::grid_details::{self, RowThreshold}; -use crate::output::grid_details::{self, RowThreshold}; use crate::output::table::{Columns, Options as TableOptions, SizeFormat, TimeTypes, UserFormat}; use crate::output::time::TimeFormat; use crate::output::{details, grid, Mode, TerminalWidth, View}; @@ -339,6 +338,12 @@ impl TimeFormat { fn deduce(matches: &Opts, vars: &V) -> Result { let word = if let Some(ref w) = matches.time_style { w.clone() + } else { + use crate::options::vars; + match vars.get(vars::TIME_STYLE) { + Some(ref t) if !t.is_empty() => t.clone(), + _ => return Ok(Self::DefaultFormat), + } }; match word.to_string_lossy().as_ref() { From e059118dbc158e87d3c7f048e4af9133fa38fb34 Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Mon, 25 Sep 2023 13:52:04 +0200 Subject: [PATCH 14/47] fix(view_options): git-repo-no-status was not working as intended --- src/options/filter.rs | 15 ++++++++++++--- src/options/parser.rs | 2 +- src/options/view.rs | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/options/filter.rs b/src/options/filter.rs index 66f515236..2304c5ebd 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -41,10 +41,17 @@ impl SortField { /// Returns the default sort field if none is given, or `Err` if the /// value doesn’t correspond to a sort field we know about. fn deduce(matches: &Opts) -> Result { - let Some(ref word) = matches.sort else { return Ok(Self::default()) }; + let Some(ref word) = matches.sort else { + return Ok(Self::default()); + }; // Get String because we can’t match an OsStr - let Some(word) = word.to_str() else { return Err(OptionsError::BadArgument("SORT".to_string(), word.to_string_lossy().to_string())) }; + let Some(word) = word.to_str() else { + return Err(OptionsError::BadArgument( + "SORT".to_string(), + word.to_string_lossy().to_string(), + )); + }; let field = match word { "name" | "filename" => Self::Name(SortCase::AaBbCc), @@ -161,7 +168,9 @@ impl IgnorePatterns { pub fn deduce(matches: &Opts) -> Result { // If there are no inputs, we return a set of patterns that doesn’t // match anything, rather than, say, `None`. - let Some(ref inputs) = matches.ignore_glob else { return Ok(Self::empty()) }; + let Some(ref inputs) = matches.ignore_glob else { + return Ok(Self::empty()); + }; // Awkwardly, though, a glob pattern can be invalid, and we need to // deal with invalid patterns somehow. diff --git a/src/options/parser.rs b/src/options/parser.rs index 98038587e..595191d85 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -143,7 +143,7 @@ pub struct Opts { /// list root of git-tree status. #[arg(long = "git-repos", action = clap::ArgAction::Count)] pub git_repos: u8, - /// + ///List each git-repos branch name (much faster) #[arg(long = "git-repos-no-status", action = clap::ArgAction::Count)] pub git_repos_no_status: u8, /// list each file's permission in octal format. diff --git a/src/options/view.rs b/src/options/view.rs index 092b84b01..4b72ee815 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -276,7 +276,7 @@ impl Columns { let git = matches.git > 0 && matches.no_git == 0; let subdir_git_repos = matches.git_repos > 0 && matches.no_git == 0; let subdir_git_repos_no_stat = - subdir_git_repos && matches.git_repos_no_status > 0 && matches.no_git == 0; + !subdir_git_repos && matches.git_repos_no_status > 0 && matches.no_git == 0; let blocksize = matches.blocksize > 0; let group = matches.group > 0; From ff3dad09e704a47e09a8f475514851ad7e4e5917 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 25 Sep 2023 17:36:24 +0200 Subject: [PATCH 15/47] fix(treefmt): remove outdated rustfmt excludes Signed-off-by: Sandro-Alessio Gierens --- treefmt.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/treefmt.nix b/treefmt.nix index e7040a6c5..5946e61f9 100644 --- a/treefmt.nix +++ b/treefmt.nix @@ -10,6 +10,5 @@ }; settings = { formatter.shellcheck.includes = ["*.sh" "./completions/bash/eza"]; - formatter.rustfmt.excludes = ["src/options/flags.rs"]; }; } From 8aec53fad4fb389a3a7ca5bb549b736566366f48 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Wed, 27 Sep 2023 16:37:21 +0200 Subject: [PATCH 16/47] docs: fix help on --octal-permissions to match new --octal --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cd237215..542517aae 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ These options are available when running with `--long` (`-l`): - **--no-git**: suppress Git status (always overrides `--git`, `--git-repos`, `--git-repos-no-status`) - **--time-style**: how to format timestamps - **--no-permissions**: suppress the permissions field -- **-o**, **--octal-permissions**: list each file's permission in octal format +- **-o**, **--octal**: list each file's permission in octal format - **--no-filesize**: suppress the filesize field - **--no-user**: suppress the user field - **--no-time**: suppress the time field From c20e0312d2ae34882a7dc0a974b936751c7658ad Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Tue, 3 Oct 2023 10:45:35 +0200 Subject: [PATCH 17/47] fix: fixed everything broken with merge of main --- Cargo.lock | 32 +++++++++++++++--- src/main.rs | 2 +- src/options/file_name.rs | 14 ++++---- src/options/mod.rs | 72 ---------------------------------------- src/options/parser.rs | 4 +++ src/options/view.rs | 5 ++- 6 files changed, 44 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f850729d..eabf42fba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon 2.1.0", + "colorchoice", + "utf8parse", +] + [[package]] name = "anstream" version = "0.6.4" @@ -50,7 +64,7 @@ dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", - "anstyle-wincon", + "anstyle-wincon 3.0.1", "colorchoice", "utf8parse", ] @@ -79,6 +93,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anstyle-wincon" version = "3.0.1" @@ -196,7 +220,7 @@ version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" dependencies = [ - "anstream", + "anstream 0.5.0", "anstyle", "clap_lex", "strsim", @@ -1043,7 +1067,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b377c0b6e4715c116473d8e40d51e3fa5b0a2297ca9b2a931ba800667b259ed" dependencies = [ - "anstream", + "anstream 0.6.4", "anstyle", "content_inspector", "dunce", @@ -1065,7 +1089,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1559baff8a696add3322b9be3e940d433e7bb4e38d79017205fd37ff28b28e" dependencies = [ - "anstream", + "anstream 0.6.4", ] [[package]] diff --git a/src/main.rs b/src/main.rs index 91e36a97b..b3503f12b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,7 +37,7 @@ use crate::fs::filter::GitIgnore; use crate::fs::{Dir, File}; use crate::options::parser::Opts; use crate::options::{vars, Options, Vars}; -use crate::output::{details, escape, grid, grid_details, lines, Mode, View}; +use crate::output::{details, escape, file_name, grid, grid_details, lines, Mode, View}; use crate::theme::Theme; mod fs; diff --git a/src/options/file_name.rs b/src/options/file_name.rs index 61916a4c2..d586aef66 100644 --- a/src/options/file_name.rs +++ b/src/options/file_name.rs @@ -2,15 +2,15 @@ use crate::options::vars::{self, Vars}; use crate::options::{NumberSource, OptionsError}; use crate::options::parser::Opts; -use crate::output::file_name::{Classify, EmbedHyperlinks, Options, ShowIcons}; +use crate::output::file_name::{Classify, EmbedHyperlinks, Options, QuoteStyle, ShowIcons}; impl Options { pub fn deduce(matches: &Opts, vars: &V) -> Result { let classify = Classify::deduce(matches); let show_icons = ShowIcons::deduce(matches, vars)?; - let quote_style = QuoteStyle::deduce(matches)?; - let embed_hyperlinks = EmbedHyperlinks::deduce(matches)?; + let quote_style = QuoteStyle::deduce(matches); + let embed_hyperlinks = EmbedHyperlinks::deduce(matches); Ok(Self { classify, @@ -52,11 +52,11 @@ impl ShowIcons { } impl QuoteStyle { - pub fn deduce(matches: &MatchedFlags<'_>) -> Result { - if matches.has(&flags::NO_QUOTES)? { - Ok(Self::NoQuotes) + pub fn deduce(matches: &Opts) -> Self { + if matches.no_quotes > 0 { + Self::NoQuotes } else { - Ok(Self::QuoteSpaces) + Self::QuoteSpaces } } } diff --git a/src/options/mod.rs b/src/options/mod.rs index 6d6b31c8e..a37f918cf 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -163,75 +163,3 @@ impl Options { }) } } - -/// The result of the `Options::parse` function. -/// -/// NOTE: We disallow the `large_enum_variant` lint here, because we're not -/// overly concerned about variant fragmentation. We can do this because we are -/// reasonably sure that the error variant will be rare, and only on faulty -/// program execution and thus boxing the large variant will be a waste of -/// resources, but should we come to use it more, we should reconsider. -/// -/// See -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub enum OptionsResult<'args> { - /// The options were parsed successfully. - Ok(Options, Vec<&'args OsStr>), - - /// There was an error parsing the arguments. - InvalidOptions(OptionsError), - - /// One of the arguments was `--help`, so display help. - Help(HelpString), - - /// One of the arguments was `--version`, so display the version number. - Version(VersionString), -} - -#[cfg(test)] -pub mod test { - use crate::options::parser::{Arg, MatchedFlags}; - use std::ffi::OsStr; - - #[derive(PartialEq, Eq, Debug)] - pub enum Strictnesses { - Last, - Complain, - Both, - } - - /// This function gets used by the other testing modules. - /// It can run with one or both strictness values: if told to run with - /// both, then both should resolve to the same result. - /// - /// It returns a vector with one or two elements in. - /// These elements can then be tested with `assert_eq` or what have you. - pub fn parse_for_test( - inputs: &[&str], - args: &'static [&'static Arg], - strictnesses: Strictnesses, - get: F, - ) -> Vec - where - F: Fn(&MatchedFlags<'_>) -> T, - { - use self::Strictnesses::*; - use crate::options::parser::{Args, Strictness}; - - let bits = inputs.iter().map(OsStr::new).collect::>(); - let mut result = Vec::new(); - - if strictnesses == Last || strictnesses == Both { - let results = Args(args).parse(bits.clone(), Strictness::UseLastArguments); - result.push(get(&results.unwrap().flags)); - } - - if strictnesses == Complain || strictnesses == Both { - let results = Args(args).parse(bits, Strictness::ComplainAboutRedundantArguments); - result.push(get(&results.unwrap().flags)); - } - - result - } -} diff --git a/src/options/parser.rs b/src/options/parser.rs index 595191d85..e7e16d697 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -164,6 +164,9 @@ pub struct Opts { /// Show only files #[arg(short = 'f', long = "only-files", action = clap::ArgAction::Count)] pub only_files: u8, + /// Don't Show quotes + #[arg(long = "no-quotes", action = clap::ArgAction::Count)] + pub no_quotes: u8, } impl Opts { @@ -224,6 +227,7 @@ impl Opts { colour: None, colour_scale: 0, only_files: 0, + no_quotes: 0, } } } diff --git a/src/options/view.rs b/src/options/view.rs index 67797b3c5..77ea0de74 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -355,7 +355,10 @@ impl TimeFormat { fmt if fmt.starts_with('+') => Ok(Self::Custom { fmt: fmt[1..].to_owned(), }), - _ => Err(OptionsError::BadArgument(&flags::TIME_STYLE, word)), + _ => Err(OptionsError::BadArgument( + "TIME_STYLE".to_string(), + word.to_string_lossy().to_string(), + )), } } } From 1289bc40abe21224709b6efaf944ea14d48becd8 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 3 Oct 2023 10:42:45 +0200 Subject: [PATCH 18/47] docs(readme): add zsh with homebrew part to completions section Signed-off-by: Sandro-Alessio Gierens --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 4fba54f4b..64e4d567b 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,25 @@ echo 'export FPATH="/completions/zsh:$FPATH"' >> ~/.zshrc source ~/.zshrc ``` + +#### For zsh with homebrew: + +In case zsh completions don't work out of the box with homebrew, add the +following to your `~/.zshrc`: + +```bash +if type brew &>/dev/null; then + FPATH="$(brew --prefix)/share/zsh/site-functions:${FPATH}" + autoload -Uz compinit + compinit +fi +``` + +For reference: +- https://docs.brew.sh/Shell-Completion#configuring-completions-in-zsh +- https://github.com/Homebrew/brew/issues/8984 + + --- Click sections to expand. From 7c6a93029fbef8ac2338cbb404fb5b9d8a769d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Mon, 2 Oct 2023 16:36:18 +0200 Subject: [PATCH 19/47] build(checksums): make checksums easier to copy-paste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- Justfile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Justfile b/Justfile index e048b95a2..3550455e4 100644 --- a/Justfile +++ b/Justfile @@ -139,15 +139,15 @@ binary_static BINARY TARGET: just zip_static {{BINARY}} {{TARGET}} checksum: - echo "# Checksums" - echo "## sha256sum" - echo '```' - sha256sum ./target/"bin-$(convco version)"/* - echo '```' - echo "## md5sum" - echo '```' - md5sum ./target/"bin-$(convco version)"/* - echo '```' + @echo "# Checksums" + @echo "## sha256sum" + @echo '```' + @sha256sum ./target/"bin-$(convco version)"/* + @echo '```' + @echo "## md5sum" + @echo '```' + @md5sum ./target/"bin-$(convco version)"/* + @echo '```' alias c := cross From 73cc969103cc29dd5e3a8509bb76f39c1fcb3082 Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Tue, 3 Oct 2023 14:53:17 +0200 Subject: [PATCH 20/47] fix(binaries): diabling static linked binaries due to segfault segfaulting on multithreading => need to investigate how to bypass --- Justfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Justfile b/Justfile index 3550455e4..4cd1ca824 100644 --- a/Justfile +++ b/Justfile @@ -164,9 +164,9 @@ alias c := cross ## Linux ### x86 just binary eza x86_64-unknown-linux-gnu - just binary_static eza x86_64-unknown-linux-gnu + # just binary_static eza x86_64-unknown-linux-gnu just binary eza x86_64-unknown-linux-musl - just binary_static eza x86_64-unknown-linux-musl + # just binary_static eza x86_64-unknown-linux-musl ### aarch just binary eza aarch64-unknown-linux-gnu @@ -174,7 +174,7 @@ alias c := cross ### arm just binary eza arm-unknown-linux-gnueabihf - just binary_static eza arm-unknown-linux-gnueabihf + # just binary_static eza arm-unknown-linux-gnueabihf ## MacOS # TODO: just binary eza x86_64-apple-darwin @@ -182,7 +182,7 @@ alias c := cross ## Windows ### x86 just binary eza.exe x86_64-pc-windows-gnu - just binary_static eza.exe x86_64-pc-windows-gnu + # just binary_static eza.exe x86_64-pc-windows-gnu # TODO: just binary eza.exe x86_64-pc-windows-gnullvm # TODO: just binary eza.exe x86_64-pc-windows-msvc From 1730b6158ba52b42e13ce3815ef90c7ab034abcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Tue, 3 Oct 2023 19:10:55 +0200 Subject: [PATCH 21/47] fix(main): make os error 13 fail loud MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref: #473, #319 Signed-off-by: Christina Sørensen --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index b3503f12b..6d00088ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -181,7 +181,7 @@ impl<'args> Exa<'args> { match f.to_dir() { Ok(d) => dirs.push(d), Err(e) if e.kind() == ErrorKind::PermissionDenied => { - warn!("Permission Denied: {e}"); + eprintln!("{file_path:?}: {e}"); exit(exits::PERMISSION_DENIED); } Err(e) => writeln!(io::stderr(), "{file_path:?}: {e}")?, From f10cb9d39338685667cbd642c6155dfb31b826f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Wed, 4 Oct 2023 13:29:27 +0200 Subject: [PATCH 22/47] build(release): improve release automation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- Justfile | 25 ++++++++++++++++++------- cliff.toml | 3 +-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Justfile b/Justfile index 4cd1ca824..2d58696ce 100644 --- a/Justfile +++ b/Justfile @@ -95,20 +95,31 @@ all-release: build-release test-release # release # #---------------# +new_version := "$(convco version --bump)" + # If you're not cafkafk and she isn't dead, don't run this! # # usage: release major, release minor, release patch -@release version: - cargo bump '{{version}}' - git cliff -t $(grep '^version' Cargo.toml | head -n 1 | grep -E '([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?' -o) > CHANGELOG.md +@release: + cargo bump "{{new_version}}" + git cliff -t "$(convco version)" > CHANGELOG.md cargo check nix build -L ./#clippy - git checkout -b cafk-release-$(grep '^version' Cargo.toml | head -n 1 | grep -E '([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?' -o) - git commit -asm "chore: release $(grep '^version' Cargo.toml | head -n 1 | grep -E '([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?' -o)" + git checkout -b "cafk-release-v$(convco version)" + git commit -asm "chore: release eza v$(convco version)" git push echo "waiting 10 seconds for github to catch up..." sleep 10 - gh pr create --draft --title "chore: release $(grep '^version' Cargo.toml | head -n 1 | grep -E '([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?' -o)" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk + gh pr create --draft --title "chore: release v$(convco version)" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk + @echo "Now go review that and come back and run gh-release" + +@gh-release: + git tag -a "v$(convco version --bump)" -m "auto generated by the justfile for eza v$(convco version)" + mkdir -p ./target/"release-notes-$(convco version)" + git cliff -t "v$(convco version)" -u > ./target/"release-notes-$(convco version)/RELEASE.md" ./target/"bin-$(convco version)"/* + just checksum >> ./target/"release-notes-$(convco version)/RELEASE.md" + + gh release create "v$(convco version)" --title "eza v$(convco version)" -d -F ./target/"release-notes-$(convco version)/RELEASE.md" #----------------# # binaries # @@ -187,7 +198,7 @@ alias c := cross # TODO: just binary eza.exe x86_64-pc-windows-msvc # Generate Checksums - just checksum + # TODO: moved to gh-release just checksum #---------------------# # Integration testing # diff --git a/cliff.toml b/cliff.toml index 0a8821899..72c0a8ee4 100644 --- a/cliff.toml +++ b/cliff.toml @@ -9,7 +9,6 @@ # changelog header header = """ # Changelog\n -All notable changes to this project will be documented in this file.\n """ # template for the changelog body # https://tera.netlify.app/docs @@ -30,7 +29,7 @@ body = """ trim = true # changelog footer footer = """ - + """ [git] From a380a3f365523e25675df124fb848780e9d23e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Wed, 4 Oct 2023 13:35:14 +0200 Subject: [PATCH 23/47] build(release): fix version bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- Justfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Justfile b/Justfile index 2d58696ce..16ad4a2f2 100644 --- a/Justfile +++ b/Justfile @@ -100,21 +100,21 @@ new_version := "$(convco version --bump)" # If you're not cafkafk and she isn't dead, don't run this! # # usage: release major, release minor, release patch -@release: +release: cargo bump "{{new_version}}" - git cliff -t "$(convco version)" > CHANGELOG.md + git cliff -t "{{new_version}}" > CHANGELOG.md cargo check nix build -L ./#clippy - git checkout -b "cafk-release-v$(convco version)" - git commit -asm "chore: release eza v$(convco version)" + git checkout -b "cafk-release-v{{new_version}}" + git commit -asm "chore: release eza v{{new_version}}" git push echo "waiting 10 seconds for github to catch up..." sleep 10 - gh pr create --draft --title "chore: release v$(convco version)" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk + gh pr create --draft --title "chore: release v{{new_version}}" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk @echo "Now go review that and come back and run gh-release" @gh-release: - git tag -a "v$(convco version --bump)" -m "auto generated by the justfile for eza v$(convco version)" + git tag -a "v{{new_version}}" -m "auto generated by the justfile for eza v$(convco version)" mkdir -p ./target/"release-notes-$(convco version)" git cliff -t "v$(convco version)" -u > ./target/"release-notes-$(convco version)/RELEASE.md" ./target/"bin-$(convco version)"/* just checksum >> ./target/"release-notes-$(convco version)/RELEASE.md" From acf24b421906b1ad9583a1584516b6f63034bb37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Wed, 4 Oct 2023 13:36:54 +0200 Subject: [PATCH 24/47] build(release): fix double echo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 16ad4a2f2..a18fad57a 100644 --- a/Justfile +++ b/Justfile @@ -108,7 +108,7 @@ release: git checkout -b "cafk-release-v{{new_version}}" git commit -asm "chore: release eza v{{new_version}}" git push - echo "waiting 10 seconds for github to catch up..." + @echo "waiting 10 seconds for github to catch up..." sleep 10 gh pr create --draft --title "chore: release v{{new_version}}" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk @echo "Now go review that and come back and run gh-release" From 39122e3599a8136906862c3423aa449e4f2a6e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Wed, 4 Oct 2023 13:53:31 +0200 Subject: [PATCH 25/47] build(release): automate gh release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- Justfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index a18fad57a..b13cf9380 100644 --- a/Justfile +++ b/Justfile @@ -114,12 +114,14 @@ release: @echo "Now go review that and come back and run gh-release" @gh-release: + git tag -d "v{{new_version}}" || echo "tag not found, creating"; git tag -a "v{{new_version}}" -m "auto generated by the justfile for eza v$(convco version)" + just cross mkdir -p ./target/"release-notes-$(convco version)" - git cliff -t "v$(convco version)" -u > ./target/"release-notes-$(convco version)/RELEASE.md" ./target/"bin-$(convco version)"/* + git cliff -t "v$(convco version)" --current > ./target/"release-notes-$(convco version)/RELEASE.md" just checksum >> ./target/"release-notes-$(convco version)/RELEASE.md" - gh release create "v$(convco version)" --title "eza v$(convco version)" -d -F ./target/"release-notes-$(convco version)/RELEASE.md" + gh release create "v$(convco version)" --target "$(git rev-parse HEAD)" --title "eza v$(convco version)" -d -F ./target/"release-notes-$(convco version)/RELEASE.md" ./target/"bin-$(convco version)"/* #----------------# # binaries # From 6d1d94a4476d7fb2cd691c6da1ef4661f512a0b7 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Sun, 8 Oct 2023 10:50:19 +0200 Subject: [PATCH 26/47] fix(colors): root group not painted as expected when eza used by root --- src/output/render/groups.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output/render/groups.rs b/src/output/render/groups.rs index d8e76537e..c0654bd08 100644 --- a/src/output/render/groups.rs +++ b/src/output/render/groups.rs @@ -42,7 +42,7 @@ impl Render for Option { } } - if group.gid() == 0 { + if group.gid() == 0 && style != colours.yours() { style = colours.root_group(); } From d45c8ae9ac19e8cd405b0a2a7bc4949a99528e65 Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Thu, 5 Oct 2023 11:12:41 +0200 Subject: [PATCH 27/47] feat(bin): readded musl static bin as it works All the other ones are broken --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index b13cf9380..956b656e7 100644 --- a/Justfile +++ b/Justfile @@ -179,7 +179,7 @@ alias c := cross just binary eza x86_64-unknown-linux-gnu # just binary_static eza x86_64-unknown-linux-gnu just binary eza x86_64-unknown-linux-musl - # just binary_static eza x86_64-unknown-linux-musl + just binary_static eza x86_64-unknown-linux-musl ### aarch just binary eza aarch64-unknown-linux-gnu From 5d19d71fa42b8b3ec84f51b8f8a9a19fc3bd0a7c Mon Sep 17 00:00:00 2001 From: Chris Gorski Date: Tue, 3 Oct 2023 14:20:32 -0400 Subject: [PATCH 28/47] fix: adjust change width calculations for hyperlink and classify This fix adjusts the calculations used when hyperlink and classify options are both specified on the command line, correcting behavior that would cause columns to be misaligned. Resolves #267 --- src/output/file_name.rs | 4 ++-- src/output/grid.rs | 28 +++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/output/file_name.rs b/src/output/file_name.rs index 232207573..11bc18913 100644 --- a/src/output/file_name.rs +++ b/src/output/file_name.rs @@ -314,7 +314,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { /// The character to be displayed after a file when classifying is on, if /// the file’s type has one associated with it. #[cfg(unix)] - fn classify_char(&self, file: &File<'_>) -> Option<&'static str> { + pub(crate) fn classify_char(&self, file: &File<'_>) -> Option<&'static str> { if file.is_executable_file() { Some("*") } else if file.is_directory() { @@ -331,7 +331,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { } #[cfg(windows)] - fn classify_char(&self, file: &File<'_>) -> Option<&'static str> { + pub(crate) fn classify_char(&self, file: &File<'_>) -> Option<&'static str> { if file.is_directory() { Some("/") } else if file.is_link() { diff --git a/src/output/grid.rs b/src/output/grid.rs index d84d1ec2e..96d3b21fb 100644 --- a/src/output/grid.rs +++ b/src/output/grid.rs @@ -4,7 +4,7 @@ use term_grid as tg; use crate::fs::filter::FileFilter; use crate::fs::File; -use crate::output::file_name::Options as FileStyle; +use crate::output::file_name::{Classify, Options as FileStyle}; use crate::output::file_name::{EmbedHyperlinks, ShowIcons}; use crate::theme::Theme; @@ -44,11 +44,29 @@ impl<'a> Render<'a> { self.filter.sort_files(&mut self.files); for file in &self.files { let filename = self.file_style.for_file(file, self.theme); + + // Calculate classification width + let classification_width = + if let Classify::AddFileIndicators = filename.options.classify { + match filename.classify_char(file) { + Some(s) => s.len(), + None => 0, + } + } else { + 0 + }; + let contents = filename.paint(); - #[rustfmt::skip] - let width = match (filename.options.embed_hyperlinks, filename.options.show_icons) { - (EmbedHyperlinks::On, ShowIcons::On(spacing)) => filename.bare_width() + 1 + spacing, - (EmbedHyperlinks::On, ShowIcons::Off) => filename.bare_width(), + let width = match ( + filename.options.embed_hyperlinks, + filename.options.show_icons, + ) { + (EmbedHyperlinks::On, ShowIcons::On(spacing)) => { + filename.bare_width() + 1 + spacing + classification_width + } + (EmbedHyperlinks::On, ShowIcons::Off) => { + filename.bare_width() + classification_width + } (EmbedHyperlinks::Off, _) => *contents.width(), }; From 328792ed355b5c34b9c8255826295d2fa08f2185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Thu, 5 Oct 2023 13:53:48 +0200 Subject: [PATCH 29/47] test(all): classify-hyperlink test case for width 50 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- tests/cmd/classify-hyperlink-width-50_all.stderr | 0 tests/cmd/classify-hyperlink-width-50_all.stdout | 5 +++++ tests/cmd/classify-hyperlink-width-50_all.toml | 2 ++ 3 files changed, 7 insertions(+) create mode 100644 tests/cmd/classify-hyperlink-width-50_all.stderr create mode 100644 tests/cmd/classify-hyperlink-width-50_all.stdout create mode 100644 tests/cmd/classify-hyperlink-width-50_all.toml diff --git a/tests/cmd/classify-hyperlink-width-50_all.stderr b/tests/cmd/classify-hyperlink-width-50_all.stderr new file mode 100644 index 000000000..e69de29bb diff --git a/tests/cmd/classify-hyperlink-width-50_all.stdout b/tests/cmd/classify-hyperlink-width-50_all.stdout new file mode 100644 index 000000000..59a37fb87 --- /dev/null +++ b/tests/cmd/classify-hyperlink-width-50_all.stdout @@ -0,0 +1,5 @@ +]8;;file:///home/ces/org/src/git/eza/tests/itest/a/a]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/exa/exa]8;;// ]8;;file:///home/ces/org/src/git/eza/tests/itest/image.jpg.img.c.rs.log.png/image.jpg.img.c.rs.log.png]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/m/m]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/vagrant/vagrant]8;;// +]8;;file:///home/ces/org/src/git/eza/tests/itest/b/b]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/f/f]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/index.svg/index.svg]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/n/n]8;;/ +]8;;file:///home/ces/org/src/git/eza/tests/itest/c/c]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/g/g]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/j/j]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/o/o]8;;/ +]8;;file:///home/ces/org/src/git/eza/tests/itest/d/d]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/h/h]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/k/k]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/p/p]8;;/ +]8;;file:///home/ces/org/src/git/eza/tests/itest/e/e]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/i/i]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/l/l]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/q/q]8;;/ diff --git a/tests/cmd/classify-hyperlink-width-50_all.toml b/tests/cmd/classify-hyperlink-width-50_all.toml new file mode 100644 index 000000000..f5ae718f4 --- /dev/null +++ b/tests/cmd/classify-hyperlink-width-50_all.toml @@ -0,0 +1,2 @@ +bin.name = "eza" +args = "tests/itest/ --width 50 --classify --hyperlink" From 58fafe60420f7c2df05fea29a694657eb44d87a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Thu, 5 Oct 2023 13:54:30 +0200 Subject: [PATCH 30/47] refactor(grid.rs): consistent argument order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- src/output/grid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output/grid.rs b/src/output/grid.rs index 96d3b21fb..cd0d900e3 100644 --- a/src/output/grid.rs +++ b/src/output/grid.rs @@ -62,7 +62,7 @@ impl<'a> Render<'a> { filename.options.show_icons, ) { (EmbedHyperlinks::On, ShowIcons::On(spacing)) => { - filename.bare_width() + 1 + spacing + classification_width + filename.bare_width() + classification_width + 1 + spacing } (EmbedHyperlinks::On, ShowIcons::Off) => { filename.bare_width() + classification_width From 00395db3a7ab0dfef8a3a8fe160271785c39fbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Thu, 5 Oct 2023 14:00:18 +0200 Subject: [PATCH 31/47] test(local): move classify tests to local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Here, we introduce the `nix-local` feature, for features meant only to exist locally in the nix sandbox. This is an attempt to avoid the CI runner's unit testing, as that fails to set the grid width correctly. Signed-off-by: Christina Sørensen --- Cargo.toml | 2 ++ Justfile | 2 +- flake.nix | 18 +++++++++++++++++- tests/cli_tests.rs | 6 ++++++ .../cmd/classify-hyperlink-width-50_all.stdout | 5 ----- ...assify-hyperlink-width-50_nix_local.stderr} | 0 ...lassify-hyperlink-width-50_nix_local.stdout | 5 +++++ ...classify-hyperlink-width-50_nix_local.toml} | 0 8 files changed, 31 insertions(+), 7 deletions(-) delete mode 100644 tests/cmd/classify-hyperlink-width-50_all.stdout rename tests/cmd/{classify-hyperlink-width-50_all.stderr => classify-hyperlink-width-50_nix_local.stderr} (100%) create mode 100644 tests/cmd/classify-hyperlink-width-50_nix_local.stdout rename tests/cmd/{classify-hyperlink-width-50_all.toml => classify-hyperlink-width-50_nix_local.toml} (100%) diff --git a/Cargo.toml b/Cargo.toml index 380fc1a5d..4187605e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,8 @@ vendored-openssl = ["git2/vendored-openssl"] vendored-libgit2 = ["git2/vendored-libgit2"] # Should only be used inside of flake.nix nix = [] +# Should only be used inside of flake.nix locally (not on CI) +nix-local = [] # make dev builds faster by excluding debug symbols diff --git a/Justfile b/Justfile index 956b656e7..0d94bc0fd 100644 --- a/Justfile +++ b/Justfile @@ -272,7 +272,7 @@ gen_test_dir: # # Required nix, likely won't work on windows. @itest: - nix build -L ./#trycmd + nix build -L ./#trycmd-local # Runs integration tests in nix sandbox, and dumps outputs. # diff --git a/flake.nix b/flake.nix index f383d3278..8de8d1e00 100644 --- a/flake.nix +++ b/flake.nix @@ -124,6 +124,22 @@ inherit buildInputs; }; + # TODO: add conditionally to checks. + # Run `nix build .#trycmd` to run integration tests + trycmd-local = naersk'.buildPackage { + src = ./.; + mode = "test"; + doCheck = true; + # No reason to wait for release build + release = false; + # buildPhase files differ between dep and main phase + singleStep = true; + # set itests files creation date to unix epoch + buildPhase = ''touch --date=@0 tests/itest/*''; + cargoTestOptions = opts: opts ++ ["--features nix" "--features nix-local"]; + inherit buildInputs; + }; + # Run `nix build .#trydump` to dump testing files trydump = naersk'.buildPackage { src = ./.; @@ -135,7 +151,7 @@ singleStep = true; # set itests files creation date to unix epoch buildPhase = ''touch --date=@0 tests/itest/*; rm tests/cmd/*.stdout || echo; rm tests/cmd/*.stderr || echo;''; - cargoTestOptions = opts: opts ++ ["--features nix"]; + cargoTestOptions = opts: opts ++ ["--features nix" "--features nix-local"]; TRYCMD = "dump"; postInstall = '' cp dump $out -r diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index d6c6e436a..cf1027adc 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -20,3 +20,9 @@ fn cli_windows_tests() { fn cli_nix_tests() { trycmd::TestCases::new().case("tests/cmd/*_nix.toml"); } + +#[test] +#[cfg(feature = "nix-local")] +fn cli_nix_local_tests() { + trycmd::TestCases::new().case("tests/cmd/*_nix_local.toml"); +} diff --git a/tests/cmd/classify-hyperlink-width-50_all.stdout b/tests/cmd/classify-hyperlink-width-50_all.stdout deleted file mode 100644 index 59a37fb87..000000000 --- a/tests/cmd/classify-hyperlink-width-50_all.stdout +++ /dev/null @@ -1,5 +0,0 @@ -]8;;file:///home/ces/org/src/git/eza/tests/itest/a/a]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/exa/exa]8;;// ]8;;file:///home/ces/org/src/git/eza/tests/itest/image.jpg.img.c.rs.log.png/image.jpg.img.c.rs.log.png]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/m/m]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/vagrant/vagrant]8;;// -]8;;file:///home/ces/org/src/git/eza/tests/itest/b/b]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/f/f]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/index.svg/index.svg]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/n/n]8;;/ -]8;;file:///home/ces/org/src/git/eza/tests/itest/c/c]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/g/g]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/j/j]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/o/o]8;;/ -]8;;file:///home/ces/org/src/git/eza/tests/itest/d/d]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/h/h]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/k/k]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/p/p]8;;/ -]8;;file:///home/ces/org/src/git/eza/tests/itest/e/e]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/i/i]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/l/l]8;;/ ]8;;file:///home/ces/org/src/git/eza/tests/itest/q/q]8;;/ diff --git a/tests/cmd/classify-hyperlink-width-50_all.stderr b/tests/cmd/classify-hyperlink-width-50_nix_local.stderr similarity index 100% rename from tests/cmd/classify-hyperlink-width-50_all.stderr rename to tests/cmd/classify-hyperlink-width-50_nix_local.stderr diff --git a/tests/cmd/classify-hyperlink-width-50_nix_local.stdout b/tests/cmd/classify-hyperlink-width-50_nix_local.stdout new file mode 100644 index 000000000..1a79ff3fa --- /dev/null +++ b/tests/cmd/classify-hyperlink-width-50_nix_local.stdout @@ -0,0 +1,5 @@ +]8;;file:///build/source/tests/itest/a/a]8;;/ ]8;;file:///build/source/tests/itest/exa/exa]8;;// ]8;;file:///build/source/tests/itest/image.jpg.img.c.rs.log.png/image.jpg.img.c.rs.log.png]8;;/ ]8;;file:///build/source/tests/itest/m/m]8;;/ ]8;;file:///build/source/tests/itest/vagrant/vagrant]8;;// +]8;;file:///build/source/tests/itest/b/b]8;;/ ]8;;file:///build/source/tests/itest/f/f]8;;/ ]8;;file:///build/source/tests/itest/index.svg/index.svg]8;;/ ]8;;file:///build/source/tests/itest/n/n]8;;/ +]8;;file:///build/source/tests/itest/c/c]8;;/ ]8;;file:///build/source/tests/itest/g/g]8;;/ ]8;;file:///build/source/tests/itest/j/j]8;;/ ]8;;file:///build/source/tests/itest/o/o]8;;/ +]8;;file:///build/source/tests/itest/d/d]8;;/ ]8;;file:///build/source/tests/itest/h/h]8;;/ ]8;;file:///build/source/tests/itest/k/k]8;;/ ]8;;file:///build/source/tests/itest/p/p]8;;/ +]8;;file:///build/source/tests/itest/e/e]8;;/ ]8;;file:///build/source/tests/itest/i/i]8;;/ ]8;;file:///build/source/tests/itest/l/l]8;;/ ]8;;file:///build/source/tests/itest/q/q]8;;/ diff --git a/tests/cmd/classify-hyperlink-width-50_all.toml b/tests/cmd/classify-hyperlink-width-50_nix_local.toml similarity index 100% rename from tests/cmd/classify-hyperlink-width-50_all.toml rename to tests/cmd/classify-hyperlink-width-50_nix_local.toml From 2b81a75373f76b8ac8326ec4d6cb0e9babc7b040 Mon Sep 17 00:00:00 2001 From: dundargoc Date: Wed, 4 Oct 2023 20:43:20 +0200 Subject: [PATCH 32/47] ci: treat warnings as errors --- .github/workflows/unit-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e573834ab..0779bdd5d 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -22,6 +22,7 @@ concurrency: env: CARGO_TERM_COLOR: always + RUSTFLAGS: --deny warnings jobs: unit-tests: From 69c6c09b2fed2d6e10ce1ebe8be50d1ecfa6edff Mon Sep 17 00:00:00 2001 From: Lena <126529524+acuteenvy@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:43:24 +0200 Subject: [PATCH 33/47] build: add `codegen-units = 1` and remove `opt-level = 3` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4187605e2..11823e4e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,7 +132,7 @@ debug = false [profile.release] lto = true strip = true -opt-level = 3 +codegen-units = 1 [[bench]] name = "my_benchmark" From d49f32233957006313777c97860815f76a459ccf Mon Sep 17 00:00:00 2001 From: Lena <126529524+acuteenvy@users.noreply.github.com> Date: Sat, 7 Oct 2023 15:16:19 +0200 Subject: [PATCH 34/47] build: add back `opt-level = 3` --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 11823e4e3..14f092e66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,7 @@ debug = false [profile.release] lto = true strip = true +opt-level = 3 codegen-units = 1 [[bench]] From 273c8e8536a0b9671b9be9aee6fd3a7be7cfabc9 Mon Sep 17 00:00:00 2001 From: Pedro Carreno <34664891+Pkcarreno@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:43:34 -0400 Subject: [PATCH 35/47] docs(readme): installation on fedora updated --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64e4d567b..2e97ae000 100644 --- a/README.md +++ b/README.md @@ -168,9 +168,13 @@ The preceding repository also contains the Bash, Fish, and Zsh completions. ### Fedora -Fedora support is in the works. +[![Fedora package](https://repology.org/badge/version-for-repo/fedora_39/rust:eza.svg)](https://repology.org/project/eza/versions) -https://bugzilla.redhat.com/show_bug.cgi?id=2238264 +Eza is available as the [eza](https://packages.fedoraproject.org/pkgs/rust-eza/eza/) package in the official Fedora repository. + +```bash +sudo dnf install eza +``` ### Void Linux @@ -422,3 +426,4 @@ The Nix Flake has a few features: ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=eza-community/eza&type=Date)](https://star-history.com/#eza-community/eza&Date) + From d7e2e3f5bd70fcc61c6b253de3dd3d71dcd9a80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20S=C3=B8rensen?= Date: Sun, 8 Oct 2023 11:41:38 +0200 Subject: [PATCH 36/47] chore: release eza v0.14.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christina Sørensen --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3865f94..bf403035a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,52 @@ # Changelog -All notable changes to this project will be documented in this file. +## [0.14.1] - 2023-10-08 + +### Bug Fixes + +- Replace left-over exa in fish completion +- Diabling static linked binaries due to segfault +- Make os error 13 fail loud +- Adjust change width calculations for hyperlink and classify +- Root group not painted as expected when eza used by root + +### Documentation + +- Fix typos +- Add zsh with homebrew part to completions section +- Installation on fedora updated + +### Features + +- Add basic nushell completion file +- Add codeowner for nu completions +- Readded musl static bin as it works + +### Refactor + +- Align completions +- Do not match for numbers and remove single-use fn +- Consistent argument order + +### Testing + +- Classify-hyperlink test case for width 50 +- Move classify tests to local + +### Build + +- Make checksums easier to copy-paste +- Bump trycmd from 0.14.17 to 0.14.19 +- Improve release automation +- Fix version bump +- Fix double echo +- Automate gh release +- Add `codegen-units = 1` and remove `opt-level = 3` +- Add back `opt-level = 3` + +### Ci + +- Treat warnings as errors ## [0.14.0] - 2023-10-02 @@ -34,6 +80,7 @@ All notable changes to this project will be documented in this file. - Description of `--color` in README, manpage, and completions - Change `color` to `colo[u]r` in the option description. - Updated man to add new colors +- Correct CONTRIBUTING.md on commit message type ### Features @@ -52,6 +99,10 @@ All notable changes to this project will be documented in this file. - Added fdmdownload icon - Adding the possibility to change git-repos colors +### Miscellaneous Tasks + +- Release 0.14.0 + ### Refactor - Ignore options/flags.rs @@ -985,4 +1036,4 @@ All notable changes to this project will be documented in this file. - :to_str -> ToString::to_string - + diff --git a/Cargo.lock b/Cargo.lock index eabf42fba..875bc5a6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -396,7 +396,7 @@ dependencies = [ [[package]] name = "eza" -version = "0.14.0" +version = "0.14.1" dependencies = [ "ansiterm", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 14f092e66..517d3d0ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ readme = "README.md" homepage = "https://github.com/eza-community/eza" license = "MIT" repository = "https://github.com/eza-community/eza" -version = "0.14.0" +version = "0.14.1" [package.metadata.deb] From 4793ba28f3ff59e6a985facc466bfbb926d77de8 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Sun, 8 Oct 2023 22:38:04 +0200 Subject: [PATCH 37/47] refactor(devtools): use musl target for amd64 deb package Signed-off-by: Sandro-Alessio Gierens --- devtools/deb-package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/deb-package.sh b/devtools/deb-package.sh index 99b5fb9b4..296c1dd20 100755 --- a/devtools/deb-package.sh +++ b/devtools/deb-package.sh @@ -15,7 +15,7 @@ echo "build man pages" just man declare -A TARGETS -TARGETS["amd64"]="x86_64-unknown-linux-gnu" +TARGETS["amd64"]="x86_64-unknown-linux-musl" TARGETS["arm64"]="aarch64-unknown-linux-gnu" TARGETS["armhf"]="arm-unknown-linux-gnueabihf" From 5b90f43934f1c041cad59e71f89c5f35c387e276 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Sun, 8 Oct 2023 22:47:15 +0200 Subject: [PATCH 38/47] fix(justfile): comment out redundant static musl build Signed-off-by: Sandro-Alessio Gierens --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 0d94bc0fd..b891615d2 100644 --- a/Justfile +++ b/Justfile @@ -179,7 +179,7 @@ alias c := cross just binary eza x86_64-unknown-linux-gnu # just binary_static eza x86_64-unknown-linux-gnu just binary eza x86_64-unknown-linux-musl - just binary_static eza x86_64-unknown-linux-musl + # just binary_static eza x86_64-unknown-linux-musl ### aarch just binary eza aarch64-unknown-linux-gnu From a29728fa44e84ce5263434a4a31855d82a3407f1 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Sun, 8 Oct 2023 22:49:06 +0200 Subject: [PATCH 39/47] style(justfile): remove trailing spaces and trailing line Signed-off-by: Sandro-Alessio Gierens --- Justfile | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/Justfile b/Justfile index b891615d2..66cd8580e 100644 --- a/Justfile +++ b/Justfile @@ -98,9 +98,9 @@ all-release: build-release test-release new_version := "$(convco version --bump)" # If you're not cafkafk and she isn't dead, don't run this! -# +# # usage: release major, release minor, release patch -release: +release: cargo bump "{{new_version}}" git cliff -t "{{new_version}}" > CHANGELOG.md cargo check @@ -110,7 +110,7 @@ release: git push @echo "waiting 10 seconds for github to catch up..." sleep 10 - gh pr create --draft --title "chore: release v{{new_version}}" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk + gh pr create --draft --title "chore: release v{{new_version}}" --body "This PR was auto-generated by our lovely just file" --reviewer cafkafk @echo "Now go review that and come back and run gh-release" @gh-release: @@ -165,9 +165,9 @@ checksum: alias c := cross # Generate release binaries for EZA -# +# # usage: cross -@cross: +@cross: # Setup Output Directory mkdir -p ./target/"bin-$(convco version)" @@ -231,7 +231,7 @@ gen_test_dir: # END grid # BEGIN git - + mkdir -p git cd git @@ -245,27 +245,27 @@ gen_test_dir: done cd .. - + # END git - + # BEGIN test_root - + sudo mkdir root sudo chmod 777 root sudo mkdir root/empty - + # END test_root - + # BEGIN mknod - + mkdir -p specials - + sudo mknod specials/block-device b 3 60 sudo mknod specials/char-device c 14 40 sudo mknod specials/named-pipe p # END test_root - + eza -l --grid; # Runs integration tests in nix sandbox @@ -278,8 +278,7 @@ gen_test_dir: # # WARNING: this can cause loss of work @idump: - rm ./tests/cmd/*nix.stderr -f || echo + rm ./tests/cmd/*nix.stderr -f || echo rm ./tests/cmd/*nix.stdout -f || echo nix build -L ./#trydump cp ./result/dump/*nix.* ./tests/cmd/ - From 4383862c76cc4a80b3e35b92ca3f9fcc5628e661 Mon Sep 17 00:00:00 2001 From: Gardouille Date: Mon, 2 Oct 2023 16:58:16 +0200 Subject: [PATCH 40/47] refactor: Directly use one "big" awk command Replace "cat + grep + head + awk + tr" by one awk command. That's also avoid the "useless cat" case : https://github.com/koalaman/shellcheck/wiki/SC2002 Co-authored-by: gierens Co-authored-by: Gardouille --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 66cd8580e..9fb026181 100644 --- a/Justfile +++ b/Justfile @@ -74,7 +74,7 @@ all-release: build-release test-release # build the man pages @man: mkdir -p "${CARGO_TARGET_DIR:-target}/man" - version=$(cat Cargo.toml | grep ^version | head -n 1 | awk '{print $NF}' | tr -d '"'); \ + version=$(awk 'BEGIN { FS = "\"" } ; /^version/ { print $2 ; exit }' Cargo.toml); \ for page in eza.1 eza_colors.5 eza_colors-explanation.5; do \ pandoc --standalone -f markdown -t man <(cat "man/${page}.md" | sed "s/\$version/v${version}/g") > "${CARGO_TARGET_DIR:-target}/man/${page}"; \ done; From ae0bc94293042aa48e33b67cd593e194799040f8 Mon Sep 17 00:00:00 2001 From: Gardouille Date: Mon, 2 Oct 2023 17:02:36 +0200 Subject: [PATCH 41/47] fix: Refactor sed command to build manpages This also avoid "useless cat" and allow `just` command to build manpages. Resolves #458 Co-authored-by: gierens Co-authored-by: Gardouille --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 9fb026181..198c629c4 100644 --- a/Justfile +++ b/Justfile @@ -76,7 +76,7 @@ all-release: build-release test-release mkdir -p "${CARGO_TARGET_DIR:-target}/man" version=$(awk 'BEGIN { FS = "\"" } ; /^version/ { print $2 ; exit }' Cargo.toml); \ for page in eza.1 eza_colors.5 eza_colors-explanation.5; do \ - pandoc --standalone -f markdown -t man <(cat "man/${page}.md" | sed "s/\$version/v${version}/g") > "${CARGO_TARGET_DIR:-target}/man/${page}"; \ + sed "s/\$version/v${version}/g" "man/${page}.md" | pandoc --standalone -f markdown -t man > "${CARGO_TARGET_DIR:-target}/man/${page}"; \ done; # build and preview the main man page (eza.1) From ba0627831614f9c401e9c88d6c2f9ddb43fb7ed3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:42:26 +0000 Subject: [PATCH 42/47] build(deps): bump libc from 0.2.148 to 0.2.149 Bumps [libc](https://github.com/rust-lang/libc) from 0.2.148 to 0.2.149. - [Release notes](https://github.com/rust-lang/libc/releases) - [Commits](https://github.com/rust-lang/libc/compare/0.2.148...0.2.149) --- updated-dependencies: - dependency-name: libc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 875bc5a6b..c18a896cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,9 +618,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libgit2-sys" From 7eb616ebe1c43669a4b11c35a37654b3ede5bd2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:53:27 +0000 Subject: [PATCH 43/47] build(deps): bump DeterminateSystems/nix-installer-action from 4 to 5 Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 4 to 5. - [Release notes](https://github.com/determinatesystems/nix-installer-action/releases) - [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v4...v5) --- updated-dependencies: - dependency-name: DeterminateSystems/nix-installer-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flake.yml b/.github/workflows/flake.yml index 213d990dc..c7e22fa7d 100644 --- a/.github/workflows/flake.yml +++ b/.github/workflows/flake.yml @@ -16,6 +16,6 @@ jobs: - name: Check Nix flake inputs uses: DeterminateSystems/flake-checker-action@v5 # This action - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v4 + uses: DeterminateSystems/nix-installer-action@v5 - name: Nix Flake Check run: nix flake check --all-systems From 65b522b9aaefc4116e3d17c6f54b84d0a41588b4 Mon Sep 17 00:00:00 2001 From: Martin Fillon Date: Tue, 10 Oct 2023 11:17:39 +0200 Subject: [PATCH 44/47] feat(git): adding the EZA_OVERRIDE_GIT env var This var will override any --git or --git-repos --- man/eza.1.md | 4 ++++ src/options/vars.rs | 3 +++ src/options/view.rs | 14 ++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/man/eza.1.md b/man/eza.1.md index a72aaa291..578952d6f 100644 --- a/man/eza.1.md +++ b/man/eza.1.md @@ -274,6 +274,10 @@ Specifies the colour scheme used to highlight files based on their name and kind For more information on the format of these environment variables, see the [eza_colors.5.md](eza_colors.5.md) manual page. +## `EZA_OVERRIDE_GIT` + +Overrides any `--git` or `--git-repos` argument + EXIT STATUSES ============= diff --git a/src/options/vars.rs b/src/options/vars.rs index 44ecf0ac4..918974e18 100644 --- a/src/options/vars.rs +++ b/src/options/vars.rs @@ -52,6 +52,9 @@ pub static EZA_GRID_ROWS: &str = "EZA_GRID_ROWS"; pub static EXA_ICON_SPACING: &str = "EXA_ICON_SPACING"; pub static EZA_ICON_SPACING: &str = "EZA_ICON_SPACING"; +pub static EXA_OVERRIDE_GIT: &str = "EXA_OVERRIDE_GIT"; +pub static EZA_OVERRIDE_GIT: &str = "EZA_OVERRIDE_GIT"; + /// Mockable wrapper for `std::env::var_os`. pub trait Vars { fn get(&self, name: &'static str) -> Option; diff --git a/src/options/view.rs b/src/options/view.rs index 77ea0de74..02af0ff08 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -259,7 +259,7 @@ impl TableOptions { let time_format = TimeFormat::deduce(matches, vars)?; let size_format = SizeFormat::deduce(matches); let user_format = UserFormat::deduce(matches); - let columns = Columns::deduce(matches)?; + let columns = Columns::deduce(matches, vars)?; Ok(Self { size_format, time_format, @@ -270,13 +270,19 @@ impl TableOptions { } impl Columns { - fn deduce(matches: &Opts) -> Result { + fn deduce(matches: &Opts, vars: &V) -> Result { + use crate::options::vars; let time_types = TimeTypes::deduce(matches)?; - let git = matches.git > 0 && matches.no_git == 0; - let subdir_git_repos = matches.git_repos > 0 && matches.no_git == 0; + let no_git_env = vars + .get_with_fallback(vars::EXA_OVERRIDE_GIT, vars::EZA_OVERRIDE_GIT) + .is_some(); + + let git = matches.git > 0 && matches.no_git == 0 && !no_git_env; + let subdir_git_repos = matches.git_repos > 0 && matches.no_git == 0 && !no_git_env; let subdir_git_repos_no_stat = !subdir_git_repos && matches.git_repos_no_status > 0 && matches.no_git == 0; + && !no_git_env; let blocksize = matches.blocksize > 0; let group = matches.group > 0; From a12385a647a909c44ef7fb99b00f0fa07eed0e46 Mon Sep 17 00:00:00 2001 From: Chris Gorski Date: Mon, 9 Oct 2023 17:19:19 -0400 Subject: [PATCH 45/47] docs: Add missing options to man page and CLI --help info Updated the man page to include: - --dereference - --almost-all - --help - --version Additionally, added the following flags to the CLI --help information: - --dereference - --almost-all Updated shell completions. --- completions/bash/eza | 2 +- completions/nush/eza.nu | 2 +- completions/zsh/_eza | 2 +- man/eza.1.md | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/completions/bash/eza b/completions/bash/eza index df739d480..5da803d20 100644 --- a/completions/bash/eza +++ b/completions/bash/eza @@ -4,7 +4,7 @@ _eza() { prev=${COMP_WORDS[COMP_CWORD-1]} case "$prev" in - -'?'|--help|-v|--version) + --help|-v|--version) return ;; diff --git a/completions/nush/eza.nu b/completions/nush/eza.nu index 5094f8cea..67bf42c7b 100644 --- a/completions/nush/eza.nu +++ b/completions/nush/eza.nu @@ -1,6 +1,6 @@ export extern "eza" [ --version(-v) # Show version of eza - --help(-?) # Show list of command-line options + --help # Show list of command-line options --oneline(-1) # Display one entry per line --long(-l) # Display extended file metadata as a table --grid(-G) # Display entries in a grid diff --git a/completions/zsh/_eza b/completions/zsh/_eza index 63982e118..577a84ebf 100644 --- a/completions/zsh/_eza +++ b/completions/zsh/_eza @@ -11,7 +11,7 @@ __eza() { # `-S` for delimiting options with `--` like in `eza -- -a`. _arguments -s -S \ "(- *)"{-v,--version}"[Show version of eza]" \ - "(- *)"{-'\?',--help}"[Show list of command-line options]" \ + "(- *)"{--help}"[Show list of command-line options]" \ {-1,--oneline}"[Display one entry per line]" \ {-l,--long}"[Display extended file metadata as a table]" \ {-G,--grid}"[Display entries as a grid]" \ diff --git a/man/eza.1.md b/man/eza.1.md index 578952d6f..1819752b2 100644 --- a/man/eza.1.md +++ b/man/eza.1.md @@ -38,6 +38,16 @@ EXAMPLES : Displays a tree of files, three levels deep, as well as each file’s metadata. +META OPTIONS +=============== + +`--help` +: Show list of command-line options. + +`-v`, `--version` +: Show version of eza. + + DISPLAY OPTIONS =============== @@ -59,6 +69,9 @@ DISPLAY OPTIONS `-T`, `--tree` : Recurse into directories as a tree. +`-X`, `--dereference` +: Dereference symbolic links when displaying information. + `-x`, `--across` : Sort the grid across, rather than downwards. @@ -98,6 +111,9 @@ FILTERING AND SORTING OPTIONS : Show hidden and “dot” files. Use this twice to also show the ‘`.`’ and ‘`..`’ directories. +`-A`, `--almost-all` +: Equivalent to --all; included for compatibility with `ls -A`. + `-d`, `--list-dirs` : List directories as regular files, rather than recursing and listing their contents. From c426949fd61f005a145c4809f3ab5fcd8961d5ae Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 9 Oct 2023 21:03:05 +0200 Subject: [PATCH 46/47] feat(completions): add missing nu shell completions Signed-off-by: Sandro-Alessio Gierens --- completions/nush/eza.nu | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/completions/nush/eza.nu b/completions/nush/eza.nu index 67bf42c7b..9333ec1e1 100644 --- a/completions/nush/eza.nu +++ b/completions/nush/eza.nu @@ -8,6 +8,10 @@ export extern "eza" [ --recurse(-R) # Recurse into directories --tree(-T) # Recurse into directories as a tree --classify(-F) # Display type indicator by file names + --color # When to use terminal colours + --colour # When to use terminal colours + --color-scale # Highlight levels of file sizes distinctly + --colour-scale # Highlight levels of file sizes distinctly --icons # Display icons --no-icons # Don't display icons --no-quotes # Don't quote file names with spaces @@ -19,6 +23,7 @@ export extern "eza" [ --level(-L): string # Limit the depth of recursion --width(-w) # Limits column output of grid, 0 implies auto-width --reverse(-r) # Reverse the sort order + --sort(-s) # Which field to sort by --only-dirs(-D) # List only directories --only-files(-f) # List only files --binary(-b) # List file sizes with binary prefixes @@ -28,12 +33,14 @@ export extern "eza" [ --links(-H) # List each file's number of hard links --inode(-i) # List each file's inode number --blocksize(-S) # List each file's size of allocated file system blocks + --time(-t) -d # Which timestamp field to list --dereference(-X) # dereference symlinks for file information --modified(-m) # Use the modified timestamp field --numeric(-n) # List numeric user and group IDs. --changed # Use the changed timestamp field --accessed(-u) # Use the accessed timestamp field --created(-U) # Use the created timestamp field + --time-style # How to format timestamps --no-permissions # Suppress the permissions field --octal-permissions(-o) # List each file's permission in octal format --no-filesize # Suppress the filesize field From c7897bb1a99115f578d3e237e65d26d083b57f07 Mon Sep 17 00:00:00 2001 From: MartinFillon Date: Wed, 11 Oct 2023 23:12:20 +0200 Subject: [PATCH 47/47] fix: broken merge --- src/options/view.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/options/view.rs b/src/options/view.rs index 02af0ff08..a1af21ba8 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -280,9 +280,10 @@ impl Columns { let git = matches.git > 0 && matches.no_git == 0 && !no_git_env; let subdir_git_repos = matches.git_repos > 0 && matches.no_git == 0 && !no_git_env; - let subdir_git_repos_no_stat = - !subdir_git_repos && matches.git_repos_no_status > 0 && matches.no_git == 0; - && !no_git_env; + let subdir_git_repos_no_stat = !subdir_git_repos + && matches.git_repos_no_status > 0 + && matches.no_git == 0 + && !no_git_env; let blocksize = matches.blocksize > 0; let group = matches.group > 0;