diff options
author | Canop <cano.petrole@gmail.com> | 2022-03-15 18:36:52 +0100 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2022-03-15 18:36:52 +0100 |
commit | bb9e49612c5ed9174c708c9298c08a4ab576c9ec (patch) | |
tree | 8e5e70829821dbc7794c247172209c0678eda9fd | |
parent | d7dec233de26e379a334f9fbfe213ee0cfcffca0 (diff) |
CSV output
Fix #42
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rwxr-xr-x | compile-all-targets.sh | 1 | ||||
-rw-r--r-- | src/args.rs | 8 | ||||
-rw-r--r-- | src/col.rs | 4 | ||||
-rw-r--r-- | src/col_expr.rs | 2 | ||||
-rw-r--r-- | src/csv.rs | 103 | ||||
-rw-r--r-- | src/main.rs | 5 | ||||
-rw-r--r-- | website/docs/img/csv.png | bin | 0 -> 20327 bytes | |||
-rw-r--r-- | website/docs/index.md | 2 | ||||
-rw-r--r-- | website/docs/table.md | 20 |
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. @@ -249,7 +249,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lfs" -version = "2.4.0" +version = "2.5.0-dev" dependencies = [ "argh", "bet", @@ -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" } @@ -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, @@ -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 Binary files differnew file mode 100644 index 0000000..b998cdc --- /dev/null +++ b/website/docs/img/csv.png 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) + |