feat(export): docstrings, man, README, switches

This commit is contained in:
steven-omaha
2023-06-06 15:30:24 +02:00
parent 0610cd0731
commit 4843543a74
6 changed files with 133 additions and 15 deletions
+1
View File
@@ -143,6 +143,7 @@ Usage on different machines:
| Subcommand | Description | | Subcommand | Description |
|-----------------------------------|-----------------------------------------------------------------------| |-----------------------------------|-----------------------------------------------------------------------|
| `group import [<path>...]` | create a symlink to the specified group file(s) in your groups folder | | `group import [<path>...]` | create a symlink to the specified group file(s) in your groups folder |
| `group export [args] <group> ...` | export (move) a non-symlink group and re-import it as symlink |
| `group list` | list names of all groups | | `group list` | list names of all groups |
| `group new [-e] [<group>...]` | create new groups, use `-e` to edit them immediately after creation | | `group new [-e] [<group>...]` | create new groups, use `-e` to edit them immediately after creation |
| `group remove [<group>...]` | remove a previously imported group | | `group remove [<group>...]` | remove a previously imported group |
+13
View File
@@ -34,6 +34,19 @@ fn get_group_cmd() -> Command {
let export = Command::new("export") let export = Command::new("export")
.about("export one or more group files") .about("export one or more group files")
.arg_required_else_help(true) .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(
Arg::new("groups") Arg::new("groups")
.num_args(1..) .num_args(1..)
+7 -2
View File
@@ -8,8 +8,7 @@ pub enum Arguments {
#[derive(Debug)] #[derive(Debug)]
pub enum GroupAction { pub enum GroupAction {
Edit(Groups), Edit(Groups),
// TODO: optional output dir Export(Groups, OutputDir, Force),
Export(Groups),
Import(Groups), Import(Groups),
List, List,
New(Groups, Edit), New(Groups, Edit),
@@ -40,3 +39,9 @@ pub struct Edit(pub bool);
#[derive(Debug)] #[derive(Debug)]
pub struct Noconfirm(pub bool); pub struct Noconfirm(pub bool);
#[derive(Debug)]
pub struct Force(pub bool);
#[derive(Debug)]
pub struct OutputDir(pub Option<String>);
+9 -1
View File
@@ -17,7 +17,7 @@ fn parse_group_args(args: &clap::ArgMatches) -> GroupAction {
match args.subcommand() { match args.subcommand() {
Some(("edit", args)) => Edit(get_groups(args)), 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(("import", args)) => Import(get_groups(args)),
Some(("list", _)) => List, Some(("list", _)) => List,
Some(("new", args)) => New(get_groups(args), get_edit(args)), Some(("new", args)) => New(get_groups(args), get_edit(args)),
@@ -69,3 +69,11 @@ fn get_groups(args: &clap::ArgMatches) -> Groups {
.collect(), .collect(),
) )
} }
fn get_output_dir(args: &clap::ArgMatches) -> OutputDir {
OutputDir(args.get_one::<String>("output_dir").cloned())
}
fn get_force(args: &clap::ArgMatches) -> Force {
Force(get_one_arg(args, "force"))
}
+81 -11
View File
@@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env::current_dir; 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::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -65,7 +65,9 @@ impl Pacdef {
match args { match args {
Edit(args::Groups(groups)) => self.edit_groups(groups), 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), Import(args::Groups(groups)) => self.import_groups(groups),
List => self.show_groups(), List => self.show_groups(),
New(args::Groups(groups), args::Edit(edit)) => self.new_groups(groups, *edit), New(args::Groups(groups), args::Edit(edit)) => self.new_groups(groups, *edit),
@@ -399,24 +401,92 @@ impl Pacdef {
Ok(()) 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 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 { for group in &groups {
ensure!(!&group.path.is_symlink(), "cannot export symlinks");
let mut exported_path = output_dir.clone(); let mut exported_path = output_dir.clone();
exported_path.push(PathBuf::from(&group.name)); 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)?; create_parent(&exported_path)
symlink(&exported_path, &group.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(()) 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<P, Q>(from: P, to: Q) -> Result<()> fn move_file<P, Q>(from: P, to: Q) -> Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
@@ -433,8 +503,8 @@ where
if e.kind() == std::io::ErrorKind::PermissionDenied { if e.kind() == std::io::ErrorKind::PermissionDenied {
bail!(e); bail!(e);
} }
copy(from, to)?; copy(from, to).with_context(|| format!("copying {from:?} to {to:?}"))?;
remove_file(from)?; remove_file(from).with_context(|| format!("deleting {from:?}"))?;
} }
}; };
Ok(()) Ok(())
@@ -444,8 +514,8 @@ where
/// ///
/// # Errors /// # Errors
/// ///
/// This function will return an error if any of the file names /// This function will return an error if any of the file names do not match one
/// do not match one of group names. /// of group names.
fn find_groups_by_name<'a>(names: &[String], groups: &'a HashSet<Group>) -> Result<Vec<&'a Group>> { fn find_groups_by_name<'a>(names: &[String], groups: &'a HashSet<Group>) -> Result<Vec<&'a Group>> {
let name_group_map: HashMap<&str, &Group> = let name_group_map: HashMap<&str, &Group> =
groups.iter().map(|g| (g.name.as_str(), g)).collect(); groups.iter().map(|g| (g.name.as_str(), g)).collect();
+22 -1
View File
@@ -41,12 +41,33 @@ The main subcommands are 'group', 'package' and 'version'.
.RS 4 .RS 4
All actions related to managing groups. All actions related to managing groups.
.sp .sp
<e|edit> <group> [...] <ed|edit> <group> [...]
.RS 4 .RS 4
edit the content of an existing group edit the content of an existing group
.RE .RE
. .
.sp .sp
<ex|export> [<args>] <group> [...]
.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
<i|import> <file> [...] <i|import> <file> [...]
.RS 4 .RS 4
import a new group file or group dir structure import a new group file or group dir structure