summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2022-03-03 22:16:22 +0100
committerCanop <cano.petrole@gmail.com>2022-03-03 22:16:22 +0100
commite51128ac7a1391578ff05270741faf609c21884e (patch)
tree06f9b8bf7a812333ad3f5d507cc268fa420407c6
parent09b2d9d9826693c750df139422cf6ee0fff98d35 (diff)
add the --filter argument
Fix #41
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock9
-rw-r--r--Cargo.toml3
-rw-r--r--bacon.toml68
-rw-r--r--src/args.rs5
-rw-r--r--src/col_expr.rs322
-rw-r--r--src/filter.rs80
-rw-r--r--src/main.rs11
-rw-r--r--src/table.rs2
9 files changed, 497 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bc7358..dcb3f7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
### next
- 'unreachable' information available in JSON and in the table (in the 'use' column). This mostly concerns disconnected remote filesystems.
+- `--filter` argument to filter the displayed filesystems - Fix #41
<a name="v2.3.1"></a>
### v2.3.1 - 2022/03/01
diff --git a/Cargo.lock b/Cargo.lock
index 28cb771..7c6c333 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -56,6 +56,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
+name = "bet"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e218e587658fb0b595e7d27c222a4caf56748a47644bc31483973d8dc6d8670"
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -243,9 +249,10 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lfs"
-version = "2.3.2-dev"
+version = "2.4.0-dev"
dependencies = [
"argh",
+ "bet",
"crossterm",
"file-size",
"lfs-core",
diff --git a/Cargo.toml b/Cargo.toml
index 5fea71a..23075a3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "lfs"
-version = "2.3.2-dev"
+version = "2.4.0-dev"
authors = ["dystroy <denys.seguret@gmail.com>"]
edition = "2021"
keywords = ["linux", "filesystem", "fs"]
@@ -13,6 +13,7 @@ rust-version = "1.56"
[dependencies]
argh = "0.1.7"
+bet = "1.0.0"
crossterm = "0.22.1"
file-size = "1.0.3"
lfs-core = "0.10.2"
diff --git a/bacon.toml b/bacon.toml
new file mode 100644
index 0000000..6aa0d59
--- /dev/null
+++ b/bacon.toml
@@ -0,0 +1,68 @@
+# This is a configuration file for the bacon tool
+# More info at https://github.com/Canop/bacon
+
+default_job = "check"
+
+[jobs]
+
+[jobs.check]
+command = ["cargo", "check", "--color", "always"]
+need_stdout = false
+
+[jobs.check-all]
+command = ["cargo", "check", "--all-targets", "--color", "always"]
+need_stdout = false
+watch = ["tests", "benches", "examples"]
+
+[jobs.clippy]
+command = [
+ "cargo", "clippy",
+ "--color", "always",
+ "--",
+ "-A", "clippy::collapsible_else_if",
+ "-A", "clippy::collapsible_if",
+ "-A", "clippy::enum_variant_names",
+ "-A", "clippy::match_like_matches_macro",
+]
+need_stdout = false
+
+[jobs.clippy-all]
+command = ["cargo", "clippy", "--all-targets", "--color", "always"]
+need_stdout = false
+watch = ["tests", "benches", "examples"]
+
+[jobs.test]
+command = ["cargo", "test", "--color", "always"]
+need_stdout = true
+watch = ["tests"]
+
+[jobs.doc]
+command = ["cargo", "doc", "--color", "always", "--no-deps"]
+need_stdout = false
+
+# if the doc compiles, then it opens in your browser and bacon switches
+# to the previous job
+[jobs.doc-open]
+command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
+need_stdout = false
+on_success = "back" # so that we don't open the browser at each change
+
+# You can run your application and have the result displayed in bacon,
+# *if* it makes sense for this crate. You can run an example the same
+# way. Don't forget the `--color always` part or the errors won't be
+# properly parsed.
+[jobs.run]
+command = ["cargo", "run", "--color", "always"]
+need_stdout = true
+
+# You may define here keybindings that would be specific to
+# a project, for example a shortcut to launch a specific job.
+# Shortcuts to internal functions (scrolling, toggling, etc.)
+# should go in your personal prefs.toml file instead.
+[keybindings]
+a = "job:check-all"
+i = "job:initial"
+c = "job:clippy"
+d = "job:doc-open"
+t = "job:test"
+r = "job:run"
diff --git a/src/args.rs b/src/args.rs
index 7d9e777..ef2fecd 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -1,6 +1,7 @@
use {
crate::{
cols::Cols,
+ filter::Filter,
units::Units,
sorting::Sorting,
},
@@ -34,6 +35,10 @@ pub struct Args {
#[argh(option, default = "Default::default()", short = 'c')]
pub cols: Cols,
+ /// filter, eg `-f '(size<35G | remote=false) & type=xfs'`
+ #[argh(option, default = "Default::default()", short = 'f')]
+ pub filter: Filter,
+
/// sort, eg `-s inodes` or `-s size-asc`
#[argh(option, default = "Default::default()", short = 's')]
pub sort: Sorting,
diff --git a/src/col_expr.rs b/src/col_expr.rs
new file mode 100644
index 0000000..9ff5402
--- /dev/null
+++ b/src/col_expr.rs
@@ -0,0 +1,322 @@
+use {
+ crate::{
+ col::*,
+ },
+ lfs_core::*,
+ std::{
+ fmt,
+ str::FromStr,
+ },
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ColOperator {
+ Lower,
+ LowerOrEqual,
+ Like,
+ Equal,
+ NotEqual,
+ GreaterOrEqual,
+ Greater,
+}
+
+impl ColOperator {
+ pub fn eval<T: PartialOrd+PartialEq>(self, a: T, b: T) -> bool {
+ match self {
+ Self::Lower => a < b,
+ Self::LowerOrEqual => a <= b,
+ Self::Equal | Self::Like => a == b,
+ Self::NotEqual => a != b,
+ Self::GreaterOrEqual => a >= b,
+ Self::Greater => a > b,
+ }
+ }
+ pub fn eval_option<T: PartialOrd+PartialEq>(self, a: Option<T>, b: T) -> bool {
+ match a {
+ Some(a) => self.eval(a, b),
+ None => false,
+ }
+ }
+ pub fn eval_str(self, a: &str, b: &str) -> bool {
+ match self {
+ Self::Like => a.to_lowercase().contains(&b.to_lowercase()),
+ _ => self.eval(a, b),
+ }
+ }
+ pub fn eval_option_str(self, a: Option<&str>, b: &str) -> bool {
+ match (a, self) {
+ (Some(a), Self::Like) => a.to_lowercase().contains(&b.to_lowercase()),
+ _ => self.eval_option(a, b),
+ }
+ }
+}
+
+/// A leaf in the filter expression tree, an expression which
+/// may return true or false for any filesystem
+#[derive(Debug, Clone, PartialEq)]
+pub struct ColExpr {
+ col: Col,
+ operator: ColOperator,
+ value: String,
+}
+
+impl ColExpr {
+ #[cfg(test)]
+ pub fn new<S: Into<String>>(col: Col, operator: ColOperator, value: S) -> Self {
+ Self {
+ col,
+ operator,
+ value: value.into(),
+ }
+ }
+ pub fn eval(&self, mount: &Mount) -> Result<bool, EvalExprError> {
+ Ok(match self.col {
+ Col::Id => self.operator.eval(
+ mount.info.id,
+ self.value.parse::<MountId>()
+ .map_err(|_| EvalExprError::NotAnId(self.value.to_string()))?,
+ ),
+ Col::Dev => self.operator.eval(
+ mount.info.dev,
+ self.value.parse::<DeviceId>()
+ .map_err(|_| EvalExprError::NotADeviceId(self.value.to_string()))?,
+ ),
+ Col::Filesystem => self.operator.eval_str(
+ &mount.info.fs,
+ &self.value,
+ ),
+ Col::Label => self.operator.eval_option_str(
+ mount.fs_label.as_ref().map(|s| s.as_str()),
+ &self.value,
+ ),
+ Col::Type => self.operator.eval_str(
+ &mount.info.fs_type,
+ &self.value,
+ ),
+ Col::Remote => self.operator.eval(
+ mount.info.is_remote(),
+ parse_bool(&self.value),
+ ),
+ Col::Disk => self.operator.eval_option_str(
+ mount.disk.as_ref().map(|d| d.name.as_str()),
+ &self.value,
+ ),
+ Col::Used => self.operator.eval_option(
+ mount.stats().as_ref().map(|s| s.used()),
+ parse_integer(&self.value)?,
+ ),
+ Col::Use | Col::UsePercent => self.operator.eval_option(
+ mount.stats().as_ref().map(|s| s.use_share()),
+ parse_float(&self.value)?,
+ ),
+ Col::Free => self.operator.eval_option(
+ mount.stats().as_ref().map(|s| s.available()),
+ parse_integer(&self.value)?,
+ ),
+ Col::Size => self.operator.eval_option(
+ mount.stats().as_ref().map(|s| s.size()),
+ parse_integer(&self.value)?,
+ ),
+ Col::InodesUsed => self.operator.eval_option(
+ mount.inodes().as_ref().map(|i| i.used()),
+ parse_integer(&self.value)?,
+ ),
+ Col::InodesUse | Col::InodesUsePercent => self.operator.eval_option(
+ mount.inodes().as_ref().map(|i| i.use_share()),
+ parse_float(&self.value)?,
+ ),
+ Col::InodesFree => self.operator.eval_option(
+ mount.inodes().as_ref().map(|i| i.favail),
+ parse_integer(&self.value)?,
+ ),
+ Col::InodesCount => self.operator.eval_option(
+ mount.inodes().as_ref().map(|i| i.files),
+ parse_integer(&self.value)?,
+ ),
+ Col::MountPoint => self.operator.eval_str(
+ &mount.info.mount_point.to_string_lossy().to_string(),
+ &self.value,
+ ),
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct ParseExprError {
+ /// the string which couldn't be parsed
+ pub raw: String,
+ /// why
+ pub message: String,
+}
+impl ParseExprError {
+ pub fn new<R: Into<String>, M: Into<String>>(raw: R, message: M) -> Self {
+ Self {
+ raw: raw.into(),
+ message: message.into(),
+ }
+ }
+}
+impl fmt::Display for ParseExprError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "{:?} can't be parsed as an expression: {}",
+ self.raw,
+ self.message
+ )
+ }
+}
+impl std::error::Error for ParseExprError {}
+
+impl FromStr for ColExpr {
+ type Err = ParseExprError;
+ fn from_str(input: &str) -> Result<Self, ParseExprError> {
+ let mut chars_indices = input.char_indices();
+ let mut op_idx = 0;
+ for (idx, c) in &mut chars_indices {
+ if c == '<' || c == '>' || c == '=' {
+ op_idx = idx;
+ break;
+ }
+ }
+ if op_idx == 0 {
+ return Err(ParseExprError::new(input, "Invalid expression; expected <column><operator><value>"));
+ }
+ let mut val_idx = op_idx + 1;
+ for (idx, c) in &mut chars_indices {
+ if c != '<' && c != '>' && c != '=' {
+ val_idx = idx;
+ break;
+ }
+ }
+ if val_idx == input.len() {
+ return Err(ParseExprError::new(input, "no value"));
+ }
+ let col = &input[..op_idx];
+ let col = col.parse()
+ .map_err(|e: ParseColError| ParseExprError::new(input, e.to_string()))?;
+ let operator = match &input[op_idx..val_idx] {
+ "<" => ColOperator::Lower,
+ "<=" => ColOperator::LowerOrEqual,
+ "=" => ColOperator::Like,
+ "==" => ColOperator::Equal,
+ "<>" => ColOperator::NotEqual,
+ ">=" => ColOperator::GreaterOrEqual,
+ ">" => ColOperator::Greater,
+ op => {
+ return Err(ParseExprError::new(
+ input,
+ format!("unknown operator: {:?}", op),
+ ));
+ }
+ };
+ let value = &input[val_idx..];
+ let value = value.into();
+ Ok(Self { col, operator, value })
+ }
+}
+
+#[test]
+fn test_col_filter_parsing() {
+ assert_eq!(
+ "remote=false".parse::<ColExpr>().unwrap(),
+ ColExpr::new(Col::Remote, ColOperator::Like, "false"),
+ );
+ assert_eq!(
+ "size<32G".parse::<ColExpr>().unwrap(),
+ ColExpr::new(Col::Size, ColOperator::Lower, "32G"),
+ );
+}
+
+#[derive(Debug, PartialEq)]
+pub enum EvalExprError {
+ NotANumber(String),
+ NotAnId(String),
+ NotADeviceId(String),
+}
+impl EvalExprError {
+}
+impl fmt::Display for EvalExprError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::NotANumber(s) => {
+ write!(f, "{:?} can't be evaluated as a number", &s)
+ }
+ Self::NotAnId(s) => {
+ write!(f, "{:?} can't be evaluated as an id", &s)
+ }
+ Self::NotADeviceId(s) => {
+ write!(f, "{:?} can't be evaluated as a device id", &s)
+ }
+ }
+ }
+}
+impl std::error::Error for EvalExprError {}
+
+fn parse_bool(s: &str) -> bool {
+ let s = s.to_lowercase();
+ s == "x" || s == "true" || s == "yes" || s == "1"
+}
+
+/// Parse numbers like "1234", "32G", "4kB", "54Gib", "1.2M"
+fn parse_integer(input: &str) -> Result<u64, EvalExprError> {
+ let s = input.to_lowercase();
+ let s = s.trim_end_matches('b');
+ let (s, binary) = match s.strip_suffix('i') {
+ Some(s) => (s, true),
+ None => (s, false),
+ };
+ let cut = s.find(|c: char| !(c.is_digit(10) || c=='.'));
+ let (digits, factor): (&str, u64) = match cut {
+ Some(idx) => (
+ &s[..idx],
+ match (&s[idx..], binary) {
+ ("k", false) => 1000,
+ ("k", true) => 1024,
+ ("m", false) => 1000*1000,
+ ("m", true) => 1024*1024,
+ ("g", false) => 1000*1000*1000,
+ ("g", true) => 1024*1024*1024,
+ ("t", false) => 1000*1000*1000*1000,
+ ("t", true) => 1024*1024*1024*1024,
+ _ => {
+ // it's not a number
+ return Err(EvalExprError::NotANumber(input.to_string()));
+ }
+ }
+ ),
+ None => (s, 1),
+ };
+ match digits.parse::<f64>() {
+ Ok(n) => Ok((n * factor as f64).ceil() as u64),
+ _ => Err(EvalExprError::NotANumber(input.to_string())),
+ }
+}
+
+#[test]
+fn test_parse_integer(){
+ assert_eq!(parse_integer("33"), Ok(33));
+ assert_eq!(parse_integer("55G"), Ok(55_000_000_000));
+ assert_eq!(parse_integer("1.23kiB"), Ok(1260));
+}
+
+/// parse numbers like "0.25", "50%"
+fn parse_float(input: &str) -> Result<f64, EvalExprError> {
+ let s = input.to_lowercase();
+ let (s, percent) = match s.strip_suffix('%') {
+ Some(s) => (s, true),
+ None => (s.as_str(), false),
+ };
+ let mut n = s.parse::<f64>()
+ .map_err(|_| EvalExprError::NotANumber(input.to_string()))?;
+ if percent {
+ n /= 100.0;
+ }
+ Ok(n)
+}
+
+#[test]
+fn test_parse_float(){
+ assert_eq!(parse_float("50%").unwrap().to_string(), "0.5".to_string());
+}
+
diff --git a/src/filter.rs b/src/filter.rs
new file mode 100644
index 0000000..7db7ce2
--- /dev/null
+++ b/src/filter.rs
@@ -0,0 +1,80 @@
+use {
+ crate::{
+ col_expr::*,
+ },
+ bet::*,
+ lfs_core::*,
+ std::{
+ str::FromStr,
+ },
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum BoolOperator {
+ And,
+ Or,
+ Not,
+}
+
+#[derive(Debug, Default)]
+pub struct Filter {
+ expr: BeTree<BoolOperator, ColExpr>,
+}
+
+impl Filter {
+ pub fn eval(&self, mount: &Mount) -> Result<bool, EvalExprError> {
+ self.expr.eval_faillible(
+ // leaf evaluation
+ |col_expr| col_expr.eval(mount),
+ // bool operation
+ |op, a, b| match (op, b) {
+ (BoolOperator::And, Some(b)) => Ok(a & b),
+ (BoolOperator::Or, Some(b)) => Ok(a | b),
+ (BoolOperator::Not, None) => Ok(!a),
+ _ => { unreachable!() }
+ },
+ // when to short-circuit
+ |op, a| match (op, a) {
+ (BoolOperator::And, false) => true,
+ (BoolOperator::Or, true) => true,
+ _ => false,
+ },
+ ).map(|b| b.unwrap_or(true))
+ }
+ pub fn filter<'m>(&self, mounts: &'m[Mount]) -> Result<Vec<&'m Mount>, EvalExprError> {
+ let mut filtered = Vec::new();
+ for mount in mounts {
+ if self.eval(mount)? {
+ filtered.push(mount);
+ }
+ }
+ Ok(filtered)
+ }
+}
+
+impl FromStr for Filter {
+ type Err = ParseExprError;
+ fn from_str(input: &str) -> Result<Self, ParseExprError> {
+
+ // we start by reading the global structure
+ let mut expr: BeTree<BoolOperator, String> = BeTree::new();
+ for c in input.chars() {
+ match c {
+ '&' => expr.push_operator(BoolOperator::And),
+ '|' => expr.push_operator(BoolOperator::Or),
+ '!' => expr.push_operator(BoolOperator::Not),
+ ' ' => {},
+ '(' => expr.open_par(),
+ ')' => expr.close_par(),
+ _ => expr.mutate_or_create_atom(String::new).push(c),
+ }
+ }
+
+ // then we parse each leaf
+ let expr = expr.try_map_atoms(|raw| raw.parse())?;
+
+ Ok(Self { expr })
+ }
+}
+
+
diff --git a/src/main.rs b/src/main.rs
index 7e3b7a3..ede0a5b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,8 @@
mod args;
mod col;
+mod col_expr;
mod cols;
+mod filter;
mod json;
mod list_cols;
mod normal;
@@ -58,11 +60,18 @@ fn main() {
);
return;
}
+ args.sort.sort(&mut mounts);
+ let mounts = match args.filter.filter(&mounts) {
+ Ok(mounts) => mounts,
+ Err(e) => {
+ eprintln!("Error in filter evaluation: {}", e);
+ return;
+ }
+ };
if mounts.is_empty() {
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/table.rs b/src/table.rs
index 07e1c9d..d048529 100644
--- a/src/table.rs
+++ b/src/table.rs
@@ -20,7 +20,7 @@ static SIZE_COLOR: u8 = 172;
static BAR_WIDTH: usize = 5;
static INODES_BAR_WIDTH: usize = 5;
-pub fn print(mounts: &[Mount], color: bool, args: &Args) {
+pub fn print(mounts: &[&Mount], color: bool, args: &Args) {
if args.cols.is_empty() {
return;
}