From 4843543a74443a73f56cce98fba4438839faccb2 Mon Sep 17 00:00:00 2001 From: steven-omaha <35634100+steven-omaha@users.noreply.github.com> Date: Tue, 6 Jun 2023 14:59:02 +0200 Subject: [PATCH] feat(export): docstrings, man, README, switches --- README.md | 1 + crates/pacdef_core/src/args/cli.rs | 13 +++ crates/pacdef_core/src/args/datastructure.rs | 9 +- crates/pacdef_core/src/args/parsing.rs | 10 ++- crates/pacdef_core/src/core.rs | 92 +++++++++++++++++--- man/pacdef.8 | 23 ++++- 6 files changed, 133 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a1a65eb..5773095 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ Usage on different machines: | Subcommand | Description | |-----------------------------------|-----------------------------------------------------------------------| | `group import [...]` | create a symlink to the specified group file(s) in your groups folder | +| `group export [args] ...` | export (move) a non-symlink group and re-import it as symlink | | `group list` | list names of all groups | | `group new [-e] [...]` | create new groups, use `-e` to edit them immediately after creation | | `group remove [...]` | remove a previously imported group | diff --git a/crates/pacdef_core/src/args/cli.rs b/crates/pacdef_core/src/args/cli.rs index c7a3278..dc29ff6 100644 --- a/crates/pacdef_core/src/args/cli.rs +++ b/crates/pacdef_core/src/args/cli.rs @@ -34,6 +34,19 @@ fn get_group_cmd() -> Command { let export = Command::new("export") .about("export one or more group files") .arg_required_else_help(true) + .arg( + Arg::new("force") + .short('f') + .long("force") + .action(clap::ArgAction::SetTrue) + .help("overwrite output files if they exist"), + ) + .arg( + Arg::new("output_dir") + .short('o') + .long("output") + .help("(optional) the directory under which to save the group"), + ) .arg( Arg::new("groups") .num_args(1..) diff --git a/crates/pacdef_core/src/args/datastructure.rs b/crates/pacdef_core/src/args/datastructure.rs index 45cf893..854e917 100644 --- a/crates/pacdef_core/src/args/datastructure.rs +++ b/crates/pacdef_core/src/args/datastructure.rs @@ -8,8 +8,7 @@ pub enum Arguments { #[derive(Debug)] pub enum GroupAction { Edit(Groups), - // TODO: optional output dir - Export(Groups), + Export(Groups, OutputDir, Force), Import(Groups), List, New(Groups, Edit), @@ -40,3 +39,9 @@ pub struct Edit(pub bool); #[derive(Debug)] pub struct Noconfirm(pub bool); + +#[derive(Debug)] +pub struct Force(pub bool); + +#[derive(Debug)] +pub struct OutputDir(pub Option); diff --git a/crates/pacdef_core/src/args/parsing.rs b/crates/pacdef_core/src/args/parsing.rs index 35a6694..083cf3f 100644 --- a/crates/pacdef_core/src/args/parsing.rs +++ b/crates/pacdef_core/src/args/parsing.rs @@ -17,7 +17,7 @@ fn parse_group_args(args: &clap::ArgMatches) -> GroupAction { match args.subcommand() { Some(("edit", args)) => Edit(get_groups(args)), - Some(("export", args)) => Export(get_groups(args)), + Some(("export", args)) => Export(get_groups(args), get_output_dir(args), get_force(args)), Some(("import", args)) => Import(get_groups(args)), Some(("list", _)) => List, Some(("new", args)) => New(get_groups(args), get_edit(args)), @@ -69,3 +69,11 @@ fn get_groups(args: &clap::ArgMatches) -> Groups { .collect(), ) } + +fn get_output_dir(args: &clap::ArgMatches) -> OutputDir { + OutputDir(args.get_one::("output_dir").cloned()) +} + +fn get_force(args: &clap::ArgMatches) -> Force { + Force(get_one_arg(args, "force")) +} diff --git a/crates/pacdef_core/src/core.rs b/crates/pacdef_core/src/core.rs index 633b5c0..6a33c41 100644 --- a/crates/pacdef_core/src/core.rs +++ b/crates/pacdef_core/src/core.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::env::current_dir; -use std::fs::{copy, remove_file, rename, File}; +use std::fs::{copy, create_dir_all, remove_file, rename, File}; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; @@ -65,7 +65,9 @@ impl Pacdef { match args { Edit(args::Groups(groups)) => self.edit_groups(groups), - Export(args::Groups(groups)) => self.export_groups(groups), + Export(args::Groups(groups), args::OutputDir(dir), args::Force(force)) => { + self.export_groups(groups, dir.as_ref(), *force) + } Import(args::Groups(groups)) => self.import_groups(groups), List => self.show_groups(), New(args::Groups(groups), args::Edit(edit)) => self.new_groups(groups, *edit), @@ -399,24 +401,92 @@ impl Pacdef { Ok(()) } - fn export_groups(&self, names: &[String]) -> Result<()> { + /// Export pacdef groups by moving a group file to an output dir. The path of the + /// group file relative to the group base dir will be replicated under the output + /// directory. + /// + /// By default, the output dir is the current working directory. `output_dir` may be + /// specified to the path of another directory, in which case `output_dir` must + /// exist. + /// + /// If `force` is `true`, the output file will be overwritten if it exists. + /// + /// # Errors + /// + /// This function will return an error if + /// - the group file is a symlink (in which case exporting makes no sense), + /// - the output file exists and `force` is not `true`, or + /// - the user does not have permission to write to the output dir. + /// + /// # Limitations + /// + /// At the moment we cannot export nested group dirs. The user would have to + /// export every group file individually, or use a shell glob. + fn export_groups( + &self, + names: &[String], + output_dir: Option<&String>, + force: bool, + ) -> Result<()> { let groups = find_groups_by_name(names, &self.groups)?; - let output_dir = current_dir()?; + let output_dir = match output_dir.map(PathBuf::from) { + Some(p) => p, + None => current_dir().context("no output dir specified, getting current directory")?, + }; + + ensure!( + output_dir.exists() && output_dir.is_dir(), + "output must be a directory and exist" + ); for group in &groups { + ensure!(!&group.path.is_symlink(), "cannot export symlinks"); + let mut exported_path = output_dir.clone(); exported_path.push(PathBuf::from(&group.name)); - ensure!(!exported_path.exists(), "{exported_path:?} already exists"); + ensure!( + !force && !exported_path.exists(), + "{exported_path:?} already exists" + ); - move_file(&group.path, &exported_path)?; - symlink(&exported_path, &group.path)?; + create_parent(&exported_path) + .with_context(|| format!("creating parent dir of {exported_path:?}"))?; + move_file(&group.path, &exported_path).context("moving file")?; + symlink(&exported_path, &group.path).context("creating symlink to exported file")?; } Ok(()) } } +/// Create the parent directory of the `path` if that directory does not exist. +/// Does nothing otherwise. +/// +/// # Panics +/// +/// Panics if the path does not have a parent. +/// +/// # Errors +/// +/// This function will propagate errors from [`std::fs::create_dir_all`]. +fn create_parent(path: &Path) -> Result<()> { + let parent = &path.parent().expect("this should never be /"); + if !parent.is_dir() { + create_dir_all(parent).context("creating parent dir")?; + } + Ok(()) +} + +/// Move a file from one place to another. +/// +/// At first [`std::fs::rename`] is used, which fails if `from` and `to` reside under +/// different filesystems. In case that happens, we will resort to copying the files +/// and then removing `from`. +/// +/// # Errors +/// +/// This function will return an error if we lack permission to write the file. fn move_file(from: P, to: Q) -> Result<()> where P: AsRef, @@ -433,8 +503,8 @@ where if e.kind() == std::io::ErrorKind::PermissionDenied { bail!(e); } - copy(from, to)?; - remove_file(from)?; + copy(from, to).with_context(|| format!("copying {from:?} to {to:?}"))?; + remove_file(from).with_context(|| format!("deleting {from:?}"))?; } }; Ok(()) @@ -444,8 +514,8 @@ where /// /// # Errors /// -/// This function will return an error if any of the file names -/// do not match one of group names. +/// This function will return an error if any of the file names do not match one +/// of group names. fn find_groups_by_name<'a>(names: &[String], groups: &'a HashSet) -> Result> { let name_group_map: HashMap<&str, &Group> = groups.iter().map(|g| (g.name.as_str(), g)).collect(); diff --git a/man/pacdef.8 b/man/pacdef.8 index 1033386..5ba7ca4 100644 --- a/man/pacdef.8 +++ b/man/pacdef.8 @@ -41,12 +41,33 @@ The main subcommands are 'group', 'package' and 'version'. .RS 4 All actions related to managing groups. .sp - [...] + [...] .RS 4 edit the content of an existing group .RE . .sp + [] [...] +.RS 4 +Export non-symlink groups by moving and re-importing them. +By default, the output path is the current workdir. +The file path relative to the group base dir will be replicated under the output directory. + +If a specified group is not a symlink, pacdef will return an error. +.sp +-f|--force +.RS 4 +Overwrite the output file if it exists. +.RE +.sp +-o|--output +.RS 4 +The output dir to use instead of the current workdir. +The dir must exist. +.RE +.RE +. +.sp [...] .RS 4 import a new group file or group dir structure