diff --git a/.gitignore b/.gitignore index f418fbbf8..57eac7648 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,7 @@ stage out.gif tests/tmp +# Visual Studio Code +.vscode ## Dynamically generated tests/test_dir diff --git a/Cargo.lock b/Cargo.lock index 5cbf91801..c18a896cb 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" @@ -109,9 +133,9 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" @@ -127,11 +151,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -186,6 +211,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -194,8 +220,22 @@ version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" dependencies = [ + "anstream 0.5.0", "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]] @@ -360,7 +400,9 @@ version = "0.14.1" dependencies = [ "ansiterm", "chrono", + "clap", "criterion", + "gethostname", "git2", "glob", "lazy_static", @@ -404,14 +446,23 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "matches", "percent-encoding", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets", +] + [[package]] name = "git2" version = "0.18.1" @@ -444,6 +495,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[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" @@ -491,11 +548,10 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -538,9 +594,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jobserver" -version = "0.1.22" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] @@ -581,9 +637,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.2" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" dependencies = [ "cc", "libc", @@ -612,12 +668,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "matches" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" - [[package]] name = "memchr" version = "2.6.3" @@ -647,9 +697,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -684,20 +734,19 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "openssl-src" -version = "111.26.0+1.1.1u" +version = "300.1.3+3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" +checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.61" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313752393519e876837e09e1fa183ddef0be7735868dced3196f4472d536277f" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ - "autocfg", "cc", "libc", "openssl-src", @@ -774,9 +823,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plotters" @@ -808,9 +857,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1018,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", @@ -1040,14 +1089,20 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1559baff8a696add3322b9be3e940d433e7bb4e38d79017205fd37ff28b28e" dependencies = [ - "anstream", + "anstream 0.6.4", ] +[[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" +version = "2.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "59bf04c28bee9043ed9ea1e41afc0552288d3aba9c6efdd78903b802926f4879" dependencies = [ "proc-macro2", "quote", @@ -1124,18 +1179,18 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml_datetime" @@ -1177,24 +1232,21 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.17" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] @@ -1207,13 +1259,12 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "url" -version = "2.2.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -1235,9 +1286,9 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wait-timeout" diff --git a/Cargo.toml b/Cargo.toml index 649ad9431..517d3d0ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,9 @@ name = "eza" [dependencies] ansiterm = "0.12.2" -chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +gethostname = "0.4.3" +chrono = { version = "0.4.30", default-features = false, features = ["clock"] } +clap = { version = "4.4.2", features = ["derive", "string"] } glob = "0.3" lazy_static = "1.3" libc = "0.2" diff --git a/README.md b/README.md index cf7c2e688..2e97ae000 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,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. valid timestamp styles are ‘`default`’, ‘`iso`’, ‘`long-iso`’, ‘`full-iso`’, ‘`relative`', or you can use a `custom` style with '`+`' as prefix. (Ex: "`+%Y/%m/%d, %H:%M`" => "`2023/9/30, 12:00`"). [more about format syntax](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). - **--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 diff --git a/src/main.rs b/src/main.rs index dbba5e965..6d00088ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ #![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, IsTerminal, Write}; @@ -34,7 +35,8 @@ use log::*; use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; use crate::fs::{Dir, File}; -use crate::options::{vars, Options, OptionsResult, Vars}; +use crate::options::parser::Opts; +use crate::options::{vars, Options, Vars}; use crate::output::{details, escape, file_name, grid, grid_details, lines, Mode, View}; use crate::theme::Theme; @@ -57,68 +59,48 @@ fn main() { if let Err(e) = ansiterm::enable_ansi_support() { warn!("Failed to enable ANSI support: {}", e); } - let stdout_istty = io::stdout().is_terminal(); - 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(stdout_istty); - let exa = Exa { - options, - writer, - input_paths, - theme, - console_width, - git, - }; - - info!("matching on exa.run"); - match exa.run() { - Ok(exit_status) => { - trace!("exa.run: exit Ok(exit_status)"); - exit(exit_status); - } - - Err(e) if e.kind() == ErrorKind::BrokenPipe => { - warn!("Broken pipe error: {e}"); - exit(exits::SUCCESS); - } - - Err(e) => { - eprintln!("{e}"); - trace!("exa.run: exit RUNTIME_ERROR"); - exit(exits::RUNTIME_ERROR); - } - } + 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(".")); + } + let options = match Options::deduce(&cli, &LiveVars) { + Ok(o) => o, + Err(e) => { + eprintln!("{e}"); + exit(exits::OPTIONS_ERROR); } - - OptionsResult::Help(help_text) => { - print!("{help_text}"); + }; + + 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(stdout_istty); + let exa = Exa { + options, + writer, + input_paths, + theme, + console_width, + git, + }; + + 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/README.md b/src/options/README.md new file mode 100644 index 000000000..eecdbd9fd --- /dev/null +++ b/src/options/README.md @@ -0,0 +1,48 @@ +# 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 remember 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 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 environment var needed: + +```rust +pub fn deduce(matches: &Opts) -> Result +``` + +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 diff --git a/src/options/dir_action.rs b/src/options/dir_action.rs index cd62277f8..edd97e1b1 100644 --- a/src/options/dir_action.rs +++ b/src/options/dir_action.rs @@ -1,41 +1,47 @@ //! Parsing the options for `DirAction`. -use crate::options::parser::MatchedFlags; -use crate::options::{flags, NumberSource, OptionsError}; +use crate::options::OptionsError; use crate::fs::dir_action::{DirAction, RecurseOptions}; +use crate::options::parser::Opts; impl DirAction { /// Determine which action to perform when trying to list a directory. /// 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 { + if !recurse && !tree && matches.level.is_some() { return Err(OptionsError::Useless2( - &flags::LEVEL, - &flags::RECURSE, - &flags::TREE, + "--level".to_string(), + "--recurse".to_string(), + "--tree".to_string(), )); } else if recurse && as_file { - return Err(OptionsError::Conflict(&flags::RECURSE, &flags::LIST_DIRS)); + return Err(OptionsError::Conflict( + "--recurse".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) } else { @@ -49,24 +55,10 @@ 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, } } } @@ -74,61 +66,106 @@ impl RecurseOptions { #[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, + }) + ); } - // Default behaviour - test!(empty: DirAction <- []; Both => Ok(DirAction::List)); - - // Listing files as directories - test!(dirs_short: DirAction <- ["-d"]; Both => Ok(DirAction::AsFile)); - test!(dirs_long: DirAction <- ["--list-dirs"]; Both => Ok(DirAction::AsFile)); - - // 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 }))); - - // 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!(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))); - - // 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')))); + #[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() + }; + + assert!(DirAction::deduce(&matches, false, true).is_err()); + } + + #[test] + fn deduce_strict_recurse_and_as_file_option() { + let matches = Opts { + recurse: 1, + list_dirs: 1, + ..Opts::default() + }; + + assert!(DirAction::deduce(&matches, false, true).is_err()); + } + + #[test] + fn deduce_strict_tree_and_as_file_option() { + let matches = Opts { + tree: 1, + list_dirs: 1, + ..Opts::default() + }; + + 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() + }; + + 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 adc566511..83656f755 100644 --- a/src/options/error.rs +++ b/src/options/error.rs @@ -1,35 +1,28 @@ -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, @@ -41,78 +34,45 @@ 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 { - /// It came... from a command-line argument! - Arg(&'static Arg), - - /// It came... from the environment! - Env(&'static str), -} - -impl From for OptionsError { - fn from(error: glob::PatternError) -> Self { - Self::FailedGlobPattern(error.to_string()) - } + Var(String), + Arg(String), } 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}"), + Self::Var(s) => write!(f, "variable {s}"), + Self::Arg(s) => write!(f, "argument {s}"), } } } -impl fmt::Display for OptionsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use crate::options::parser::TakesValue; - - #[rustfmt::skip] - return 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") - } - } - 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"), - 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}"), - }; +impl From for OptionsError { + fn from(error: glob::PatternError) -> Self { + Self::FailedGlobPattern(error.to_string()) } } -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 +impl fmt::Display for OptionsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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::BadArgument(arg, attempt) => { + write!(f, "Argument {arg} doesn’t take {attempt}") } - Self::Parse(ParseError::NeedsValue { ref flag, .. }) if *flag == Flag::Short(b't') => { - Some("To sort newest files last, try \"--sort newest\", or just \"-snew\"") + 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}") } - _ => None, + 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}"), } } } diff --git a/src/options/file_name.rs b/src/options/file_name.rs index c69fa7c77..d586aef66 100644 --- a/src/options/file_name.rs +++ b/src/options/file_name.rs @@ -1,16 +1,16 @@ -use crate::options::parser::MatchedFlags; use crate::options::vars::{self, Vars}; -use crate::options::{flags, NumberSource, OptionsError}; +use crate::options::{NumberSource, OptionsError}; +use crate::options::parser::Opts; use crate::output::file_name::{Classify, EmbedHyperlinks, Options, QuoteStyle, ShowIcons}; 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 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, @@ -22,20 +22,17 @@ impl Options { } 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_with_fallback(vars::EZA_ICON_SPACING, vars::EXA_ICON_SPACING) @@ -44,10 +41,7 @@ impl ShowIcons { match columns.parse() { Ok(width) => Ok(Self::On(width)), Err(e) => { - let source = NumberSource::Env( - vars.source(vars::EZA_ICON_SPACING, vars::EXA_ICON_SPACING) - .unwrap(), - ); + let source = NumberSource::Var(vars::EXA_ICON_SPACING.to_string()); Err(OptionsError::FailedParse(columns, source, e)) } } @@ -58,23 +52,60 @@ 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 } } } impl EmbedHyperlinks { - fn deduce(matches: &MatchedFlags<'_>) -> Result { - let flagged = matches.has(&flags::HYPERLINK)?; - - if flagged { - Ok(Self::On) - } else { - Ok(Self::Off) + 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() }; + + assert_eq!(Classify::deduce(&matches), Classify::JustFilenames); } } diff --git a/src/options/filter.rs b/src/options/filter.rs index 745fda689..2304c5ebd 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -5,34 +5,33 @@ use crate::fs::filter::{ }; use crate::fs::DotFilter; -use crate::options::parser::MatchedFlags; -use crate::options::{flags, OptionsError}; +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 { - use FileFilterFlags as FFF; + pub fn deduce(matches: &Opts, strictness: bool) -> Result { let mut filter_flags: Vec = vec![]; - for (has, flag) in &[ - (matches.has(&flags::REVERSE)?, FFF::Reverse), - (matches.has(&flags::ONLY_DIRS)?, FFF::OnlyDirs), - (matches.has(&flags::ONLY_FILES)?, FFF::OnlyFiles), + 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(flag.clone()); + filter_flags.push(flags.clone()); } } - #[rustfmt::skip] - return Ok(Self { - list_dirs_first: matches.has(&flags::DIRS_FIRST)?, + Ok(Self { + list_dirs_first: matches.dirs_first > 0, flags: filter_flags, - sort_field: SortField::deduce(matches)?, - dot_filter: DotFilter::deduce(matches)?, - ignore_patterns: IgnorePatterns::deduce(matches)?, - git_ignore: GitIgnore::deduce(matches)?, - }); + sort_field: SortField::deduce(matches)?, + dot_filter: DotFilter::deduce(matches, strictness)?, + ignore_patterns: IgnorePatterns::deduce(matches)?, + git_ignore: GitIgnore::deduce(matches)?, + }) } } @@ -41,14 +40,17 @@ 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 { + 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())); + return Err(OptionsError::BadArgument( + "SORT".to_string(), + word.to_string_lossy().to_string(), + )); }; let field = match word { @@ -79,7 +81,7 @@ impl SortField { "type" => Self::FileType, "none" => Self::Unsorted, _ => { - return Err(OptionsError::BadArgument(&flags::SORT, word.into())); + return Err(OptionsError::BadArgument("SORT".to_string(), word.into())); } }; @@ -136,21 +138,21 @@ 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) } @@ -163,10 +165,10 @@ 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 { + let Some(ref inputs) = matches.ignore_glob else { return Ok(Self::empty()); }; @@ -184,128 +186,239 @@ 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 crate::options::flags; - use crate::options::parser::Flag; - use std::ffi::OsString; - - 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); - } - } + + #[test] + fn deduce_git_ignore_ok() { + let opts = Opts { + git_ignore: 1, + ..Opts::default() }; + assert_eq!(GitIgnore::deduce(&opts).unwrap(), GitIgnore::CheckAndIgnore); } - 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")))); + #[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()) + ); } - mod dot_filters { - use super::*; + #[test] + fn deduce_ignore_patterns() { + let opts = Opts { ..Opts::default() }; + assert_eq!( + IgnorePatterns::deduce(&opts).unwrap(), + IgnorePatterns::empty() + ); + } - // Default behaviour - test!(empty: DotFilter <- []; Both => Ok(DotFilter::JustFiles)); + #[test] + fn deduce_dot_filter_just_files() { + let opts = Opts { ..Opts::default() }; + assert_eq!( + DotFilter::deduce(&opts, false).unwrap(), + 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] + fn deduce_dot_filter_dotfiles() { + let opts = Opts { + all: 1, + ..Opts::default() + }; + assert_eq!( + DotFilter::deduce(&opts, false).unwrap(), + DotFilter::Dotfiles + ); + } - test!(all_all_3: DotFilter <- ["-aaa"]; Last => Ok(DotFilter::DotfilesAndDots)); - test!(all_all_4: DotFilter <- ["-aaa"]; Complain => Err(OptionsError::Conflict(&flags::ALL, &flags::ALL))); + #[test] + fn deduce_dot_filter_dotfiles_and_dots() { + let opts = Opts { + all: 2, + ..Opts::default() + }; + assert_eq!( + DotFilter::deduce(&opts, false).unwrap(), + DotFilter::DotfilesAndDots + ); + } - // --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)); + #[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 + ); + } - // --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)); + #[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()) + ); } - mod ignore_patterns { - use super::*; - use std::iter::FromIterator; + #[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) + ); + } - fn pat(string: &'static str) -> glob::Pattern { - glob::Pattern::new(string).unwrap() - } + #[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) + ); + } - // 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')))); + #[test] + fn deduce_sort_field_size() { + let opts = Opts { + sort: Some("size".into()), + ..Opts::default() + }; + assert_eq!(SortField::deduce(&opts).unwrap(), SortField::Size); } - mod git_ignores { - use super::*; + #[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!(off: GitIgnore <- []; Both => Ok(GitIgnore::Off)); - test!(on: GitIgnore <- ["--git-ignore"]; Both => Ok(GitIgnore::CheckAndIgnore)); + #[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()) + ); } } diff --git a/src/options/flags.rs b/src/options/flags.rs deleted file mode 100644 index 175a309ed..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 NO_QUOTES:Arg = Arg { short: None, long: "no-quotes",takes_value: TakesValue::Forbidden }; - -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, &NO_QUOTES, - - &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 16b174ea9..000000000 --- a/src/options/help.rs +++ /dev/null @@ -1,156 +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 - -X, --dereference dereference symbolic links when displaying information - -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) - --no-quotes don't quote file names with spaces - --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 - -A, --almost-all equivalent to --all; included for compatibility with `ls -A` - -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, or a custom style with '+' as prefix. Ex: '+%Y/%m/%d') - --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 80eb22229..a37f918cf 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -68,36 +68,25 @@ //! --grid --long` shouldn’t complain about `--long` being given twice when //! 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::options::parser::Opts; use crate::output::{details, grid_details, Mode, View}; use crate::theme::Options as ThemeOptions; mod dir_action; mod file_name; mod filter; -#[rustfmt::skip] // this module becomes unreadable with rustfmt -mod flags; +pub(crate) mod parser; mod theme; mod view; mod error; pub use self::error::{NumberSource, OptionsError}; -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)] @@ -120,43 +109,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}; - - #[rustfmt::skip] - let strictness = match vars.get_with_fallback(vars::EZA_STRICT, 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. @@ -184,20 +136,23 @@ 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 { - if cfg!(not(feature = "git")) - && matches - .has_where_any(|f| f.matches(&flags::GIT) || f.matches(&flags::GIT_IGNORE)) - .is_some() - { + pub fn deduce(matches: &Opts, vars: &V) -> Result { + 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 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), 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 filter = FileFilter::deduce(matches, strictness)?; let theme = ThemeOptions::deduce(matches, vars)?; Ok(Self { @@ -208,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 ee68f40d5..e7e16d697 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -1,767 +1,233 @@ -//! 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::{Choices, OptionsError}; - -/// 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)?; +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, + #[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, + /// 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, + ///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. + #[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)] + 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, + /// 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, + /// Don't Show quotes + #[arg(long = "no-quotes", action = clap::ArgAction::Count)] + pub no_quotes: u8, +} + +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, + mount: 0, + colour: None, + colour_scale: 0, + only_files: 0, + no_quotes: 0, } - - 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. - /// - /// 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. - /// - /// 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) - } - } - - // 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"]; - - #[rustfmt::skip] - 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 13422782f..76b6e767a 100644 --- a/src/options/theme.rs +++ b/src/options/theme.rs @@ -1,11 +1,12 @@ -use crate::options::parser::MatchedFlags; -use crate::options::{flags, vars, OptionsError, Vars}; +use crate::options::{vars, OptionsError, Vars}; use crate::theme::{ColourScale, Definitions, Options, UseColours}; +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() @@ -22,18 +23,26 @@ 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 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(), + )), + } + } + fn get_color(word: String) -> Result { if word == "always" { Ok(Self::Always) } else if word == "auto" || word == "automatic" { @@ -41,21 +50,17 @@ impl UseColours { } else if word == "never" { Ok(Self::Never) } else { - Err(OptionsError::BadArgument(&flags::COLOR, word.into())) + Err(OptionsError::BadArgument("--color".to_string(), word)) } } } 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 || matches.colour_scale > 0 { + return Self::Gradient; } + Self::Fixed } } @@ -72,145 +77,23 @@ impl Definitions { } #[cfg(test)] -mod terminal_test { +mod tests { use super::*; - use crate::options::flags; - use crate::options::parser::{Arg, Flag}; - use std::ffi::OsString; - - 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::EZA_COLORS || 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/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 40ba52779..a1af21ba8 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -1,6 +1,6 @@ use crate::fs::feature::xattr; -use crate::options::parser::MatchedFlags; -use crate::options::{flags, NumberSource, OptionsError, Vars}; +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::table::{Columns, Options as TableOptions, SizeFormat, TimeTypes, UserFormat}; @@ -8,11 +8,15 @@ use crate::output::time::TimeFormat; use crate::output::{details, grid, Mode, TerminalWidth, View}; 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, @@ -31,33 +35,26 @@ 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) - }); - - let Some(flag) = flag else { - Self::strict_check_long_flags(matches)?; - let grid = grid::Options::deduce(matches)?; + pub fn deduce( + matches: &Opts, + vars: &V, + strictness: bool, + ) -> Result { + let flag = matches.long > 0 || matches.oneline > 0 || matches.grid > 0 || matches.tree > 0; + + 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)?) - { - let _ = matches.has(&flags::LONG)?; - let details = details::Options::deduce_long(matches, vars)?; + if matches.long > 0 { + 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, @@ -71,128 +68,162 @@ 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, - &flags::MOUNTS, - ] { - 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(), + )); + } else if matches.mount > 0 { + return Err(OptionsError::Useless( + "--mount".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)?, - mounts: matches.has(&flags::MOUNTS)?, - }; - - Ok(details) + xattr: xattr::ENABLED && matches.extended > 0, + secattr: xattr::ENABLED && matches.security_context > 0, + mounts: matches.mount > 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)); - } else if matches.has(&flags::ONE_LINE)? { - return Err(OptionsError::Useless(&flags::ONE_LINE, 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.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)?, - mounts: matches.has(&flags::MOUNTS)?, + header: matches.header > 0, + xattr: xattr::ENABLED && matches.extended > 0, + secattr: xattr::ENABLED && matches.security_context > 0, + mounts: matches.mount > 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() { Ok(width) => 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)) } } @@ -213,10 +244,7 @@ impl RowThreshold { match columns.parse() { Ok(rows) => Ok(Self::MinimumRows(rows)), Err(e) => { - let source = NumberSource::Env( - vars.source(vars::EZA_GRID_ROWS, vars::EXA_GRID_ROWS) - .unwrap(), - ); + let source = NumberSource::Var(vars::EXA_GRID_ROWS.to_string()); Err(OptionsError::FailedParse(columns, source, e)) } } @@ -227,10 +255,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, vars)?; Ok(Self { size_format, @@ -242,7 +270,7 @@ impl TableOptions { } impl Columns { - fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { + fn deduce(matches: &Opts, vars: &V) -> Result { use crate::options::vars; let time_types = TimeTypes::deduce(matches)?; @@ -250,24 +278,23 @@ impl Columns { .get_with_fallback(vars::EXA_OVERRIDE_GIT, vars::EZA_OVERRIDE_GIT) .is_some(); - let git = matches.has(&flags::GIT)? && !matches.has(&flags::NO_GIT)? && !no_git_env; - let subdir_git_repos = - matches.has(&flags::GIT_REPOS)? && !matches.has(&flags::NO_GIT)? && !no_git_env; + 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.has(&flags::GIT_REPOS_NO_STAT)? - && !matches.has(&flags::NO_GIT)? + && matches.git_repos_no_status > 0 + && matches.no_git == 0 && !no_git_env; - 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, @@ -296,22 +323,28 @@ 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 + } } } impl TimeFormat { /// Determine how time should be formatted in timestamp columns. - fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { - let word = if let Some(w) = matches.get(&flags::TIME_STYLE)? { - w.to_os_string() + 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) { @@ -329,15 +362,22 @@ 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(), + )), } } } 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 + } } } @@ -352,14 +392,14 @@ 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; #[rustfmt::skip] let time_types = if no_time { @@ -371,14 +411,18 @@ impl TimeTypes { } } else if let Some(word) = possible_word { if modified { - return Err(OptionsError::Useless(&flags::MODIFIED, true, &flags::TIME)); - } else if changed { - return Err(OptionsError::Useless(&flags::CHANGED, true, &flags::TIME)); - } else if accessed { - return Err(OptionsError::Useless(&flags::ACCESSED, true, &flags::TIME)); - } else if created { - return Err(OptionsError::Useless(&flags::CREATED, true, &flags::TIME)); - } else if word == "mod" || word == "modified" { + return Err(OptionsError::Useless("--modified".to_string(), true, "--time".to_string())); + } + else if changed { + return Err(OptionsError::Useless("--changed".to_string(), true, "--time".to_string())); + } + else if accessed { + return Err(OptionsError::Useless("--accessed".to_string(), true, "--time".to_string())); + } + else if created { + 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 } } else if word == "ch" || word == "changed" { Self { modified: false, changed: true, accessed: false, created: false } @@ -386,8 +430,9 @@ impl TimeTypes { Self { modified: false, changed: false, accessed: true, created: false } } else if word == "cr" || word == "created" { Self { modified: false, changed: false, accessed: false, created: true } - } else { - return Err(OptionsError::BadArgument(&flags::TIME, word.into())); + } + else { + return Err(OptionsError::BadArgument("--time".to_string(), word.to_string_lossy().to_string())); } } else if modified || changed || accessed || created { Self { @@ -406,275 +451,255 @@ impl TimeTypes { #[cfg(test)] mod test { + use super::*; - use crate::options::flags; - use crate::options::parser::{Arg, Flag}; use std::ffi::OsString; - 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, - ]; - - #[allow(unused_macro_rules)] - 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); - } + #[test] + fn deduce_time_type() { + let matches = Opts { ..Opts::default() }; + + assert_eq!(TimeTypes::deduce(&matches).unwrap(), TimeTypes::default()); + } + + #[test] + fn deduce_time_type_modified() { + let matches = Opts { + modified: 1, + ..Opts::default() + }; + + assert_eq!( + TimeTypes::deduce(&matches).unwrap(), + TimeTypes { + modified: true, + ..TimeTypes::default() } + ); + } + + #[test] + fn deduce_time_type_changed() { + let matches = Opts { + changed: 1, + ..Opts::default() }; - ($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); - } + 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() }; - ($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), - } - } + assert_eq!( + TimeTypes::deduce(&matches).unwrap(), + TimeTypes { + accessed: true, + modified: false, + ..TimeTypes::default() } + ); + } + + #[test] + fn deduce_time_type_created() { + let matches = Opts { + created: 1, + ..Opts::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); - } + assert_eq!( + TimeTypes::deduce(&matches).unwrap(), + TimeTypes { + created: true, + modified: false, + ..TimeTypes::default() } + ); + } + + #[test] + fn deduce_time_type_mod_string() { + let matches = Opts { + time: Some(OsString::from("mod")), + ..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_ch_string() { + let matches = Opts { + time: Some(OsString::from("ch")), + ..Opts::default() }; + + assert_eq!( + TimeTypes::deduce(&matches).unwrap(), + TimeTypes { + changed: true, + modified: false, + ..TimeTypes::default() + } + ); } - mod size_formats { - use super::*; + #[test] + fn deduce_time_type_acc_string() { + let matches = Opts { + time: Some(OsString::from("acc")), + ..Opts::default() + }; - // Default behaviour - test!(empty: SizeFormat <- []; Both => Ok(SizeFormat::DecimalBytes)); + assert_eq!( + TimeTypes::deduce(&matches).unwrap(), + TimeTypes { + accessed: true, + modified: false, + ..TimeTypes::default() + } + ); + } - // Individual flags - test!(binary: SizeFormat <- ["--binary"]; Both => Ok(SizeFormat::BinaryBytes)); - test!(bytes: SizeFormat <- ["--bytes"]; Both => Ok(SizeFormat::JustBytes)); + #[test] + fn deduce_time_type_cr_string() { + let matches = Opts { + time: Some(OsString::from("cr")), + ..Opts::default() + }; - // 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)); + assert_eq!( + TimeTypes::deduce(&matches).unwrap(), + TimeTypes { + created: true, + modified: false, + ..TimeTypes::default() + } + ); + } - 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"))); + #[test] + fn deduce_time_type_useless_mod() { + let matches = Opts { + time: Some(OsString::from("mod")), + modified: 1, + ..Opts::default() + }; + + assert!(TimeTypes::deduce(&matches).is_err()); + } + + #[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()); + } + + #[test] + fn deduce_time_type_useless_acc() { + let matches = Opts { + time: Some(OsString::from("acc")), + accessed: 1, + ..Opts::default() + }; + + 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() + }; + + assert!(TimeTypes::deduce(&matches).is_err()); } - mod time_formats { - use super::*; + #[test] + fn deduce_user_format() { + let matches = Opts { ..Opts::default() }; + + assert_eq!(UserFormat::deduce(&matches), UserFormat::Name); + } - // These tests use pattern matching because TimeFormat doesn’t - // implement PartialEq. + #[test] + fn deduce_user_format_numeric() { + let matches = Opts { + numeric: 1, + ..Opts::default() + }; - // Default behaviour - test!(empty: TimeFormat <- [], None; Both => like Ok(TimeFormat::DefaultFormat)); + assert_eq!(UserFormat::deduce(&matches), UserFormat::Numeric); + } - // 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!(custom_style: TimeFormat <- ["--time-style", "+%Y/%m/%d"], None; Both => like Ok(TimeFormat::Custom { .. })); - test!(bad_custom_style: TimeFormat <- ["--time-style", "%Y/%m/%d"], None; Both => err OptionsError::BadArgument(&flags::TIME_STYLE, OsString::from("%Y/%m/%d"))); + #[test] + fn deduce_size_format() { + let matches = Opts { ..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!(SizeFormat::deduce(&matches), SizeFormat::DecimalBytes); + } - 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_size_format_binary() { + let matches = Opts { + binary: 1, + ..Opts::default() + }; - // Errors - test!(daily: TimeFormat <- ["--time-style=24-hour"], None; Both => err OptionsError::BadArgument(&flags::TIME_STYLE, OsString::from("24-hour"))); + assert_eq!(SizeFormat::deduce(&matches), SizeFormat::BinaryBytes); + } - // `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_size_format_bytes() { + let matches = Opts { + bytes: 1, + ..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!(SizeFormat::deduce(&matches), SizeFormat::JustBytes); } - mod time_types { - use super::*; + #[test] + fn deduce_dtails_tree() { + let matches = Opts { + tree: 1, + ..Opts::default() + }; - // Default behaviour - test!(empty: TimeTypes <- []; Both => Ok(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 })); - - // 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 })); - - // 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 })); - - // 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 })); - - // Multiples - test!(time_uu: TimeTypes <- ["-u", "--modified"]; Both => Ok(TimeTypes { modified: true, changed: false, accessed: true, created: false })); - - // 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"))); - - // 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'))); - } - - mod views { - use super::*; - - use crate::output::grid::Options as GridOptions; - - // Default - test!(empty: Mode <- [], None; Both => like Ok(Mode::Grid(_))); - - // 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, .. }))); - - // Lines views - test!(lines: Mode <- ["--oneline"], None; Both => like Ok(Mode::Lines)); - test!(prima: Mode <- ["-1"], None; Both => like Ok(Mode::Lines)); - - // Details views - test!(long: Mode <- ["--long"], None; Both => like Ok(Mode::Details(_))); - test!(ell: Mode <- ["-l"], None; Both => like Ok(Mode::Details(_))); - - // Grid-details views - test!(lid: Mode <- ["--long", "--grid"], None; Both => like Ok(Mode::GridDetails(_))); - test!(leg: Mode <- ["-lG"], None; Both => like Ok(Mode::GridDetails(_))); - - // Options that do nothing with --long - test!(long_across: Mode <- ["--long", "--across"], None; Last => like Ok(Mode::Details(_))); - - // 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(_))); - - #[cfg(feature = "git")] - test!(just_git: Mode <- ["--git"], None; Last => like Ok(Mode::Grid(_))); - - 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)); - - #[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, + mounts: matches.mount > 0, + } + ); } } 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"]; }; }