use std::cell::RefCell;
use std::fmt::Debug;
use std::ops::Add;
use std::rc::Rc;
use log::{debug, info, LevelFilter};
use rustyline::completion::Completer;
use rustyline::config::CompletionType;
use rustyline::config::Configurer;
use rustyline::{EditMode, Editor};
use rustyline_derive::{Helper, Highlighter, Hinter, Validator};
use std::collections::HashMap;
use std::io::{BufWriter, Write};
use std::result::Result as stdResult;
#[derive(thiserror::Error, Debug)]
pub enum XcliError {
#[error("Bad syntax")]
BadSyntax,
#[error("Missing Handler: {0} not found")]
MissingHandler(String),
#[error("Bad Argument: {0:?}")]
BadArgument(Option<anyhow::Error>),
#[error("{0:?}")]
Other(anyhow::Error),
}
pub type XcliResult = stdResult<CmdExeCode, XcliError>;
type CmdAction = fn(&App, &[&str]) -> XcliResult;
#[derive(Debug, PartialEq, Eq)]
pub enum CmdExeCode {
Ok,
Exit,
}
type IAny = Box<dyn std::any::Any>;
pub struct App<'a> {
pub(crate) name: String,
pub(crate) version: Option<&'a str>,
pub(crate) author: Option<&'a str>,
pub(crate) tree: Command<'a>,
pub(crate) rl: Rc<RefCell<Editor<PrefixCompleter>>>,
pub(crate) handlers: HashMap<&'a str, IAny>,
}
#[derive(Default)]
pub struct Command<'a> {
pub(crate) name: String,
pub(crate) about: Option<&'a str>,
pub(crate) usage: Option<&'a str>,
pub(crate) subcommands: Vec<Command<'a>>,
pub(crate) action: Option<CmdAction>,
}
impl<'a> App<'a> {
pub fn new<S: Into<String>>(n: S) -> Self {
let builtin_cmds = Command::new("")
.about("Interactive CLI")
.subcommand(
Command::new("tree")
.about("prints the whole command tree")
.usage("tree")
.action(|app: &App, _| -> XcliResult {
app.show_tree();
Ok(CmdExeCode::Ok)
}),
)
.subcommand(
Command::new("mode")
.about("manages the line editor mode, vi/emcas")
.usage("mode [vi|emacs]")
.action(cli_mode),
)
.subcommand(
Command::new("log")
.about("manages log level filter")
.usage("log [off|error|warn|info|debug|trace]")
.action(cli_log),
)
.subcommand(
Command::new("help")
.about("displays help information")
.usage("help [command]")
.action(cli_help),
)
.subcommand(
Command::new("test")
.about("controls testing features")
.subcommand(
Command::new("c1")
.about("controls testing features")
.action(|_app, _| -> XcliResult {
println!("c1 tested");
Ok(CmdExeCode::Ok)
}),
),
)
.subcommand(
Command::new("exit")
.about("quits CLI and exits to shell")
.action(|_, _| -> XcliResult { Ok(CmdExeCode::Exit) }),
)
.subcommand(
Command::new("version")
.about("shows version information")
.action(|app, _| -> XcliResult {
println!(
"{}\n{}\n{}\n",
app.get_name(),
app.get_author(),
app.get_version()
);
Ok(CmdExeCode::Ok)
}),
);
let rl = Rc::new(RefCell::new(Editor::<PrefixCompleter>::new()));
let handlers = HashMap::default();
App {
name: n.into(),
version: None,
author: None,
tree: builtin_cmds,
rl,
handlers,
}
}
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_author(&self) -> &str {
self.author.unwrap_or("")
}
pub fn get_version(&self) -> &str {
self.version.unwrap_or("")
}
pub fn author<S: Into<&'a str>>(mut self, author: S) -> Self {
self.author = Some(author.into());
self
}
pub fn version<S: Into<&'a str>>(mut self, ver: S) -> Self {
self.version = Some(ver.into());
self
}
pub fn add_subcommand(&mut self, subcmd: Command<'a>) {
self.tree.subcommands.push(subcmd);
}
pub fn show_tree(&self) {
self.rl.borrow().helper().unwrap().print_tree("");
}
pub fn register(&mut self, key: &'static str, value: IAny) {
self.handlers.insert(key, value);
}
pub fn get_handler(&self, key: &str) -> stdResult<&IAny, XcliError> {
self.handlers
.get(key)
.ok_or_else(|| XcliError::MissingHandler(key.to_string()))
}
fn _run(&mut self, args: Vec<&str>) -> XcliResult {
self.tree.run_sub(&self, &args)
}
pub fn run(mut self) {
info!("starting CLI loop...");
self.rl
.borrow_mut()
.set_completion_type(CompletionType::List);
self.rl
.borrow_mut()
.set_helper(Some(PrefixCompleter::new(&self.tree)));
if self.rl.borrow_mut().load_history("history.txt").is_err() {
println!("No previous history.");
}
loop {
let readline = self.rl.borrow_mut().readline("# ");
let line = match readline {
Ok(line) => {
self.rl.borrow_mut().add_history_entry(line.as_str());
debug!("Line: {}", line);
line
}
Err(err) => {
println!("Error: {:?}", err);
let l = self
.rl
.borrow_mut()
.readline("Do you realy want to quit? [y/N]")
.unwrap_or_else(|_| "n".parse().unwrap());
match l.as_str() {
"Y" | "y" => break,
_ => "".to_string(),
}
}
};
let args: Vec<_> = line.split_ascii_whitespace().collect();
if !args.is_empty() {
if let Ok(CmdExeCode::Exit) = self._run(args) {
break;
}
}
}
self.rl.borrow_mut().save_history("history.txt").unwrap();
}
}
impl<'a> Command<'a> {
pub fn new<S: Into<String>>(n: S) -> Self {
Command {
name: n.into(),
about: None,
usage: None,
subcommands: vec![],
action: None,
}
}
pub fn get_name(&self) -> &str {
&self.name
}
pub fn action(mut self, action: CmdAction) -> Self {
self.action = Some(action);
self
}
pub fn about<S: Into<&'a str>>(mut self, about: S) -> Self {
self.about = Some(about.into());
self
}
pub fn usage<S: Into<&'a str>>(mut self, about: S) -> Self {
self.usage = Some(about.into());
self
}
pub fn get_subcommands(&self) -> &[Command<'a>] {
&self.subcommands
}
pub fn subcommand(mut self, subcmd: Command<'a>) -> Self {
self.subcommands.push(subcmd);
self
}
pub fn subcommands<I>(mut self, subcmds: I) -> Self
where
I: IntoIterator<Item = Command<'a>>,
{
for subcmd in subcmds {
self.subcommands.push(subcmd);
}
self
}
pub fn show_command_help(&self) {
println!(
"Command: {}\nUsage: {}\nDescription: {}",
self.name,
self.usage.unwrap_or_else(|| self.name.as_ref()),
self.about.unwrap_or("")
);
}
pub fn show_subcommand_help(&self) {
for cmd in &self.subcommands {
println!(
"{:12}: {:14}",
cmd.name,
cmd.usage.unwrap_or_else(|| cmd.name.as_ref())
)
}
}
pub fn locate_subcommand(&self, args: &[&str]) -> Option<&Command> {
if !args.is_empty() {
if let Some(found) = self
.subcommands
.iter()
.find(|&c| c.name.as_str() == args[0])
{
found.locate_subcommand(args[1..].to_vec().as_ref())
} else {
None
}
} else {
Some(self)
}
}
pub fn run_sub(&self, app: &App, args: &[&str]) -> XcliResult {
if !args.is_empty() {
for cmd in &self.subcommands {
if args[0] == cmd.name {
return cmd.run_sub(app, args[1..].to_vec().as_ref());
}
}
}
if let Some(action) = &self.action {
debug!("action for {}, arg={:?}", self.name, args);
let ret = action(app, &args);
match &ret {
Err(XcliError::BadArgument(err)) => {
if let Some(err) = err {
println!("Bad argument : '{}'", err);
} else {
println!("Missing argument");
}
self.show_command_help();
}
Err(XcliError::BadSyntax) => {
println!("Bad syntax : {:?}", args);
self.show_command_help();
}
Err(XcliError::Other(err)) => {
println!("{}", err);
}
_ => {}
}
return ret;
} else {
if !args.is_empty() {
debug!("command without action, but with some args {:?}", args);
println!("Unknown command or arguments : {:?}", args)
} else {
debug!("command with no action defined");
self.show_command_help();
self.show_subcommand_help();
}
}
Ok(CmdExeCode::Ok)
}
pub fn for_each<F>(&self, path: &str, f: &mut F)
where
F: FnMut(&Self, &str),
{
f(&self, path);
for a in self.get_subcommands() {
a.for_each(format!("{}/{}", path, a.name).as_str(), f);
}
}
}
#[derive(Helper, Hinter, Validator, Highlighter)]
pub struct PrefixCompleter {
tree: PrefixNode,
}
#[derive(Debug, Clone)]
pub struct PrefixNode {
name: String,
children: Vec<PrefixNode>,
}
impl PrefixNode {
fn new(cmd: &Command) -> PrefixNode {
PrefixNode {
name: cmd.name.clone().add(" "),
children: vec![],
}
}
fn add_children(&mut self, child: PrefixNode) {
self.children.push(child);
}
}
impl PrefixCompleter {
pub fn new(cmd_tree: &Command) -> Self {
let mut prefix_tree = PrefixNode::new(cmd_tree);
for cmd in &cmd_tree.subcommands {
PrefixCompleter::generate_cmd_tree(&mut prefix_tree, cmd);
}
Self { tree: prefix_tree }
}
fn generate_cmd_tree(parent: &mut PrefixNode, cmd: &Command) {
let mut node = PrefixNode::new(cmd);
for cmd in &cmd.subcommands {
PrefixCompleter::generate_cmd_tree(&mut node, cmd);
}
debug!("prefix {} added", node.name);
parent.add_children(node);
}
pub fn complete_cmd(&self, line: &str, pos: usize) -> rustyline::Result<(usize, Vec<String>)> {
debug!("line={} pos={}", line, pos);
let v = PrefixCompleter::_complete_cmd(&self.tree, line, pos);
Ok((pos, v))
}
pub fn _complete_cmd(node: &PrefixNode, line: &str, pos: usize) -> Vec<String> {
debug!("cli to complete {} for node {}", line, node.name);
let line = line[..pos].trim_start();
let mut go_next = false;
let mut new_line: Vec<String> = vec![];
let mut offset: usize = 0;
let mut next_node = None;
for child in &node.children {
if line.len() >= child.name.len() {
if line.starts_with(&child.name) {
if line.len() == child.name.len() {
new_line.push(" ".to_string());
} else {
new_line.push(child.name.to_string());
}
offset = child.name.len();
next_node = Some(child);
go_next = true;
}
} else if child.name.starts_with(line) {
new_line.push(child.name[line.len()..].to_string());
offset = line.len();
next_node = Some(child);
}
}
if new_line.len() != 1 {
debug!("offset={}, candidates={:?}", offset, new_line);
return new_line;
}
if go_next {
let line = line[offset..].trim_start();
return PrefixCompleter::_complete_cmd(next_node.unwrap(), line, line.len());
}
debug!("offset={}, nl={:?}", offset, new_line);
new_line
}
fn print_tree(&self, prefix: &str) {
let s: Vec<u8> = vec![];
let mut writer = BufWriter::new(s);
let _ = PrefixCompleter::_print_tree(&self.tree, prefix, 0, &mut writer);
println!("{}", String::from_utf8_lossy(writer.buffer()));
}
fn _print_tree(
node: &PrefixNode,
prefix: &str,
level: u32,
buf: &mut BufWriter<Vec<u8>>,
) -> std::io::Result<()> {
let mut level = level;
if !node.name.is_empty() {
write!(buf, "{}", prefix)?;
if level > 0 {
write!(buf, "├{}", "─".repeat((level as usize * 4) - 2))?;
}
writeln!(buf, "{}", node.name)?;
level += 1;
}
for child in &node.children {
let _ = PrefixCompleter::_print_tree(child, prefix, level, buf);
}
Ok(())
}
}
impl Completer for PrefixCompleter {
type Candidate = String;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<String>)> {
self.complete_cmd(line, pos)
}
}
fn cli_help(app: &App, args: &[&str]) -> XcliResult {
if args.is_empty() {
app.tree.show_subcommand_help();
} else if let Some(cmd) = app.tree.locate_subcommand(args) {
cmd.show_command_help();
} else {
println!("Unrecognized command {:?}", args)
}
Ok(CmdExeCode::Ok)
}
fn cli_log(_app: &App, args: &[&str]) -> XcliResult {
match args.len() {
0 => {
println!("Global log level is: {}", log::max_level().to_string());
}
1 => match args[0].parse::<LevelFilter>() {
Ok(level) => log::set_max_level(level),
Err(err) => {
let err = format!("{}, {}", args[0], err);
return Err(XcliError::BadArgument(Some(anyhow::Error::msg(err))));
}
},
_ => return Err(XcliError::BadSyntax),
}
Ok(CmdExeCode::Ok)
}
fn cli_mode(app: &App, args: &[&str]) -> XcliResult {
match args.len() {
0 => {
let mode = app.rl.borrow_mut().config_mut().edit_mode();
let mode_str = if mode == EditMode::Vi { "Vi" } else { "Emacs" };
println!("Current edit mode is: {}", mode_str);
}
1 => match args[0].to_lowercase().as_ref() {
"vi" => app.rl.borrow_mut().set_edit_mode(EditMode::Vi),
"emacs" => app.rl.borrow_mut().set_edit_mode(EditMode::Emacs),
bad => {
return {
Err(XcliError::BadArgument(Some(anyhow::Error::msg(
bad.to_string(),
))))
}
}
},
_ => return Err(XcliError::BadSyntax),
}
Ok(CmdExeCode::Ok)
}