From 7ac9fbcde713044f038501b40ba4fad1adb824e3 Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Sat, 15 Feb 2025 20:57:37 +0200 Subject: [PATCH 1/5] `findmnt`: Set up command entrypoint --- Cargo.lock | 9 +++++++++ Cargo.toml | 5 +++++ src/uu/findmnt/Cargo.toml | 15 +++++++++++++++ src/uu/findmnt/findmnt.md | 7 +++++++ src/uu/findmnt/src/findmnt.rs | 19 +++++++++++++++++++ src/uu/findmnt/src/main.rs | 1 + 6 files changed, 56 insertions(+) create mode 100644 src/uu/findmnt/Cargo.toml create mode 100644 src/uu/findmnt/findmnt.md create mode 100644 src/uu/findmnt/src/findmnt.rs create mode 100644 src/uu/findmnt/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5e59468a..08b90078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,6 +988,7 @@ dependencies = [ "uu_blockdev", "uu_ctrlaltdel", "uu_dmesg", + "uu_findmnt", "uu_fsfreeze", "uu_last", "uu_lscpu", @@ -1031,6 +1032,14 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_findmnt" +version = "0.0.1" +dependencies = [ + "clap", + "uucore", +] + [[package]] name = "uu_fsfreeze" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 3c625d74..3ad9bc64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ feat_common_core = [ "blockdev", "ctrlaltdel", "dmesg", + "findmnt", "fsfreeze", "last", "lscpu", @@ -38,6 +39,9 @@ feat_common_core = [ "setsid", ] +[workspace] +members = ["src/uu/findmnt"] + [workspace.dependencies] clap = { version = "4.4", features = ["wrap_help", "cargo"] } clap_complete = "4.4" @@ -73,6 +77,7 @@ uucore = { workspace = true } blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" } ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" } dmesg = { optional = true, version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg" } +findmnt = { optional = true, version = "0.0.1", package = "uu_findmnt", path = "src/uu/findmnt" } fsfreeze = { optional = true, version = "0.0.1", package = "uu_fsfreeze", path = "src/uu/fsfreeze" } last = { optional = true, version = "0.0.1", package = "uu_last", path = "src/uu/last" } lscpu = { optional = true, version = "0.0.1", package = "uu_lscpu", path = "src/uu/lscpu" } diff --git a/src/uu/findmnt/Cargo.toml b/src/uu/findmnt/Cargo.toml new file mode 100644 index 00000000..2edc3855 --- /dev/null +++ b/src/uu/findmnt/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "uu_findmnt" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/findmnt.rs" + +[[bin]] +name = "findmnt" +path = "src/main.rs" + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } diff --git a/src/uu/findmnt/findmnt.md b/src/uu/findmnt/findmnt.md new file mode 100644 index 00000000..33948d4c --- /dev/null +++ b/src/uu/findmnt/findmnt.md @@ -0,0 +1,7 @@ +# findmnt + +``` +findmnt [OPTION]... +``` + +display information about mounted filesystems diff --git a/src/uu/findmnt/src/findmnt.rs b/src/uu/findmnt/src/findmnt.rs new file mode 100644 index 00000000..daa486a7 --- /dev/null +++ b/src/uu/findmnt/src/findmnt.rs @@ -0,0 +1,19 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, Command}; +use uucore::error::UResult; + +#[uucore::main] +pub fn uumain(_args: impl uucore::Args) -> UResult<()> { + println!("uu_findmnt: Hello world"); + + Ok(()) +} + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) +} diff --git a/src/uu/findmnt/src/main.rs b/src/uu/findmnt/src/main.rs new file mode 100644 index 00000000..c30a45ec --- /dev/null +++ b/src/uu/findmnt/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_findmnt); From 6bdaee07f20d9c0ec611fbf7bc9c99bdb78d812a Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Sat, 15 Feb 2025 21:36:39 +0200 Subject: [PATCH 2/5] `findmnt`: Implement basic functionality --- src/uu/findmnt/src/findmnt.rs | 54 +++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/uu/findmnt/src/findmnt.rs b/src/uu/findmnt/src/findmnt.rs index daa486a7..03ebce8f 100644 --- a/src/uu/findmnt/src/findmnt.rs +++ b/src/uu/findmnt/src/findmnt.rs @@ -3,17 +3,65 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::fs; + use clap::{crate_version, Command}; use uucore::error::UResult; +#[derive(Debug)] +struct Mount { + target: String, + source: String, + fs_type: String, + options: String, +} + +impl Mount { + // Parses a line from `/proc/mounts`, which follows the format described under fstab(5) + // Each line contains six space-separated fields, last two being unused in `/proc/mounts` + fn parse(input: &str) -> Self { + let parts: Vec<_> = input.trim().split(" ").collect(); + assert_eq!(parts.len(), 6); + + let source = parts[0].to_string(); + let target = parts[1].to_string(); + let fs_type = parts[2].to_string(); + let options = parts[3].to_string(); + //Ignore fields 5 and 6 as they are not used in /proc/mounts and are only populated for compatibility purposes + + Self { + source, + target, + fs_type, + options, + } + } +} + +fn read_mounts() -> Vec { + let content = fs::read_to_string("/proc/mounts").expect("Could not read /proc/mounts"); + let mounts: Vec<_> = content.lines().map(Mount::parse).collect(); + mounts +} + +fn print_output(mounts: Vec) { + for mount in mounts { + println!( + "{}\t{}\t{}\t{}", + mount.target, mount.source, mount.fs_type, mount.options + ) + } +} + #[uucore::main] pub fn uumain(_args: impl uucore::Args) -> UResult<()> { - println!("uu_findmnt: Hello world"); + let mounts = read_mounts(); + + print_output(mounts); Ok(()) } pub fn uu_app() -> Command { - Command::new(uucore::util_name()) - .version(crate_version!()) + Command::new(uucore::util_name()).version(crate_version!()) } From e60b2d117cf0a494f28ff7bb4baa353b46e16d5a Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Sun, 16 Feb 2025 03:06:49 +0200 Subject: [PATCH 3/5] `findmnt`: Parse hierarchy of mounted filesystems --- Cargo.lock | 2 + src/uu/findmnt/Cargo.toml | 2 + src/uu/findmnt/src/findmnt.rs | 170 +++++++++++++++++++++++++++++----- 3 files changed, 149 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08b90078..a008dff3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1037,6 +1037,8 @@ name = "uu_findmnt" version = "0.0.1" dependencies = [ "clap", + "serde", + "serde_json", "uucore", ] diff --git a/src/uu/findmnt/Cargo.toml b/src/uu/findmnt/Cargo.toml index 2edc3855..e95f8594 100644 --- a/src/uu/findmnt/Cargo.toml +++ b/src/uu/findmnt/Cargo.toml @@ -13,3 +13,5 @@ path = "src/main.rs" [dependencies] uucore = { workspace = true } clap = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/src/uu/findmnt/src/findmnt.rs b/src/uu/findmnt/src/findmnt.rs index 03ebce8f..a92dedb8 100644 --- a/src/uu/findmnt/src/findmnt.rs +++ b/src/uu/findmnt/src/findmnt.rs @@ -3,65 +3,185 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::fs; +use std::{collections::HashMap, fs}; use clap::{crate_version, Command}; -use uucore::error::UResult; +use serde::Serialize; +use uucore::{error::UResult, format_usage, help_about, help_usage}; #[derive(Debug)] +struct MountData { + filesystems: Vec, +} + +#[derive(Debug, Serialize, Clone)] struct Mount { target: String, source: String, - fs_type: String, + fstype: String, + options: String, + + #[serde(skip_serializing_if = "Vec::is_empty")] + children: Vec, +} + +// Flat mount information structure as described in: +// https://www.man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html +#[derive(Debug)] +struct MountEntry { + id: usize, + parent_id: usize, + _root: String, + target: String, + source: String, options: String, + fstype: String, } -impl Mount { - // Parses a line from `/proc/mounts`, which follows the format described under fstab(5) - // Each line contains six space-separated fields, last two being unused in `/proc/mounts` +impl MountEntry { + // Parses a line from `/proc/self/mountinfo`, which follows the format described under proc_pid_mountinfo(5) + // We ignore some of the fields as they are not relevant for the purposes of findmnt fn parse(input: &str) -> Self { - let parts: Vec<_> = input.trim().split(" ").collect(); - assert_eq!(parts.len(), 6); + let mut parts = input.trim().split(" "); + + let id = parts + .next() + .unwrap() + .parse::() + .expect("Could not parse Mount ID"); + let parent_id = parts + .next() + .unwrap() + .parse::() + .expect("Could not parse Parent ID"); + parts.next(); // Skip field 3 + + let root = parts.next().unwrap().to_string(); + let target = parts.next().unwrap().to_string(); + let options = parts.next().unwrap().to_string(); + + // Field 7 is a variable-length list of space-separated optional values, it's end is marked by a `-` separator + // Skip everything until the separator, and the separator itself + let mut parts = parts.skip_while(|s| *s != "-").skip(1); - let source = parts[0].to_string(); - let target = parts[1].to_string(); - let fs_type = parts[2].to_string(); - let options = parts[3].to_string(); - //Ignore fields 5 and 6 as they are not used in /proc/mounts and are only populated for compatibility purposes + let fstype = parts.next().unwrap().to_string(); + let source = parts.next().unwrap().to_string(); + + // Ignore the rest Self { + id, + parent_id, + _root: root, source, target, - fs_type, + fstype, options, } } } fn read_mounts() -> Vec { - let content = fs::read_to_string("/proc/mounts").expect("Could not read /proc/mounts"); - let mounts: Vec<_> = content.lines().map(Mount::parse).collect(); - mounts + let content = + fs::read_to_string("/proc/self/mountinfo").expect("Could not read /proc/self/mountinfo"); + let mount_entries: HashMap<_, _> = content + .lines() + .map(MountEntry::parse) + .map(|me| (me.id, me)) + .collect(); + + // Very odd if this happens but technically possible + if mount_entries.is_empty() { + return vec![]; + } + + // Try finding the "proper" root mounts, ie. ones were id == parent_id + let mut root_mounts: Vec<_> = mount_entries + .iter() + .filter(|(_, e)| e.parent_id == e.id) + .map(|(_, e)| e) + .collect(); + + // In many cases there will be no "proper" roots, so just use the mount with the lowest parent_id + if root_mounts.is_empty() { + let root_entry = mount_entries + .iter() + .min_by_key(|(_, e)| e.parent_id) + .unwrap() + .1; + root_mounts.push(root_entry) + } + + fn with_children(m: &MountEntry, haystack: &HashMap) -> Mount { + let child_entries: Vec<&MountEntry> = haystack + .iter() + .filter(|(_, e)| e.parent_id == m.id) + .map(|(_, e)| e) + .collect(); + + // TODO: Use iterator + let mut children: Vec = vec![]; + for entry in child_entries { + children.push(with_children(entry, haystack)); + } + + Mount { + target: m.target.clone(), + source: m.source.clone(), + fstype: m.fstype.clone(), + options: m.options.clone(), + children, + } + } + + root_mounts + .iter() + .map(|e| with_children(e, &mount_entries)) + .collect() } -fn print_output(mounts: Vec) { - for mount in mounts { +fn print_output(fs: MountData) { + fn indent(depth: usize) -> usize { + depth * 2 + } + + fn print_mount(mount: Mount, depth: usize) { println!( - "{}\t{}\t{}\t{}", - mount.target, mount.source, mount.fs_type, mount.options - ) + "{}{}\t{}\t{}\t{}", + " ".repeat(indent(depth)), + mount.target, + mount.source, + mount.fstype, + mount.options + ); + for child in mount.children { + print_mount(child, depth + 1) + } + } + + for mount in fs.filesystems { + print_mount(mount, 0); } } #[uucore::main] pub fn uumain(_args: impl uucore::Args) -> UResult<()> { - let mounts = read_mounts(); + let fs = MountData { + filesystems: read_mounts(), + }; - print_output(mounts); + print_output(fs); Ok(()) } +const ABOUT: &str = help_about!("findmnt.md"); +const USAGE: &str = help_usage!("findmnt.md"); + pub fn uu_app() -> Command { - Command::new(uucore::util_name()).version(crate_version!()) + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) } From baf98108764ba9b2dcde5fbb18df94d56d495b4c Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Sun, 16 Feb 2025 14:08:35 +0200 Subject: [PATCH 4/5] `findmnt`: Add JSON output option --- src/uu/findmnt/src/findmnt.rs | 37 ++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/uu/findmnt/src/findmnt.rs b/src/uu/findmnt/src/findmnt.rs index a92dedb8..f4c468b8 100644 --- a/src/uu/findmnt/src/findmnt.rs +++ b/src/uu/findmnt/src/findmnt.rs @@ -5,11 +5,11 @@ use std::{collections::HashMap, fs}; -use clap::{crate_version, Command}; +use clap::{crate_version, Arg, ArgAction, Command}; use serde::Serialize; use uucore::{error::UResult, format_usage, help_about, help_usage}; -#[derive(Debug)] +#[derive(Debug, Serialize)] struct MountData { filesystems: Vec, } @@ -140,7 +140,13 @@ fn read_mounts() -> Vec { .collect() } -fn print_output(fs: MountData) { +fn print_output(fs: MountData, options: OutputOptions) { + if options.json { + let json = serde_json::to_string_pretty(&fs).unwrap(); + println!("{}", json); + return; + } + fn indent(depth: usize) -> usize { depth * 2 } @@ -164,13 +170,23 @@ fn print_output(fs: MountData) { } } +struct OutputOptions { + json: bool, +} + #[uucore::main] -pub fn uumain(_args: impl uucore::Args) -> UResult<()> { +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + + let output_opts = OutputOptions { + json: matches.get_flag(options::JSON), + }; + let fs = MountData { filesystems: read_mounts(), }; - print_output(fs); + print_output(fs, output_opts); Ok(()) } @@ -178,10 +194,21 @@ pub fn uumain(_args: impl uucore::Args) -> UResult<()> { const ABOUT: &str = help_about!("findmnt.md"); const USAGE: &str = help_usage!("findmnt.md"); +mod options { + pub const JSON: &str = "json"; +} + pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .arg( + Arg::new(options::JSON) + .short('J') + .long("json") + .help("Use JSON output format") + .action(ArgAction::SetTrue), + ) } From 763641c38643332823991d5d085225a904aea065 Mon Sep 17 00:00:00 2001 From: alxndrv <> Date: Sun, 16 Feb 2025 15:22:50 +0200 Subject: [PATCH 5/5] `findmnt`: Print output as basic table, tree structure is gone for now --- src/uu/findmnt/src/findmnt.rs | 160 ++++++++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 36 deletions(-) diff --git a/src/uu/findmnt/src/findmnt.rs b/src/uu/findmnt/src/findmnt.rs index f4c468b8..3cfda128 100644 --- a/src/uu/findmnt/src/findmnt.rs +++ b/src/uu/findmnt/src/findmnt.rs @@ -3,28 +3,12 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::{collections::HashMap, fs}; +use std::{collections::HashMap, fs, string}; use clap::{crate_version, Arg, ArgAction, Command}; use serde::Serialize; use uucore::{error::UResult, format_usage, help_about, help_usage}; -#[derive(Debug, Serialize)] -struct MountData { - filesystems: Vec, -} - -#[derive(Debug, Serialize, Clone)] -struct Mount { - target: String, - source: String, - fstype: String, - options: String, - - #[serde(skip_serializing_if = "Vec::is_empty")] - children: Vec, -} - // Flat mount information structure as described in: // https://www.man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html #[derive(Debug)] @@ -81,6 +65,38 @@ impl MountEntry { } } +// Data structures used for the final output +#[derive(Debug, Serialize)] +struct MountData { + filesystems: Vec, +} + +#[derive(Debug, Serialize, Clone)] +struct Mount { + target: String, + source: String, + fstype: String, + options: String, + + #[serde(skip_serializing_if = "Vec::is_empty")] + children: Vec, +} + +impl Mount { + fn get_value(&self, field: &Column) -> String { + match field { + Column::FsRoot => todo!(), + Column::FsType => self.fstype.clone(), + Column::FsOptions => todo!(), + Column::Id => todo!(), + Column::Options => self.options.clone(), + Column::Parent => todo!(), + Column::Source => self.source.clone(), + Column::Target => self.target.clone(), + } + } +} + fn read_mounts() -> Vec { let content = fs::read_to_string("/proc/self/mountinfo").expect("Could not read /proc/self/mountinfo"); @@ -140,6 +156,66 @@ fn read_mounts() -> Vec { .collect() } +// TODO: Add the remaining columns supported by `findmnt` +#[derive(Debug, Clone)] +enum Column { + FsRoot, + FsType, + FsOptions, + Id, + Options, + Parent, + Source, + Target, +} + +impl Column { + fn header_text(&self) -> &'static str { + match self { + Column::FsRoot => "FSROOT", + Column::FsType => "FSTYPE", + Column::FsOptions => "FS-OPTIONS", + Column::Id => "ID", + Column::Options => "OPTIONS", + Column::Parent => "PARENT", + Column::Source => "SOURCE", + Column::Target => "TARGET", + } + } + + fn header_width(&self) -> usize { + self.header_text().len() + } +} + +const DEFAULT_COLS: &[Column] = &[ + Column::Target, + Column::Source, + Column::FsType, + Column::Options, +]; + +struct OutputOptions { + json: bool, + cols: Vec, +} + +fn get_column_widths(cols: &Vec, rows: &Vec) -> Vec { + // Initialize max_widths with the width of the column headers + let mut max_widths: Vec<_> = cols.iter().map(|col| col.header_width()).collect(); + + // Go through all table rows, and check if any values are wider than the header text + // Set that as the new max_width for that column + for row in rows { + for (i, col) in cols.iter().enumerate() { + let value_width = row.get_value(col).len(); + max_widths[i] = max_widths[i].max(value_width); + } + } + + max_widths +} + fn print_output(fs: MountData, options: OutputOptions) { if options.json { let json = serde_json::to_string_pretty(&fs).unwrap(); @@ -147,31 +223,40 @@ fn print_output(fs: MountData, options: OutputOptions) { return; } - fn indent(depth: usize) -> usize { - depth * 2 - } + // Before printing, the mount tree needs to be flatten into a single vector of rows + let mut flattened_mounts: Vec = vec![]; - fn print_mount(mount: Mount, depth: usize) { - println!( - "{}{}\t{}\t{}\t{}", - " ".repeat(indent(depth)), - mount.target, - mount.source, - mount.fstype, - mount.options - ); - for child in mount.children { - print_mount(child, depth + 1) + fn flatten(mnt: &Mount, acc: &mut Vec) { + acc.push(mnt.clone()); + for child in &mnt.children { + flatten(&child, acc); } } - for mount in fs.filesystems { - print_mount(mount, 0); + for rootfs in &fs.filesystems { + flatten(rootfs, &mut flattened_mounts); } -} -struct OutputOptions { - json: bool, + let col_widths = get_column_widths(&options.cols, &flattened_mounts); + + // Print headers + let headers: Vec<_> = options + .cols + .iter() + .enumerate() + .map(|(i, col)| format!("{: = options + .cols + .iter() + .enumerate() + .map(|(i, col)| format!("{: UResult<()> { let output_opts = OutputOptions { json: matches.get_flag(options::JSON), + + // TODO: Use arguments to control which cols are printed out + cols: Vec::from(DEFAULT_COLS), }; let fs = MountData {