use crate::args::{PACMAN_FLAGS, PACMAN_GLOBALS};
use crate::config::{Colors, Config};
use std::fmt;
use anyhow::{anyhow, bail, ensure, Context, Result};
use url::Url;
#[derive(Debug, Copy, Clone)]
enum Arg<'a> {
Short(char),
Long(&'a str),
}
impl<'a> fmt::Display for Arg<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Arg::Short(c) => write!(f, "-{}", c),
Arg::Long(l) => write!(f, "--{}", l),
}
}
}
impl<'a> Arg<'a> {
fn arg(self) -> String {
match self {
Arg::Long(arg) => arg.to_string(),
Arg::Short(arg) => arg.to_string(),
}
}
fn is_pacman_arg(self) -> bool {
match self {
Arg::Long(arg) => PACMAN_FLAGS.contains(&arg),
Arg::Short(arg) => {
let mut buff = [0, 0, 0, 0];
let arg = arg.encode_utf8(&mut buff);
let arg: &str = arg;
PACMAN_FLAGS.contains(&arg)
}
}
}
fn is_pacman_global(self) -> bool {
match self {
Arg::Long(arg) => PACMAN_GLOBALS.contains(&arg),
Arg::Short(arg) => {
let mut buff = [0, 0, 0, 0];
let arg = arg.encode_utf8(&mut buff);
let arg: &str = arg;
PACMAN_GLOBALS.contains(&arg)
}
}
}
}
#[derive(PartialEq)]
enum TakesValue {
Required,
No,
Optional,
}
impl Config {
pub fn parse_arg(
&mut self,
arg: &str,
value: Option<&str>,
op_count: &mut u8,
end_of_ops: &mut bool,
) -> Result<bool> {
let mut forced = false;
if arg == "-" || *end_of_ops {
self.targets.push(arg.to_string());
return Ok(false);
}
if arg == "--" {
*end_of_ops = true;
return Ok(false);
}
if arg.starts_with("--") {
let mut value = value;
let mut split = arg.splitn(2, '=');
let arg = split.next().unwrap();
let arg = Arg::Long(arg.trim_start_matches("--"));
let mut used_next = takes_value(arg) == TakesValue::Required;
if let Some(val) = split.next() {
value = Some(val);
used_next = false;
forced = true;
}
self.handle_arg(arg, value, op_count, forced)?;
Ok(used_next)
} else if arg.starts_with('-') {
let mut chars = arg.chars();
chars.next().unwrap();
while let Some(c) = chars.next() {
let arg = Arg::Short(c);
if takes_value(arg) == TakesValue::Required {
self.handle_arg(arg, Some(chars.as_str()), op_count, false)?;
return Ok(true);
}
self.handle_arg(arg, None, op_count, false)?;
}
Ok(false)
} else {
self.targets.push(arg.to_string());
Ok(false)
}
}
fn handle_arg(
&mut self,
arg: Arg,
mut value: Option<&str>,
op_count: &mut u8,
forced: bool,
) -> Result<()> {
let yes_no_ask = &["yes", "no", "ask"];
let yes_no = &["yes", "no"];
let _no_all_tree = &["no", "all", "tree"];
let sort_by = &[
"votes",
"popularity",
"name",
"base",
"submitted",
"modified",
"id",
"baseid",
];
let search_by = &[
"name",
"name-desc",
"maintainer",
"depends",
"checkdepends",
"makedepends",
"optdepends",
];
match takes_value(arg) {
TakesValue::Required if value.is_none() => bail!("option {} expects a value", arg),
TakesValue::No if forced => bail!("option {} does not allow a value", arg),
_ => (),
}
if takes_value(arg) != TakesValue::Required && !forced {
value = None;
}
if arg.is_pacman_global() {
self.globals.args.push(crate::args::Arg {
key: arg.arg(),
value: value.map(|s| s.to_string()),
});
self.args.args.push(crate::args::Arg {
key: arg.arg(),
value: value.map(|s| s.to_string()),
});
}
if arg.is_pacman_arg() {
self.args.args.push(crate::args::Arg {
key: arg.arg(),
value: value.map(|s| s.to_string()),
});
}
let mut set_op = |op: &str| {
self.op = op.into();
*op_count += 1;
};
let value = value.with_context(|| format!("option {} does not allow a value", arg));
match arg {
Arg::Long("help") | Arg::Short('h') => self.help = true,
Arg::Long("version") | Arg::Short('V') => self.version = true,
Arg::Long("aururl") => self.aur_url = Url::parse(value?)?,
Arg::Long("makepkg") => self.makepkg_bin = value?.to_string(),
Arg::Long("pacman") => self.pacman_bin = value?.to_string(),
Arg::Long("git") => self.git_bin = value?.to_string(),
Arg::Long("gpg") => self.gpg_bin = value?.to_string(),
Arg::Long("sudo") => self.sudo_bin = value?.to_string(),
Arg::Long("asp") => self.asp_bin = value?.to_string(),
Arg::Long("fm") => self.fm = Some(value?.to_string()),
Arg::Long("config") => self.pacman_conf = Some(value?.to_string()),
Arg::Long("makepkgconf") => self.makepkg_conf = Some(value?.to_string()),
Arg::Long("mflags") => self.mflags = split_whitespace(value?),
Arg::Long("gitflags") => self.git_flags = split_whitespace(value?),
Arg::Long("gpgflags") => self.gpg_flags = split_whitespace(value?),
Arg::Long("sudoflags") => self.sudo_flags = split_whitespace(value?),
Arg::Long("fmflags") => self.fm_flags = split_whitespace(value?),
Arg::Long("completioninterval") => {
self.completion_interval = value?
.parse()
.map_err(|_| anyhow!("option {} must be a number", arg))?
}
Arg::Long("sortby") => self.sort_by = validate(value?, sort_by)?,
Arg::Long("searchby") => self.search_by = validate(value?, search_by)?,
Arg::Long("news") | Arg::Short('w') => self.news += 1,
Arg::Long("removemake") => {
self.remove_make = validate(value.unwrap_or("yes"), yes_no_ask)?
}
Arg::Long("upgrademenu") => {
self.upgrade_menu = true;
if let Ok(value) = value {
self.answer_upgrade = Some(value.to_string());
}
}
Arg::Long("noremovemake") => self.remove_make = "no".to_string(),
Arg::Long("cleanafter") => self.clean_after = true,
Arg::Long("nocleanafter") => self.clean_after = false,
Arg::Long("redownload") => self.redownload = validate(value.unwrap_or("yes"), yes_no)?,
Arg::Long("noredownload") => self.redownload = "no".to_string(),
Arg::Long("rebuild") => self.rebuild = validate(value.unwrap_or("yes"), yes_no)?,
Arg::Long("norebuild") => self.rebuild = "no".into(),
Arg::Long("topdown") => self.sort_mode = "topdown".to_string(),
Arg::Long("bottomup") => self.sort_mode = "bottomup".to_string(),
Arg::Long("aur") | Arg::Short('a') => self.mode = "aur".to_string(),
Arg::Long("repo") => self.mode = "repo".to_string(),
Arg::Long("gendb") => self.gendb = true,
Arg::Long("devel") => self.devel = true,
Arg::Long("nodevel") => self.devel = false,
Arg::Long("provides") => self.provides = true,
Arg::Long("noprovides") => self.provides = false,
Arg::Long("pgpfetch") => self.pgp_fetch = true,
Arg::Long("nopgpfetch") => self.pgp_fetch = false,
Arg::Long("useask") => self.use_ask = true,
Arg::Long("nouseask") => self.use_ask = false,
Arg::Long("combinedupgrade") => self.combined_upgrade = true,
Arg::Long("nocombinedupgrade") => self.combined_upgrade = false,
Arg::Long("batchinstall") => self.batch_install = true,
Arg::Long("nobatchinstall") => self.batch_install = false,
Arg::Long("sudoloop") => self.sudo_loop = true,
Arg::Long("nosudoloop") => self.sudo_loop = false,
Arg::Long("clean") => self.clean += 1,
Arg::Long("complete") => self.complete = true,
Arg::Short('c') => {
self.complete = true;
self.clean += 1
}
Arg::Long("print") | Arg::Short('p') => self.print = true,
Arg::Long("database") | Arg::Short('D') => set_op("database"),
Arg::Long("files") | Arg::Short('F') => set_op("files"),
Arg::Long("query") | Arg::Short('Q') => set_op("query"),
Arg::Long("remove") | Arg::Short('R') => set_op("remove"),
Arg::Long("sync") | Arg::Short('S') => set_op("sync"),
Arg::Long("deptest") | Arg::Short('T') => set_op("deptest"),
Arg::Long("upgrade") | Arg::Short('U') => set_op("upgrade"),
Arg::Long("show") | Arg::Short('P') => set_op("show"),
Arg::Long("getpkgbuild") | Arg::Short('G') => set_op("getpkgbuild"),
Arg::Long("noconfirm") => self.no_confirm = true,
Arg::Long("confirm") => self.no_confirm = false,
Arg::Long("dbpath") | Arg::Short('b') => self.db_path = Some(value?.to_string()),
Arg::Long("root") | Arg::Short('r') => self.root = Some(value?.to_string()),
Arg::Long("verbose") | Arg::Short('v') => self.verbose = true,
Arg::Long("ask") => {
if let Ok(n) = value?.to_string().parse() {
self.ask = n
}
}
Arg::Long("arch") => self.arch = Some(value?.to_string()),
Arg::Long("color") => self.color = Colors::from(value.unwrap_or("always")),
Arg::Long(a) if !arg.is_pacman_arg() => bail!("unkown option --{}", a),
Arg::Short(a) if !arg.is_pacman_arg() => bail!("unkown option -{}", a),
_ => (),
}
Ok(())
}
}
fn split_whitespace(s: &str) -> Vec<String> {
s.split_whitespace().map(|s| s.to_string()).collect()
}
fn takes_value(arg: Arg) -> TakesValue {
match arg {
Arg::Long("aururl") => TakesValue::Required,
Arg::Long("editor") => TakesValue::Required,
Arg::Long("makepkg") => TakesValue::Required,
Arg::Long("pacman") => TakesValue::Required,
Arg::Long("git") => TakesValue::Required,
Arg::Long("gpg") => TakesValue::Required,
Arg::Long("sudo") => TakesValue::Required,
Arg::Long("asp") => TakesValue::Required,
Arg::Long("fm") => TakesValue::Required,
Arg::Long("makepkgconf") => TakesValue::Required,
Arg::Long("editorflags") => TakesValue::Required,
Arg::Long("mflags") => TakesValue::Required,
Arg::Long("gitflags") => TakesValue::Required,
Arg::Long("gpgflags") => TakesValue::Required,
Arg::Long("sudoflags") => TakesValue::Required,
Arg::Long("fmflags") => TakesValue::Required,
Arg::Long("completioninterval") => TakesValue::Required,
Arg::Long("sortby") => TakesValue::Required,
Arg::Long("searchby") => TakesValue::Required,
Arg::Long("removemake") => TakesValue::Optional,
Arg::Long("redownload") => TakesValue::Optional,
Arg::Long("dbpath") | Arg::Short('b') => TakesValue::Required,
Arg::Long("root") | Arg::Short('r') => TakesValue::Required,
Arg::Long("ask") => TakesValue::Required,
Arg::Long("arch") => TakesValue::Required,
Arg::Long("cachedir") => TakesValue::Required,
Arg::Long("color") => TakesValue::Optional,
Arg::Long("config") => TakesValue::Required,
Arg::Long("gpgdir") => TakesValue::Required,
Arg::Long("hookdir") => TakesValue::Required,
Arg::Long("logfile") => TakesValue::Required,
Arg::Long("sysroot") => TakesValue::Required,
Arg::Long("ignore") => TakesValue::Required,
Arg::Long("ignoregroup") => TakesValue::Required,
Arg::Long("assumeinstalled") => TakesValue::Required,
Arg::Long("print-format") => TakesValue::Required,
_ => TakesValue::No,
}
}
pub fn validate(key: &str, valid: &[&str]) -> Result<String> {
ensure!(
valid.contains(&key),
"invalid value for '{}', expected: {}",
key,
valid.join("|")
);
Ok(key.to_string())
}