diff options
author | Canop <cano.petrole@gmail.com> | 2022-02-26 17:38:43 +0100 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2022-02-26 17:39:12 +0100 |
commit | e6c7dcb4853568970ef59747f5a0f531ab207024 (patch) | |
tree | 31eef4ba2d0703024e3d643d39d2f5893d4b7d78 | |
parent | 0616e40a3698fa34e1078487503b1e1d48e01122 (diff) |
rows can be sorted with `--sort`
Fix #37
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | Cargo.lock | 6 | ||||
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | src/args.rs | 5 | ||||
-rw-r--r-- | src/col.rs | 117 | ||||
-rw-r--r-- | src/cols.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 11 | ||||
-rw-r--r-- | src/order.rs | 43 | ||||
-rw-r--r-- | src/sorting.rs | 83 | ||||
-rw-r--r-- | website/docs/img/s=dev.png | bin | 0 -> 31133 bytes | |||
-rw-r--r-- | website/docs/img/s=free-d.png | bin | 0 -> 27182 bytes | |||
-rw-r--r-- | website/docs/index.md | 2 | ||||
-rw-r--r-- | website/docs/table.md | 18 |
13 files changed, 269 insertions, 31 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 141094f..22e35f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### next +- `--sort` launch argument for sorting rows in table + <a name="v2.1.1"></a> ### v2.1.1 - 2022/02/25 - `--list-cols` launch argument for knowing the columns and their names @@ -243,7 +243,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lfs" -version = "2.1.1" +version = "2.2.0-dev" dependencies = [ "argh", "crossterm", @@ -256,9 +256,9 @@ dependencies = [ [[package]] name = "lfs-core" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabae380aa016094ec0d38f5dcf6557c228cc3d15b17cb35c3234bc9155fe52c" +checksum = "39bbfb9f99dd92414b005d727b770f0f1c5a81a9f63809743294e6d1dc5d8ca2" dependencies = [ "lazy-regex", "libc", @@ -1,6 +1,6 @@ [package] name = "lfs" -version = "2.1.1" +version = "2.2.0-dev" authors = ["dystroy <denys.seguret@gmail.com>"] edition = "2021" keywords = ["linux", "filesystem", "fs"] @@ -15,7 +15,7 @@ rust-version = "1.56" argh = "0.1.7" crossterm = "0.22.1" file-size = "1.0.3" -lfs-core = "0.9.0" +lfs-core = "0.9.1" serde = "1.0" serde_json = "1.0" termimad = "0.20.0" diff --git a/src/args.rs b/src/args.rs index 8195815..7d9e777 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,6 +2,7 @@ use { crate::{ cols::Cols, units::Units, + sorting::Sorting, }, crossterm::tty::IsTty, argh::FromArgs, @@ -33,6 +34,10 @@ pub struct Args { #[argh(option, default = "Default::default()", short = 'c')] pub cols: Cols, + /// sort, eg `-s inodes` or `-s size-asc` + #[argh(option, default = "Default::default()", short = 's')] + pub sort: Sorting, + /// output as JSON #[argh(switch, short = 'j')] pub json: bool, @@ -1,5 +1,8 @@ use { + crate::order::Order, + lfs_core::Mount, std::{ + cmp::Ordering, fmt, str::FromStr, }, @@ -150,6 +153,101 @@ impl Col { Self::MountPoint => "mount point", } } + pub fn comparator(self) -> impl for<'a, 'b> FnMut(&'a Mount, &'b Mount) -> Ordering { + match self { + Self::Id => |a: &Mount, b: &Mount| a.info.id.cmp(&b.info.id), + Self::Dev => |a: &Mount, b: &Mount| a.info.dev.cmp(&b.info.dev), + Self::Filesystem => |a: &Mount, b: &Mount| a.info.fs.cmp(&b.info.fs), + Self::Label => |a: &Mount, b: &Mount| match (&a.fs_label, &b.fs_label) { + (Some(a), Some(b)) => a.cmp(b), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => Ordering::Equal, + }, + Self::Type => |a: &Mount, b: &Mount| a.info.fs_type.cmp(&b.info.fs_type), + Self::Disk => |a: &Mount, b: &Mount| match (&a.disk, &b.disk) { + (Some(a), Some(b)) => a.disk_type().to_lowercase().cmp(&b.disk_type().to_lowercase()), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::Used => |a: &Mount, b: &Mount| match (&a.stats, &b.stats) { + (Some(a), Some(b)) => a.used().cmp(&b.used()), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::Use | Self::UsePercent => |a: &Mount, b: &Mount| match (&a.stats, &b.stats) { + // SAFETY: use_share() doesn't return NaN + (Some(a), Some(b)) => a.use_share().partial_cmp(&b.use_share()).unwrap(), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::Free => |a: &Mount, b: &Mount| match (&a.stats, &b.stats) { + (Some(a), Some(b)) => a.available().cmp(&b.available()), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::Size => |a: &Mount, b: &Mount| match (&a.stats, &b.stats) { + (Some(a), Some(b)) => a.size().cmp(&b.size()), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::InodesUsed => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { + (Some(a), Some(b)) => a.used().cmp(&b.used()), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::InodesUsePercent | Self::InodesUse => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { + // SAFETY: use_share() doesn't return NaN + (Some(a), Some(b)) => a.use_share().partial_cmp(&b.use_share()).unwrap(), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::InodesFree => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { + (Some(a), Some(b)) => a.favail.cmp(&b.favail), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::InodesCount => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { + (Some(a), Some(b)) => a.files.cmp(&b.files), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + }, + Self::MountPoint => |a: &Mount, b: &Mount| a.info.mount_point.cmp(&b.info.mount_point), + } + } + pub fn default_sort_order(self) -> Order { + match self { + Self::Id => Order::Asc, + Self::Dev => Order::Asc, + Self::Filesystem => Order::Asc, + Self::Label => Order::Asc, + Self::Type => Order::Asc, + Self::Disk => Order::Asc, + Self::Used => Order::Asc, + Self::Use => Order::Desc, + Self::UsePercent => Order::Asc, + Self::Free => Order::Asc, + Self::Size => Order::Desc, + Self::InodesUsed => Order::Asc, + Self::InodesUse => Order::Asc, + Self::InodesUsePercent => Order::Asc, + Self::InodesFree => Order::Asc, + Self::InodesCount => Order::Asc, + Self::MountPoint => Order::Asc, + } + } + pub fn default_sort_col() -> Self { + Self::Size + } } @@ -165,20 +263,11 @@ impl ParseColError { } impl fmt::Display for ParseColError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?} can't be parsed as a column; expected one of ", self.raw)?; - let mut names = ALL_COLS.iter().map(|c| c.name()).peekable(); - write!(f, "{:?}", names.next().unwrap())?; - loop { - if let Some(name) = names.next() { - if names.peek().is_none() { - write!(f, ", or {:?}", name)?; - break; - } else { - write!(f, ", {:?}", name)?; - } - } - } - Ok(()) + write!( + f, + "{:?} can't be parsed as a column; use 'lfs --list-cols' to see all column names", + self.raw, + ) } } impl std::error::Error for ParseColError {} diff --git a/src/cols.rs b/src/cols.rs index 5f3c92c..d9a40a8 100644 --- a/src/cols.rs +++ b/src/cols.rs @@ -167,11 +167,9 @@ mod cols_parsing { #[test] fn bad_cols(){ - assert!( - "nothing".parse::<Cols>() - .unwrap_err() - .to_string() - .starts_with(r#""nothing" can't be parsed as a column; expected"#), + assert_eq!( + "nothing".parse::<Cols>().unwrap_err().to_string(), + r#""nothing" can't be parsed as a column; use 'lfs --list-cols' to see all column names"#, ); } diff --git a/src/main.rs b/src/main.rs index fd0eb9b..ee82617 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,14 @@ mod col; mod cols; mod json; mod list_cols; +mod order; +mod sorting; mod table; mod units; use { crate::args::*, std::{ - cmp::Reverse, fs, os::unix::fs::MetadataExt, }, @@ -61,10 +62,10 @@ fn main() { return; } if mounts.is_empty() { - println!("no disk was found - try\n lfs -a"); - } else { - mounts.sort_by_key(|m| Reverse(m.size())); - table::print(&mounts, args.color(), &args); + println!("no mount to display - try\n lfs -a"); + return; } + args.sort.sort(&mut mounts); + table::print(&mounts, args.color(), &args); } diff --git a/src/order.rs b/src/order.rs new file mode 100644 index 0000000..072264a --- /dev/null +++ b/src/order.rs @@ -0,0 +1,43 @@ +use { + std::{ + fmt, + str::FromStr, + }, +}; + +/// one of the two sorting directions +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Order { + Asc, + Desc, +} + + +#[derive(Debug)] +pub struct ParseOrderError { + /// the string which couldn't be parsed + pub raw: String, +} +impl ParseOrderError { + pub fn new<S: Into<String>>(s: S) -> Self { + Self { raw: s.into() } + } +} +impl fmt::Display for ParseOrderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?} can't be parsed as a sort order. Use 'asc' or 'desc' (or nothing)", self.raw) + } +} +impl std::error::Error for ParseOrderError {} + +impl FromStr for Order { + type Err = ParseOrderError; + fn from_str(s: &str) -> Result<Self, ParseOrderError> { + let s = s.to_lowercase(); + match s.as_ref() { + "a" | "asc" => Ok(Self::Asc), + "d" | "desc" => Ok(Self::Desc), + _ => Err(ParseOrderError::new(s)) + } + } +} diff --git a/src/sorting.rs b/src/sorting.rs new file mode 100644 index 0000000..f684ee3 --- /dev/null +++ b/src/sorting.rs @@ -0,0 +1,83 @@ +use { + crate::{ + col::Col, + order::Order, + }, + lfs_core::Mount, + std::{ + error, + fmt, + str::FromStr, + }, +}; + +/// Sorting directive: the column and the order (asc or desc) +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Sorting { + col: Col, + order: Order, +} + +impl Default for Sorting { + fn default() -> Self { + let col = Col::default_sort_col(); + let order = col.default_sort_order(); + Self { col, order } + } +} + +impl Sorting { + pub fn sort(self, mounts: &mut [Mount]) { + let comparator = self.col.comparator(); + mounts.sort_by(comparator); + if self.order == Order::Desc { + mounts.reverse(); + } + } +} + +#[derive(Debug)] +pub struct ParseSortingError { + raw: String, + reason: Box<dyn error::Error>, +} +impl ParseSortingError { + pub fn new<S: Into<String>>(raw: S, reason: Box<dyn error::Error>) -> Self { + Self { + raw: raw.into(), + reason, + } + } +} +impl fmt::Display for ParseSortingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?} can't be parsed as a sort expression because {}", self.raw, self.reason) + } +} +impl error::Error for ParseSortingError {} + +impl FromStr for Sorting { + type Err = ParseSortingError; + fn from_str(s: &str) -> Result<Self, ParseSortingError> { + let cut_idx_len = s + .char_indices() + .find(|(_idx, c)| c.is_whitespace() || *c == '-') + .map(|(idx, c)| (idx, c.len_utf8())); + let (s_col, s_order) = match cut_idx_len { + Some((idx, len)) => (&s[..idx], Some(&s[idx+len..])), + None => (s, None), + }; + let col: Col = s_col.parse() + .map_err(|pce| ParseSortingError::new(s, Box::new(pce)))?; + let order = match s_order { + Some(s_order) => { + s_order.parse() + .map_err(|poe| ParseSortingError::new(s, Box::new(poe)))? + } + None => { + col.default_sort_order() + } + }; + Ok(Self { col, order }) + } +} diff --git a/website/docs/img/s=dev.png b/website/docs/img/s=dev.png Binary files differnew file mode 100644 index 0000000..670a864 --- /dev/null +++ b/website/docs/img/s=dev.png diff --git a/website/docs/img/s=free-d.png b/website/docs/img/s=free-d.png Binary files differnew file mode 100644 index 0000000..00e1e02 --- /dev/null +++ b/website/docs/img/s=free-d.png diff --git a/website/docs/index.md b/website/docs/index.md index 62026b0..ea42e26 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -26,7 +26,7 @@ The default display of **lfs** is a table, which can be configured with the colu ![screen](img/c=label+.png) -See [Table](./table) for the definition of the columns and the syntax for choosing them. +See [Table](./table) for the definition of the columns and the syntax for choosing them, or on how to [sort rows](./table#sort). # JSON diff --git a/website/docs/table.md b/website/docs/table.md index a29a02a..2d1b9ab 100644 --- a/website/docs/table.md +++ b/website/docs/table.md @@ -29,7 +29,7 @@ inodescount | | total number of inodes in the filesystem mount | ✓ | mounting path -## --cols argument +## Choose columns With the `--cols` launch argument, shortened as `-c`, you can change the displayed columns or their order. @@ -88,3 +88,19 @@ To see *all* filesystems of your system, do `lfs --all`: ![screen](img/rows-all.png) This list can be quite big with virtual file systems, docker use, etc. + +## Sort + +With the `--sort` launch argument, shortened as `-s`, you can specify the order of displayed rows. + +The argument's value must be either a column name, for example `lfs -s dev`, or a column name and a direction, for example `lfs --sort size-desc`. + +The `desc` and `asc` directions can be abbreviated into `d` and `a`. + +For example, sorting on the device id: + +![screen](img/s=dev.png) + +Or sorting on the remaining free space, in descending order: + +![screen](img/s=free-d.png) |