diff --git a/Cargo.lock b/Cargo.lock index 0ef07a1..c613691 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,7 @@ dependencies = [ "serde_json", "serde_yaml", "termios", + "walkdir", ] [[package]] @@ -481,6 +482,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scratch" version = "1.0.5" @@ -618,6 +628,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "winapi" version = "0.2.8" diff --git a/crates/pacdef_core/Cargo.toml b/crates/pacdef_core/Cargo.toml index 705a92d..d72960d 100644 --- a/crates/pacdef_core/Cargo.toml +++ b/crates/pacdef_core/Cargo.toml @@ -19,6 +19,7 @@ const_format = { version = "0.2", default-features = false } path-absolutize = "3.0" regex = { version = "1.7", default-features = false, features = ["std"] } termios = "0.3" +walkdir = "2.3" serde = "1.0" serde_derive = "1.0" diff --git a/crates/pacdef_core/src/grouping/group.rs b/crates/pacdef_core/src/grouping/group.rs index b1b843f..ecf72d5 100644 --- a/crates/pacdef_core/src/grouping/group.rs +++ b/crates/pacdef_core/src/grouping/group.rs @@ -6,6 +6,9 @@ use std::io::Write; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; +use walkdir::WalkDir; + +use crate::path::get_relative_path; use super::{Package, Section}; @@ -20,7 +23,7 @@ pub struct Group { } impl Group { - /// Load all group files from the pacdef group dir. + /// Load all group files from the pacdef group dir by recursing through the group dir. /// /// This method will print a warning if /// - there are no files under `group_dir`, or @@ -39,19 +42,25 @@ impl Group { create_dir(group_dir).context("group dir does not exist, creating")?; } - for entry in group_dir.read_dir().context("reading group dir")? { - let file = entry.context("getting group file")?; + for entry in WalkDir::new(group_dir).follow_links(true).min_depth(1) { + let file = entry?; let path = file.path(); + if path.is_dir() { + continue; + } + if warn_not_symlinks && !path.is_symlink() { + // TODO is there an efficient way to make sure *any* of the elements in the path + // is a symlink? eprintln!( "WARNING: group file {} is not a symlink", path.to_string_lossy() ); } - let group = - Self::try_from(&path).with_context(|| format!("reading group file {path:?}"))?; + let group = Self::try_from(path, group_dir) + .with_context(|| format!("reading group file {path:?}"))?; result.insert(group); } @@ -96,17 +105,14 @@ impl Eq for Group { } impl Group { - fn try_from

(p: P) -> Result + fn try_from

(path: P, group_dir: P) -> Result where P: AsRef, { - let path = p.as_ref(); + let path = path.as_ref(); let content = read_to_string(path).context("reading file content")?; - let name = path - .file_name() - .context("getting file name")? - .to_string_lossy() - .to_string(); + + let name = extract_group_name(path, group_dir.as_ref()); let mut lines = content.lines().peekable(); let mut sections = HashSet::new(); @@ -158,6 +164,30 @@ impl Group { } } +/// Extract the group name from its path relative to the group path. +/// All subdirectories are concatenated using `'/'`. +/// +/// # Example +/// +/// If the group dir is `~/.config/pacdef/groups`, and the group file is +/// `~/.config/pacdef/groups/generic/base`, then the group name is +/// `"generic/base"`. +/// +/// # Panics +/// +/// Panics if `path` and `group_path` are identical. +fn extract_group_name(path: &Path, group_path: &Path) -> String { + get_relative_path(path, group_path) + .iter() + .map(|p| p.to_string_lossy().to_string()) + .reduce(|mut a, b| { + a.push('/'); + a.push_str(&b); + a + }) + .expect("must have at least one element") +} + impl Display for Group { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut sections: Vec<_> = self.sections.iter().collect(); @@ -220,3 +250,18 @@ fn add_new_section_with_packages( group_file_content.push_str(&format!("{p}\n")); } } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + #[test] + fn extract_group_name() { + let path = PathBuf::from("/a/b/c/d/e"); + let group_path = PathBuf::from("/a/b/c"); + let expected = String::from("d/e"); + + let result = super::extract_group_name(&path, &group_path); + assert_eq!(result, expected); + } +} diff --git a/crates/pacdef_core/src/path.rs b/crates/pacdef_core/src/path.rs index 833a70b..fd355c5 100644 --- a/crates/pacdef_core/src/path.rs +++ b/crates/pacdef_core/src/path.rs @@ -2,8 +2,8 @@ All functions related to `pacdef`'s internal paths. */ -use std::env; use std::path::PathBuf; +use std::{env, path::Path}; use anyhow::{Context, Result}; @@ -99,3 +99,46 @@ pub(crate) fn binary_in_path(name: &str) -> Result { } Ok(false) } + +/// Determine the relative path of `full_path` in relation to `base_path`. +/// +/// # Panics +/// +/// Panics if at least one element in `base_path` does not match the corresponding +/// element in `full_path`. +pub(crate) fn get_relative_path

(full_path: P, base_path: P) -> PathBuf +where + P: AsRef, +{ + let mut file_iter = full_path.as_ref().iter(); + base_path + .as_ref() + .iter() + .zip(&mut file_iter) + .for_each(|(a, b)| assert_eq!(a, b)); + let relative_path: PathBuf = file_iter.collect(); + relative_path +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::get_relative_path; + + #[test] + fn relative_path() { + let full = PathBuf::from("/a/b/c/d/e"); + let base = PathBuf::from("/a/b/c"); + let relative = get_relative_path(full, base); + assert_eq!(relative, PathBuf::from("d/e")); + } + + #[test] + #[should_panic] + fn relative_path_panic() { + let full = PathBuf::from("/a/b/z/d/e"); + let base = PathBuf::from("/a/b/c"); + get_relative_path(full, base); + } +}