use std::{
error::Error,
fmt,
sync::{Arc, Mutex, mpsc},
thread,
};
use futures_channel::oneshot;
use crate::cli::ValueType;
use crate::core::logging::LogLevel;
use crate::{
Argument, ArgumentDefinition, ArgumentValue, Command, StrategyError, SwitchDefinition,
cli::CommandCatalogue,
};
pub mod logging {
#[repr(u8)]
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
pub trait LogSink: Send + Sync {
fn log(&self, level: LogLevel, message: &str);
fn info(&self, message: &str) {
self.log(LogLevel::Info, message);
}
fn warn(&self, message: &str) {
self.log(LogLevel::Warn, message);
}
fn error(&self, message: &str) {
self.log(LogLevel::Error, message);
}
fn debug(&self, message: &str) {
self.log(LogLevel::Debug, message);
}
}
pub struct ConsoleLogSink {
verbosity: LogLevel,
}
impl ConsoleLogSink {
pub fn new(verbosity: LogLevel) -> Self {
Self { verbosity }
}
}
impl LogSink for ConsoleLogSink {
fn log(&self, level: LogLevel, message: &str) {
match level {
LogLevel::Error => eprintln!("{}", message),
_ => {
if self.verbosity as i8 - level as i8 <= 0 {
println!("{}", message)
}
}
}
}
}
}
pub trait HelpRenderer: Send + Sync {
fn render(&self, caller: &str, registered_commands: &[&Command]) -> String;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InvocationElement {
Argument(Argument),
Switch(String),
Param(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct InvocationArgs {
pub name: String,
pub args: Vec<Argument>,
pub switches: Vec<String>,
pub params: Vec<String>,
pub order: Vec<InvocationElement>,
pub subcommand: Option<Box<InvocationArgs>>,
}
impl InvocationArgs {
pub fn leaf_name(&self) -> &str {
self.subcommand
.as_deref()
.map_or(self.name.as_str(), InvocationArgs::leaf_name)
}
}
pub trait ArgumentInterpreter: Send + Sync {
fn interpret(
&self,
arg: &[String],
registered_commands: &[&Command],
) -> Result<InvocationArgs, CMDKitError>;
}
#[derive(Clone)]
pub struct ExecutionContext {
pub logger: Arc<dyn logging::LogSink>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PlainTextArgumentInterpreter;
impl PlainTextArgumentInterpreter {
fn find_declared_argument<'a>(
command: &'a Command,
flag: &str,
) -> Option<&'a ArgumentDefinition> {
command.metadata.arguments.iter().find(|argument| {
argument.name == flag || argument.aliases.iter().any(|alias| alias == flag)
})
}
fn find_declared_switch<'a>(command: &'a Command, flag: &str) -> Option<&'a SwitchDefinition> {
command
.metadata
.options
.iter()
.find(|option| option.name == flag || option.aliases.iter().any(|alias| alias == flag))
}
fn resolve_command<'a>(commands: &'a [&'a Command], token: &str) -> Option<&'a Command> {
commands.iter().find_map(|command| {
((command.metadata.name == token)
|| command.metadata.aliases.iter().any(|alias| alias == token))
.then_some(*command)
})
}
fn upsert_argument(arguments: &mut Vec<Argument>, argument: Argument) {
if let Some(index) = arguments
.iter()
.position(|existing| existing.name == argument.name)
{
arguments.remove(index);
}
arguments.push(argument);
}
fn validate_required_arguments(
command: &Command,
arguments: &[Argument],
) -> Result<(), CMDKitError> {
for required in command.metadata.arguments.iter().filter(|arg| arg.required) {
let value = arguments
.iter()
.find(|argument| argument.name == required.name);
if value.is_none() {
return Err(Self::invalid_arguments(
command,
format!("missing value for required argument '--{}'", required.name),
));
}
}
Ok(())
}
fn invalid_arguments(command: &Command, message: impl Into<String>) -> CMDKitError {
CMDKitError::StrategyExecution {
command: command.metadata.name.clone(),
source: StrategyError::invalid_arguments(message),
}
}
fn parse_command(
&self,
command: &Command,
args: &[String],
) -> Result<InvocationArgs, CMDKitError> {
Self::parse_command_impl(command, args)
}
fn parse_command_impl(
command: &Command,
args: &[String],
) -> Result<InvocationArgs, CMDKitError> {
let mut switches = Vec::new();
let mut arguments = Vec::new();
let mut params = Vec::new();
let mut order = Vec::new();
let mut index = 0;
while index < args.len() {
let token = &args[index];
if params.is_empty()
&& let Some(subcommand) = command.resolve_subcommand(token)
{
Self::validate_required_arguments(command, &arguments)?;
return Ok(InvocationArgs {
name: command.metadata.name.clone(),
args: arguments,
switches,
params,
order,
subcommand: Some(Box::new(Self::parse_command_impl(
&subcommand,
&args[index + 1..],
)?)),
});
}
let Some(flag) = token.strip_prefix("--") else {
params.push(token.clone());
order.push(InvocationElement::Param(token.clone()));
index += 1;
continue;
};
if let Some((flag_name, inline_value)) = flag.split_once('=') {
if let Some(declared_argument) = Self::find_declared_argument(command, flag_name) {
if inline_value.is_empty() {
index += 1;
continue;
}
let argument = Self::convert_to_argument_value(
declared_argument,
&inline_value.to_string(),
);
let order_arg = argument.clone();
Self::upsert_argument(&mut arguments, argument);
order.push(InvocationElement::Argument(order_arg));
index += 1;
continue;
}
if let Some(option_decl) = Self::find_declared_switch(command, flag_name) {
return Err(Self::invalid_arguments(
command,
format!("switch '--{}' does not take a value", option_decl.name),
));
}
return Err(Self::invalid_arguments(
command,
format!("unknown flag '--{}'", flag_name),
));
}
if let Some(declared_argument) = Self::find_declared_argument(command, flag) {
let Some(argument_value) = args.get(index + 1) else {
let msg = if declared_argument.required {
format!(
"missing value for required argument '--{}'",
declared_argument.name
)
} else {
format!("missing value for argument '--{}'", declared_argument.name)
};
return Err(Self::invalid_arguments(command, msg));
};
if argument_value.is_empty()
|| argument_value.starts_with("--")
|| command.resolve_subcommand(argument_value).is_some()
{
let msg = if declared_argument.required {
format!(
"missing value for required argument '--{}'",
declared_argument.name
)
} else {
format!("missing value for argument '--{}'", declared_argument.name)
};
return Err(Self::invalid_arguments(command, msg));
}
let argument = Self::convert_to_argument_value(declared_argument, argument_value);
let order_arg = argument.clone();
Self::upsert_argument(&mut arguments, argument);
order.push(InvocationElement::Argument(order_arg));
index += 2;
continue;
}
if let Some(option_decl) = Self::find_declared_switch(command, flag) {
let switch = &option_decl.name;
switches.push(switch.clone());
order.push(InvocationElement::Switch(switch.clone()));
index += 1;
continue;
}
return Err(Self::invalid_arguments(
command,
format!("unknown flag '--{}'", flag),
));
}
Self::validate_required_arguments(command, &arguments)?;
Ok(InvocationArgs {
name: command.metadata.name.clone(),
args: arguments,
switches,
params,
order,
subcommand: None,
})
}
fn convert_to_argument_value(
declared_argument: &ArgumentDefinition,
argument_value: &String,
) -> Argument {
let value = match declared_argument.value_type {
ValueType::Float => ArgumentValue::Float(argument_value.parse().unwrap()),
ValueType::Int => ArgumentValue::Int(argument_value.parse().unwrap()),
ValueType::Bool => ArgumentValue::Bool(argument_value.parse().unwrap()),
_ => ArgumentValue::String(argument_value.to_string()),
};
declared_argument.set_value(value)
}
}
impl ArgumentInterpreter for PlainTextArgumentInterpreter {
fn interpret(
&self,
arg: &[String],
registered_commands: &[&Command],
) -> Result<InvocationArgs, CMDKitError> {
let Some(command_name) = arg.first() else {
return Err(CMDKitError::MissingCommand {
help: String::new(),
});
};
let command =
Self::resolve_command(registered_commands, command_name).ok_or_else(|| {
CMDKitError::UnknownCommand {
command: command_name.clone(),
help: String::new(),
}
})?;
self.parse_command(command, &arg[1..])
}
}
pub struct PlainTextHelpRenderer;
impl HelpRenderer for PlainTextHelpRenderer {
fn render(&self, caller: &str, registered_commands: &[&Command]) -> String {
fn render_recursive(command: &Command, depth: usize, path: String, out: &mut Vec<String>) {
let indent = " ".repeat(depth);
out.push(format!(
" {indent}- {path}: {}",
command.metadata.description
));
if let Some(usage) = &command.metadata.usage {
out.push(format!(" {indent} usage: {usage}"));
}
if let Some(long_description) = &command.metadata.long_description {
out.push(format!(" {indent} details: {long_description}"));
}
if !command.metadata.aliases.is_empty() {
out.push(format!(
" {indent} aliases: {}",
command.metadata.aliases.join(", ")
));
}
if !command.metadata.examples.is_empty() {
out.push(format!(" {indent} examples:"));
for example in &command.metadata.examples {
out.push(format!(" {indent} - {example}"));
}
}
if !command.metadata.options.is_empty() {
out.push(format!(" {indent} switches:"));
for option in &command.metadata.options {
if option.aliases.is_empty() {
out.push(format!(
" {indent} - --{}: {}",
option.name, option.description
));
} else {
out.push(format!(
" {indent} - --{} (aliases: {}): {}",
option.name,
option.aliases.join(", "),
option.description
));
}
}
}
if !command.metadata.arguments.is_empty() {
out.push(format!(" {indent} arguments:"));
for argument in &command.metadata.arguments {
let required_suffix = if argument.required { " [required]" } else { "" };
if argument.aliases.is_empty() {
out.push(format!(
" {indent} - --{}{}: {}",
argument.name, required_suffix, argument.description
));
} else {
out.push(format!(
" {indent} - --{}{} (aliases: {}): {}",
argument.name,
required_suffix,
argument.aliases.join(", "),
argument.description
));
}
}
}
if let Some(catalog) = command.subcommand_catalog() {
for child in catalog.subcommands() {
let child_path = format!("{path} {}", child.metadata.name);
render_recursive(&child, depth + 1, child_path, out);
}
}
}
let mut lines = Vec::new();
for command in registered_commands {
render_recursive(command, 0, command.metadata.name.clone(), &mut lines);
}
let command_lines = lines.join("\n");
format!(
r#"Usage: {} <command> [args...]
Registered commands are listed below.
supported commands:
- help : Display help information
{}
"#,
caller, command_lines
)
}
}
#[derive(Clone)]
pub struct CoreConfig {
pub help_renderer: Arc<dyn HelpRenderer>,
pub argument_interpreter: Arc<dyn ArgumentInterpreter>,
pub logger: Arc<dyn logging::LogSink>,
}
impl CoreConfig {
pub fn new() -> Self {
Self {
help_renderer: Arc::new(PlainTextHelpRenderer),
argument_interpreter: Arc::new(PlainTextArgumentInterpreter),
logger: Arc::new(logging::ConsoleLogSink::new(LogLevel::Info)),
}
}
pub fn execution_context(&self) -> ExecutionContext {
ExecutionContext {
logger: Arc::clone(&self.logger),
}
}
pub fn with_logger<L>(mut self, logger: L) -> Self
where
L: logging::LogSink + 'static,
{
self.logger = Arc::new(logger);
self
}
pub fn with_help_renderer<R>(mut self, renderer: R) -> Self
where
R: HelpRenderer + 'static,
{
self.help_renderer = Arc::new(renderer);
self
}
pub fn with_argument_interpreter<I>(mut self, interpreter: I) -> Self
where
I: ArgumentInterpreter + 'static,
{
self.argument_interpreter = Arc::new(interpreter);
self
}
}
impl Default for CoreConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum CMDKitError {
Registration { message: String },
ExecutorUnavailable { message: String },
MissingCommand { help: String },
UnknownCommand { command: String, help: String },
StrategyExecution {
command: String,
source: StrategyError,
},
}
impl fmt::Display for CMDKitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Registration { message } => {
write!(f, "Command registration failed: {message}")
}
Self::ExecutorUnavailable { message } => {
write!(f, "Executor unavailable: {message}")
}
Self::MissingCommand { help } => {
write!(f, "No command provided.\n\n{help}")
}
Self::UnknownCommand { command, help } => {
write!(f, "Unknown command: {command}\n\n{help}")
}
Self::StrategyExecution { command, source } => {
write!(f, "Strategy execution failed for '{command}': {source}")
}
}
}
}
impl Error for CMDKitError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::StrategyExecution { source, .. } => Some(source),
_ => None,
}
}
}
pub struct CMDKit {
registry: CommandCatalogue,
config: CoreConfig,
}
impl CMDKit {
pub fn builder() -> CMDKitBuilder {
CMDKitBuilder::new()
}
pub fn get(&self, name: &str) -> Option<&Command> {
self.registry.get(name)
}
pub fn get_all(&self) -> Vec<&Command> {
self.registry.get_all()
}
pub fn run_with_commands(commands: &[Command]) {
if let Err(e) = Self::try_run_with_commands(commands) {
eprintln!("{e}");
}
}
pub fn try_run_with_commands(commands: &[Command]) -> Result<(), CMDKitError> {
Self::builder()
.try_with_commands(commands)?
.build()
.try_run_from_env()
}
pub fn try_run_from_args(&self, args: &[String]) -> Result<(), CMDKitError> {
let binary = args
.iter()
.next()
.cloned()
.unwrap_or_else(|| "cli".to_string());
if args.get(1).is_some_and(|arg| arg == "help") {
println!("{}", self.render_help(&binary));
return Ok(());
}
let registered_commands = self.get_all();
let invocation = self
.config
.argument_interpreter
.interpret(args.get(1..).unwrap_or(&[]), ®istered_commands[..])
.map_err(|error| self.attach_help(error, &binary))?;
let command = self
.resolve_registered_command(®istered_commands[..], &invocation.name)
.ok_or_else(|| CMDKitError::UnknownCommand {
command: invocation.name.clone(),
help: self.render_help(&binary),
})?;
let command_name = invocation.leaf_name().to_string();
command
.execute(&self.config.execution_context(), invocation)
.map_err(|source| CMDKitError::StrategyExecution {
command: command_name,
source,
})
}
fn render_help(&self, caller: &str) -> String {
let registered_commands = self.get_all();
self.config
.help_renderer
.render(caller, ®istered_commands[..])
}
fn try_run_from_env(&self) -> Result<(), CMDKitError> {
let argv = std::env::args().collect::<Vec<String>>();
self.try_run_from_args(&argv)
}
fn attach_help(&self, error: CMDKitError, caller: &str) -> CMDKitError {
match error {
CMDKitError::MissingCommand { .. } => CMDKitError::MissingCommand {
help: self.render_help(caller),
},
CMDKitError::UnknownCommand { command, .. } => CMDKitError::UnknownCommand {
command,
help: self.render_help(caller),
},
other => other,
}
}
fn resolve_registered_command<'a>(
&self,
commands: &'a [&'a Command],
name: &str,
) -> Option<&'a Command> {
commands.iter().find_map(|command| {
((command.metadata.name == name)
|| command.metadata.aliases.iter().any(|alias| alias == name))
.then_some(*command)
})
}
}
pub struct CMDKitBuilder {
config: CoreConfig,
registry: CommandCatalogue,
}
impl CMDKitBuilder {
pub fn with_config(mut self, config: CoreConfig) -> Self {
self.config = config;
self
}
pub fn register(self, command: Command) -> Self {
self.try_register(command)
.expect("command registration should succeed")
}
pub fn try_register(mut self, command: Command) -> Result<Self, CMDKitError> {
self.registry
.register(command)
.map_err(|message| CMDKitError::Registration { message })?;
Ok(self)
}
fn new() -> CMDKitBuilder {
Self {
config: Default::default(),
registry: Default::default(),
}
}
pub fn with_commands(self, commands: &[Command]) -> Self {
self.try_with_commands(commands)
.expect("bulk command registration should succeed")
}
pub fn try_with_commands(mut self, commands: &[Command]) -> Result<Self, CMDKitError> {
for cmd in commands {
self.registry
.register(cmd.clone())
.map_err(|message| CMDKitError::Registration { message })?;
}
Ok(self)
}
pub fn with_argument_interpreter<I>(mut self, interpreter: I) -> Self
where
I: ArgumentInterpreter + 'static,
{
self.config.argument_interpreter = Arc::new(interpreter);
self
}
pub fn with_logger<L>(mut self, log_sink: L) -> Self
where
L: logging::LogSink + 'static,
{
self.config.logger = Arc::new(log_sink);
self
}
pub fn build(&self) -> CMDKit {
CMDKit {
registry: self.registry.clone(),
config: self.config.clone(),
}
}
pub fn as_master_executor(&self, config: CoreConfig, worker_count: usize) -> CMDKitMaster {
CMDKitMaster::new(self.registry.clone(), config, worker_count)
}
}
type WorkerResult = Result<(), CMDKitError>;
pub type ExecutionHandle = oneshot::Receiver<WorkerResult>;
struct QueuedInvocation {
args: Vec<String>,
completion_tx: oneshot::Sender<WorkerResult>,
}
pub struct CMDKitMaster {
submit_tx: mpsc::Sender<QueuedInvocation>,
config: CoreConfig,
}
impl CMDKitMaster {
fn new(registry: CommandCatalogue, config: CoreConfig, worker_count: usize) -> Self {
let (submit_tx, submit_rx) = mpsc::channel::<QueuedInvocation>();
let shared_rx = Arc::new(Mutex::new(submit_rx));
for _ in 0..worker_count.max(1) {
let rx = Arc::clone(&shared_rx);
let worker_registry = registry.clone();
let worker_config = config.clone();
thread::spawn(move || {
let cmdkit = CMDKit {
registry: worker_registry,
config: worker_config,
};
loop {
let next_job = {
let guard = rx
.lock()
.expect("executor queue mutex should not be poisoned");
guard.recv()
};
let Ok(job) = next_job else {
break;
};
let result = cmdkit.try_run_from_args(&job.args);
let _ = job.completion_tx.send(result);
}
});
}
Self { submit_tx, config }
}
fn resolved_handle(result: WorkerResult) -> ExecutionHandle {
let (tx, rx) = oneshot::channel();
let _ = tx.send(result);
rx
}
fn dispatch(&self, args: &[String]) -> Result<ExecutionHandle, CMDKitError> {
let (completion_tx, completion_rx) = oneshot::channel();
self.submit_tx
.send(QueuedInvocation {
args: args.to_vec(),
completion_tx,
})
.map_err(|_| CMDKitError::ExecutorUnavailable {
message: "worker queue is closed".to_string(),
})?;
Ok(completion_rx)
}
pub fn try_run_from_args(&self, args: &[String]) -> Result<ExecutionHandle, CMDKitError> {
let binary = args
.iter()
.next()
.cloned()
.unwrap_or_else(|| "cli".to_string());
if args.get(1).is_some_and(|arg| arg == "help") {
println!("{}", self.config.help_renderer.render(&binary, &[]));
return Ok(Self::resolved_handle(Ok(())));
}
self.dispatch(args)
}
pub fn try_run_from_env(&self) -> Result<ExecutionHandle, CMDKitError> {
let argv = std::env::args().collect::<Vec<String>>();
self.try_run_from_args(&argv)
}
}
#[cfg(test)]
mod tests;