~saiko/cmdparser

15641819d6b4bcbfa7d1458d5ccddbc98db5462b — 2xsaiko 6 months ago
Initial commit
7 files changed, 683 insertions(+), 0 deletions(-)

A .gitignore
A Cargo.lock
A Cargo.toml
A src/base.rs
A src/engine/builtin.rs
A src/engine/mod.rs
A src/lib.rs
A  => .gitignore +2 -0
@@ 1,2 @@
.idea/
target/
\ No newline at end of file

A  => Cargo.lock +23 -0
@@ 1,23 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "cmdparser"
version = "0.1.0"
dependencies = [
 "itertools",
]

[[package]]
name = "either"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"

[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
 "either",
]

A  => Cargo.toml +15 -0
@@ 1,15 @@
[package]
name = "cmdparser"
version = "0.1.0"
authors = ["2xsaiko <git@dblsaiko.net>"]
edition = "2018"

[profile.release]
lto = true

[features]
default = ["engine"]
engine = ["itertools"]

[dependencies]
itertools = { version = "0.9.0", optional = true }
\ No newline at end of file

A  => src/base.rs +239 -0
@@ 1,239 @@
use std::borrow::Cow;
use std::fs::read_to_string;
use std::io;
use std::mem::{replace, swap};
use std::path::Path;
use std::sync::{Arc, Mutex};

/// A command scheduler. This may be freely shared across threads to schedule commands from anywhere.
#[derive(Default, Clone)]
pub struct CommandScheduler {
    scheduled: Arc<Mutex<Vec<ExecutionState>>>,
}

impl CommandScheduler {
    /// Parse and schedule the given script source code for execution.
    pub fn exec(&self, script: &str, source: ExecSource) {
        let lines = tokenize(script);
        self.scheduled.lock().unwrap().push(ExecutionState::new(lines, source));
    }

    /// Parse and schedule the given script file for execution.
    pub fn exec_path(&self, script: impl AsRef<Path>, source: ExecSource) -> Result<(), io::Error> {
        read_to_string(&script).map(|s| self.exec(&s, source))
    }
}

/// The command dispatcher used for actually executing commands.
pub struct CommandDispatcher<U: CommandExecutor> {
    executor: U,
    scheduler: CommandScheduler,
}

impl<U: CommandExecutor> CommandDispatcher<U> {
    pub fn new(executor: U) -> Self {
        CommandDispatcher {
            executor,
            scheduler: CommandScheduler::default(),
        }
    }

    pub fn scheduler(&self) -> &CommandScheduler { &self.scheduler }

    /// Execute one step of the script (until completion or [`ExecState::Suspend`] is reached)
    ///
    /// [`ExecState::Suspend`]: enum.ExecState.html#variant.Suspend
    fn step(executor: &mut U, script: &mut ExecutionState) {
        let source = script.source;
        while !script.is_done() {
            let next = script.next().unwrap();
            if let Some(command) = next.first() {
                let args = &next[1..];
                let next_state = executor.exec(command, &args.iter().map(|s| s.as_str()).collect::<Vec<_>>(), source);
                match next_state {
                    ExecState::Continue => {}
                    ExecState::EnterSubroutine(source) => {
                        script.enter_subroutine(tokenize(&source));
                    }
                    ExecState::Suspend => {
                        break;
                    }
                    ExecState::Abort => {
                        script.ret();
                        break;
                    }
                }
            }
        }
    }

    /// Execute all pending scripts for one step (until a command returns [`ExecState::Suspend`])
    ///
    /// [`ExecState::Suspend`]: enum.ExecState.html#variant.Suspend
    pub fn resume(&mut self) {
        let mut suspended = replace(&mut *self.scheduler.scheduled.lock().unwrap(), vec![]);
        for entry in suspended.iter_mut() {
            Self::step(&mut self.executor, entry);
        }
        let mut s1 = self.scheduler.scheduled.lock().unwrap();
        swap(&mut *s1, &mut suspended);
        suspended.into_iter()
            .filter(|el| !el.is_done())
            .for_each(|el| s1.push(el));
    }

    /// Execute all pending scripts until completion.
    /// This will respect a command returning [`ExecState::Suspend`] by yielding execution for other scripts,
    /// but will eventually continue executing that script until completion.
    ///
    /// [`ExecState::Suspend`]: enum.ExecState.html#variant.Suspend
    pub fn resume_until_empty(&mut self) {
        let mut suspended = replace(&mut *self.scheduler.scheduled.lock().unwrap(), vec![]);
        while !suspended.is_empty() {
            for entry in suspended.iter_mut() {
                Self::step(&mut self.executor, entry);
            }
            suspended.retain(|el| !el.is_done());
            self.scheduler.scheduled.lock().unwrap().drain(..).for_each(|el| suspended.push(el));
        }
        let mut s1 = self.scheduler.scheduled.lock().unwrap();
        swap(&mut *s1, &mut suspended);
        suspended.into_iter()
            .filter(|el| !el.is_done())
            .for_each(|el| s1.push(el));
    }
}

pub trait CommandExecutor {
    fn exec(&mut self, command: &str, args: &[&str], source: ExecSource) -> ExecState;
}

pub struct SimpleExecutor<T: FnMut(&str, &[&str])> {
    op: T,
}

impl<T: FnMut(&str, &[&str])> SimpleExecutor<T> {
    pub fn new(op: T) -> Self {
        SimpleExecutor { op }
    }
}

impl<T: FnMut(&str, &[&str])> CommandExecutor for SimpleExecutor<T> {
    fn exec(&mut self, command: &str, args: &[&str], _source: ExecSource) -> ExecState {
        (self.op)(command, args);
        ExecState::Continue
    }
}

pub struct ExecutionState {
    source: ExecSource,
    stack: Vec<SubroutineState>,
}

impl ExecutionState {
    pub fn new(script: Vec<Vec<String>>, source: ExecSource) -> Self {
        ExecutionState {
            source,
            stack: vec![SubroutineState { line: 0, script }],
        }
    }

    pub fn enter_subroutine(&mut self, script: Vec<Vec<String>>) {
        self.stack.push(SubroutineState { line: 0, script })
    }

    #[allow(clippy::should_implement_trait)]
    pub fn next(&mut self) -> Option<Cow<[String]>> {
        let last_insn = {
            let tos = self.stack.last_mut()?;
            tos.line + 1 == tos.script.len()
        };
        if last_insn {
            let mut tos = self.stack.pop().unwrap();
            Some(Cow::Owned(tos.script.pop().unwrap()))
        } else {
            let tos = self.stack.last_mut().unwrap();
            let r = &tos.script[tos.line];
            tos.line += 1;
            Some(Cow::Borrowed(r))
        }
    }

    pub fn is_done(&self) -> bool { !self.stack.iter().any(|el| !el.script.is_empty()) }

    pub fn ret(&mut self) { self.stack.pop(); }
}

pub struct SubroutineState {
    line: usize,
    script: Vec<Vec<String>>,
}

#[derive(Debug, Clone, Copy)]
pub enum ExecSource {
    Console,
    Key,
    Event,
    Other,
}

pub enum ExecState {
    Continue,
    EnterSubroutine(String),
    Suspend,
    Abort,
}

/// Tokenize script source, removing comments (starting with `//`).
/// Returns a list of command executions (command + arguments)
fn tokenize(s: &str) -> Vec<Vec<String>> {
    let mut esc = false;
    let mut quoted = false;
    let mut commands = vec![];
    let mut current = vec![];
    let mut sb = String::new();

    fn next_token(sb: &mut String, current: &mut Vec<String>) {
        if !sb.trim().is_empty() {
            current.push((*sb).clone());
        }
        sb.clear();
    }
    ;

    fn next_command(sb: &mut String, current: &mut Vec<String>, commands: &mut Vec<Vec<String>>) {
        next_token(sb, current);
        if !current.is_empty() {
            commands.push((*current).clone());
        }
        current.clear();
    }
    ;

    for line in s.lines() {
        let get = |i| line.chars().nth(i);

        for (pos, c) in line.chars().enumerate() {
            if esc {
                sb.push(c);
                esc = false;
            } else if !quoted && c == '/' && get(pos + 1) == Some('/') {
                break;
            } else if !quoted && c == ';' {
                next_command(&mut sb, &mut current, &mut commands);
            } else if !quoted && c == ' ' {
                next_token(&mut sb, &mut current);
            } else if c == '"' {
                quoted = !quoted;
            } else if c == '\\' {
                esc = true;
            } else {
                sb.push(c);
            }
        }

        next_command(&mut sb, &mut current, &mut commands);
    }

    commands
}

A  => src/engine/builtin.rs +67 -0
@@ 1,67 @@
use std::collections::HashMap;
use std::io::Write;

use itertools::Itertools;

use crate::{command, EngineBuilder};

pub fn register_commands<T: Write>(builder: EngineBuilder<T>) -> EngineBuilder<T> {
    builder
        .with_command("list", box command(|_args, ctx, output| {
            let builtins = ["alias", "unalias", "wait"];

            enum EntryType {
                Builtin,
                Command,
                Alias,
                ConVar,
            }

            struct HelpEntry<'a> {
                name: &'a str,
                desc: Option<&'a str>,
                entry_type: EntryType,
            }

            let mut map = HashMap::new();

            builtins.iter()
                .map(|name| HelpEntry {
                    name,
                    desc: ctx.registry.help_strs.get(*name).map(|s| s.as_str()),
                    entry_type: EntryType::Builtin,
                })
                .chain(
                    ctx.registry.commands.iter()
                        .map(|(name, _)| HelpEntry {
                            name,
                            desc: ctx.registry.help_strs.get(name).map(|s| s.as_str()),
                            entry_type: EntryType::Command,
                        }))
                .chain(ctx.registry.cvars.iter()
                    .map(|(name, _)| HelpEntry {
                        name,
                        desc: ctx.registry.help_strs.get(name).map(|s| s.as_str()),
                        entry_type: EntryType::ConVar,
                    }))
                .chain(ctx.state.aliases.iter()
                    .map(|(name, _)| HelpEntry {
                        name,
                        desc: None,
                        entry_type: EntryType::Alias,
                    }))
                .for_each(|el| { map.insert(el.name, el); });

            map.into_iter().sorted_by_key(|(a, _)| *a).for_each(|(_, el)| {
                writeln!(output, "{} ({}) - {}", el.name, match el.entry_type {
                    EntryType::Command => "command",
                    EntryType::Alias => "alias",
                    EntryType::ConVar => "cvar",
                    EntryType::Builtin => "built-in",
                }, el.desc.unwrap_or("")).unwrap();
            });
        }))
        .with_help_text("list", "list available commands")
        .with_command("echo", box command(|args, _, output| writeln!(output, "{}", args.join(" ")).unwrap()))
        .with_help_text("echo", "print text to the console")
}
\ No newline at end of file

A  => src/engine/mod.rs +328 -0
@@ 1,328 @@
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::fmt::Display;
use std::io::Write;
use std::rc::Rc;
use std::str::FromStr;

use crate::{CommandDispatcher, CommandExecutor, CommandScheduler, ExecSource, ExecState};

mod builtin;

pub struct EngineBuilder<T: Write> {
    output: T,
    registry: Registry,
}

impl<T: Write> EngineBuilder<T> {
    pub fn new(output: T) -> Self {
        EngineBuilder {
            output,
            registry: Registry::default(),
        }
    }

    pub fn with_default(self) -> Self {
        builtin::register_commands(self)
    }

    pub fn with_command(mut self, name: &str, command: Box<dyn Command>) -> Self {
        if let Some(_) = self.registry.commands.insert(name.to_owned(), command) {
            panic!("Command '{}' already registered!", name);
        }
        self
    }

    pub fn with_cvar(mut self, name: &str, cvar: ConVar) -> Self {
        if let Some(_) = self.registry.cvars.insert(name.to_owned(), cvar) {
            panic!("ConVar '{}' already registered!", name);
        }
        self
    }

    pub fn with_help_text(mut self, name: &str, text: &str) -> Self {
        if let Some(_) = self.registry.help_strs.insert(name.to_owned(), text.to_owned()) {
            panic!("Help text for '{}' already registered!", name);
        }
        self
    }

    pub fn build(self) -> ConsoleEngine<T> {
        ConsoleEngine::new(self.registry, self.output)
    }
}

#[derive(Default)]
pub struct Registry {
    commands: HashMap<String, Box<dyn Command>>,
    cvars: HashMap<String, ConVar>,
    generators: Vec<Box<dyn Persistable>>,
    help_strs: HashMap<String, String>,
}

#[derive(Default)]
pub struct State {
    aliases: HashMap<String, String>,
}

pub struct Executor<T: Write> {
    registry: Rc<Registry>,
    state: RefCell<State>,
    output: T,
}

impl<T: Write> CommandExecutor for Executor<T> {
    fn exec(&mut self, command: &str, args: &[&str], _: ExecSource) -> ExecState {
        match command {
            "alias" => {
                match *args {
                    [] => {}
                    [alias, ] => {
                        match self.state.borrow().aliases.get(alias) {
                            None => {
                                writeln!(self.output, "'{}' is not an alias", alias).unwrap();
                            }
                            Some(script) => {
                                writeln!(self.output, "'{}' = '{}'", alias, script).unwrap();
                            }
                        }
                    }
                    [alias, script, ..] => {
                        self.state.borrow_mut().aliases.insert(alias.to_owned(), script.to_owned());
                    }
                }
                ExecState::Continue
            }
            "unalias" => {
                if let [alias] = *args {
                    self.state.borrow_mut().aliases.remove(alias);
                }
                ExecState::Continue
            }
            "wait" => {
                ExecState::Suspend
            }
            _ => {
                if let Some(script) = self.state.borrow().aliases.get(command) {
                    ExecState::EnterSubroutine(script.clone())
                } else if let Some(cvar) = self.registry.cvars.get(command) {
                    if args.is_empty() {
                        writeln!(self.output, "{} = {}", command, cvar.fmt_values()).unwrap();
                    } else {
                        cvar.set_values(args);
                    }
                    ExecState::Continue
                } else if let Some(cmd) = self.registry.commands.get(command) {
                    let ec = ExecutionContext {
                        registry: &self.registry,
                        state: &*self.state.borrow(),
                    };
                    cmd.exec(args, &ec, &mut self.output)
                } else {
                    writeln!(self.output, "Command not found: {}", command).unwrap();
                    ExecState::Continue
                }
            }
        }
    }
}

pub struct ExecutionContext<'a> {
    pub registry: &'a Registry,
    pub state: &'a State,
}

pub struct ConsoleEngine<T: Write> {
    registry: Rc<Registry>,
    dispatcher: CommandDispatcher<Executor<T>>,
}

impl<T: Write> ConsoleEngine<T> {
    fn new(registry: Registry, output: T) -> Self {
        let reg = Rc::new(registry);
        ConsoleEngine {
            registry: reg.clone(),
            dispatcher: CommandDispatcher::new(Executor {
                registry: reg,
                state: RefCell::new(State::default()),
                output,
            }),
        }
    }

    pub fn get_cvar(&self, name: &str) -> Option<&ConVar> { self.registry.cvars.get(name) }

    pub fn scheduler(&self) -> &CommandScheduler { self.dispatcher.scheduler() }

    pub fn dispatcher(&self) -> &CommandDispatcher<Executor<T>> { &self.dispatcher }

    pub fn dispatcher_mut(&mut self) -> &mut CommandDispatcher<Executor<T>> { &mut self.dispatcher }
}

pub trait Command {
    fn exec(&self, args: &[&str], ctx: &ExecutionContext, output: &mut dyn Write) -> ExecState;
}

pub fn command(op: impl Fn(&[&str], &ExecutionContext, &mut dyn Write)) -> impl Command {
    struct Impl<T: Fn(&[&str], &ExecutionContext, &mut dyn Write)> {
        op: T,
    }

    impl<T: Fn(&[&str], &ExecutionContext, &mut dyn Write)> Command for Impl<T> {
        fn exec(&self, args: &[&str], ctx: &ExecutionContext, output: &mut dyn Write) -> ExecState {
            (self.op)(args, ctx, output);
            ExecState::Continue
        }
    }

    Impl { op }
}

pub fn expand_command(op: impl Fn(&[&str], &ExecutionContext, &mut dyn Write) -> Option<String>) -> impl Command {
    struct Impl<T: Fn(&[&str], &ExecutionContext, &mut dyn Write) -> Option<String>> {
        op: T,
    }

    impl<T: Fn(&[&str], &ExecutionContext, &mut dyn Write) -> Option<String>> Command for Impl<T> {
        fn exec(&self, args: &[&str], ctx: &ExecutionContext, output: &mut dyn Write) -> ExecState {
            match (self.op)(args, ctx, output) {
                None => ExecState::Continue,
                Some(s) => ExecState::EnterSubroutine(s),
            }
        }
    }

    Impl { op }
}

pub trait Persistable {
    fn file(&self) -> &str;

    fn write(&self, out: &mut dyn Write);
}

#[derive(Debug, Clone)]
pub enum ConVar {
    String {
        default: String,
        value: RefCell<String>,
    },
    Number {
        default: f64,
        value: Cell<f64>,
        min: Option<f64>,
        max: Option<f64>,
    },
    Integer {
        default: i64,
        value: Cell<i64>,
        min: Option<i64>,
        max: Option<i64>,
    },
}

impl ConVar {
    pub fn of_string(default: &str) -> Self {
        ConVar::String {
            default: default.to_owned(),
            value: RefCell::new(default.to_owned()),
        }
    }

    pub fn of_num(default: f64, min: Option<f64>, max: Option<f64>) -> Self {
        ConVar::Number {
            default,
            value: Cell::new(default),
            min,
            max,
        }
    }

    pub fn of_int(default: i64, min: Option<i64>, max: Option<i64>) -> Self {
        ConVar::Integer {
            default,
            value: Cell::new(default),
            min,
            max,
        }
    }

    pub fn of_bool(default: bool) -> Self {
        ConVar::of_int(if default { 1 } else { 0 }, Some(0), Some(1))
    }

    pub fn fmt_values(&self) -> String {
        fn fmt_num<T: Display>(def: &T, value: &T, min: &Option<T>, max: &Option<T>) -> String {
            let mut s = format!("{} (default {}", value, def);
            if let Some(min) = min { s.push_str(&format!(", min {}", min)); }
            if let Some(max) = max { s.push_str(&format!(", max {}", max)); }
            s.push(')');
            s
        }

        match self {
            ConVar::String { default, value } => format!("'{}' (default '{}')", value.borrow(), default),
            ConVar::Number { default, value, min, max } => fmt_num(default, &value.get(), min, max),
            ConVar::Integer { default, value, min, max } => fmt_num(default, &value.get(), min, max),
        }
    }

    pub fn set_values(&self, args: &[&str]) {
        fn parse_num<T: FromStr + Copy>(s: &str, value: &Cell<T>, min: Option<impl Fn(T) -> T>, max: Option<impl Fn(T) -> T>) {
            let val = s.parse::<T>();
            if let Ok(mut val) = val {
                if let Some(min) = min { val = min(val) }
                if let Some(max) = max { val = max(val) }
                value.set(val);
            }
        }

        if let Some(&s) = args.get(0) {
            match self {
                ConVar::String { value, .. } => {
                    value.replace(s.to_owned());
                }
                ConVar::Number { value, min, max, .. } => {
                    parse_num(s, value,
                              min.map(|min| move |v| min.max(v)),
                              max.map(|max| move |v| max.min(v)));
                }
                ConVar::Integer { value, min, max, .. } => {
                    parse_num(s, value,
                              min.map(|min| move |v| min.max(v)),
                              max.map(|max| move |v| max.min(v)));
                }
            }
        }
    }

    pub fn get_str_value(&self) -> String {
        match self {
            ConVar::String { value, .. } => value.borrow().to_owned(),
            ConVar::Number { value, .. } => format!("{}", value.get()),
            ConVar::Integer { value, .. } => format!("{}", value.get()),
        }
    }

    pub fn get_int_value(&self) -> Option<i64> {
        match self {
            ConVar::Integer { value, .. } => Some(value.get()),
            _ => None
        }
    }

    pub fn get_float_value(&self) -> Option<f64> {
        match self {
            ConVar::Number { value, .. } => Some(value.get()),
            _ => None
        }
    }

    pub fn get_bool_value(&self) -> bool {
        match self {
            ConVar::Number { value, .. } => value.get() != 0.0,
            ConVar::Integer { value, .. } => value.get() != 0,
            _ => false,
        }
    }
}

A  => src/lib.rs +9 -0
@@ 1,9 @@
#![feature(box_syntax)]

pub use base::*;
#[cfg(engine)]
pub use engine::*;

mod base;
#[cfg(engine)]
mod engine;
\ No newline at end of file