From fb2a6088dcc7b57a2c1ac93ec6a8fbcc52584734 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 May 2020 13:49:43 +0200 Subject: first attempt at sorting output This does currently not work yet, as we cannot call .finish() on dyn Aggregator. This needs to be adjusted. However, this provides the basic infrastructure for producing sorted output, including the required command line parsing. --- src/main.rs | 16 +++-- src/output/aggregators.rs | 46 ++++++++++++-- src/output/mod.rs | 11 +++- src/output/pipeline.rs | 7 ++- src/output/sorting.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 13 deletions(-) create mode 100644 src/output/sorting.rs (limited to 'src') diff --git a/src/main.rs b/src/main.rs index ee1c232..ad996ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ use filters::{log::LogFilter, Inclusion}; mod guilds; mod logger; mod output; +use output::sorting::Sorting; mod paths; mod playerclass; use playerclass::PlayerClass; @@ -79,6 +80,10 @@ pub struct Opt { #[structopt(long = "no-color")] no_color: bool, + /// Sort the output. + #[structopt(short = "s", long = "sort")] + sorting: Option, + /// Print more debugging information to stderr. #[structopt(long = "debug")] debug: bool, @@ -150,7 +155,7 @@ impl Ord for Player { } /// Outcome of the fight. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum FightOutcome { Success, Wipe, @@ -377,7 +382,8 @@ fn build_filter(expr_string: &str) -> Result> { /// Run the grep search with the given options. fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { - let pipeline = &output::build_pipeline(opt); + let pipeline = output::build_pipeline(opt); + let pipeline_ref = &pipeline; rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { @@ -391,7 +397,7 @@ fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { let search = search_log(&entry, filter); match search { Ok(None) => (), - Ok(Some(result)) => pipeline.push_item(&result), + Ok(Some(result)) => pipeline_ref.push_item(result), Err(err) => { debug!("Runtime error while scanning {:?}: {}", entry.path(), err); } @@ -400,7 +406,9 @@ fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { }); } Ok(()) - }) + }) as Result<()>; + pipeline.finish(); + Ok(()) } /// Search the given single log. diff --git a/src/output/aggregators.rs b/src/output/aggregators.rs index 1b04af3..4fa2558 100644 --- a/src/output/aggregators.rs +++ b/src/output/aggregators.rs @@ -6,12 +6,15 @@ //! //! Aggregators must be shareable across threads, as the search will be multi-threaded. This is why //! an Aggregator must make sure that the data is protected by a mutex or similar. -use super::{super::LogResult, formats::Format}; +use super::{super::LogResult, formats::Format, sorting::Sorting}; -use std::io::Write; +use std::{ + io::Write, + sync::Mutex, +}; pub trait Aggregator: Sync { - fn push_item(&self, item: &LogResult, format: &dyn Format, stream: &mut dyn Write); + fn push_item(&self, item: LogResult, format: &dyn Format, stream: &mut dyn Write); fn finish(self, format: &dyn Format, stream: &mut dyn Write); } @@ -20,11 +23,44 @@ pub trait Aggregator: Sync { pub struct WriteThrough; impl Aggregator for WriteThrough { - fn push_item(&self, item: &LogResult, format: &dyn Format, stream: &mut dyn Write) { - let text = format.format_result(item); + fn push_item(&self, item: LogResult, format: &dyn Format, stream: &mut dyn Write) { + let text = format.format_result(&item); stream.write_all(text.as_bytes()).unwrap(); stream.flush().unwrap(); } fn finish(self, _: &dyn Format, _: &mut dyn Write) {} } + +/// An aggregator that keeps all found logs in memory and sorts them before outputting them. +#[derive(Debug)] +pub struct SortedOutput { + sorting: Sorting, + items: Mutex>, +} + +impl SortedOutput { + pub fn new(sorting: Sorting) -> Self { + SortedOutput { + sorting, + items: Mutex::new(vec![]), + } + } +} + +impl Aggregator for SortedOutput { + fn push_item(&self, item: LogResult, _: &dyn Format, _: &mut dyn Write) { + self.items.lock().unwrap().push(item) + } + + fn finish(self, format: &dyn Format, stream: &mut dyn Write) { + let SortedOutput { sorting, items } = self; + let mut items = items.into_inner().unwrap(); + items.sort_unstable_by(|a, b| sorting.cmp(a, b)); + for item in items { + let text = format.format_result(&item); + stream.write_all(text.as_bytes()).unwrap(); + } + stream.flush().unwrap(); + } +} diff --git a/src/output/mod.rs b/src/output/mod.rs index 0fd92d9..18ab84b 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -5,15 +5,20 @@ use std::io; pub mod aggregators; pub mod formats; pub mod pipeline; +pub mod sorting; pub use self::pipeline::Pipeline; -use self::formats::Format; +use self::{formats::Format, aggregators::Aggregator}; /// Build an pipeline for the given command line options. pub fn build_pipeline(opt: &Opt) -> Pipeline { let stream = io::stdout(); - let aggregator = aggregators::WriteThrough; + let aggregator: Box = if let Some(sorting) = &opt.sorting { + Box::new(aggregators::SortedOutput::new(sorting.clone())) + } else { + Box::new(aggregators::WriteThrough) + }; let format: Box = if opt.file_name_only { Box::new(formats::FileOnly) @@ -23,5 +28,5 @@ pub fn build_pipeline(opt: &Opt) -> Pipeline { }) }; - Pipeline::new(Box::new(stream), format, Box::new(aggregator)) + Pipeline::new(Box::new(stream), format, aggregator) } diff --git a/src/output/pipeline.rs b/src/output/pipeline.rs index 9664928..9b7f461 100644 --- a/src/output/pipeline.rs +++ b/src/output/pipeline.rs @@ -22,8 +22,13 @@ impl Pipeline { } } - pub fn push_item(&self, item: &LogResult) { + pub fn push_item(&self, item: LogResult) { let mut writer = self.writer.lock().unwrap(); self.aggregator.push_item(item, &*self.format, &mut *writer); } + + pub fn finish(self) { + let mut writer = self.writer.lock().unwrap(); + self.aggregator.finish(&*self.format, &mut *writer); + } } diff --git a/src/output/sorting.rs b/src/output/sorting.rs new file mode 100644 index 0000000..8b271a9 --- /dev/null +++ b/src/output/sorting.rs @@ -0,0 +1,148 @@ +//! Definitions and functions to produce sorted output. + +use std::{ + cmp::Ordering, + fmt::{self, Display, Formatter}, + str::FromStr, +}; + +use itertools::Itertools; +use thiserror::Error; + +use crate::LogResult; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Error)] +#[error("an invalid sorting component was given")] +pub struct InvalidComponent; + +/// A [`Component`][Component] is anything that can be used to sort the result by. +/// +/// Note that some components may not provide a true "sorting" (is Skorvald coming before Dhuum or +/// not?), but are more of a convenience that allows to group logs of the same component together. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum Component { + /// Sort the result date. + Date, + /// Sort by the boss. + Boss, + /// Sort by the outcome. + Outcome, + /// Sort based on whether the Challenge Mote was active or not. + ChallengeMote, + + /// Sort by the given component but in reverse direction. + Reverse(Box), +} + +impl FromStr for Component { + type Err = InvalidComponent; + + fn from_str(s: &str) -> Result { + if s.starts_with('-') { + return s[1..].parse().map(|c| Component::Reverse(Box::new(c))); + } + match &s.to_lowercase() as &str { + "date" => Ok(Component::Date), + "boss" => Ok(Component::Boss), + "outcome" => Ok(Component::Outcome), + "cm" => Ok(Component::ChallengeMote), + _ => Err(InvalidComponent), + } + } +} + +impl Display for Component { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Component::Date => write!(f, "date"), + Component::Boss=> write!(f, "boss"), + Component::Outcome => write!(f, "outcome"), + Component::ChallengeMote => write!(f, "cm"), + Component::Reverse(ref inner) => write!(f, "-{}", inner), + } + } +} + +fn boss_id(log: &LogResult) -> u32 { + log.boss.map(|x| x as u32).unwrap_or(0) +} + +impl Component { + pub fn cmp(&self, lhs: &LogResult, rhs: &LogResult) -> Ordering { + match self { + Component::Date => lhs.time.cmp(&rhs.time), + Component::Boss => boss_id(lhs).cmp(&boss_id(rhs)), + Component::Outcome => lhs.outcome.cmp(&rhs.outcome), + Component::ChallengeMote => lhs.is_cm.cmp(&rhs.is_cm), + Component::Reverse(ref inner) => inner.cmp(lhs, rhs).reverse(), + } + } +} + +/// A sorting. +/// +/// The sorting goes lexicographically, in the sense that if the first chosen component is the same +/// for two elements, the next component will be used to compare two logs. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +pub struct Sorting(Vec); + +impl FromStr for Sorting { + type Err = InvalidComponent; + + fn from_str(s: &str) -> Result { + if s == "" { + return Ok(Sorting::default()); + } + let parts = s.split(','); + parts.map(FromStr::from_str).collect::, _>>().map(Sorting::new) + } +} + +impl Display for Sorting { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let string = self.0.iter().map(ToString::to_string).join(","); + f.write_str(&string) + } +} + +impl Sorting { + pub fn new(components: Vec) -> Sorting { + Sorting(components) + } + + pub fn cmp(&self, lhs: &LogResult, rhs: &LogResult) -> Ordering { + for component in &self.0 { + let result = component.cmp(lhs, rhs); + if result != Ordering::Equal { + return result; + } + } + Ordering::Equal + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_component() { + assert_eq!("date".parse(), Ok(Component::Date)); + assert_eq!("boss".parse(), Ok(Component::Boss)); + assert_eq!("outcome".parse(), Ok(Component::Outcome)); + assert_eq!("cm".parse(), Ok(Component::ChallengeMote)); + + assert_eq!("foobar".parse::(), Err(InvalidComponent)); + + assert_eq!("-date".parse(), Ok(Component::Reverse(Box::new(Component::Date)))); + } + + #[test] + fn parse_sorting() { + use Component::*; + assert_eq!("date".parse(), Ok(Sorting::new(vec![Date]))); + assert_eq!("date,boss".parse(), Ok(Sorting::new(vec![Date, Boss]))); + assert_eq!("date,-boss".parse(), Ok(Sorting::new(vec![Date, Reverse(Box::new(Boss))]))); + assert_eq!("".parse(), Ok(Sorting::default())); + } +} -- cgit v1.2.3 From b6a13c465983993ac581051bb24a13d4296731a1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 May 2020 16:33:27 +0200 Subject: fix Aggregator::finish for trait objects --- src/main.rs | 5 +++-- src/output/aggregators.rs | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index ad996ea..cb914d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -384,7 +384,7 @@ fn build_filter(expr_string: &str) -> Result> { fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { let pipeline = output::build_pipeline(opt); let pipeline_ref = &pipeline; - rayon::scope(|s| { + let result: Result<()> = rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { let entry = entry?; @@ -406,7 +406,8 @@ fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { }); } Ok(()) - }) as Result<()>; + }); + result?; pipeline.finish(); Ok(()) } diff --git a/src/output/aggregators.rs b/src/output/aggregators.rs index 4fa2558..24adbf8 100644 --- a/src/output/aggregators.rs +++ b/src/output/aggregators.rs @@ -15,7 +15,8 @@ use std::{ pub trait Aggregator: Sync { fn push_item(&self, item: LogResult, format: &dyn Format, stream: &mut dyn Write); - fn finish(self, format: &dyn Format, stream: &mut dyn Write); + // When the `unsized_locals` feature is stable, we could rewrite this to finish(self, ...). + fn finish(self: Box, format: &dyn Format, stream: &mut dyn Write); } /// An aggregator that just pushes through each item to the output stream without any sorting or @@ -29,7 +30,7 @@ impl Aggregator for WriteThrough { stream.flush().unwrap(); } - fn finish(self, _: &dyn Format, _: &mut dyn Write) {} + fn finish(self: Box, _: &dyn Format, _: &mut dyn Write) {} } /// An aggregator that keeps all found logs in memory and sorts them before outputting them. @@ -53,8 +54,8 @@ impl Aggregator for SortedOutput { self.items.lock().unwrap().push(item) } - fn finish(self, format: &dyn Format, stream: &mut dyn Write) { - let SortedOutput { sorting, items } = self; + fn finish(self: Box, format: &dyn Format, stream: &mut dyn Write) { + let SortedOutput { sorting, items } = *self; let mut items = items.into_inner().unwrap(); items.sort_unstable_by(|a, b| sorting.cmp(a, b)); for item in items { -- cgit v1.2.3 From 5272800a7e397f6fcc7933b474d182fec4d18b08 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 May 2020 16:36:57 +0200 Subject: change sorting reversing prefix to ~ Since - leads to structopt interpreting the component as the start of another argument, we need to use a different one. --- src/output/sorting.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/output/sorting.rs b/src/output/sorting.rs index 8b271a9..4f5cf19 100644 --- a/src/output/sorting.rs +++ b/src/output/sorting.rs @@ -38,7 +38,7 @@ impl FromStr for Component { type Err = InvalidComponent; fn from_str(s: &str) -> Result { - if s.starts_with('-') { + if s.starts_with('~') { return s[1..].parse().map(|c| Component::Reverse(Box::new(c))); } match &s.to_lowercase() as &str { @@ -58,7 +58,7 @@ impl Display for Component { Component::Boss=> write!(f, "boss"), Component::Outcome => write!(f, "outcome"), Component::ChallengeMote => write!(f, "cm"), - Component::Reverse(ref inner) => write!(f, "-{}", inner), + Component::Reverse(ref inner) => write!(f, "~{}", inner), } } } -- cgit v1.2.3 From 57b74f50e59a3fab2d74bbb6165d48ce3e6e2049 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 May 2020 17:03:58 +0200 Subject: fix formatting --- src/output/aggregators.rs | 5 +---- src/output/mod.rs | 2 +- src/output/sorting.rs | 17 +++++++++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/output/aggregators.rs b/src/output/aggregators.rs index 24adbf8..83171eb 100644 --- a/src/output/aggregators.rs +++ b/src/output/aggregators.rs @@ -8,10 +8,7 @@ //! an Aggregator must make sure that the data is protected by a mutex or similar. use super::{super::LogResult, formats::Format, sorting::Sorting}; -use std::{ - io::Write, - sync::Mutex, -}; +use std::{io::Write, sync::Mutex}; pub trait Aggregator: Sync { fn push_item(&self, item: LogResult, format: &dyn Format, stream: &mut dyn Write); diff --git a/src/output/mod.rs b/src/output/mod.rs index 18ab84b..c1cd787 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -9,7 +9,7 @@ pub mod sorting; pub use self::pipeline::Pipeline; -use self::{formats::Format, aggregators::Aggregator}; +use self::{aggregators::Aggregator, formats::Format}; /// Build an pipeline for the given command line options. pub fn build_pipeline(opt: &Opt) -> Pipeline { diff --git a/src/output/sorting.rs b/src/output/sorting.rs index 4f5cf19..32ebc6c 100644 --- a/src/output/sorting.rs +++ b/src/output/sorting.rs @@ -55,7 +55,7 @@ impl Display for Component { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Component::Date => write!(f, "date"), - Component::Boss=> write!(f, "boss"), + Component::Boss => write!(f, "boss"), Component::Outcome => write!(f, "outcome"), Component::ChallengeMote => write!(f, "cm"), Component::Reverse(ref inner) => write!(f, "~{}", inner), @@ -94,7 +94,10 @@ impl FromStr for Sorting { return Ok(Sorting::default()); } let parts = s.split(','); - parts.map(FromStr::from_str).collect::, _>>().map(Sorting::new) + parts + .map(FromStr::from_str) + .collect::, _>>() + .map(Sorting::new) } } @@ -134,7 +137,10 @@ mod tests { assert_eq!("foobar".parse::(), Err(InvalidComponent)); - assert_eq!("-date".parse(), Ok(Component::Reverse(Box::new(Component::Date)))); + assert_eq!( + "-date".parse(), + Ok(Component::Reverse(Box::new(Component::Date))) + ); } #[test] @@ -142,7 +148,10 @@ mod tests { use Component::*; assert_eq!("date".parse(), Ok(Sorting::new(vec![Date]))); assert_eq!("date,boss".parse(), Ok(Sorting::new(vec![Date, Boss]))); - assert_eq!("date,-boss".parse(), Ok(Sorting::new(vec![Date, Reverse(Box::new(Boss))]))); + assert_eq!( + "date,-boss".parse(), + Ok(Sorting::new(vec![Date, Reverse(Box::new(Boss))])) + ); assert_eq!("".parse(), Ok(Sorting::default())); } } -- cgit v1.2.3 From a5e326bdd9b8c751653047bc28e34ded3e340431 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 14 May 2020 15:35:35 +0200 Subject: fix tests We changed the descending prefix to be ~ earlier, so now we need to adjust the tests as well. --- src/output/sorting.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/output/sorting.rs b/src/output/sorting.rs index 32ebc6c..ef00123 100644 --- a/src/output/sorting.rs +++ b/src/output/sorting.rs @@ -138,7 +138,7 @@ mod tests { assert_eq!("foobar".parse::(), Err(InvalidComponent)); assert_eq!( - "-date".parse(), + "~date".parse(), Ok(Component::Reverse(Box::new(Component::Date))) ); } @@ -149,7 +149,7 @@ mod tests { assert_eq!("date".parse(), Ok(Sorting::new(vec![Date]))); assert_eq!("date,boss".parse(), Ok(Sorting::new(vec![Date, Boss]))); assert_eq!( - "date,-boss".parse(), + "date,~boss".parse(), Ok(Sorting::new(vec![Date, Reverse(Box::new(Boss))])) ); assert_eq!("".parse(), Ok(Sorting::default())); -- cgit v1.2.3 From 61889e2d289a6d0d83d55afc9b1c98e6dd112249 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 14 May 2020 15:55:41 +0200 Subject: add a small test for sorting with Sorting --- src/main.rs | 2 +- src/output/sorting.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index cb914d3..43a6f59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,7 +107,7 @@ pub struct Opt { } /// A log that matches the search criteria. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct LogResult { /// The path to the log file. log_file: PathBuf, diff --git a/src/output/sorting.rs b/src/output/sorting.rs index ef00123..f46a95c 100644 --- a/src/output/sorting.rs +++ b/src/output/sorting.rs @@ -126,10 +126,14 @@ impl Sorting { #[cfg(test)] mod tests { + use super::super::FightOutcome; use super::*; + use chrono::prelude::*; + use evtclib::Boss as B; + #[test] - fn parse_component() { + fn test_parse_component() { assert_eq!("date".parse(), Ok(Component::Date)); assert_eq!("boss".parse(), Ok(Component::Boss)); assert_eq!("outcome".parse(), Ok(Component::Outcome)); @@ -144,7 +148,7 @@ mod tests { } #[test] - fn parse_sorting() { + fn test_parse_sorting() { use Component::*; assert_eq!("date".parse(), Ok(Sorting::new(vec![Date]))); assert_eq!("date,boss".parse(), Ok(Sorting::new(vec![Date, Boss]))); @@ -154,4 +158,72 @@ mod tests { ); assert_eq!("".parse(), Ok(Sorting::default())); } + + #[test] + fn test_sorting_cmp() { + use Component::*; + + let logs: &[&LogResult] = &[ + &LogResult { + log_file: "".into(), + time: Utc.ymd(2020, 4, 3).and_hms(12, 0, 0), + boss: Some(B::Dhuum), + players: vec![], + outcome: FightOutcome::Success, + is_cm: false, + }, + &LogResult { + log_file: "".into(), + time: Utc.ymd(2020, 4, 3).and_hms(13, 0, 0), + boss: Some(B::Dhuum), + players: vec![], + outcome: FightOutcome::Success, + is_cm: false, + }, + &LogResult { + log_file: "".into(), + time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0), + boss: Some(B::Dhuum), + players: vec![], + outcome: FightOutcome::Success, + is_cm: false, + }, + &LogResult { + log_file: "".into(), + time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0), + boss: Some(B::Qadim), + players: vec![], + outcome: FightOutcome::Success, + is_cm: false, + }, + &LogResult { + log_file: "".into(), + time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0), + boss: Some(B::Dhuum), + players: vec![], + outcome: FightOutcome::Success, + is_cm: false, + }, + ]; + + let sortings: &[(&[Component], &[&LogResult])] = &[ + (&[Date], &[logs[2], logs[3], logs[4], logs[0], logs[1]]), + ( + &[Reverse(Box::new(Date))], + &[logs[1], logs[0], logs[2], logs[3], logs[4]], + ), + (&[Boss], &[logs[0], logs[1], logs[2], logs[4], logs[3]]), + ( + &[Boss, Date], + &[logs[2], logs[4], logs[0], logs[1], logs[3]], + ), + ]; + + for (sorting, expected) in sortings { + let mut data = logs.to_vec(); + let sorting = Sorting::new(sorting.to_vec()); + data.sort_by(|a, b| sorting.cmp(a, b)); + assert_eq!(&data, expected, "Sorting with {:?} failed", sorting); + } + } } -- cgit v1.2.3 From e2d23d4b76000263e9f939637353bbc4bb9289fd Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 14 May 2020 16:08:51 +0200 Subject: add documentation about --sort --- src/main.rs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 43a6f59..84d1063 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,6 +81,9 @@ pub struct Opt { no_color: bool, /// Sort the output. + /// + /// Valid sorting fields are date, boss, cm and outcome. Prefix the field with ~ to sort in + /// descending order. #[structopt(short = "s", long = "sort")] sorting: Option, -- cgit v1.2.3