summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2022-02-26 17:38:43 +0100
committerCanop <cano.petrole@gmail.com>2022-02-26 17:39:12 +0100
commite6c7dcb4853568970ef59747f5a0f531ab207024 (patch)
tree31eef4ba2d0703024e3d643d39d2f5893d4b7d78
parent0616e40a3698fa34e1078487503b1e1d48e01122 (diff)
rows can be sorted with `--sort`
Fix #37
-rw-r--r--CHANGELOG.md3
-rw-r--r--Cargo.lock6
-rw-r--r--Cargo.toml4
-rw-r--r--src/args.rs5
-rw-r--r--src/col.rs117
-rw-r--r--src/cols.rs8
-rw-r--r--src/main.rs11
-rw-r--r--src/order.rs43
-rw-r--r--src/sorting.rs83
-rw-r--r--website/docs/img/s=dev.pngbin0 -> 31133 bytes
-rw-r--r--website/docs/img/s=free-d.pngbin0 -> 27182 bytes
-rw-r--r--website/docs/index.md2
-rw-r--r--website/docs/table.md18
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
diff --git a/Cargo.lock b/Cargo.lock
index c4aa7da..4636603 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 601f8c5..1628cd5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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,
diff --git a/src/col.rs b/src/col.rs
index 9120920..c9a5fba 100644
--- a/src/col.rs
+++ b/src/col.rs
@@ -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
new file mode 100644
index 0000000..670a864
--- /dev/null
+++ b/website/docs/img/s=dev.png
Binary files differ
diff --git a/website/docs/img/s=free-d.png b/website/docs/img/s=free-d.png
new file mode 100644
index 0000000..00e1e02
--- /dev/null
+++ b/website/docs/img/s=free-d.png
Binary files differ
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)