summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2022-03-15 18:36:52 +0100
committerCanop <cano.petrole@gmail.com>2022-03-15 18:36:52 +0100
commitbb9e49612c5ed9174c708c9298c08a4ab576c9ec (patch)
tree8e5e70829821dbc7794c247172209c0678eda9fd
parentd7dec233de26e379a334f9fbfe213ee0cfcffca0 (diff)
CSV output
Fix #42
-rw-r--r--CHANGELOG.md3
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml7
-rw-r--r--README.md7
-rwxr-xr-xcompile-all-targets.sh1
-rw-r--r--src/args.rs8
-rw-r--r--src/col.rs4
-rw-r--r--src/col_expr.rs2
-rw-r--r--src/csv.rs103
-rw-r--r--src/main.rs5
-rw-r--r--website/docs/img/csv.pngbin0 -> 20327 bytes
-rw-r--r--website/docs/index.md2
-rw-r--r--website/docs/table.md20
13 files changed, 157 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f901b0..09dbed6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+### next
+- with `--csv`, the table is written in CSV. The `--csv-separator` argument lets you change the separator. Filters, sorting, and column choices work for CSV output too - Fix #42
+
<a name="v2.4.0"></a>
### v2.4.0 - 2022/03/04
- 'unreachable' information available in JSON and in the table (in the 'use' column). This mostly concerns disconnected remote filesystems.
diff --git a/Cargo.lock b/Cargo.lock
index 469bc8b..76fcab8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -249,7 +249,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lfs"
-version = "2.4.0"
+version = "2.5.0-dev"
dependencies = [
"argh",
"bet",
diff --git a/Cargo.toml b/Cargo.toml
index 499ac74..3d03501 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "lfs"
-version = "2.4.0"
+version = "2.5.0-dev"
authors = ["dystroy <denys.seguret@gmail.com>"]
edition = "2021"
keywords = ["linux", "filesystem", "fs"]
@@ -9,7 +9,7 @@ categories = ["filesystem", "command-line-utilities"]
description = "give information on mounted filesystems"
repository = "https://github.com/Canop/lfs"
readme = "README.md"
-rust-version = "1.56"
+rust-version = "1.59"
[dependencies]
argh = "0.1.7"
@@ -21,6 +21,9 @@ serde = "1.0"
serde_json = "1.0"
termimad = "0.20.0"
+[profile.release]
+strip = true
+
[patch.crates-io]
# minimad = { path = "../minimad" }
# termimad = { path = "../termimad" }
diff --git a/README.md b/README.md
index 8530e00..fc6374b 100644
--- a/README.md
+++ b/README.md
@@ -35,8 +35,15 @@ Complete documentation lives at **[https://dystroy.org/lfs](https://dystroy.org/
![screenshot](website/docs/img/json-jq-tour.png)
+(you can output the table as CSV too)
+
### Filters
![screenshot](website/docs/img/filters.png)
+### Sort
+
+![screenshot](website/docs/img/s=free-d.png)
+
+
diff --git a/compile-all-targets.sh b/compile-all-targets.sh
index 635a5b2..f3cf882 100755
--- a/compile-all-targets.sh
+++ b/compile-all-targets.sh
@@ -20,7 +20,6 @@ echo " build cleaned"
target="x86_64-linux"
echo -e "${H2}Compiling the linux version - $target${EH}"
cargo build --release
-strip target/release/lfs
mkdir "build/$target/"
cp target/release/lfs "build/$target/"
diff --git a/src/args.rs b/src/args.rs
index d0b10ba..4b3eb47 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -47,6 +47,14 @@ pub struct Args {
#[argh(option, default = "Default::default()", short = 's')]
pub sort: Sorting,
+ /// output as CSV
+ #[argh(switch)]
+ pub csv: bool,
+
+ /// CSV separator (default: ',')
+ #[argh(option, default = "','")]
+ pub csv_separator: char,
+
/// output as JSON
#[argh(switch, short = 'j')]
pub json: bool,
diff --git a/src/col.rs b/src/col.rs
index a57f0a5..29af918 100644
--- a/src/col.rs
+++ b/src/col.rs
@@ -92,12 +92,12 @@ col_enum!(
Remote "remote" "rem": "remote",
Disk "disk" "dsk": "disk" default,
Used "used": "used" default,
- Use "use": "use%" default,
+ Use "use": "use" default,
UsePercent "use_percent": "use%",
Free "free": "free" default,
Size "size": "size" default,
InodesUsed "inodes_used" "iused": "used inodes",
- InodesUse "inodes" "ino" "inodes_use" "iuse": "inodes%",
+ InodesUse "inodes" "ino" "inodes_use" "iuse": "inodes",
InodesUsePercent "inodes_use_percent" "iuse_percent": "inodes%",
InodesFree "inodes_free" "ifree": "free inodes",
InodesCount "inodes_total" "inodes_count" "itotal": "inodes total",
diff --git a/src/col_expr.rs b/src/col_expr.rs
index d2b7082..2524878 100644
--- a/src/col_expr.rs
+++ b/src/col_expr.rs
@@ -86,7 +86,7 @@ impl ColExpr {
&self.value,
),
Col::Label => self.operator.eval_option_str(
- mount.fs_label.as_ref().map(|s| s.as_str()),
+ mount.fs_label.as_deref(),
&self.value,
),
Col::Type => self.operator.eval_str(
diff --git a/src/csv.rs b/src/csv.rs
new file mode 100644
index 0000000..3b4d2c0
--- /dev/null
+++ b/src/csv.rs
@@ -0,0 +1,103 @@
+use {
+ crate::{
+ Args, col::Col,
+ },
+ lfs_core::*,
+ std::{
+ fmt::Display,
+ io::Write,
+ },
+};
+
+/// Utility to write in CSV
+struct Csv<W: Write> {
+ separator: char,
+ w: W,
+}
+
+impl<W: Write> Csv<W> {
+ pub fn new(separator: char, w: W) -> Self {
+ Self { separator, w }
+ }
+ pub fn cell<D: Display>(&mut self, content: D) -> Result<(), std::io::Error> {
+ let s = content.to_string();
+ let needs_quotes = s.contains(self.separator) || s.contains('"') || s.contains('\n');
+ if needs_quotes {
+ write!(self.w, "\"")?;
+ for c in s.chars() {
+ if c == '"' {
+ write!(self.w, "\"\"")?;
+ } else {
+ write!(self.w, "{}", c)?;
+ }
+ }
+ write!(self.w, "\"")?;
+ } else {
+ write!(self.w, "{}", s)?;
+ }
+ write!(self.w, "{}", self.separator)
+ }
+ pub fn cell_opt<D: Display>(&mut self, content: Option<D>) -> Result<(), std::io::Error> {
+ if let Some(c) = content {
+ self.cell(c)
+ } else {
+ write!(self.w, "{}", self.separator)
+ }
+ }
+ pub fn end_line(&mut self) -> Result<(), std::io::Error> {
+ writeln!(self.w)
+ }
+}
+
+pub fn print(mounts: &[&Mount], args: &Args) -> Result<(), std::io::Error> {
+ let units = args.units;
+ let mut csv = Csv::new(args.csv_separator, std::io::stdout());
+ for col in args.cols.cols() {
+ csv.cell(col.title())?;
+ }
+ csv.end_line()?;
+ for mount in mounts {
+ for col in args.cols.cols() {
+ match col {
+ Col::Id => csv.cell(&mount.info.id),
+ Col::Dev => csv.cell(format!("{}:{}", mount.info.dev.major, mount.info.dev.minor)),
+ Col::Filesystem => csv.cell(&mount.info.fs),
+ Col::Label => csv.cell_opt(mount.fs_label.as_ref()),
+ Col::Type => csv.cell(&mount.info.fs_type),
+ Col::Remote => csv.cell(if mount.info.is_remote() { "yes" } else { "no" }),
+ Col::Disk => csv.cell_opt(mount.disk.as_ref().map(|d| d.disk_type())),
+ Col::Used => csv.cell_opt(mount.stats().map(|s| units.fmt(s.used()))),
+ Col::Use => csv.cell_opt(mount.stats().map(|s| s.use_share())),
+ Col::UsePercent => csv.cell_opt(mount.stats().map(|s| format!("{:.0}%", 100.0 * s.use_share()))),
+ Col::Free => csv.cell_opt(mount.stats().map(|s| units.fmt(s.available()))),
+ Col::Size => csv.cell_opt(mount.stats().map(|s| units.fmt(s.size()))),
+ Col::InodesUsed => csv.cell_opt(mount.inodes().map(|i| i.used())),
+ Col::InodesUse => csv.cell_opt(mount.inodes().map(|i| i.use_share())),
+ Col::InodesUsePercent => csv.cell_opt(mount.inodes().map(|i| format!("{:.0}%", 100.0 * i.use_share()))),
+ Col::InodesFree => csv.cell_opt(mount.inodes().map(|i| i.favail)),
+ Col::InodesCount => csv.cell_opt(mount.inodes().map(|i| i.files)),
+ Col::MountPoint => csv.cell(&mount.info.mount_point.to_string_lossy()),
+ }?;
+ }
+ csv.end_line()?;
+ }
+ Ok(())
+}
+
+#[test]
+fn test_csv() {
+ use std::io::Cursor;
+ let mut w = Cursor::new(Vec::new());
+ let mut csv = Csv::new(';', &mut w);
+ csv.cell("1;2;3").unwrap();
+ csv.cell("\"").unwrap();
+ csv.cell("").unwrap();
+ csv.end_line().unwrap();
+ csv.cell(3).unwrap();
+ let s = String::from_utf8(w.into_inner()).unwrap();
+ assert_eq!(
+ s,
+r#""1;2;3";"""";;
+3;"#,
+ );
+}
diff --git a/src/main.rs b/src/main.rs
index 2a92000..4957c18 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@ mod args;
mod col;
mod col_expr;
mod cols;
+mod csv;
mod filter;
mod json;
mod list_cols;
@@ -66,6 +67,10 @@ fn main() {
return;
}
};
+ if args.csv {
+ csv::print(&mounts, &args).expect("writing csv failed");
+ return;
+ }
if args.json {
println!(
"{}",
diff --git a/website/docs/img/csv.png b/website/docs/img/csv.png
new file mode 100644
index 0000000..b998cdc
--- /dev/null
+++ b/website/docs/img/csv.png
Binary files differ
diff --git a/website/docs/index.md b/website/docs/index.md
index 31ac611..1e70793 100644
--- a/website/docs/index.md
+++ b/website/docs/index.md
@@ -28,6 +28,8 @@ The default display of **lfs** is a table, which can be configured with the colu
See [Table](./table) for the definition of the columns and the syntax for choosing them, or on how to [sort rows](./table#sort).
+The table can also be exported in [CSV](./table#csv).
+
# JSON
`lfs --json` outputs the result as JSON which can be used for your own scripts or programs.
diff --git a/website/docs/table.md b/website/docs/table.md
index ad8aaab..4079114 100644
--- a/website/docs/table.md
+++ b/website/docs/table.md
@@ -87,3 +87,23 @@ For example, sorting on the device id:
Or sorting on the remaining free space, in descending order:
![screen](img/s=free-d.png)
+
+# CSV
+
+With the `--csv` argument, you can ask lfs to output the table in CSV:
+
+```bash
+lfs --csv > mounts.csv
+```
+
+You may choose the separator with the `--csv-separator` argument.
+
+Filters, sorting, and column selection work the same than for standard tables so you may do this:
+
+```bash
+lfs --csv -f 'size>100G' -c remote+default+inodes > mounts.csv
+```
+which would give something like this:
+
+![screen](img/csv.png)
+