From 0fefc78cbb646a77a8e2ba56e5bc31a36d0bc628 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 13 Sep 2017 23:26:06 +0100 Subject: [PATCH 1/5] Add more modified date aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I don’t really see the modified date as the *modified* date, rather just the *date* field, because it’s the date field I refer to like 99.9% of the time. So now it has aliases to match. Also are included are aliases for the reverse order, because I’d rather write “new” than “the reverse of old”. --- README.md | 2 +- contrib/completions.bash | 2 +- contrib/completions.fish | 5 +++++ contrib/completions.zsh | 2 +- contrib/man/exa.1 | 1 + src/fs/filter.rs | 18 +++++++++++++++--- src/options/filter.rs | 10 +++++++++- src/options/help.rs | 3 ++- xtests/dates_deifidom | 4 ++++ xtests/help | 3 ++- xtests/run.sh | 4 +++- 11 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 xtests/dates_deifidom diff --git a/README.md b/README.md index c72ce280..919a9869 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ These options are available when running with --long (`-l`): - **--time-style**: how to format timestamps - Valid **--color** options are **always**, **automatic**, and **never**. -- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. +- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **oldest**, while its reverse has the aliases **age** and **newest**. - Valid time fields are **modified**, **accessed**, and **created**. - Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**. diff --git a/contrib/completions.bash b/contrib/completions.bash index 9a54c23b..c058ba43 100644 --- a/contrib/completions.bash +++ b/contrib/completions.bash @@ -14,7 +14,7 @@ _exa() ;; -s|--sort) - COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension modified accessed created type inode none --' -- "$cur" ) ) + COMPREPLY=( $( compgen -W 'name filename Name Filename size filesize extension Extension date time modified accessed created type inode oldest newest age none --' -- "$cur" ) ) return ;; diff --git a/contrib/completions.fish b/contrib/completions.fish index 57462b05..8cf43b0f 100644 --- a/contrib/completions.fish +++ b/contrib/completions.fish @@ -23,7 +23,9 @@ complete -c exa -s 'L' -l 'level' -d "Limit the depth of recursion" -a "1 2 complete -c exa -s 'r' -l 'reverse' -d "Reverse the sort order" complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a " accessed\t'Sort by file accessed time' + age\t'Sort by file modified time (newest first)' created\t'Sort by file modified time' + date\t'Sort by file modified time' ext\t'Sort by file extension' Ext\t'Sort by file extension (uppercase first)' extension\t'Sort by file extension' @@ -34,8 +36,11 @@ complete -c exa -s 's' -l 'sort' -x -d "Which field to sort by" -a " modified\t'Sort by file modified time' name\t'Sort by filename' Name\t'Sort by filename (uppercase first)' + newest\t'Sort by file modified time (newest first)' none\t'Do not sort files at all' + oldest\t'Sort by file modified time' size\t'Sort by file size' + time\t'Sort by file modified time' type\t'Sort by file type' " diff --git a/contrib/completions.zsh b/contrib/completions.zsh index 0f5f04d9..6536e2c6 100644 --- a/contrib/completions.zsh +++ b/contrib/completions.zsh @@ -18,7 +18,7 @@ __exa() { {-d,--list-dirs}"[List directories like regular files]" \ {-L,--level}"+[Limit the depth of recursion]" \ {-r,--reverse}"[Reverse the sort order]" \ - {-s,--sort}"[Which field to sort by]:(sort field):(accessed created extension Extension filename Filename inode modified name Name none size type)" \ + {-s,--sort}"[Which field to sort by]:(sort field):(accessed age created date extension Extension filename Filename inode modified oldest name Name newest none size time type)" \ {-I,--ignore-glob}"[Ignore files that match these glob patterns]" \ {-b,--binary}"[List file sizes with binary prefixes]" \ {-B,--bytes}"[List file sizes in bytes, without any prefixes]" \ diff --git a/contrib/man/exa.1 b/contrib/man/exa.1 index 86d0703e..fb201e8d 100644 --- a/contrib/man/exa.1 +++ b/contrib/man/exa.1 @@ -77,6 +77,7 @@ reverse the sort order .B \-s, \-\-sort=\f[I]SORT_FIELD\f[] which field to sort by. Valid fields are name, Name, extension, Extension, size, modified, accessed, created, inode, type, and none. +The modified field has the aliases date, time, and oldest, and its reverse order has the aliases age and newest. Fields starting with a capital letter will sort uppercase before lowercase: 'A' then 'B' then 'a' then 'b'. Fields starting with a lowercase letter will mix them: 'A' then 'a' then 'B' then 'b'. .RS diff --git a/src/fs/filter.rs b/src/fs/filter.rs index 279c5d15..12d54aa9 100644 --- a/src/fs/filter.rs +++ b/src/fs/filter.rs @@ -142,14 +142,14 @@ pub enum SortField { /// files were created on the filesystem, more or less. FileInode, - /// The time this file was modified (the “mtime”). + /// The time the file was modified (the “mtime”). /// /// As this is stored as a Unix timestamp, rather than a local time /// instance, the time zone does not matter and will only be used to /// display the timestamps, not compare them. ModifiedDate, - /// The time file was accessed (the “atime”). + /// The time the was accessed (the “atime”). /// /// Oddly enough, this field rarely holds the *actual* accessed time. /// Recording a read time means writing to the file each time it’s read @@ -159,7 +159,7 @@ pub enum SortField { /// http://unix.stackexchange.com/a/8842 AccessedDate, - /// The time this file was changed or created (the “ctime”). + /// The time the file was changed or created (the “ctime”). /// /// Contrary to the name, this field is used to mark the time when a /// file’s metadata changed -- its permissions, owners, or link count. @@ -173,6 +173,17 @@ pub enum SortField { /// Files are ordered according to the `PartialOrd` implementation of /// `fs::fields::Type`, so changing that will change this. FileType, + + /// The “age” of the file, which is the time it was modified sorted + /// backwards. The reverse of the `ModifiedDate` ordering! + /// + /// It turns out that listing the most-recently-modified files first is a + /// common-enough use case that it deserves its own variant. This would be + /// implemented by just using the modified date and setting the reverse + /// flag, but this would make reversing *that* output not work, which is + /// bad, even though that’s kind of nonsensical. So it’s its own variant + /// that can be reversed like usual. + ModifiedAge, } /// Whether a field should be sorted case-sensitively or case-insensitively. @@ -219,6 +230,7 @@ impl SortField { SortField::ModifiedDate => a.metadata.mtime().cmp(&b.metadata.mtime()), SortField::AccessedDate => a.metadata.atime().cmp(&b.metadata.atime()), SortField::CreatedDate => a.metadata.ctime().cmp(&b.metadata.ctime()), + SortField::ModifiedAge => b.metadata.mtime().cmp(&a.metadata.mtime()), // flip b and a SortField::FileType => match a.type_char().cmp(&b.type_char()) { // todo: this recomputes Ordering::Equal => natord::compare(&*a.name, &*b.name), diff --git a/src/options/filter.rs b/src/options/filter.rs index bfe4da71..d9613686 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -53,9 +53,12 @@ impl SortField { else if word == "Ext" || word == "Extension" { Ok(SortField::Extension(SortCase::ABCabc)) } - else if word == "mod" || word == "modified" { + else if word == "date" || word == "time" || word == "mod" || word == "modified" || word == "old" || word == "oldest" { Ok(SortField::ModifiedDate) } + else if word == "age" || word == "new" || word == "newest" { + Ok(SortField::ModifiedAge) + } else if word == "acc" || word == "accessed" { Ok(SortField::AccessedDate) } @@ -216,6 +219,11 @@ mod test { 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", "old"]; Both => Ok(SortField::ModifiedDate)); + test!(oldest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedDate)); + test!(new: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedAge)); + test!(newest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedAge)); + test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge)); // Errors test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::bad_argument(&flags::SORT, &os("colour"), super::SORTS))); diff --git a/src/options/help.rs b/src/options/help.rs index ed564689..e71492cc 100644 --- a/src/options/help.rs +++ b/src/options/help.rs @@ -28,7 +28,8 @@ FILTERING AND SORTING OPTIONS --group-directories-first list directories before other files -I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore Valid sort fields: name, Name, extension, Extension, size, type, - modified, accessed, created, inode, none + modified, accessed, created, inode, and none. + date, time, old, and new all refer to modified. "##; static LONG_OPTIONS: &str = r##" diff --git a/xtests/dates_deifidom b/xtests/dates_deifidom new file mode 100644 index 00000000..d16bf181 --- /dev/null +++ b/xtests/dates_deifidom @@ -0,0 +1,4 @@ +Permissions Size User Date Modified Name +.rw-rw-r-- 0 cassowary 22 Dec 2009 plum +.rw-rw-r-- 0 cassowary 15 Jun 2006 peach +.rw-rw-r-- 0 cassowary  3 Mar 2003 pear diff --git a/xtests/help b/xtests/help index 9206d8cb..50eb9899 100644 --- a/xtests/help +++ b/xtests/help @@ -23,7 +23,8 @@ FILTERING AND SORTING OPTIONS --group-directories-first list directories before other files -I, --ignore-glob GLOBS glob patterns (pipe-separated) of files to ignore Valid sort fields: name, Name, extension, Extension, size, type, - modified, accessed, created, inode, none + 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 diff --git a/xtests/run.sh b/xtests/run.sh index 143d7a58..572c3b79 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -143,9 +143,11 @@ $exa $testcases/file-names-exts/music.* -I "*.OGG|*.mp3" -1 2>&1 | diff -q - $re # Dates and times $exa $testcases/dates -lh --accessed --sort=accessed 2>&1 | diff -q - $results/dates_accessed || exit 1 $exa $testcases/dates -lh --sort=modified 2>&1 | diff -q - $results/dates_modified || exit 1 +$exa $testcases/dates -lh -r --sort=newest 2>&1 | diff -q - $results/dates_modified || exit 1 +$exa $testcases/dates -lh --sort=newest 2>&1 | diff -q - $results/dates_deifidom || exit 1 $exa $testcases/dates -l --time-style=long-iso 2>&1 | diff -q - $results/dates_long_iso || exit 1 $exa $testcases/dates -l --time-style=full-iso 2>&1 | diff -q - $results/dates_full_iso || exit 1 -$exa $testcases/dates -l --time-style=iso 2>&1 | diff -q - $results/dates_iso || exit 1 +$exa $testcases/dates -l --time-style=iso 2>&1 | diff -q - $results/dates_iso || exit 1 # Locales # These two are used in particular because they have 5-long and 4-long From 43bbf004781f06d62acf3952a4c5dd343ce1079a Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 13 Sep 2017 23:47:19 +0100 Subject: [PATCH 2/5] =?UTF-8?q?Show=20a=20warning=20when=20running=20?= =?UTF-8?q?=E2=80=98exa=20-ltr=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raised in #243 and #284. exa isn’t able to override the -t option like this, so the least it can do is detect that case (which is going to be an error case anyway) and show a suggestion. --- src/bin/main.rs | 8 +++++++- src/options/misfire.rs | 14 +++++++++++++- xtests/error_ltr | 2 ++ xtests/run.sh | 3 +++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 xtests/error_ltr diff --git a/src/bin/main.rs b/src/bin/main.rs index 0119ee56..7e4a0821 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -28,7 +28,13 @@ fn main() { }, Err(ref e) if e.is_error() => { - writeln!(stderr(), "{}", e).unwrap(); + let mut stderr = stderr(); + writeln!(stderr, "{}", e).unwrap(); + + if let Some(s) = e.suggestion() { + let _ = writeln!(stderr, "{}", s); + } + exit(exits::OPTIONS_ERROR); }, diff --git a/src/options/misfire.rs b/src/options/misfire.rs index 1b28179d..42e4bc2f 100644 --- a/src/options/misfire.rs +++ b/src/options/misfire.rs @@ -4,7 +4,7 @@ use std::num::ParseIntError; use glob; -use options::{HelpString, VersionString}; +use options::{flags, HelpString, VersionString}; use options::parser::{Arg, Flag, ParseError}; @@ -120,3 +120,15 @@ impl fmt::Display for ParseError { } } } + +impl Misfire { + pub fn suggestion(&self) -> Option<&'static str> { + if let Misfire::BadArgument(ref time, ref r, ref _choices) = *self { + if *time == &flags::TIME && r == "r" { + return Some("To sort newest files first, try \"--sort modified\", or just \"-stime\""); + } + } + + None + } +} diff --git a/xtests/error_ltr b/xtests/error_ltr new file mode 100644 index 00000000..84bbe475 --- /dev/null +++ b/xtests/error_ltr @@ -0,0 +1,2 @@ +Option --time (-t) has no "r" setting (choices: modified, accessed, created) +To sort newest files first, try "--sort modified", or just "-stime" diff --git a/xtests/run.sh b/xtests/run.sh index 572c3b79..e2164edd 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -263,6 +263,9 @@ $exa -l --long 2>&1 | diff -q - $results/error_duplicate || exit 1 $exa -ll 2>&1 | diff -q - $results/error_twice || exit 1 $exa -l --time-style=24 2>&1 | diff -q - $results/error_setting || exit 1 +# Error suggestions +$exa -ltr 2>&1 | diff -q - $results/error_ltr || exit 1 + # Debug mode # (uses an empty directory so it prints nothing to stdout) From 1824313cda6dda521973fa32bd8f3090520fe077 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Wed, 13 Sep 2017 23:49:30 +0100 Subject: [PATCH 3/5] Put misfire.rs in a nicer order The main type is now at the top. --- src/options/misfire.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/options/misfire.rs b/src/options/misfire.rs index 42e4bc2f..b2a716e6 100644 --- a/src/options/misfire.rs +++ b/src/options/misfire.rs @@ -8,16 +8,6 @@ use options::{flags, HelpString, VersionString}; use options::parser::{Arg, Flag, ParseError}; -/// A list of legal choices for an argument-taking option -#[derive(PartialEq, Debug)] -pub struct Choices(&'static [&'static str]); - -impl fmt::Display for Choices { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "choices: {}", self.0.join(", ")) - } -} - /// A **misfire** is a thing that can happen instead of listing files -- a /// catch-all for anything outside the program’s normal execution. #[derive(PartialEq, Debug)] @@ -132,3 +122,14 @@ impl Misfire { None } } + + +/// A list of legal choices for an argument-taking option. +#[derive(PartialEq, Debug)] +pub struct Choices(&'static [&'static str]); + +impl fmt::Display for Choices { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "choices: {}", self.0.join(", ")) + } +} From a8bf99067449ef969cbb6cff1d376ec8556a6a18 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 14 Sep 2017 01:22:37 +0100 Subject: [PATCH 4/5] Tie value suggestions to their arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes the definition of Arg so that it knows about which values it can accept, and can display them in the help text. They were already being shown in the help text, but they were passed in separately, so one argument could show two different sets of options if it wanted. Now, the argument itself knows whether there are suggestions, so it doesn’t have to be passed in separately. This means we can use it for other things, including listing choices when an option is missed out, without having to repeat the list. With Misfire::BadArgument now only having two fields, it’s not worth using a constructor function anymore. --- src/options/filter.rs | 14 ++------- src/options/flags.rs | 22 +++++++++----- src/options/misfire.rs | 33 ++++++++++---------- src/options/parser.rs | 69 ++++++++++++++++++++++++++++-------------- src/options/style.rs | 13 ++------ src/options/view.rs | 21 +++---------- xtests/error_value | 2 +- 7 files changed, 88 insertions(+), 86 deletions(-) diff --git a/src/options/filter.rs b/src/options/filter.rs index d9613686..6d4b2f5e 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -21,10 +21,6 @@ impl FileFilter { } } -const SORTS: &[&str] = &[ "name", "Name", "size", "extension", - "Extension", "modified", "accessed", - "created", "inode", "type", "none" ]; - impl SortField { /// Determines which sort field to use based on the `--sort` argument. @@ -75,7 +71,7 @@ impl SortField { Ok(SortField::Unsorted) } else { - Err(Misfire::bad_argument(&flags::SORT, word, SORTS)) + Err(Misfire::BadArgument(&flags::SORT, word.into())) } } } @@ -185,12 +181,6 @@ mod test { use options::flags; use options::parser::Flag; - pub fn os(input: &'static str) -> OsString { - let mut os = OsString::new(); - os.push(input); - os - } - macro_rules! test { ($name:ident: $type:ident <- $inputs:expr; $stricts:expr => $result:expr) => { #[test] @@ -226,7 +216,7 @@ mod test { test!(age: SortField <- ["-sage"]; Both => Ok(SortField::ModifiedAge)); // Errors - test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::bad_argument(&flags::SORT, &os("colour"), super::SORTS))); + test!(error: SortField <- ["--sort=colour"]; Both => Err(Misfire::BadArgument(&flags::SORT, OsString::from("colour")))); // Overriding test!(overridden: SortField <- ["--sort=cr", "--sort", "mod"]; Last => Ok(SortField::ModifiedDate)); diff --git a/src/options/flags.rs b/src/options/flags.rs index 8f24dab7..3f8eb201 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -1,4 +1,4 @@ -use options::parser::{Arg, Args, TakesValue}; +use options::parser::{Arg, Args, Values, TakesValue}; // exa options @@ -14,8 +14,9 @@ pub static RECURSE: Arg = Arg { short: Some(b'R'), long: "recurse", takes_valu 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 COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary }; -pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary }; +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 }; @@ -23,11 +24,14 @@ pub static COLOUR_SCALE: Arg = Arg { short: None, long: "colour-scale", takes_va // filtering and sorting options pub static ALL: Arg = Arg { short: Some(b'a'), long: "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 }; +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 }; -pub static IGNORE_GLOB: Arg = Arg { short: Some(b'I'), long: "ignore-glob", takes_value: TakesValue::Necessary }; +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 DIRS_FIRST: Arg = Arg { short: None, long: "group-directories-first", takes_value: TakesValue::Forbidden }; +const SORTS: Values = &[ "name", "Name", "size", "extension", + "Extension", "modified", "accessed", + "created", "inode", "type", "none" ]; // display options pub static BINARY: Arg = Arg { short: Some(b'b'), long: "binary", takes_value: TakesValue::Forbidden }; @@ -38,10 +42,12 @@ pub static INODE: Arg = Arg { short: Some(b'i'), long: "inode", takes_ 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 BLOCKS: Arg = Arg { short: Some(b'S'), long: "blocks", takes_value: TakesValue::Forbidden }; -pub static TIME: Arg = Arg { short: Some(b't'), long: "time", takes_value: TakesValue::Necessary }; +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 }; +pub static TIME_STYLE: Arg = Arg { short: None, long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) }; +const TIMES: Values = &["modified", "accessed", "created"]; +const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"]; // optional feature options pub static GIT: Arg = Arg { short: None, long: "git", takes_value: TakesValue::Forbidden }; diff --git a/src/options/misfire.rs b/src/options/misfire.rs index b2a716e6..58c1ef42 100644 --- a/src/options/misfire.rs +++ b/src/options/misfire.rs @@ -1,4 +1,4 @@ -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; use std::fmt; use std::num::ParseIntError; @@ -17,7 +17,7 @@ pub enum Misfire { InvalidOptions(ParseError), /// The user supplied an illegal choice to an Argument. - BadArgument(&'static Arg, OsString, Choices), + BadArgument(&'static Arg, OsString), /// The user asked for help. This isn’t strictly an error, which is why /// this enum isn’t named Error! @@ -60,14 +60,6 @@ impl Misfire { _ => true, } } - - /// The Misfire that happens when an option gets given the wrong - /// argument. This has to use one of the `getopts` failure - /// variants--it’s meant to take just an option name, rather than an - /// option *and* an argument, but it works just as well. - pub fn bad_argument(option: &'static Arg, otherwise: &OsStr, legal: &'static [&'static str]) -> Misfire { - Misfire::BadArgument(option, otherwise.to_os_string(), Choices(legal)) - } } impl From for Misfire { @@ -78,10 +70,18 @@ impl From for Misfire { impl fmt::Display for Misfire { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use options::parser::TakesValue; use self::Misfire::*; match *self { - BadArgument(ref a, ref b, ref c) => write!(f, "Option {} has no {:?} setting ({})", a, b, c), + BadArgument(ref arg, ref attempt) => { + if let TakesValue::Necessary(Some(values)) = arg.takes_value { + write!(f, "Option {} has no {:?} setting ({})", arg, attempt, Choices(values)) + } + else { + write!(f, "Option {} has no {:?} setting", arg, attempt) + } + }, InvalidOptions(ref e) => write!(f, "{}", e), Help(ref text) => write!(f, "{}", text), Version(ref version) => write!(f, "{}", version), @@ -103,17 +103,18 @@ impl fmt::Display for ParseError { use self::ParseError::*; match *self { - NeedsValue { ref flag } => write!(f, "Flag {} needs a value", flag), - ForbiddenValue { ref flag } => write!(f, "Flag {} cannot take a value", flag), - UnknownShortArgument { ref attempt } => write!(f, "Unknown argument -{}", *attempt as char), - UnknownArgument { ref attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()), + NeedsValue { ref flag, values: None } => write!(f, "Flag {} needs a value", flag), + NeedsValue { ref flag, values: Some(cs) } => write!(f, "Flag {} needs a value ({})", flag, Choices(cs)), + ForbiddenValue { ref flag } => write!(f, "Flag {} cannot take a value", flag), + UnknownShortArgument { ref attempt } => write!(f, "Unknown argument -{}", *attempt as char), + UnknownArgument { ref attempt } => write!(f, "Unknown argument --{}", attempt.to_string_lossy()), } } } impl Misfire { pub fn suggestion(&self) -> Option<&'static str> { - if let Misfire::BadArgument(ref time, ref r, ref _choices) = *self { + if let Misfire::BadArgument(ref time, ref r) = *self { if *time == &flags::TIME && r == "r" { return Some("To sort newest files first, try \"--sort modified\", or just \"-stime\""); } diff --git a/src/options/parser.rs b/src/options/parser.rs index 29bd709f..f84f15e9 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -43,6 +43,13 @@ pub type ShortArg = u8; /// 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, Debug, Clone)] @@ -88,7 +95,9 @@ pub enum Strictness { pub enum TakesValue { /// This flag has to be followed by a value. - Necessary, + /// 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, @@ -171,8 +180,8 @@ impl Args { let arg = self.lookup_long(before)?; let flag = Flag::Long(arg.long); match arg.takes_value { - Necessary => result_flags.push((flag, Some(after))), - Forbidden => return Err(ParseError::ForbiddenValue { flag }) + Necessary(_) => result_flags.push((flag, Some(after))), + Forbidden => return Err(ParseError::ForbiddenValue { flag }) } } @@ -182,13 +191,13 @@ impl Args { let arg = self.lookup_long(long_arg_name)?; let flag = Flag::Long(arg.long); match arg.takes_value { - Forbidden => result_flags.push((flag, None)), - Necessary => { + Forbidden => result_flags.push((flag, None)), + Necessary(values) => { if let Some(next_arg) = inputs.next() { result_flags.push((flag, Some(next_arg))); } else { - return Err(ParseError::NeedsValue { flag }) + return Err(ParseError::NeedsValue { flag, values }) } } } @@ -210,7 +219,7 @@ impl Args { // -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 + // 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) = before.as_bytes().split_last().unwrap(); @@ -220,8 +229,8 @@ impl Args { let arg = self.lookup_short(*byte)?; let flag = Flag::Short(*byte); match arg.takes_value { - Forbidden => result_flags.push((flag, None)), - Necessary => return Err(ParseError::NeedsValue { flag }) + Forbidden => result_flags.push((flag, None)), + Necessary(values) => return Err(ParseError::NeedsValue { flag, values }) } } @@ -229,15 +238,15 @@ impl Args { let arg = self.lookup_short(*arg_with_value)?; let flag = Flag::Short(arg.short.unwrap()); match arg.takes_value { - Necessary => result_flags.push((flag, Some(after))), - Forbidden => return Err(ParseError::ForbiddenValue { flag }) + Necessary(_) => result_flags.push((flag, Some(after))), + 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 + // its value, and if there’s no rest of the string, then it // uses the next one in the iterator. // // -a => ‘a’ @@ -251,8 +260,8 @@ impl Args { let arg = self.lookup_short(*byte)?; let flag = Flag::Short(*byte); match arg.takes_value { - Forbidden => result_flags.push((flag, None)), - Necessary => { + Forbidden => result_flags.push((flag, None)), + Necessary(values) => { if index < bytes.len() - 1 { let remnants = &bytes[index+1 ..]; result_flags.push((flag, Some(OsStr::from_bytes(remnants)))); @@ -262,7 +271,7 @@ impl Args { result_flags.push((flag, Some(next_arg))); } else { - return Err(ParseError::NeedsValue { flag }) + return Err(ParseError::NeedsValue { flag, values }) } } } @@ -366,7 +375,7 @@ impl<'a> MatchedFlags<'a> { } /// 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 + /// 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. @@ -407,15 +416,15 @@ impl<'a> MatchedFlags<'a> { } -/// A problem with the user's input that meant it couldn't be parsed into a +/// A problem with the user’s input that meant it couldn’t be parsed into a /// coherent list of arguments. #[derive(PartialEq, Debug)] pub enum ParseError { /// A flag that has to take a value was not given one. - NeedsValue { flag: Flag }, + NeedsValue { flag: Flag, values: Option }, - /// A flag that can't take a value *was* given one. + /// 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 @@ -543,10 +552,13 @@ mod parse_test { }; } + const SUGGESTIONS: Values = &[ "example" ]; + static TEST_ARGS: &[&Arg] = &[ &Arg { short: Some(b'l'), long: "long", takes_value: TakesValue::Forbidden }, &Arg { short: Some(b'v'), long: "verbose", takes_value: TakesValue::Forbidden }, - &Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary } + &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)) } ]; @@ -569,10 +581,15 @@ mod parse_test { // 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") }); + 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) ]); @@ -582,13 +599,19 @@ mod parse_test { // 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') }); + 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: os("quiet") }); @@ -619,7 +642,7 @@ mod matches_test { } 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 }; + static COUNT: Arg = Arg { short: Some(b'c'), long: "count", takes_value: TakesValue::Necessary(None) }; test!(short_never: [], has VERBOSE => false); diff --git a/src/options/style.rs b/src/options/style.rs index 78970f03..470a912c 100644 --- a/src/options/style.rs +++ b/src/options/style.rs @@ -34,7 +34,6 @@ impl Default for TerminalColours { } } -const COLOURS: &[&str] = &["always", "auto", "never"]; impl TerminalColours { @@ -56,7 +55,7 @@ impl TerminalColours { Ok(TerminalColours::Never) } else { - Err(Misfire::bad_argument(&flags::COLOR, word, COLOURS)) + Err(Misfire::BadArgument(&flags::COLOR, word.into())) } } } @@ -217,12 +216,6 @@ mod terminal_test { use options::test::parse_for_test; use options::test::Strictnesses::*; - pub fn os(input: &'static str) -> OsString { - let mut os = OsString::new(); - os.push(input); - os - } - static TEST_ARGS: &[&Arg] = &[ &flags::COLOR, &flags::COLOUR ]; macro_rules! test { @@ -260,8 +253,8 @@ mod terminal_test { test!(no_u_never: ["--color", "never"]; Both => Ok(TerminalColours::Never)); // Errors - test!(no_u_error: ["--color=upstream"]; Both => err Misfire::bad_argument(&flags::COLOR, &os("upstream"), super::COLOURS)); // the error is for --color - test!(u_error: ["--colour=lovers"]; Both => err Misfire::bad_argument(&flags::COLOR, &os("lovers"), super::COLOURS)); // and so is this one! + test!(no_u_error: ["--color=upstream"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("upstream"))); // the error is for --color + test!(u_error: ["--colour=lovers"]; Both => err Misfire::BadArgument(&flags::COLOR, OsString::from("lovers"))); // and so is this one! // Overriding test!(overridden_1: ["--colour=auto", "--colour=never"]; Last => Ok(TerminalColours::Never)); diff --git a/src/options/view.rs b/src/options/view.rs index deff1143..bf7b6e9f 100644 --- a/src/options/view.rs +++ b/src/options/view.rs @@ -248,8 +248,6 @@ impl SizeFormat { } -const TIME_STYLES: &[&str] = &["default", "long-iso", "full-iso", "iso"]; - impl TimeFormat { /// Determine how time should be formatted in timestamp columns. @@ -274,14 +272,12 @@ impl TimeFormat { Ok(TimeFormat::FullISO) } else { - Err(Misfire::bad_argument(&flags::TIME_STYLE, word, TIME_STYLES)) + Err(Misfire::BadArgument(&flags::TIME_STYLE, word.into())) } } } -static TIMES: &[&str] = &["modified", "accessed", "created"]; - impl TimeTypes { /// Determine which of a file’s time fields should be displayed for it @@ -320,7 +316,7 @@ impl TimeTypes { Ok(TimeTypes { accessed: false, modified: false, created: true }) } else { - Err(Misfire::bad_argument(&flags::TIME, word, TIMES)) + Err(Misfire::BadArgument(&flags::TIME, word.into())) } } else if modified || created || accessed { @@ -358,12 +354,6 @@ mod test { use options::test::parse_for_test; use options::test::Strictnesses::*; - pub fn os(input: &'static str) -> OsString { - let mut os = OsString::new(); - os.push(input); - os - } - static TEST_ARGS: &[&Arg] = &[ &flags::BINARY, &flags::BYTES, &flags::TIME_STYLE, &flags::TIME, &flags::MODIFIED, &flags::CREATED, &flags::ACCESSED, &flags::HEADER, &flags::GROUP, &flags::INODE, &flags::GIT, @@ -461,7 +451,6 @@ mod test { mod time_formats { use super::*; - use std::ffi::OsStr; // These tests use pattern matching because TimeFormat doesn’t // implement PartialEq. @@ -483,7 +472,7 @@ mod test { test!(nevermore: TimeFormat <- ["--time-style", "long-iso", "--time-style=full-iso"]; Complain => err Misfire::Duplicate(Flag::Long("time-style"), Flag::Long("time-style"))); // Errors - test!(daily: TimeFormat <- ["--time-style=24-hour"]; Both => err Misfire::bad_argument(&flags::TIME_STYLE, OsStr::new("24-hour"), TIME_STYLES)); + test!(daily: TimeFormat <- ["--time-style=24-hour"]; Both => err Misfire::BadArgument(&flags::TIME_STYLE, OsString::from("24-hour"))); } @@ -515,8 +504,8 @@ mod test { test!(time_uu: TimeTypes <- ["-uU"]; Both => Ok(TimeTypes { accessed: true, modified: false, created: true })); // Errors - test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::bad_argument(&flags::TIME, &os("tea"), super::TIMES)); - test!(time_ea: TimeTypes <- ["-tea"]; Both => err Misfire::bad_argument(&flags::TIME, &os("ea"), super::TIMES)); + test!(time_tea: TimeTypes <- ["--time=tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("tea"))); + test!(time_ea: TimeTypes <- ["-tea"]; Both => err Misfire::BadArgument(&flags::TIME, OsString::from("ea"))); // Overriding test!(overridden: TimeTypes <- ["-tcr", "-tmod"]; Last => Ok(TimeTypes { accessed: false, modified: true, created: false })); diff --git a/xtests/error_value b/xtests/error_value index 287ed229..bda48bca 100644 --- a/xtests/error_value +++ b/xtests/error_value @@ -1 +1 @@ -Flag --time needs a value +Flag --time needs a value (choices: modified, accessed, created) From c475cccce468a7177674c7dbb189192af64deb37 Mon Sep 17 00:00:00 2001 From: Benjamin Sago Date: Thu, 14 Sep 2017 09:18:17 +0100 Subject: [PATCH 5/5] Flip the new/old order, and add suggestion for -lt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I changed my mind about which way round sorting by “newest” or by “oldest” should actually go. If you’re listing a large directory, you see the last lines of the output first, so these files should be the ones with the largest whatever the sort field is. It’s about sorting *last*, not sorting *first*. Sorting by size wouldn’t say “sorts smallest files first”, it would say “sorts largest files last”. Right? Also, add a new suggestion that warns against “ls -lt”. --- README.md | 2 +- contrib/man/exa.1 | 2 +- src/options/filter.rs | 19 +++++++++++++------ src/options/misfire.rs | 11 ++++++++++- xtests/error_lt | 2 ++ xtests/error_ltr | 2 +- xtests/run.sh | 6 ++++-- 7 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 xtests/error_lt diff --git a/README.md b/README.md index 919a9869..70ef9b5f 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ These options are available when running with --long (`-l`): - **--time-style**: how to format timestamps - Valid **--color** options are **always**, **automatic**, and **never**. -- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **oldest**, while its reverse has the aliases **age** and **newest**. +- Valid sort fields are **accessed**, **created**, **extension**, **Extension**, **inode**, **modified**, **name**, **Name**, **size**, **type**, and **none**. Fields starting with a capital letter sort uppercase before lowercase. The modified field has the aliases **date**, **time**, and **newest**, while its reverse has the aliases **age** and **oldest**. - Valid time fields are **modified**, **accessed**, and **created**. - Valid time styles are **default**, **iso**, **long-iso**, and **full-iso**. diff --git a/contrib/man/exa.1 b/contrib/man/exa.1 index fb201e8d..dadb6c44 100644 --- a/contrib/man/exa.1 +++ b/contrib/man/exa.1 @@ -77,7 +77,7 @@ reverse the sort order .B \-s, \-\-sort=\f[I]SORT_FIELD\f[] which field to sort by. Valid fields are name, Name, extension, Extension, size, modified, accessed, created, inode, type, and none. -The modified field has the aliases date, time, and oldest, and its reverse order has the aliases age and newest. +The modified field has the aliases date, time, and newest, and its reverse order has the aliases age and oldest. Fields starting with a capital letter will sort uppercase before lowercase: 'A' then 'B' then 'a' then 'b'. Fields starting with a lowercase letter will mix them: 'A' then 'a' then 'B' then 'b'. .RS diff --git a/src/options/filter.rs b/src/options/filter.rs index 6d4b2f5e..6ed2a4ed 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -49,10 +49,17 @@ impl SortField { else if word == "Ext" || word == "Extension" { Ok(SortField::Extension(SortCase::ABCabc)) } - else if word == "date" || word == "time" || word == "mod" || word == "modified" || word == "old" || word == "oldest" { + else if word == "date" || word == "time" || word == "mod" || word == "modified" || word == "new" || word == "newest" { + // “new” sorts oldest at the top and newest at the bottom; “old” + // sorts newest at the top and oldest at the bottom. I think this + // is the right way round to do this: “size” puts the smallest at + // the top and the largest at the bottom, doesn’t it? Ok(SortField::ModifiedDate) } - else if word == "age" || word == "new" || word == "newest" { + else if word == "age" || word == "old" || word == "oldest" { + // Similarly, “age” means that files with the least age (the + // newest files) get sorted at the top, and files with the most + // age (the oldest) at the bottom. Ok(SortField::ModifiedAge) } else if word == "acc" || word == "accessed" { @@ -209,10 +216,10 @@ mod test { 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", "old"]; Both => Ok(SortField::ModifiedDate)); - test!(oldest: SortField <- ["--sort=oldest"]; Both => Ok(SortField::ModifiedDate)); - test!(new: SortField <- ["--sort", "new"]; Both => Ok(SortField::ModifiedAge)); - test!(newest: SortField <- ["--sort=newest"]; Both => Ok(SortField::ModifiedAge)); + 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)); // Errors diff --git a/src/options/misfire.rs b/src/options/misfire.rs index 58c1ef42..5cc0ba3b 100644 --- a/src/options/misfire.rs +++ b/src/options/misfire.rs @@ -113,10 +113,19 @@ impl fmt::Display for ParseError { } impl Misfire { + /// 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 if let Misfire::BadArgument(ref time, ref r) = *self { if *time == &flags::TIME && r == "r" { - return Some("To sort newest files first, try \"--sort modified\", or just \"-stime\""); + return Some("To sort oldest files last, try \"--sort oldest\", or just \"-sold\""); + } + } + + if let Misfire::InvalidOptions(ParseError::NeedsValue { ref flag, values: _ }) = *self { + if *flag == Flag::Short(b't') { + return Some("To sort newest files last, try \"--sort newest\", or just \"-snew\""); } } diff --git a/xtests/error_lt b/xtests/error_lt new file mode 100644 index 00000000..33f239ac --- /dev/null +++ b/xtests/error_lt @@ -0,0 +1,2 @@ +Flag -t needs a value (choices: modified, accessed, created) +To sort newest files last, try "--sort newest", or just "-snew" diff --git a/xtests/error_ltr b/xtests/error_ltr index 84bbe475..b2f3c542 100644 --- a/xtests/error_ltr +++ b/xtests/error_ltr @@ -1,2 +1,2 @@ Option --time (-t) has no "r" setting (choices: modified, accessed, created) -To sort newest files first, try "--sort modified", or just "-stime" +To sort oldest files last, try "--sort oldest", or just "-sold" diff --git a/xtests/run.sh b/xtests/run.sh index e2164edd..b88ae202 100755 --- a/xtests/run.sh +++ b/xtests/run.sh @@ -143,8 +143,9 @@ $exa $testcases/file-names-exts/music.* -I "*.OGG|*.mp3" -1 2>&1 | diff -q - $re # Dates and times $exa $testcases/dates -lh --accessed --sort=accessed 2>&1 | diff -q - $results/dates_accessed || exit 1 $exa $testcases/dates -lh --sort=modified 2>&1 | diff -q - $results/dates_modified || exit 1 -$exa $testcases/dates -lh -r --sort=newest 2>&1 | diff -q - $results/dates_modified || exit 1 -$exa $testcases/dates -lh --sort=newest 2>&1 | diff -q - $results/dates_deifidom || exit 1 +$exa $testcases/dates -lh --sort=newest 2>&1 | diff -q - $results/dates_modified || exit 1 +$exa $testcases/dates -lh -r --sort=newest 2>&1 | diff -q - $results/dates_deifidom || exit 1 +$exa $testcases/dates -lh --sort=oldest 2>&1 | diff -q - $results/dates_deifidom || exit 1 $exa $testcases/dates -l --time-style=long-iso 2>&1 | diff -q - $results/dates_long_iso || exit 1 $exa $testcases/dates -l --time-style=full-iso 2>&1 | diff -q - $results/dates_full_iso || exit 1 $exa $testcases/dates -l --time-style=iso 2>&1 | diff -q - $results/dates_iso || exit 1 @@ -265,6 +266,7 @@ $exa -l --time-style=24 2>&1 | diff -q - $results/error_setting || exit 1 # Error suggestions $exa -ltr 2>&1 | diff -q - $results/error_ltr || exit 1 +$exa -lt 2>&1 | diff -q - $results/error_lt || exit 1 # Debug mode