feat(export): docstrings, man, README, switches
This commit is contained in:
@@ -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 |
|
||||||
|
|||||||
@@ -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..)
|
||||||
|
|||||||
@@ -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>);
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user