Skip to content

Commit

Permalink
refactor: created reusuable CLI parse function
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Apr 8, 2024
1 parent 8ba775e commit 8bc895a
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 63 deletions.
56 changes: 2 additions & 54 deletions cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::{BTreeMap, VecDeque};
use std::env;
use std::fmt::{Debug, Display, Formatter};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::process::Command;

Expand All @@ -9,10 +9,10 @@ use indexmap::IndexMap;
use itertools::Itertools;
use miette::IntoDiagnostic;
use once_cell::sync::Lazy;
use strum::EnumTryAs;
use xx::process::check_status;
use xx::{XXError, XXResult};

use usage::cli::{ParseOutput, ParseValue};
use usage::{Complete, Spec, SpecArg, SpecCommand, SpecFlag};

use crate::cli::generate;
Expand Down Expand Up @@ -87,34 +87,6 @@ impl CompleteWord {
}
}

struct ParseOutput<'a> {
cmd: &'a SpecCommand,
cmds: Vec<&'a SpecCommand>,
args: IndexMap<&'a SpecArg, ParseValue>,
_flags: IndexMap<SpecFlag, ParseValue>,
available_flags: BTreeMap<String, SpecFlag>,
flag_awaiting_value: Option<SpecFlag>,
}

#[derive(EnumTryAs)]
enum ParseValue {
Bool(bool),
String(String),
MultiBool(Vec<bool>),
MultiString(Vec<String>),
}

impl Display for ParseValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ParseValue::Bool(b) => write!(f, "{}", b),
ParseValue::String(s) => write!(f, "{}", s),
ParseValue::MultiBool(b) => write!(f, "{:?}", b),
ParseValue::MultiString(s) => write!(f, "{:?}", s),
}
}
}

fn parse(spec: &Spec, mut words: VecDeque<String>) -> miette::Result<ParseOutput> {
let mut cmd = &spec.cmd;
let mut cmds = vec![];
Expand Down Expand Up @@ -394,27 +366,3 @@ fn sh(script: &str) -> XXResult<String> {
let stdout = String::from_utf8(output.stdout).expect("stdout is not utf-8");
Ok(stdout)
}

impl Debug for ParseOutput<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParseOutput")
.field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
.field(
"args",
&self
.args
.iter()
.map(|(a, w)| format!("{a}: {w}"))
.collect_vec(),
)
.field(
"flags",
&self
.available_flags
.iter()
.map(|(f, w)| format!("{f}: {w}"))
.collect_vec(),
)
.finish()
}
}
201 changes: 201 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use std::collections::{BTreeMap, VecDeque};
use std::fmt::{Debug, Display, Formatter};

use indexmap::IndexMap;
use itertools::Itertools;
use strum::EnumTryAs;

use crate::{Spec, SpecArg, SpecCommand, SpecFlag};

pub struct ParseOutput<'a> {
pub cmd: &'a SpecCommand,
pub cmds: Vec<&'a SpecCommand>,
pub args: IndexMap<&'a SpecArg, ParseValue>,
pub _flags: IndexMap<SpecFlag, ParseValue>,
pub available_flags: BTreeMap<String, SpecFlag>,
pub flag_awaiting_value: Option<SpecFlag>,
}

#[derive(Debug, EnumTryAs)]
pub enum ParseValue {
Bool(bool),
String(String),
MultiBool(Vec<bool>),
MultiString(Vec<String>),
}

pub fn parse<'a>(spec: &'a Spec, input: &[String]) -> Result<ParseOutput<'a>, miette::Error> {
let mut input = input.iter().cloned().collect::<VecDeque<_>>();
let mut cmd = &spec.cmd;
let mut cmds = vec![];
input.pop_front();
cmds.push(cmd);

let gather_flags = |cmd: &SpecCommand| {
cmd.flags
.iter()
.flat_map(|f| {
f.long
.iter()
.map(|l| (format!("--{}", l), f.clone()))
.chain(f.short.iter().map(|s| (format!("-{}", s), f.clone())))
})
.collect()
};

let mut available_flags: BTreeMap<String, SpecFlag> = gather_flags(cmd);

while !input.is_empty() {
if let Some(subcommand) = cmd.find_subcommand(&input[0]) {
available_flags.retain(|_, f| f.global);
available_flags.extend(gather_flags(subcommand));
input.pop_front();
cmds.push(subcommand);
cmd = subcommand;
} else {
break;
}
}

let mut args: IndexMap<&SpecArg, ParseValue> = IndexMap::new();
let mut flags: IndexMap<SpecFlag, ParseValue> = IndexMap::new();
let mut next_arg = cmd.args.first();
let mut flag_awaiting_value: Option<SpecFlag> = None;
let mut enable_flags = true;

while !input.is_empty() {
let w = input.pop_front().unwrap();

if let Some(flag) = flag_awaiting_value {
flag_awaiting_value = None;
if flag.var {
let arr = flags
.entry(flag)
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(w);
} else {
flags.insert(flag, ParseValue::String(w));
}
continue;
}

if w == "--" {
enable_flags = false;
continue;
}

// long flags
if enable_flags && w.starts_with("--") {
let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
if !val.is_empty() {
input.push_front(val.to_string());
}
if let Some(f) = available_flags.get(word) {
if f.arg.is_some() {
flag_awaiting_value = Some(f.clone());
} else if f.var {
let arr = flags
.entry(f.clone())
.or_insert_with(|| ParseValue::MultiBool(vec![]))
.try_as_multi_bool_mut()
.unwrap();
arr.push(true);
} else {
flags.insert(f.clone(), ParseValue::Bool(true));
}
continue;
}
}

// short flags
if enable_flags && w.starts_with('-') && w.len() > 1 {
let short = w.chars().nth(1).unwrap();
if let Some(f) = available_flags.get(&format!("-{}", short)) {
let mut next = format!("-{}", &w[2..]);
if f.arg.is_some() {
flag_awaiting_value = Some(f.clone());
next = w[2..].to_string();
}
if next != "-" {
input.push_front(next);
}
if f.var {
let arr = flags
.entry(f.clone())
.or_insert_with(|| ParseValue::MultiBool(vec![]))
.try_as_multi_bool_mut()
.unwrap();
arr.push(true);
} else {
flags.insert(f.clone(), ParseValue::Bool(true));
}
continue;
}
}

if let Some(arg) = next_arg {
if arg.var {
let arr = args
.entry(arg)
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(w);
if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
next_arg = cmd.args.get(args.len());
}
} else {
args.insert(arg, ParseValue::String(w));
next_arg = cmd.args.get(args.len());
}
continue;
}
panic!("unexpected word: {w}");
}

Ok(ParseOutput {
cmd,
cmds,
args,
_flags: flags,
available_flags,
flag_awaiting_value,
})
}

impl Display for ParseValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ParseValue::Bool(b) => write!(f, "{}", b),
ParseValue::String(s) => write!(f, "{}", s),
ParseValue::MultiBool(b) => write!(f, "{:?}", b),
ParseValue::MultiString(s) => write!(f, "{:?}", s),
}
}
}

impl Debug for ParseOutput<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParseOutput")
.field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
.field(
"args",
&self
.args
.iter()
.map(|(a, w)| format!("{a}: {w}"))
.collect_vec(),
)
.field(
"flags",
&self
.available_flags
.iter()
.map(|(f, w)| format!("{f}: {w}"))
.collect_vec(),
)
.finish()
}
}
18 changes: 10 additions & 8 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
#[macro_use]
extern crate log;
// #[macro_use]
// extern crate miette;
#[cfg(test)]
#[macro_use]
extern crate insta;
#[macro_use]
extern crate log;

pub use crate::parse::arg::SpecArg;
pub use crate::parse::cmd::SpecCommand;
pub use crate::parse::complete::Complete;
pub use crate::parse::flag::SpecFlag;
pub use crate::parse::spec::Spec;

#[macro_use]
pub mod error;
pub mod complete;
pub mod context;
pub(crate) mod env;
pub mod parse;

pub mod cli;
#[cfg(test)]
mod test;

pub use crate::parse::arg::SpecArg;
pub use crate::parse::cmd::SpecCommand;
pub use crate::parse::complete::Complete;
pub use crate::parse::flag::SpecFlag;
pub use crate::parse::spec::Spec;
2 changes: 1 addition & 1 deletion lib/src/parse/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl Spec {
schema.bin = file.file_name().unwrap().to_str().unwrap().to_string();
}
if schema.name.is_empty() {
schema.name = schema.bin.clone();
schema.name.clone_from(&schema.bin);
}
Ok((schema, body))
}
Expand Down

0 comments on commit 8bc895a

Please sign in to comment.