clipanion-core 0.8.1

A simple but powerful CLI framework
Documentation
use crate::{machine::MachineContext, runner::{OptionValue, Positional, RunState, Token}, shared::{Arg, HELP_COMMAND_INDEX}, CommandError, Error};

#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Default)]
pub enum Reducer {
    #[default]
    None,
    InhibateOptions,
    PushBatch,
    PushBound,
    PushOptional,
    PushFalse(String),
    PushNone(String),
    PushPath,
    PushPositional,
    PushRest,
    AppendStringValue,
    InitializeState(usize, Vec<String>),
    PushTrue(String),
    SetError(CommandError),
    SetOptionArityError,
    AcceptState,
    ResetStringValue,
    UseHelp,
}


#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Check {
    Always,
    IsBatchOption,
    IsBoundOption,
    IsExactOption(String),
    IsNegatedOption(String),
    IsHelp,
    IsNotOptionLike,
    IsOptionLike,
    IsUnsupportedOption,
    IsInvalidOption,
}

pub fn apply_reducer(reducer: &Reducer, context: &MachineContext, state: &RunState, arg: &Arg, segment_index: usize) -> RunState {
    match reducer {
        Reducer::InhibateOptions => {
            let mut state = state.clone();
            state.ignore_options = true;
            state
        }

        Reducer::PushBatch => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();

            for t in 1..arg.len() {
                let name = format!("-{}", &arg[t..t + 1]);

                let preferred_name
                    = context.preferred_names.get(&name).unwrap();

                let slice = match t == 1 {
                    true => (0, 2),
                    false => (t, t + 1),
                }; 

                state.options.push((
                    preferred_name.clone(),
                    OptionValue::Bool(true),
                ));

                state.tokens.push(Token::Option {
                    segment_index,
                    slice: Some(slice),
                    option: preferred_name.clone(),
                });
            }

            state
        }

        Reducer::PushBound => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();

            let (name, value) = arg.split_at(arg.find('=').unwrap());

            state.options.push((
                name.to_string(),
                OptionValue::String(value[1..].to_string()),
            ));

            state.tokens.push(Token::Option {
                segment_index,
                slice: Some((0, name.len())),
                option: name.to_string(),
            });

            state.tokens.push(Token::Assign {
                segment_index,
                slice: (name.len(), name.len() + 1),
            });

            state.tokens.push(Token::Value {
                segment_index,
                slice: Some((name.len() + 1, value.len())),
            });

            state
        }

        Reducer::PushOptional => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();
            state.positionals.push(Positional::Optional(arg.to_string()));
            state
        }

        Reducer::PushFalse(name) => {
            let mut state = state.clone();

            state.options.push((
                name.to_string(),
                OptionValue::Bool(false),
            ));

            state.tokens.push(Token::Option {
                segment_index,
                slice: None,
                option: name.to_string(),
            });

            state
        }

        Reducer::PushNone(name) => {
            let mut state = state.clone();

            state.options.push((
                name.to_string(),
                OptionValue::None,
            ));

            state.tokens.push(Token::Option {
                segment_index,
                slice: None,
                option: name.to_string(),
            });

            state
        }

        Reducer::PushPath => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();
            state.path.push(arg.to_string());
            state
        }

        Reducer::PushPositional => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();
            state.positionals.push(Positional::Required(arg.to_string()));
            state
        }

        Reducer::PushRest => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();
            state.positionals.push(Positional::Rest(arg.to_string()));
            state
        }

        Reducer::AppendStringValue => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();

            let last_option = state.options.last_mut().unwrap();

            match last_option.1 {
                OptionValue::None => {
                    last_option.1 = OptionValue::Array(vec![arg.to_string()]);
                }

                OptionValue::Array(ref mut values) => {
                    values.push(arg.to_string());
                }

                _ => {
                    panic!("Expected None or Array");
                }
            }

            state.tokens.push(Token::Value {
                segment_index,
                slice: None,
            });

            state
        }

        Reducer::PushTrue(name) => {
            let mut state = state.clone();

            state.options.push((
                name.to_string(),
                OptionValue::Bool(true),
            ));

            state.tokens.push(Token::Option {
                segment_index,
                slice: None,
                option: name.to_string(),
            });

            state
        }

        Reducer::SetError(error) => {
            let mut state = state.clone();
            state.error_message = Some(Error::CommandError(state.candidate_index, error.clone()));
            state
        }

        Reducer::SetOptionArityError => {
            let mut state = state.clone();
            state.error_message = Some(Error::CommandError(state.candidate_index, CommandError::MissingOptionArguments));
            state
        }

        Reducer::AcceptState => {
            let mut state = state.clone();
            state.selected_index = Some(state.candidate_index);
            state
        }

        Reducer::ResetStringValue => {
            let arg = arg.unwrap_user();
            let mut state = state.clone();

            let last_option = state.options.last_mut().unwrap();
            last_option.1 = OptionValue::String(arg.to_string());

            state.tokens.push(Token::Value {
                segment_index,
                slice: None,
            });

            state
        }

        Reducer::UseHelp => {
            let mut state = state.clone();
            state.selected_index = Some(HELP_COMMAND_INDEX);
            state.options = vec![("--command-index".to_string(), OptionValue::String(format!("{}", state.candidate_index)))];
            state.positionals.clear();
            state
        }

        Reducer::InitializeState(candidate_index, required_options) => {
            let mut state = state.clone();
            state.candidate_index = *candidate_index;
            state.required_options = required_options.clone();
            state
        }

        Reducer::None => {
            state.clone()
        }
    }
}

fn is_valid_option(option: &str) -> bool {
    if option.starts_with("--") {
        option.chars().skip(2).all(|c| c.is_alphanumeric() || c == '-')
    } else if option.starts_with("-") {
        option.chars().skip(1).all(|c| c.is_alphabetic())
    } else {
        false
    }
}

pub fn apply_check(check: &Check, context: &MachineContext, state: &RunState, arg: &Arg, _segment_index: usize) -> bool {
    match check {
        Check::Always => true,

        Check::IsBatchOption => {
            let arg = arg.unwrap_user();

            !state.ignore_options && arg.starts_with('-') && arg.len() > 2 && arg.chars().skip(1).all(|c| {
                c.is_ascii_alphanumeric() && context.preferred_names.contains_key(&format!("-{}", &c.to_string()))
            })
        }

        Check::IsBoundOption => {
            let arg = arg.unwrap_user();

            !state.ignore_options && arg.find('=').map_or(false, |i| {
                context.preferred_names.get(arg.split_at(i).0).map_or(false, |preferred_name| {
                    context.valid_bindings.contains(preferred_name)
                })
            })
        }

        Check::IsExactOption(needle) => {
            let arg = arg.unwrap_user();

            !state.ignore_options && arg == needle
        }

        Check::IsHelp => {
            let arg = arg.unwrap_user();

            !state.ignore_options && (arg == "--help" || arg == "-h" || arg.starts_with("--help="))
        }

        Check::IsNegatedOption(needle) => {
            let arg = arg.unwrap_user();

            !state.ignore_options && arg.starts_with("--no-") && arg[5..] == needle[2..]
        }

        Check::IsNotOptionLike => {
            let arg = arg.unwrap_user();

            state.ignore_options || arg == "-" || !arg.starts_with('-')
        }

        Check::IsOptionLike => {
            let arg = arg.unwrap_user();

            !state.ignore_options && arg != "-" && arg.starts_with('-')
        }

        Check::IsUnsupportedOption => {
            let arg = arg.unwrap_user();

            !state.ignore_options && arg.starts_with("-") && is_valid_option(arg) && !context.preferred_names.contains_key(arg)
        }

        Check::IsInvalidOption => {
            let arg = arg.unwrap_user();

            !state.ignore_options && arg.starts_with("-") && !is_valid_option(arg)
        }
    }
}