From a5880f614ebeefcca7ca5459fb050131bae82eff Mon Sep 17 00:00:00 2001 From: ripytide Date: Mon, 22 Apr 2024 14:32:46 +0100 Subject: [PATCH] declarative cli and core: refactor --- Cargo.lock | 205 +---- crates/pacdef/Cargo.toml | 8 +- crates/pacdef/src/args/cli.rs | 159 ---- crates/pacdef/src/args/datastructure.rs | 47 -- crates/pacdef/src/args/mod.rs | 14 - crates/pacdef/src/args/parsing.rs | 79 -- crates/pacdef/src/args/tests.rs | 38 - crates/pacdef/src/backend/actual/arch.rs | 2 +- crates/pacdef/src/backend/actual/debian.rs | 2 +- crates/pacdef/src/backend/actual/fedora.rs | 2 +- crates/pacdef/src/backend/actual/flatpak.rs | 2 +- crates/pacdef/src/backend/actual/python.rs | 2 +- crates/pacdef/src/backend/actual/rust.rs | 2 +- .../pacdef/src/backend/actual/rustup/mod.rs | 2 +- crates/pacdef/src/backend/actual/void.rs | 2 +- crates/pacdef/src/backend/backend_trait.rs | 7 +- crates/pacdef/src/backend/macros.rs | 2 +- crates/pacdef/src/backend/mod.rs | 2 +- crates/pacdef/src/cli.rs | 182 +++++ crates/pacdef/src/config.rs | 2 +- crates/pacdef/src/core.rs | 730 +++++++++--------- crates/pacdef/src/grouping/group.rs | 11 +- crates/pacdef/src/grouping/mod.rs | 1 + crates/pacdef/src/grouping/section.rs | 2 +- crates/pacdef/src/lib.rs | 11 +- crates/pacdef/src/main.rs | 18 +- crates/pacdef/src/review/datastructures.rs | 22 +- crates/pacdef/src/review/mod.rs | 23 +- crates/pacdef/src/review/strategy.rs | 16 +- crates/pacdef/src/search.rs | 8 +- 30 files changed, 634 insertions(+), 969 deletions(-) delete mode 100644 crates/pacdef/src/args/cli.rs delete mode 100644 crates/pacdef/src/args/datastructure.rs delete mode 100644 crates/pacdef/src/args/mod.rs delete mode 100644 crates/pacdef/src/args/parsing.rs delete mode 100644 crates/pacdef/src/args/tests.rs create mode 100644 crates/pacdef/src/cli.rs diff --git a/Cargo.lock b/Cargo.lock index 370e2f7..f01b2a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,12 +84,6 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" -[[package]] -name = "autocfg" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" - [[package]] name = "bitflags" version = "2.5.0" @@ -102,12 +96,6 @@ version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - [[package]] name = "clap" version = "4.5.4" @@ -115,6 +103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -129,6 +118,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.7.0" @@ -243,113 +244,18 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indexmap" version = "2.2.6" @@ -411,10 +317,8 @@ dependencies = [ "libc", "path-absolutize", "regex", - "rstest", "rust-apt", "serde", - "serde_derive", "serde_json", "serde_yaml", "termios", @@ -439,18 +343,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.30" @@ -504,41 +396,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" -[[package]] -name = "relative-path" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" - -[[package]] -name = "rstest" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" -dependencies = [ - "futures", - "futures-timer", - "rstest_macros", - "rustc_version", -] - -[[package]] -name = "rstest_macros" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" -dependencies = [ - "cfg-if", - "glob", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn", - "unicode-ident", -] - [[package]] name = "rust-apt" version = "0.7.0" @@ -550,15 +407,6 @@ dependencies = [ "terminal_size", ] -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.38.32" @@ -593,12 +441,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" -[[package]] -name = "semver" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" - [[package]] name = "serde" version = "1.0.197" @@ -643,15 +485,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "strsim" version = "0.11.1" diff --git a/crates/pacdef/Cargo.toml b/crates/pacdef/Cargo.toml index 8d331fb..d378fa4 100644 --- a/crates/pacdef/Cargo.toml +++ b/crates/pacdef/Cargo.toml @@ -12,7 +12,7 @@ categories.workspace = true [dependencies] anyhow = "1.0" -clap = "4.5" +clap = { version = "4.5", features = ["derive"] } const_format = { version = "0.2", default-features = false } path-absolutize = "3.1" regex = { version = "1.10", default-features = false, features = ["std"] } @@ -21,8 +21,7 @@ walkdir = "2.5" libc = "0.2" enum_dispatch = "0.3" -serde = "1.0" -serde_derive = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" @@ -30,9 +29,6 @@ serde_yaml = "0.9" alpm = { version = "3.0", optional = true } rust-apt = { version = "0.7", optional = true } -[dev-dependencies] -rstest = "0.19" - [features] default = [] arch = ["dep:alpm"] diff --git a/crates/pacdef/src/args/cli.rs b/crates/pacdef/src/args/cli.rs deleted file mode 100644 index dc29ff6..0000000 --- a/crates/pacdef/src/args/cli.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::core::get_version_string; -use clap::{Arg, ArgAction, Command}; - -#[must_use] -pub(super) fn build_cli() -> Command { - let package_cmd = get_package_cmd(); - let group_cmd = get_group_cmd(); - let version_cmd = Command::new("version").about("show version info"); - - Command::new("pacdef") - .about("multi-backend declarative package manager for Linux") - .version(get_version_string()) - .arg_required_else_help(true) - .subcommand_required(true) - .subcommands([group_cmd, package_cmd, version_cmd]) - .subcommand_value_name("subcommand") - .disable_help_subcommand(true) - .disable_version_flag(true) -} - -/// Build the `pacdef group` subcommand. -fn get_group_cmd() -> Command { - let edit = Command::new("edit") - .about("edit one or more existing group") - .arg_required_else_help(true) - .arg( - Arg::new("groups") - .num_args(1..) - .required(true) - .help("a previously imported group"), - ) - .visible_alias("ed"); - - 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..) - .required(true) - .help("the file to export as group"), - ) - .visible_alias("ex"); - - let import = Command::new("import") - .about("import one or more group files") - .arg_required_else_help(true) - .arg( - Arg::new("groups") - .num_args(1..) - .required(true) - .help("the file to import as group"), - ) - .visible_alias("i"); - - let list = Command::new("list") - .about("list names of imported groups") - .visible_alias("l"); - - let new = Command::new("new") - .about("create new group files") - .arg_required_else_help(true) - .arg( - Arg::new("edit") - .short('e') - .long("edit") - .help("edit the new group files after creation") - .action(clap::ArgAction::SetTrue), - ) - .arg(Arg::new("groups").num_args(1..).required(true)) - .visible_alias("n"); - - let remove = Command::new("remove") - .about("remove one or more previously imported groups") - .arg_required_else_help(true) - .arg( - Arg::new("groups") - .num_args(1..) - .required(true) - .help("a previously imported group that will be removed"), - ) - .visible_alias("r"); - - let show = Command::new("show") - .about("show packages under an imported group") - .arg_required_else_help(true) - .arg( - Arg::new("groups") - .num_args(1..) - .required(true) - .help("group file(s) to show"), - ) - .visible_alias("s"); - - Command::new("group") - .arg_required_else_help(true) - .about("manage groups") - .visible_alias("g") - .subcommand_required(true) - .subcommands([edit, export, import, list, new, remove, show]) -} - -/// Build the `pacdef package` subcommand. -fn get_package_cmd() -> Command { - let sync = Command::new("sync") - .about("install packages from all imported groups") - .visible_alias("sy") - .arg(build_noconfirm_arg()); - - let clean = Command::new("clean") - .about("remove unmanaged packages") - .visible_alias("c") - .arg(build_noconfirm_arg()); - - let unmanaged = Command::new("unmanaged") - .about("show explicitly installed packages not managed by pacdef") - .visible_alias("u"); - - let review = Command::new("review") - .about("review unmanaged packages") - .visible_alias("r"); - - let search = Command::new("search") - .visible_alias("se") - .about("search for packages which match a provided regex") - .arg_required_else_help(true) - .arg( - Arg::new("regex") - .required(true) - .help("the regular expression the package must match"), - ); - - Command::new("package") - .arg_required_else_help(true) - .about("manage packages") - .visible_alias("p") - .subcommand_required(true) - .subcommands([clean, review, search, sync, unmanaged]) -} - -fn build_noconfirm_arg() -> Arg { - Arg::new("noconfirm") - .long("noconfirm") - .help("do not ask for any confirmation") - .action(ArgAction::SetTrue) -} diff --git a/crates/pacdef/src/args/datastructure.rs b/crates/pacdef/src/args/datastructure.rs deleted file mode 100644 index 1b6eb8e..0000000 --- a/crates/pacdef/src/args/datastructure.rs +++ /dev/null @@ -1,47 +0,0 @@ -#[derive(Debug, PartialEq)] -pub enum Arguments { - Group(GroupAction), - Package(PackageAction), - Version, -} - -#[derive(Debug, PartialEq)] -pub enum GroupAction { - Edit(Groups), - Export(Groups, OutputDir, Force), - Import(Groups), - List, - New(Groups, Edit), - Remove(Groups), - Show(Groups), -} - -#[derive(Debug, PartialEq)] -pub struct Files(pub Vec); - -#[derive(Debug, PartialEq)] -pub struct Groups(pub Vec); - -#[derive(Debug, PartialEq)] -pub enum PackageAction { - Clean(Noconfirm), - Review, - Search(Regex), - Sync(Noconfirm), - Unmanaged, -} - -#[derive(Debug, PartialEq)] -pub struct Regex(pub String); - -#[derive(Debug, PartialEq)] -pub struct Edit(pub bool); - -#[derive(Debug, PartialEq)] -pub struct Noconfirm(pub bool); - -#[derive(Debug, PartialEq)] -pub struct Force(pub bool); - -#[derive(Debug, PartialEq)] -pub struct OutputDir(pub Option); diff --git a/crates/pacdef/src/args/mod.rs b/crates/pacdef/src/args/mod.rs deleted file mode 100644 index c57e2ee..0000000 --- a/crates/pacdef/src/args/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -use self::datastructure::Arguments; - -mod cli; -pub mod datastructure; -mod parsing; -#[cfg(test)] -mod tests; - -/// Get and parse the CLI arguments. -#[must_use] -pub fn get() -> Arguments { - let args = cli::build_cli().get_matches(); - parsing::parse(args) -} diff --git a/crates/pacdef/src/args/parsing.rs b/crates/pacdef/src/args/parsing.rs deleted file mode 100644 index db3601d..0000000 --- a/crates/pacdef/src/args/parsing.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::datastructure::{ - Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex, -}; - -const ARGS_CONSISTENT: &str = "argument declaration and parsing must be consistent"; - -pub(super) fn parse(args: clap::ArgMatches) -> Arguments { - match args.subcommand() { - Some(("group", args)) => Arguments::Group(parse_group_args(args)), - Some(("package", args)) => Arguments::Package(parse_package_args(args)), - Some(("version", _)) => Arguments::Version, - Some(value) => panic!("main subcommand was not matched: {value:?}"), - None => unreachable!("prevented by clap"), - } -} - -fn parse_group_args(args: &clap::ArgMatches) -> GroupAction { - match args.subcommand() { - Some(("edit", args)) => GroupAction::Edit(get_groups(args)), - Some(("export", args)) => { - GroupAction::Export(get_groups(args), get_output_dir(args), get_force(args)) - } - Some(("import", args)) => GroupAction::Import(get_groups(args)), - Some(("list", _)) => GroupAction::List, - Some(("new", args)) => GroupAction::New(get_groups(args), get_edit(args)), - Some(("remove", args)) => GroupAction::Remove(get_groups(args)), - Some(("show", args)) => GroupAction::Show(get_groups(args)), - Some(value) => panic!("group subcommand was not matched: {value:?}"), - None => unreachable!("prevented by clap"), - } -} - -fn parse_package_args(args: &clap::ArgMatches) -> PackageAction { - match args.subcommand() { - Some(("clean", args)) => PackageAction::Clean(get_noconfirm(args)), - Some(("review", _)) => PackageAction::Review, - Some(("search", args)) => PackageAction::Search(get_regex(args)), - Some(("sync", args)) => PackageAction::Sync(get_noconfirm(args)), - Some(("unmanaged", _)) => PackageAction::Unmanaged, - Some(value) => panic!("package subcommand was not matched: {value:?}"), - None => unreachable!("prevented by clap"), - } -} - -fn get_one_arg(args: &clap::ArgMatches, id: &str) -> T -where - T: std::any::Any + Clone + Sync + Send + 'static, -{ - args.get_one::(id).expect(ARGS_CONSISTENT).to_owned() -} - -fn get_regex(args: &clap::ArgMatches) -> Regex { - Regex(get_one_arg(args, "regex")) -} - -fn get_noconfirm(args: &clap::ArgMatches) -> Noconfirm { - Noconfirm(get_one_arg(args, "noconfirm")) -} - -fn get_edit(args: &clap::ArgMatches) -> Edit { - Edit(get_one_arg(args, "edit")) -} - -fn get_groups(args: &clap::ArgMatches) -> Groups { - Groups( - args.get_many::("groups") - .expect(ARGS_CONSISTENT) - .cloned() - .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/src/args/tests.rs b/crates/pacdef/src/args/tests.rs deleted file mode 100644 index a4b34d1..0000000 --- a/crates/pacdef/src/args/tests.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::cli::build_cli; -use super::datastructure::*; -use super::parsing::parse; - -use rstest::rstest; - -#[rstest] -#[case(vec!["pacdef", "group", "list"], - Arguments::Group(GroupAction::List))] -#[case(vec!["pacdef", "g", "l"], - Arguments::Group(GroupAction::List))] -#[case(vec!["pacdef", "version"], - Arguments::Version)] -#[case(vec!["pacdef", "group", "export", "-o", "/tmp", "--force", "base"], - Arguments::Group(GroupAction::Export(Groups(vec!["base".to_string()]), OutputDir(Some("/tmp".to_string())), Force(true))))] -#[case(vec!["pacdef", "package", "sync"], - Arguments::Package(PackageAction::Sync(Noconfirm(false))))] -#[case(vec!["pacdef", "package", "sync", "--noconfirm"], - Arguments::Package(PackageAction::Sync(Noconfirm(true))))] -#[case(vec!["pacdef", "p", "se", "myregex"], - Arguments::Package(PackageAction::Search(Regex("myregex".to_string()))))] -fn arg_parsing(#[case] input: Vec<&str>, #[case] expected: Arguments) { - let args = build_cli().get_matches_from(input); - let parsed = parse(args); - - assert_eq!(parsed, expected); -} - -#[rstest] -#[should_panic] -#[case(vec!["pacdef", "package", "search"], "regex missing")] -#[should_panic] -#[case(vec!["pacdef", "p", "s"], "can be either 'sync' or 'search'")] -#[should_panic] -#[case(vec!["pacdef", "group", "edit"], "group missing")] -fn arg_parsing_invalid(#[case] input: Vec<&str>, #[case] err_msg: &str) { - build_cli().try_get_matches_from(input).expect(err_msg); -} diff --git a/crates/pacdef/src/backend/actual/arch.rs b/crates/pacdef/src/backend/actual/arch.rs index e853841..8615e24 100644 --- a/crates/pacdef/src/backend/actual/arch.rs +++ b/crates/pacdef/src/backend/actual/arch.rs @@ -8,7 +8,7 @@ use anyhow::{Context, Result}; use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; use crate::cmd::run_external_command; -use crate::{Group, Package}; +use crate::Package; #[derive(Debug, Clone)] pub struct Arch { diff --git a/crates/pacdef/src/backend/actual/debian.rs b/crates/pacdef/src/backend/actual/debian.rs index 1daca15..70a556b 100644 --- a/crates/pacdef/src/backend/actual/debian.rs +++ b/crates/pacdef/src/backend/actual/debian.rs @@ -8,7 +8,7 @@ use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; use crate::backend::root::build_base_command_with_privileges; use crate::cmd::run_external_command; -use crate::{Group, Package}; +use crate::Package; #[derive(Debug, Clone)] pub struct Debian { diff --git a/crates/pacdef/src/backend/actual/fedora.rs b/crates/pacdef/src/backend/actual/fedora.rs index 8fc7522..34a7322 100644 --- a/crates/pacdef/src/backend/actual/fedora.rs +++ b/crates/pacdef/src/backend/actual/fedora.rs @@ -6,7 +6,7 @@ use anyhow::Result; use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; use crate::cmd::run_external_command; -use crate::{Group, Package}; +use crate::Package; #[derive(Debug, Clone)] pub struct Fedora { diff --git a/crates/pacdef/src/backend/actual/flatpak.rs b/crates/pacdef/src/backend/actual/flatpak.rs index 6cd3ed7..b33c4dc 100644 --- a/crates/pacdef/src/backend/actual/flatpak.rs +++ b/crates/pacdef/src/backend/actual/flatpak.rs @@ -6,7 +6,7 @@ use anyhow::Result; use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; use crate::cmd::run_external_command; -use crate::{Group, Package}; +use crate::Package; #[derive(Debug, Clone)] pub struct Flatpak { diff --git a/crates/pacdef/src/backend/actual/python.rs b/crates/pacdef/src/backend/actual/python.rs index 1bfbdb8..c2ab322 100644 --- a/crates/pacdef/src/backend/actual/python.rs +++ b/crates/pacdef/src/backend/actual/python.rs @@ -7,7 +7,7 @@ use serde_json::Value; use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; -use crate::{Group, Package}; +use crate::Package; macro_rules! ERROR{ ($bin:expr) => { diff --git a/crates/pacdef/src/backend/actual/rust.rs b/crates/pacdef/src/backend/actual/rust.rs index b109814..95e3058 100644 --- a/crates/pacdef/src/backend/actual/rust.rs +++ b/crates/pacdef/src/backend/actual/rust.rs @@ -8,7 +8,7 @@ use serde_json::Value; use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; -use crate::{Group, Package}; +use crate::Package; #[derive(Debug, Clone)] pub struct Rust { diff --git a/crates/pacdef/src/backend/actual/rustup/mod.rs b/crates/pacdef/src/backend/actual/rustup/mod.rs index 275f85e..f40511e 100644 --- a/crates/pacdef/src/backend/actual/rustup/mod.rs +++ b/crates/pacdef/src/backend/actual/rustup/mod.rs @@ -4,7 +4,7 @@ mod types; use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; use crate::cmd::run_external_command; -use crate::{Group, Package}; +use crate::Package; use anyhow::{bail, Context, Result}; use std::collections::HashSet; use std::process::Command; diff --git a/crates/pacdef/src/backend/actual/void.rs b/crates/pacdef/src/backend/actual/void.rs index 3b4b141..f84784c 100644 --- a/crates/pacdef/src/backend/actual/void.rs +++ b/crates/pacdef/src/backend/actual/void.rs @@ -8,7 +8,7 @@ use crate::backend::backend_trait::{Backend, Switches, Text}; use crate::backend::macros::impl_backend_constants; use crate::backend::root::build_base_command_with_privileges; use crate::cmd::run_external_command; -use crate::{Group, Package}; +use crate::Package; #[derive(Debug, Clone)] pub struct Void { diff --git a/crates/pacdef/src/backend/backend_trait.rs b/crates/pacdef/src/backend/backend_trait.rs index 11c7ecf..859dfc2 100644 --- a/crates/pacdef/src/backend/backend_trait.rs +++ b/crates/pacdef/src/backend/backend_trait.rs @@ -2,12 +2,11 @@ use std::cmp::{Eq, Ord}; use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::process::Command; -use std::rc::Rc; use anyhow::{Context, Result}; use crate::cmd::run_external_command; -use crate::{Group, Package}; +use crate::{Group, Groups, Package}; pub type Switches = &'static [&'static str]; pub type Text = &'static str; @@ -51,7 +50,7 @@ pub trait Backend { /// Load all packages from a set of groups. The backend will visit all groups, /// find its own section, and clone all packages into its own struct. - fn load(&mut self, groups: &HashSet); + fn load(&mut self, groups: &Groups); /// Get all managed packages for this backend, i.e. all packages /// under the corresponding section in all group files. @@ -75,7 +74,7 @@ pub trait Backend { /// Assign each of the packages to an individual group by editing the /// group files. - fn assign_group(&self, to_assign: Vec<(Package, Rc)>) -> Result<()> { + fn assign_group(&self, to_assign: Vec<(Package, Group)>) -> Result<()> { let group_package_map = to_hashmap(to_assign); let section_header = format!("[{}]", self.get_section()); diff --git a/crates/pacdef/src/backend/macros.rs b/crates/pacdef/src/backend/macros.rs index bc1ec1b..81920cc 100644 --- a/crates/pacdef/src/backend/macros.rs +++ b/crates/pacdef/src/backend/macros.rs @@ -34,7 +34,7 @@ macro_rules! impl_backend_constants { &self.packages } - fn load(&mut self, groups: &HashSet) { + fn load(&mut self, groups: &crate::Groups) { let own_section_name = self.get_section(); groups diff --git a/crates/pacdef/src/backend/mod.rs b/crates/pacdef/src/backend/mod.rs index 09e8c12..78e15ef 100644 --- a/crates/pacdef/src/backend/mod.rs +++ b/crates/pacdef/src/backend/mod.rs @@ -7,11 +7,11 @@ pub mod todo_per_backend; use crate::backend::backend_trait::Switches; use crate::backend::backend_trait::Text; use crate::Group; +use crate::Groups; use crate::Package; use anyhow::Result; use backend_trait::Backend; use std::collections::HashSet; -use std::rc::Rc; use self::actual::{ fedora::Fedora, flatpak::Flatpak, python::Python, rust::Rust, rustup::Rustup, void::Void, diff --git a/crates/pacdef/src/cli.rs b/crates/pacdef/src/cli.rs new file mode 100644 index 0000000..abcbe78 --- /dev/null +++ b/crates/pacdef/src/cli.rs @@ -0,0 +1,182 @@ +//! The clap declarative command line interface + +use std::path::PathBuf; + +use clap::{Args, Parser, Subcommand}; + +#[derive(Parser)] +#[command( + version, + author, + arg_required_else_help(true), + subcommand_required(true), + disable_help_subcommand(true), + disable_version_flag(true) +)] +/// multi-backend declarative package manager for Linux +pub struct MainArguments { + #[command(subcommand)] + pub subcommand: MainSubcommand, +} + +#[derive(Subcommand)] +pub enum MainSubcommand { + Group(GroupArguments), + Package(PackageArguments), + Version(VersionArguments), +} + +#[derive(Args)] +#[command( + arg_required_else_help(true), + visible_alias("g"), + subcommand_required(true) +)] +/// manage groups +pub struct GroupArguments { + #[command(subcommand)] + pub group_action: GroupAction, +} + +#[derive(Subcommand)] +pub enum GroupAction { + Edit(EditGroupAction), + Export(ExportGroupAction), + Import(ImportGroupAction), + List(ListGroupAction), + New(NewGroupAction), + Remove(RemoveGroupAction), + Show(ShowGroupAction), +} + +#[derive(Args)] +#[command(arg_required_else_help(true), visible_alias("ed"))] +/// edit one or more existing group +pub struct EditGroupAction { + #[arg(required(true), num_args(1..))] + /// a previously imported group + pub edit_groups: Vec, +} + +#[derive(Args)] +#[command(arg_required_else_help(true), visible_alias("ex"))] +/// export one or more group files +pub struct ExportGroupAction { + #[arg(required(true), num_args(1..))] + /// the file to export as group + pub export_groups: Vec, + + #[arg(short, long)] + /// (optional) the directory under which to save the group + pub output_dir: Option, + + #[arg(short, long)] + /// overwrite output files if they exist + pub force: bool, +} + +#[derive(Args)] +#[command(arg_required_else_help(true), visible_alias("i"))] +/// import one or more group files +pub struct ImportGroupAction { + #[arg(required(true), num_args(1..))] + /// the file to import as group + pub import_groups: Vec, +} + +#[derive(Args)] +#[command(visible_alias("l"))] +/// list names of imported groups +pub struct ListGroupAction {} + +#[derive(Args)] +#[command(arg_required_else_help(true), visible_alias("n"))] +/// create new group files +pub struct NewGroupAction { + #[arg(required(true), num_args(1..))] + /// the groups to create + pub new_groups: Vec, + + #[arg(short, long)] + /// edit the new group files after creation + pub edit: bool, +} + +#[derive(Args)] +#[command(arg_required_else_help(true), visible_alias("r"))] +/// remove one or more previously imported groups +pub struct RemoveGroupAction { + #[arg(required(true), num_args(1..))] + /// a previously imported group that will be removed + pub remove_groups: Vec, +} + +#[derive(Args)] +#[command(arg_required_else_help(true), visible_alias("s"))] +/// show packages under an imported group +pub struct ShowGroupAction { + #[arg(required(true), num_args(1..))] + /// group file(s) to show + pub show_groups: Vec, +} + +#[derive(Args)] +#[command( + arg_required_else_help(true), + subcommand_required(true), + visible_alias("p") +)] +/// manage packages +pub struct PackageArguments { + #[command(subcommand)] + pub package_action: PackageAction, +} + +#[derive(Subcommand)] +pub enum PackageAction { + Clean(CleanPackageAction), + Review(ReviewPackageAction), + Search(SearchPackageAction), + Sync(SyncPackageAction), + Unmanaged(UnmanagedPackageAction), +} + +#[derive(Args)] +#[command(visible_alias("c"))] +/// remove unmanaged packages +pub struct CleanPackageAction { + #[arg(long)] + /// do not ask for any confirmation + pub no_confirm: bool, +} + +#[derive(Args)] +#[command(visible_alias("r"))] +/// review unmanaged packages +pub struct ReviewPackageAction {} + +#[derive(Args)] +#[command(arg_required_else_help(true), visible_alias("se"))] +/// search for packages which match a provided regex +pub struct SearchPackageAction { + #[arg(required(true))] + /// the regular expression the package must match + pub regex: String, +} + +#[derive(Args)] +#[command(visible_alias("sy"))] +/// install packages from all imported groups +pub struct SyncPackageAction { + #[arg(long)] + /// do not ask for any confirmation + pub no_confirm: bool, +} + +#[derive(Args)] +#[command(visible_alias("u"))] +/// show explicitly installed packages not managed by pacdef +pub struct UnmanagedPackageAction {} + +#[derive(Args)] +pub struct VersionArguments {} diff --git a/crates/pacdef/src/config.rs b/crates/pacdef/src/config.rs index c917df3..77ff2df 100644 --- a/crates/pacdef/src/config.rs +++ b/crates/pacdef/src/config.rs @@ -3,7 +3,7 @@ use std::io::{ErrorKind, Write}; use std::path::Path; use anyhow::{bail, Context, Result}; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; // Update the master README if fields change. /// Config for the program, as listed in `$XDG_CONFIG_HOME/pacdef/pacdef.yaml`. diff --git a/crates/pacdef/src/core.rs b/crates/pacdef/src/core.rs index 88112b8..417ffb3 100644 --- a/crates/pacdef/src/core.rs +++ b/crates/pacdef/src/core.rs @@ -1,49 +1,32 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::env::current_dir; use std::fs::{copy, create_dir_all, remove_file, rename, File}; use std::os::unix::fs::symlink; use std::path::{Path, PathBuf}; +use std::process::Command; use anyhow::{bail, ensure, Context, Result}; use const_format::formatcp; -use crate::args::datastructure::{ - Arguments, Edit, Force, GroupAction, Groups, Noconfirm, OutputDir, PackageAction, Regex, -}; use crate::backend::backend_trait::Backend; use crate::backend::todo_per_backend::ToDoPerBackend; use crate::backend::AnyBackend; -use crate::cmd::run_edit_command; -use crate::env::should_print_debug_info; +use crate::cli::{ + CleanPackageAction, EditGroupAction, ExportGroupAction, GroupAction, GroupArguments, + ImportGroupAction, ListGroupAction, MainArguments, MainSubcommand, NewGroupAction, + PackageAction, PackageArguments, RemoveGroupAction, ReviewPackageAction, SearchPackageAction, + ShowGroupAction, SyncPackageAction, UnmanagedPackageAction, VersionArguments, +}; +use crate::cmd::{run_edit_command, run_external_command}; +use crate::env::{get_editor, should_print_debug_info}; use crate::path::{binary_in_path, get_absolutized_file_paths, get_group_dir}; -use crate::search; +use crate::review::review; +use crate::search::search_packages; use crate::ui::get_user_confirmation; -use crate::Config; use crate::Group; -use crate::{review, Error}; - -/// Most data that is required during runtime of the program. -pub struct Pacdef { - /// The command line arguments. Is an `Option` so that we can take ownership later without cloning. - args: Option, - /// The config of the program. - config: Config, - /// The hashset of all groups. - groups: HashSet, -} - -impl Pacdef { - /// Creates a new [`Pacdef`]. `config` should be passed from [`Config::load`], and `args` from - /// [`args::get`]. - #[must_use] - pub const fn new(args: Arguments, config: Config, groups: HashSet) -> Self { - Self { - args: Some(args), - config, - groups, - } - } +use crate::{Config, Error, Groups}; +impl MainArguments { /// Run the action that was provided by the user as first argument. /// /// For convenience sake, all called functions take a `&self` argument, even if @@ -52,305 +35,132 @@ impl Pacdef { /// # Errors /// /// This function propagates errors from the underlying functions. - /// - /// # Panics - /// - /// This function panics if the `args` field is `None`. - pub fn run_action_from_arg(mut self) -> Result<()> { - match self - .args - .take() - .expect("if there were no args we would not get to here") - { - Arguments::Group(group_args) => self.run_group_subcommand(&group_args), - Arguments::Package(package_args) => self.run_package_subcommand(&package_args), - Arguments::Version => { - self.show_version(); - - Ok(()) - } + pub fn run(self, groups: &Groups, config: &Config) -> Result<()> { + match self.subcommand { + MainSubcommand::Group(group) => group.run(groups), + MainSubcommand::Package(package) => package.run(groups, config), + MainSubcommand::Version(version) => version.run(), } } +} - fn run_group_subcommand(self, args: &GroupAction) -> Result<()> { - match args { - GroupAction::Edit(Groups(groups)) => self.edit_groups(groups), - GroupAction::Export(Groups(groups), OutputDir(dir), Force(force)) => { - self.export_groups(groups, dir.as_ref(), *force) - } - GroupAction::Import(Groups(groups)) => self.import_groups(groups), - GroupAction::List => self.show_groups(), - GroupAction::New(Groups(groups), Edit(edit)) => self.new_groups(groups, *edit), - GroupAction::Remove(Groups(groups)) => self.remove_groups(groups), - GroupAction::Show(Groups(groups)) => self.show_group_content(groups), +impl VersionArguments { + /// If the crate was compiled from git, return `pacdef, ()`. + /// Otherwise return `pacdef, `. + fn run(self) -> Result<()> { + let backends = get_included_backends(); + let mut result = format!("pacdef, version: {}\n", get_version_string()); + result.push_str("supported backends:"); + for b in backends { + result.push_str("\n "); + result.push_str(b); + } + + println!("{}", result); + + Ok(()) + } +} + +impl GroupArguments { + fn run(self, groups: &Groups) -> Result<()> { + match self.group_action { + GroupAction::Edit(edit) => edit.run(groups), + GroupAction::Export(export) => export.run(groups), + GroupAction::Import(import) => import.run(), + GroupAction::List(list) => list.run(groups), + GroupAction::New(new) => new.run(), + GroupAction::Remove(remove) => remove.run(groups), + GroupAction::Show(show) => show.run(groups), } } +} - fn run_package_subcommand(mut self, args: &PackageAction) -> Result<()> { - match args { - PackageAction::Clean(Noconfirm(noconfirm)) => self.clean_packages(*noconfirm), - PackageAction::Review => review::review(self.get_unmanaged_packages()?, self.groups), - PackageAction::Search(Regex(regex)) => { - self.warn_about_groups_that_arent_symlinks(); - search::search_packages(regex, &self.groups) - } - PackageAction::Sync(Noconfirm(noconfirm)) => self.install_packages(*noconfirm), - PackageAction::Unmanaged => self.show_unmanaged_packages(), - } - } - - fn get_missing_packages(&mut self) -> Result { - let mut to_install = ToDoPerBackend::new(); - - if self.groups.is_empty() { +impl EditGroupAction { + fn run(self, groups: &Groups) -> Result<()> { + if groups.is_empty() { eprintln!("WARNING: no group files found"); } - for mut backend in AnyBackend::iter() { - if self - .config - .disabled_backends - .contains(&backend.get_section().to_string()) - { - continue; - } - - if !binary_in_path(backend.get_binary())? { - continue; - } - - self.overwrite_values_from_config(&mut backend); - backend.load(&self.groups); - - match backend.get_missing_packages_sorted() { - Ok(diff) => to_install.push((backend, diff)), - Err(error) => show_backend_query_error(&error, &backend), - }; - } - - Ok(to_install) - } - - fn overwrite_values_from_config(&mut self, backend: &mut AnyBackend) { - #[cfg(feature = "arch")] - { - if let AnyBackend::Arch(arch) = backend { - arch.binary.clone_from(&self.config.aur_helper); - arch.aur_rm_args.clone_from(&self.config.aur_rm_args); - } - } - - if let AnyBackend::Flatpak(flatpak) = backend { - flatpak.systemwide = self.config.flatpak_systemwide; - } - - if let AnyBackend::Python(python) = backend { - python.binary.clone_from(&self.config.pip_binary); - } - } - - fn install_packages(&mut self, noconfirm: bool) -> Result<()> { - self.warn_about_groups_that_arent_symlinks(); - - let to_install = self.get_missing_packages()?; - - if to_install.nothing_to_do_for_all_backends() { - println!("nothing to do"); - return Ok(()); - } - - println!("Would install the following packages:\n"); - to_install.show().context("printing things to do")?; - - println!(); - if noconfirm { - println!("proceeding without confirmation"); - } else if !get_user_confirmation()? { - return Ok(()); - } - - to_install.install_missing_packages(noconfirm) - } - - fn warn_about_groups_that_arent_symlinks(&self) { - for group in &self.groups { - if group.warn_symlink { - eprintln!( - "WARNING: group file {} is not a symlink", - group.path.to_string_lossy() - ); - } - } - } - - fn edit_groups(&self, groups: &[String]) -> Result<()> { - self.warn_about_groups_that_arent_symlinks(); - - if self.groups.is_empty() { - eprintln!("WARNING: no group files found"); - } - - let group_files: Vec<_> = find_groups_by_name(groups, &self.groups) + let group_files: Vec<_> = find_groups_by_name(&self.edit_groups, groups) .context("getting group files for args")? .into_iter() .map(|g| g.path.as_path()) .collect(); - run_edit_command(&group_files).context("running editor")?; + let mut cmd = Command::new(get_editor().context("getting suitable editor")?); + cmd.current_dir( + group_files[0] + .parent() + .context("getting parent dir of first file argument")?, + ); + for group_file in group_files { + cmd.arg(group_file.to_string_lossy().to_string()); + } + run_external_command(cmd)?; Ok(()) } +} - fn show_version(self) { - println!("{}", get_name_and_version()); - } - - fn show_unmanaged_packages(mut self) -> Result<()> { - let unmanaged_per_backend = &self.get_unmanaged_packages()?; - - if unmanaged_per_backend.nothing_to_do_for_all_backends() { - return Ok(()); - } - - unmanaged_per_backend - .show() - .context("printing things to do") - } - - /// Get a list of unmanaged packages per backend. +impl ExportGroupAction { + /// 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. /// - /// This method loops through all enabled `Backend`s whose binary is in `PATH`. + /// 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 propagate errors from the individual backends. - fn get_unmanaged_packages(&mut self) -> Result { - self.warn_about_groups_that_arent_symlinks(); - - if self.groups.is_empty() { - eprintln!("WARNING: no group files found"); - } - - let mut result = ToDoPerBackend::new(); - - for mut backend in AnyBackend::iter() { - if self - .config - .disabled_backends - .contains(&backend.get_section().to_string()) - { - continue; - } - - if !binary_in_path(backend.get_binary())? { - continue; - } - - self.overwrite_values_from_config(&mut backend); - backend.load(&self.groups); - - match backend.get_unmanaged_packages_sorted() { - Ok(unmanaged) => result.push((backend, unmanaged)), - Err(error) => show_backend_query_error(&error, &backend), - }; - } - Ok(result) - } - - /// Print the alphabetically sorted names of all groups to stdout. + /// 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. /// - /// This methods cannot return an error. It returns a `Result` to be consistent - /// with other methods. - fn show_groups(self) -> Result<()> { - self.warn_about_groups_that_arent_symlinks(); + /// # 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 run(self, groups: &Groups) -> Result<()> { + let groups = find_groups_by_name(&self.export_groups, groups)?; + let output_dir = match self.output_dir { + Some(p) => p, + None => current_dir().context("no output dir specified, getting current directory")?, + }; - if self.groups.is_empty() { - eprintln!("WARNING: no group files found"); - } - - let mut vec: Vec<_> = self.groups.iter().collect(); - vec.sort_unstable(); - for g in vec { - println!("{}", g.name); - } - - Ok(()) - } - - fn clean_packages(&mut self, noconfirm: bool) -> Result<()> { - let to_remove = self.get_unmanaged_packages()?; - - if to_remove.nothing_to_do_for_all_backends() { - println!("nothing to do"); - return Ok(()); - } - - println!("Would remove the following packages:\n"); - to_remove.show().context("printing things to do")?; - - println!(); - if noconfirm { - println!("proceeding without confirmation"); - } else if !get_user_confirmation()? { - return Ok(()); - } - - to_remove.remove_unmanaged_packages(noconfirm) - } - - fn show_group_content(&self, args: &[String]) -> Result<()> { - self.warn_about_groups_that_arent_symlinks(); - - if self.groups.is_empty() { - eprintln!("WARNING: no group files found"); - } - - let mut errors = vec![]; - let mut groups = vec![]; - - // make sure all args exist before doing anything - for arg_group in args { - let possible_group = self.groups.iter().find(|g| g.name == **arg_group); - - let Some(group) = possible_group else { - errors.push((*arg_group).clone()); - continue; - }; - - groups.push(group); - } - - // return an error if any arg was not found ensure!( - errors.is_empty(), - crate::Error::MultipleGroupsNotFound(errors) + output_dir.exists() && output_dir.is_dir(), + "output must be a directory and exist" ); - let show_more_than_one_group = args.len() > 1; + for group in &groups { + ensure!(!&group.path.is_symlink(), "cannot export symlinks"); - let mut iter = groups.into_iter().peekable(); + let mut exported_path = output_dir.clone(); + exported_path.push(PathBuf::from(&group.name)); - while let Some(group) = iter.next() { - if show_more_than_one_group { - let name = &group.name; - println!("{name}"); - for _ in 0..name.len() { - print!("-"); - } - println!(); - } + ensure!( + !self.force && !exported_path.exists(), + "{exported_path:?} already exists" + ); - println!("{group}"); - if iter.peek().is_some() { - println!(); - } + 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(()) } +} - fn import_groups(&self, file_names: &[String]) -> Result<()> { - let files = get_absolutized_file_paths(file_names)?; +impl ImportGroupAction { + fn run(self) -> Result<()> { + let files = get_absolutized_file_paths(&self.import_groups)?; let groups_dir = get_group_dir()?; for target in files { @@ -377,21 +187,29 @@ impl Pacdef { Ok(()) } +} - fn remove_groups(&self, groups: &[String]) -> Result<()> { - if self.groups.is_empty() { +impl ListGroupAction { + /// Print the alphabetically sorted names of all groups to stdout. + /// + /// This methods cannot return an error. It returns a `Result` to be consistent + /// with other methods. + fn run(self, groups: &Groups) -> Result<()> { + if groups.is_empty() { eprintln!("WARNING: no group files found"); } - let found = find_groups_by_name(groups, &self.groups)?; - - for group in found { - remove_file(&group.path)?; + let mut vec: Vec<_> = groups.iter().collect(); + vec.sort_unstable(); + for g in vec { + println!("{}", g.name); } Ok(()) } +} +impl NewGroupAction { /// Create empty group files. /// /// If `edit` is `true`, the editor will be run to edit the files after they are @@ -404,18 +222,19 @@ impl Pacdef { /// - a group with the same name already exists, /// - the editor cannot be run, or /// - if we do not have permission to write to the group dir. - fn new_groups(&self, new_groups: &[String], edit: bool) -> Result<()> { + fn run(&self) -> Result<()> { let group_path = get_group_dir()?; // prevent group names that resolve to directories - for name in new_groups { + for new_group in &self.new_groups { ensure!( - *name != "." && *name != "..", - crate::Error::InvalidGroupName(name.clone()) + new_group != "." && new_group != "..", + crate::Error::InvalidGroupName(new_group.clone()) ); } - let paths: Vec<_> = new_groups + let paths: Vec<_> = self + .new_groups .iter() .map(|name| { let mut base = group_path.clone(); @@ -435,72 +254,251 @@ impl Pacdef { File::create(file)?; } - if edit { + if self.edit { run_edit_command(&paths).context("running editor")?; } Ok(()) } +} - /// 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 = match output_dir.map(PathBuf::from) { - Some(p) => p, - None => current_dir().context("no output dir specified, getting current directory")?, - }; +impl RemoveGroupAction { + fn run(self, groups: &Groups) -> Result<()> { + if groups.is_empty() { + eprintln!("WARNING: no group files found"); + } - ensure!( - output_dir.exists() && output_dir.is_dir(), - "output must be a directory and exist" - ); + let found = find_groups_by_name(&self.remove_groups, groups)?; - 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!( - !force && !exported_path.exists(), - "{exported_path:?} already exists" - ); - - 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")?; + for group in found { + remove_file(&group.path)?; } Ok(()) } } +impl ShowGroupAction { + fn run(self, groups: &Groups) -> Result<()> { + if groups.is_empty() { + eprintln!("WARNING: no group files found"); + } + + let mut errors = vec![]; + let mut found_groups = vec![]; + + // make sure all args exist before doing anything + for show_group in &self.show_groups { + let possible_group = groups.iter().find(|group| group.name == *show_group); + + let Some(group) = possible_group else { + errors.push(show_group.to_string()); + continue; + }; + + found_groups.push(group); + } + + // return an error if any arg was not found + ensure!( + errors.is_empty(), + crate::Error::MultipleGroupsNotFound(errors) + ); + + let show_more_than_one_group = self.show_groups.len() > 1; + + let mut iter = found_groups.into_iter().peekable(); + + while let Some(group) = iter.next() { + if show_more_than_one_group { + let name = &group.name; + println!("{name}"); + for _ in 0..name.len() { + print!("-"); + } + println!(); + } + + println!("{group}"); + if iter.peek().is_some() { + println!(); + } + } + + Ok(()) + } +} + +impl PackageArguments { + fn run(self, groups: &Groups, config: &Config) -> Result<()> { + match self.package_action { + PackageAction::Clean(clean) => clean.run(groups, config), + PackageAction::Review(review) => review.run(groups, config), + PackageAction::Search(search) => search.run(groups), + PackageAction::Sync(sync) => sync.run(groups, config), + PackageAction::Unmanaged(unmanaged) => unmanaged.run(groups, config), + } + } +} + +impl CleanPackageAction { + fn run(self, groups: &Groups, config: &Config) -> Result<()> { + let to_remove = get_unmanaged_packages(groups, config)?; + + if to_remove.nothing_to_do_for_all_backends() { + println!("nothing to do"); + return Ok(()); + } + + println!("Would remove the following packages:\n"); + to_remove.show().context("printing things to do")?; + + println!(); + if self.no_confirm { + println!("proceeding without confirmation"); + } else if !get_user_confirmation()? { + return Ok(()); + } + + to_remove.remove_unmanaged_packages(self.no_confirm) + } +} + +impl ReviewPackageAction { + fn run(self, groups: &Groups, config: &Config) -> Result<()> { + review(get_unmanaged_packages(groups, config)?, groups) + } +} + +impl SearchPackageAction { + fn run(self, groups: &Groups) -> Result<()> { + search_packages(&self.regex, groups) + } +} + +impl SyncPackageAction { + fn run(self, groups: &Groups, config: &Config) -> Result<()> { + let to_install = get_missing_packages(groups, config)?; + + if to_install.nothing_to_do_for_all_backends() { + println!("nothing to do"); + return Ok(()); + } + + println!("Would install the following packages:\n"); + to_install.show().context("printing things to do")?; + + println!(); + if self.no_confirm { + println!("proceeding without confirmation"); + } else if !get_user_confirmation()? { + return Ok(()); + } + + to_install.install_missing_packages(self.no_confirm) + } +} + +impl UnmanagedPackageAction { + fn run(self, groups: &Groups, config: &Config) -> Result<()> { + let unmanaged_per_backend = &get_unmanaged_packages(groups, config)?; + + if unmanaged_per_backend.nothing_to_do_for_all_backends() { + return Ok(()); + } + + unmanaged_per_backend + .show() + .context("printing things to do") + } +} + +fn get_missing_packages(groups: &Groups, config: &Config) -> Result { + let mut to_install = ToDoPerBackend::new(); + + if groups.is_empty() { + eprintln!("WARNING: no group files found"); + } + + for mut backend in AnyBackend::iter() { + if config + .disabled_backends + .contains(&backend.get_section().to_string()) + { + continue; + } + + if !binary_in_path(backend.get_binary())? { + continue; + } + + overwrite_values_from_config(&mut backend, config); + backend.load(groups); + + match backend.get_missing_packages_sorted() { + Ok(diff) => to_install.push((backend, diff)), + Err(error) => show_backend_query_error(&error, &backend), + }; + } + + Ok(to_install) +} + +fn overwrite_values_from_config(backend: &mut AnyBackend, config: &Config) { + #[cfg(feature = "arch")] + { + if let AnyBackend::Arch(arch) = backend { + arch.binary.clone_from(&config.aur_helper); + arch.aur_rm_args.clone_from(&config.aur_rm_args); + } + } + + if let AnyBackend::Flatpak(flatpak) = backend { + flatpak.systemwide = config.flatpak_systemwide; + } + + if let AnyBackend::Python(python) = backend { + python.binary.clone_from(&config.pip_binary); + } +} + +/// Get a list of unmanaged packages per backend. +/// +/// This method loops through all enabled `Backend`s whose binary is in `PATH`. +/// +/// # Errors +/// +/// This function will propagate errors from the individual backends. +fn get_unmanaged_packages(groups: &Groups, config: &Config) -> Result { + if groups.is_empty() { + eprintln!("WARNING: no group files found"); + } + + let mut result = ToDoPerBackend::new(); + + for mut backend in AnyBackend::iter() { + if config + .disabled_backends + .contains(&backend.get_section().to_string()) + { + continue; + } + + if !binary_in_path(backend.get_binary())? { + continue; + } + + overwrite_values_from_config(&mut backend, config); + backend.load(groups); + + match backend.get_unmanaged_packages_sorted() { + Ok(unmanaged) => result.push((backend, unmanaged)), + Err(error) => show_backend_query_error(&error, &backend), + }; + } + Ok(result) +} + /// Create the parent directory of the `path` if that directory does not exist. /// /// Do nothing otherwise. @@ -558,7 +556,7 @@ where /// /// 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> { +fn find_groups_by_name<'a>(names: &[String], groups: &'a Groups) -> Result> { let name_group_map: HashMap<&str, &Group> = groups.iter().map(|g| (g.name.as_str(), g)).collect(); @@ -590,20 +588,6 @@ fn show_backend_query_error(error: &anyhow::Error, backend: &AnyBackend) { } } -/// If the crate was compiled from git, return `pacdef, ()`. -/// Otherwise return `pacdef, `. -fn get_name_and_version() -> String { - let backends = get_included_backends(); - let mut result = format!("pacdef, version: {}\n", get_version_string()); - result.push_str("supported backends:"); - for b in backends { - result.push_str("\n "); - result.push_str(b); - } - - result -} - /// If the crate was compiled from git, return ` ()`. Otherwise /// return ``. pub const fn get_version_string() -> &'static str { diff --git a/crates/pacdef/src/grouping/group.rs b/crates/pacdef/src/grouping/group.rs index 328ffc0..50f6c8b 100644 --- a/crates/pacdef/src/grouping/group.rs +++ b/crates/pacdef/src/grouping/group.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{BTreeSet, HashSet}; use std::fmt::Display; use std::fs::{create_dir, read_to_string, File}; use std::hash::Hash; @@ -13,8 +13,11 @@ use crate::path::get_relative_path; use super::{Package, Section}; +/// A set of groups +pub type Groups = BTreeSet; + /// Representation of a group file. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Group { /// Name of the group (file name from which it was read, relative to the group /// base dir). @@ -37,8 +40,8 @@ impl Group { /// /// This function will return an error if any of the files under `group_dir` cannot /// be accessed. - pub fn load(group_dir: &Path, warn_not_symlinks: bool) -> Result> { - let mut result = HashSet::new(); + pub fn load(group_dir: &Path, warn_not_symlinks: bool) -> Result { + let mut result = Groups::new(); if !group_dir.is_dir() { // we only need to create the innermost dir. The rest was already created from when diff --git a/crates/pacdef/src/grouping/mod.rs b/crates/pacdef/src/grouping/mod.rs index d6664ce..02a5e08 100644 --- a/crates/pacdef/src/grouping/mod.rs +++ b/crates/pacdef/src/grouping/mod.rs @@ -14,5 +14,6 @@ mod package; mod section; pub use group::Group; +pub use group::Groups; pub use package::Package; pub use section::Section; diff --git a/crates/pacdef/src/grouping/section.rs b/crates/pacdef/src/grouping/section.rs index 69bb64c..62ffa36 100644 --- a/crates/pacdef/src/grouping/section.rs +++ b/crates/pacdef/src/grouping/section.rs @@ -7,7 +7,7 @@ use anyhow::{ensure, Context, Result}; use super::Package; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Section { pub name: String, pub packages: HashSet, diff --git a/crates/pacdef/src/lib.rs b/crates/pacdef/src/lib.rs index d29022b..7d69559 100644 --- a/crates/pacdef/src/lib.rs +++ b/crates/pacdef/src/lib.rs @@ -20,22 +20,25 @@ missing_docs )] -mod args; pub(crate) mod backend; +#[allow(missing_docs)] +pub mod cli; + mod cmd; mod config; +#[allow(clippy::unused_self, clippy::unnecessary_wraps)] mod core; mod env; mod errors; mod grouping; -pub mod path; mod review; mod search; mod ui; -pub use crate::args::get as get_args; +pub mod path; + pub use crate::config::Config; -pub use crate::core::Pacdef; pub use crate::errors::Error; pub use crate::grouping::Group; +pub use crate::grouping::Groups; pub use crate::grouping::Package; diff --git a/crates/pacdef/src/main.rs b/crates/pacdef/src/main.rs index a8619c4..e54ca43 100644 --- a/crates/pacdef/src/main.rs +++ b/crates/pacdef/src/main.rs @@ -19,8 +19,10 @@ use std::process::{ExitCode, Termination}; use anyhow::{bail, Context, Result}; +use clap::Parser; +use pacdef::cli::MainArguments; use pacdef::path::{get_config_path, get_config_path_old_version, get_group_dir}; -use pacdef::{get_args, Config, Error as PacdefError, Group, Pacdef}; +use pacdef::{Config, Error as PacdefError, Group}; const MAJOR_UPDATE_MESSAGE: &str = "VERSION UPGRADE You seem to have used version 0.x of pacdef before. @@ -51,7 +53,7 @@ fn handle_final_result(result: Result<()>) -> ExitCode { } fn main_inner() -> Result<()> { - let args = get_args(); + let main_arguments = MainArguments::parse(); let config_file = get_config_path().context("getting config file")?; @@ -73,8 +75,16 @@ fn main_inner() -> Result<()> { let groups = Group::load(&group_dir, config.warn_not_symlinks) .with_context(|| format!("loading groups under {}", group_dir.to_string_lossy()))?; - let pacdef = Pacdef::new(args, config, groups); - pacdef.run_action_from_arg().context("running action") + for group in groups.iter() { + if group.warn_symlink { + eprintln!( + "WARNING: group file {} is not a symlink", + group.path.to_string_lossy() + ); + } + } + + main_arguments.run(&groups, &config) } fn load_default_config(config_file: &Path) -> Result { diff --git a/crates/pacdef/src/review/datastructures.rs b/crates/pacdef/src/review/datastructures.rs index 8dba080..f42926b 100644 --- a/crates/pacdef/src/review/datastructures.rs +++ b/crates/pacdef/src/review/datastructures.rs @@ -1,18 +1,16 @@ -use std::rc::Rc; - use crate::{backend::AnyBackend, Group, Package}; use super::strategy::Strategy; #[derive(Debug, PartialEq)] -pub(super) enum ReviewAction { +pub enum ReviewAction { AsDependency(Package), Delete(Package), - AssignGroup(Package, Rc), + AssignGroup(Package, Group), } #[derive(Debug)] -pub(super) enum ReviewIntention { +pub enum ReviewIntention { AsDependency, AssignGroup, Delete, @@ -24,20 +22,20 @@ pub(super) enum ReviewIntention { } #[derive(Debug)] -pub(super) struct ReviewsPerBackend { +pub struct ReviewsPerBackend { items: Vec<(AnyBackend, Vec)>, } impl ReviewsPerBackend { - pub(super) fn new() -> Self { + pub fn new() -> Self { Self { items: vec![] } } - pub(super) fn nothing_to_do(&self) -> bool { + pub fn nothing_to_do(&self) -> bool { self.items.iter().all(|(_, vec)| vec.is_empty()) } - pub(super) fn push(&mut self, value: (AnyBackend, Vec)) { + pub fn push(&mut self, value: (AnyBackend, Vec)) { self.items.push(value); } @@ -46,7 +44,7 @@ impl ReviewsPerBackend { /// /// If there are no actions for a `Backend`, then that `Backend` is removed from the return /// value. - pub(super) fn into_strategies(self) -> Vec { + pub fn into_strategies(self) -> Vec { let mut result = vec![]; for (backend, actions) in self { @@ -85,7 +83,7 @@ impl IntoIterator for ReviewsPerBackend { } } -pub(super) enum ContinueWithReview { +pub enum ContinueWithReview { Yes, No, NoAndApply, @@ -94,7 +92,7 @@ pub(super) enum ContinueWithReview { fn extract_actions( actions: Vec, to_delete: &mut Vec, - assign_group: &mut Vec<(Package, Rc)>, + assign_group: &mut Vec<(Package, Group)>, as_dependency: &mut Vec, ) { for action in actions { diff --git a/crates/pacdef/src/review/mod.rs b/crates/pacdef/src/review/mod.rs index 1944826..7a54b33 100644 --- a/crates/pacdef/src/review/mod.rs +++ b/crates/pacdef/src/review/mod.rs @@ -2,26 +2,19 @@ mod datastructures; mod strategy; use std::io::{stdin, stdout, Write}; -use std::rc::Rc; use anyhow::Result; use crate::backend::backend_trait::Backend; use crate::backend::todo_per_backend::ToDoPerBackend; use crate::ui::{get_user_confirmation, read_single_char_from_terminal}; -use crate::{Group, Package}; +use crate::{Group, Groups, Package}; use self::datastructures::{ContinueWithReview, ReviewAction, ReviewIntention, ReviewsPerBackend}; use self::strategy::Strategy; -pub fn review( - todo_per_backend: ToDoPerBackend, - groups: impl IntoIterator, -) -> Result<()> { +pub fn review(todo_per_backend: ToDoPerBackend, groups: &Groups) -> Result<()> { let mut reviews = ReviewsPerBackend::new(); - let mut groups: Vec> = groups.into_iter().map(Rc::new).collect(); - - groups.sort_unstable(); if todo_per_backend.nothing_to_do_for_all_backends() { println!("nothing to do"); @@ -32,7 +25,7 @@ pub fn review( let mut actions = vec![]; for package in packages { println!("{}: {package}", backend.get_section()); - match get_action_for_package(package, &groups, &mut actions, &backend)? { + match get_action_for_package(package, groups, &mut actions, &backend)? { ContinueWithReview::Yes => continue, ContinueWithReview::No => return Ok(()), ContinueWithReview::NoAndApply => { @@ -76,7 +69,7 @@ pub fn review( fn get_action_for_package( package: Package, - groups: &[Rc], + groups: &Groups, reviews: &mut Vec, backend: &dyn Backend, ) -> Result { @@ -92,7 +85,7 @@ fn get_action_for_package( } ReviewIntention::AssignGroup => { if let Ok(Some(group)) = ask_group(groups) { - reviews.push(ReviewAction::AssignGroup(package, group)); + reviews.push(ReviewAction::AssignGroup(package, group.clone())); break; }; } @@ -154,7 +147,7 @@ fn print_query(supports_as_dependency: bool) -> Result<()> { Ok(()) } -fn print_enumerated_groups(groups: &[Rc]) { +fn print_enumerated_groups(groups: &Groups) { let number_digits = get_amount_of_digits_for_number(groups.len()); for (i, group) in groups.iter().enumerate() { @@ -166,7 +159,7 @@ fn get_amount_of_digits_for_number(number: usize) -> usize { number.to_string().len() } -fn ask_group(groups: &[Rc]) -> Result>> { +fn ask_group(groups: &Groups) -> Result> { print_enumerated_groups(groups); let mut buf = String::new(); stdin().read_line(&mut buf)?; @@ -179,7 +172,7 @@ fn ask_group(groups: &[Rc]) -> Result>> { }; if idx < groups.len() { - Ok(Some(groups[idx].clone())) + Ok(groups.iter().nth(idx)) } else { Ok(None) } diff --git a/crates/pacdef/src/review/strategy.rs b/crates/pacdef/src/review/strategy.rs index c9bb536..b0fd4f6 100644 --- a/crates/pacdef/src/review/strategy.rs +++ b/crates/pacdef/src/review/strategy.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use anyhow::Result; use crate::{ @@ -8,19 +6,19 @@ use crate::{ }; #[derive(Debug)] -pub(super) struct Strategy { +pub struct Strategy { backend: AnyBackend, delete: Vec, as_dependency: Vec, - assign_group: Vec<(Package, Rc)>, + assign_group: Vec<(Package, Group)>, } impl Strategy { - pub(super) fn new( + pub fn new( backend: AnyBackend, delete: Vec, as_dependency: Vec, - assign_group: Vec<(Package, Rc)>, + assign_group: Vec<(Package, Group)>, ) -> Self { Self { backend, @@ -30,7 +28,7 @@ impl Strategy { } } - pub(super) fn execute(self) -> Result<()> { + pub fn execute(self) -> Result<()> { if !self.delete.is_empty() { self.backend.remove_packages(&self.delete, false)?; } @@ -46,7 +44,7 @@ impl Strategy { Ok(()) } - pub(super) fn show(&self) { + pub fn show(&self) { if self.nothing_to_do() { return; } @@ -75,7 +73,7 @@ impl Strategy { } } - pub(super) fn nothing_to_do(&self) -> bool { + pub fn nothing_to_do(&self) -> bool { self.delete.is_empty() && self.as_dependency.is_empty() && self.assign_group.is_empty() } } diff --git a/crates/pacdef/src/search.rs b/crates/pacdef/src/search.rs index 827ea6f..c824670 100644 --- a/crates/pacdef/src/search.rs +++ b/crates/pacdef/src/search.rs @@ -1,11 +1,13 @@ -use std::collections::HashSet; use std::iter::Peekable; use std::vec::IntoIter; use anyhow::{bail, Result}; use regex::Regex; -use crate::grouping::{Group, Package, Section}; +use crate::{ + grouping::{Group, Package, Section}, + Groups, +}; /// Find all packages in all groups whose name match the regex from the /// command-line arguments. Print the name of the packages per group and @@ -16,7 +18,7 @@ use crate::grouping::{Group, Package, Section}; /// This function will return an error if /// - an invalid regex was provided, or /// - no matching packages could be found. -pub fn search_packages(regex_str: &str, groups: &HashSet) -> Result<()> { +pub fn search_packages(regex_str: &str, groups: &Groups) -> Result<()> { if groups.is_empty() { eprintln!("WARNING: no group files found"); bail!(crate::errors::Error::NoPackagesFound);