use clap::{self, ArgAction, Parser};
use figment::Figment;
use itertools::Itertools;
use pacaptr::{
config::Config,
error::{Error, Result},
methods,
pm::BoxPm,
print::{println, prompt},
};
use tap::prelude::*;
use tokio::task;
use tt_call::tt_call;
use crate::_built::GIT_VERSION;
fn version() -> &'static str {
GIT_VERSION.unwrap_or(clap::crate_version!())
}
#[derive(Debug, Parser)]
#[command(
version = version(),
author = clap::crate_authors!(),
about = clap::crate_description!(),
before_help = format!("{} {}", clap::crate_name!(), version()),
subcommand_required = true,
arg_required_else_help = true,
)]
#[allow(clippy::struct_excessive_bools)]
pub struct Pacaptr {
#[command(subcommand)]
ops: Operations,
#[arg(
global = true,
number_of_values = 1,
long = "using",
alias = "package-manager",
visible_alias = "pm",
value_name = "pm"
)]
using: Option<String>,
#[arg(global = true, long, visible_alias = "dryrun")]
dry_run: bool,
#[arg(global = true, long = "needed")]
needed: bool,
#[arg(
global = true,
long,
visible_alias = "noconfirm",
visible_alias = "yes"
)]
no_confirm: bool,
#[arg(global = true, long, visible_alias = "nocache")]
no_cache: bool,
#[arg(global = true, long, conflicts_with = "dry_run")]
quiet: bool,
#[arg(global = true, name = "KEYWORDS")]
keywords: Vec<String>,
#[arg(last = true, global = true, name = "EXTRA_FLAGS")]
extra_flags: Vec<String>,
}
#[derive(Debug, Parser)]
#[command(about = clap::crate_description!())]
enum Operations {
#[command(short_flag = 'Q', long_flag = "query")]
Query {
#[arg(short, long = "changelog")]
c: bool,
#[arg(short, long = "explicit")]
e: bool,
#[arg(short, long = "info", action(ArgAction::Count))]
i: u8,
#[arg(short, long = "check")]
k: bool,
#[arg(short, long = "list")]
l: bool,
#[arg(short, long = "foreign")]
m: bool,
#[arg(short, long = "owns")]
o: bool,
#[arg(short, long = "file")]
p: bool,
#[arg(short, long = "search")]
s: bool,
#[arg(short, long = "upgrades")]
u: bool,
},
#[command(short_flag = 'R', long_flag = "remove")]
Remove {
#[arg(short, long = "nosave")]
n: bool,
#[arg(short, long = "print")]
p: bool,
#[arg(short, long = "recursive", action(ArgAction::Count))]
s: u8,
},
#[command(short_flag = 'S', long_flag = "sync")]
Sync {
#[arg(short, long = "clean", action(ArgAction::Count))]
c: u8,
#[arg(short, long = "groups")]
g: bool,
#[arg(short, long = "info", action(ArgAction::Count))]
i: u8,
#[arg(short, long = "list")]
l: bool,
#[arg(short, long = "print")]
p: bool,
#[arg(short, long = "search")]
s: bool,
#[arg(short, long = "sysupgrade")]
u: bool,
#[arg(short, long = "downloadonly")]
w: bool,
#[arg(short, long = "refresh")]
y: bool,
},
#[command(short_flag = 'U', long_flag = "update")]
Update {
#[arg(short, long = "print")]
p: bool,
},
}
impl Pacaptr {
fn cfg(&self) -> Config {
Config {
dry_run: self.dry_run,
needed: self.needed,
no_confirm: self.no_confirm,
no_cache: self.no_cache,
quiet: self.quiet.then_some(true),
default_pm: self.using.clone(),
}
}
#[allow(trivial_numeric_casts)]
async fn dispatch_from(&self, mut cfg: Config) -> Result<()> {
macro_rules! collect_options {(
$( $op:ident {
$( mappings: [$( $key:ident -> $val:ident ), *], )?
$( flags: [$( $flag:ident ), *], )?
}, )*
) => {{
let mut options = String::new();
match self.ops {
$( Operations::$op {
$( $( $key, )* )?
$( $( $flag, )* )?
} => {
options.push_str(&stringify!($op)[0..1]);
$( $(if $key {
cfg.$val = true;
})* )?
$( $(for _ in 0..(u8::from($flag)) {
options.push_str(stringify!($flag));
})* )?
} )*
}
options.chars().sorted_unstable().pipe(String::from_iter)
}};}
_ = ctrlc::set_handler(move || {
let term = console::Term::stdout();
_ = term.show_cursor();
})
.tap_err(|e| println(&*prompt::INFO, e));
let options = collect_options! {
Query {
flags: [c, e, i, k, l, m, o, p, s, u],
},
Remove {
mappings: [p -> dry_run],
flags: [n, s],
},
Sync {
mappings: [p -> dry_run],
flags: [c, g, i, l, s, u, w, y],
},
Update {
mappings: [p -> dry_run],
},
};
let pm = cfg.conv::<BoxPm>();
let kws = self.keywords.iter().map(AsRef::as_ref).collect_vec();
let flags = self.extra_flags.iter().map(AsRef::as_ref).collect_vec();
macro_rules! dispatch_match {(
methods = [{ $(
$( #[$meta:meta] )*
async fn $method:ident;
)* }]
) => {
match options.to_lowercase().as_ref() {
$(stringify!($method) => pm.$method(&kws, &flags).await,)*
_ => Err(Error::ArgParseError {
msg: format!("invalid flag combination `-{options}`"),
}),
}
};}
tt_call! {
macro = [{ methods }]
~~> dispatch_match
}
}
pub async fn dispatch(&self) -> Result<()> {
let cfg = self.cfg().join(task::block_in_place(|| {
Figment::new()
.join(Config::env_provider())
.join(Config::file_provider())
.extract::<Config>()
.map_err(Box::new)
})?);
self.dispatch_from(cfg).await
}
}
#[cfg(all(test, feature = "test"))]
mod tests {
#![allow(clippy::dbg_macro)]
use std::sync::LazyLock;
use tokio::test;
use super::*;
static MOCK_CFG: LazyLock<Config> = LazyLock::new(|| Config {
default_pm: Some("mockpm".into()),
..Config::default()
});
#[test]
#[should_panic(expected = "should run: suy")]
#[allow(clippy::semicolon_if_nothing_returned)]
async fn simple_syu() {
let opt = dbg!(Pacaptr::parse_from(["pacaptr", "-Syu"]));
let subcmd = &opt.ops;
assert!(matches!(subcmd, &Operations::Sync{ u, y, .. } if y && u));
assert!(opt.keywords.is_empty());
opt.dispatch_from(MOCK_CFG.clone()).await.unwrap();
}
#[test]
#[should_panic(expected = "should run: suy")]
#[allow(clippy::semicolon_if_nothing_returned)]
async fn long_syu() {
let opt = dbg!(Pacaptr::parse_from([
"pacaptr",
"--sync",
"--refresh",
"--sysupgrade"
]));
let subcmd = &opt.ops;
assert!(matches!(subcmd, &Operations::Sync { u, y, .. } if y && u));
assert!(opt.keywords.is_empty());
opt.dispatch_from(MOCK_CFG.clone()).await.unwrap();
}
#[test]
#[should_panic(expected = r#"should run: sw ["curl", "wget"]"#)]
#[allow(clippy::semicolon_if_nothing_returned)]
async fn simple_sw() {
let opt = dbg!(Pacaptr::parse_from(["pacaptr", "-Sw", "curl", "wget"]));
let subcmd = &opt.ops;
assert!(matches!(subcmd, &Operations::Sync { w, .. } if w));
assert_eq!(opt.keywords, &["curl", "wget"]);
opt.dispatch_from(MOCK_CFG.clone()).await.unwrap();
}
#[test]
#[should_panic(expected = r#"should run: s ["docker"]"#)]
#[allow(clippy::semicolon_if_nothing_returned)]
async fn other_flags() {
let opt = dbg!(Pacaptr::parse_from([
"pacaptr", "-S", "--dryrun", "--yes", "docker"
]));
let subcmd = &opt.ops;
assert!(opt.dry_run);
assert!(opt.no_confirm);
assert!(matches!(subcmd, &Operations::Sync { .. }));
assert_eq!(opt.keywords, &["docker"]);
opt.dispatch_from(MOCK_CFG.clone()).await.unwrap();
}
#[test]
#[should_panic(expected = r#"should run: s ["docker", "--proxy=localhost:1234"]"#)]
#[allow(clippy::semicolon_if_nothing_returned)]
async fn extra_flags() {
let opt = dbg!(Pacaptr::parse_from([
"pacaptr",
"-S",
"--yes",
"docker",
"--",
"--proxy=localhost:1234"
]));
let subcmd = &opt.ops;
assert!(opt.no_confirm);
assert!(matches!(subcmd, &Operations::Sync { .. }));
assert_eq!(opt.keywords, &["docker"]);
assert_eq!(opt.extra_flags, &["--proxy=localhost:1234"]);
opt.dispatch_from(MOCK_CFG.clone()).await.unwrap();
}
#[test]
#[should_panic(expected = r#"should run: si ["docker", "--proxy=localhost:1234"]"#)]
#[allow(clippy::semicolon_if_nothing_returned)]
async fn using() {
let opt = dbg!(Pacaptr::parse_from([
"pacaptr",
"--pm",
"mockpm",
"-Si",
"--yes",
"docker",
"--",
"--proxy=localhost:1234"
]));
let subcmd = &opt.ops;
assert!(opt.no_confirm);
assert!(matches!(subcmd, &Operations::Sync { i, .. } if i == 1));
assert_eq!(opt.keywords, &["docker"]);
assert_eq!(opt.extra_flags, &["--proxy=localhost:1234"]);
opt.dispatch_from(MOCK_CFG.clone()).await.unwrap();
}
}