use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Arg {
Boolean,
Double,
Integer,
String,
Entity,
GameProfile,
BlockPos,
ColumnPos,
Vec3,
Vec2,
BlockState,
ItemStack,
Message,
Component,
ResourceLocation,
Uuid,
Rotation,
Options(Vec<std::string::String>),
Player,
}
#[derive(Debug, Clone)]
pub enum Validation {
Auto,
Custom(std::string::String),
Disabled,
}
#[derive(Debug, Clone)]
pub struct CommandArg {
pub name: std::string::String,
pub arg_type: Arg,
pub validation: Validation,
pub required: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ArgValue {
String(std::string::String),
Integer(i64),
Double(f64),
Boolean(bool),
Vec3(f64, f64, f64),
BlockPos(i32, i32, i32),
}
#[derive(Debug)]
pub struct CommandArgs {
values: HashMap<std::string::String, ArgValue>,
raw: std::string::String,
}
impl CommandArgs {
pub fn new(raw: std::string::String) -> Self {
Self {
values: HashMap::new(),
raw,
}
}
pub fn insert(&mut self, name: std::string::String, value: ArgValue) {
self.values.insert(name, value);
}
pub fn get_string(&self, name: &str) -> Option<&str> {
match self.values.get(name) {
Some(ArgValue::String(s)) => Some(s),
_ => None,
}
}
pub fn get_integer(&self, name: &str) -> Option<i64> {
match self.values.get(name) {
Some(ArgValue::Integer(v)) => Some(*v),
_ => None,
}
}
pub fn get_double(&self, name: &str) -> Option<f64> {
match self.values.get(name) {
Some(ArgValue::Double(v)) => Some(*v),
_ => None,
}
}
pub fn get_bool(&self, name: &str) -> Option<bool> {
match self.values.get(name) {
Some(ArgValue::Boolean(v)) => Some(*v),
_ => None,
}
}
pub fn get_vec3(&self, name: &str) -> Option<(f64, f64, f64)> {
match self.values.get(name) {
Some(ArgValue::Vec3(x, y, z)) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn get_block_pos(&self, name: &str) -> Option<(i32, i32, i32)> {
match self.values.get(name) {
Some(ArgValue::BlockPos(x, y, z)) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn raw(&self) -> &str {
&self.raw
}
}
impl Arg {
pub fn token_count(&self) -> usize {
match self {
Arg::Vec3 | Arg::BlockPos => 3,
Arg::Vec2 | Arg::ColumnPos | Arg::Rotation => 2,
Arg::Message => 0, _ => 1,
}
}
}
pub fn parse_command_args(
raw: &str,
schema: &[CommandArg],
variants: &[Vec<CommandArg>],
) -> Result<CommandArgs, std::string::String> {
if variants.is_empty() {
return parse_args(raw, schema);
}
let mut sorted: Vec<&Vec<CommandArg>> = variants.iter().collect();
sorted.sort_by(|a, b| {
let count_a: usize = a.iter().map(|arg| arg.arg_type.token_count()).sum();
let count_b: usize = b.iter().map(|arg| arg.arg_type.token_count()).sum();
count_b.cmp(&count_a)
});
let mut last_err = String::new();
for variant in sorted {
match parse_args(raw, variant) {
Ok(args) => return Ok(args),
Err(e) => last_err = e,
}
}
Err(last_err)
}
pub fn parse_args(raw: &str, schema: &[CommandArg]) -> Result<CommandArgs, std::string::String> {
let tokens: Vec<&str> = raw.split_whitespace().collect();
let mut args = CommandArgs::new(raw.to_string());
let required_count = schema.iter().filter(|a| a.required).count();
if tokens.len() < required_count {
let names: Vec<&str> = schema.iter().map(|a| a.name.as_str()).collect();
let usage = names
.iter()
.map(|n| format!("<{n}>"))
.collect::<Vec<_>>()
.join(" ");
return Err(format!("Usage: {usage}"));
}
let mut tok = 0;
for arg_def in schema {
if matches!(arg_def.arg_type, Arg::Message) {
let remainder: String = tokens[tok..].join(" ");
if remainder.is_empty() && arg_def.required {
return Err(format!("Missing required argument: {}", arg_def.name));
}
if !remainder.is_empty() {
args.insert(arg_def.name.clone(), ArgValue::String(remainder));
}
break;
}
let count = arg_def.arg_type.token_count();
if tok >= tokens.len() {
if arg_def.required {
return Err(format!("Missing required argument: {}", arg_def.name));
}
continue;
}
if count > 1 {
if tok + count > tokens.len() {
if arg_def.required {
return Err(format!(
"Not enough values for '{}' (expected {count})",
arg_def.name
));
}
continue;
}
let value = match &arg_def.arg_type {
Arg::Vec3 => {
let x = tokens[tok]
.parse::<f64>()
.map_err(|_| format!("Invalid coordinate for '{}'", arg_def.name))?;
let y = tokens[tok + 1]
.parse::<f64>()
.map_err(|_| format!("Invalid coordinate for '{}'", arg_def.name))?;
let z = tokens[tok + 2]
.parse::<f64>()
.map_err(|_| format!("Invalid coordinate for '{}'", arg_def.name))?;
ArgValue::Vec3(x, y, z)
}
Arg::BlockPos => {
let x = tokens[tok]
.parse::<i32>()
.map_err(|_| format!("Invalid block coordinate for '{}'", arg_def.name))?;
let y = tokens[tok + 1]
.parse::<i32>()
.map_err(|_| format!("Invalid block coordinate for '{}'", arg_def.name))?;
let z = tokens[tok + 2]
.parse::<i32>()
.map_err(|_| format!("Invalid block coordinate for '{}'", arg_def.name))?;
ArgValue::BlockPos(x, y, z)
}
_ => {
ArgValue::String(tokens[tok..tok + count].join(" "))
}
};
args.insert(arg_def.name.clone(), value);
tok += count;
continue;
}
let token = tokens[tok];
tok += 1;
if matches!(arg_def.validation, Validation::Disabled) {
args.insert(arg_def.name.clone(), ArgValue::String(token.to_string()));
continue;
}
match &arg_def.arg_type {
Arg::String
| Arg::Player
| Arg::Entity
| Arg::GameProfile
| Arg::BlockState
| Arg::ItemStack
| Arg::Component
| Arg::ResourceLocation
| Arg::Uuid => {
args.insert(arg_def.name.clone(), ArgValue::String(token.to_string()));
}
Arg::Integer => match token.parse::<i64>() {
Ok(v) => {
args.insert(arg_def.name.clone(), ArgValue::Integer(v));
}
Err(_) => {
return Err(match &arg_def.validation {
Validation::Custom(msg) => msg.clone(),
_ => format!("Expected an integer for '{}'", arg_def.name),
});
}
},
Arg::Double => match token.parse::<f64>() {
Ok(v) => {
args.insert(arg_def.name.clone(), ArgValue::Double(v));
}
Err(_) => {
return Err(match &arg_def.validation {
Validation::Custom(msg) => msg.clone(),
_ => format!("Expected a number for '{}'", arg_def.name),
});
}
},
Arg::Options(choices) => {
if choices.iter().any(|c| c == token) {
args.insert(arg_def.name.clone(), ArgValue::String(token.to_string()));
} else {
return Err(match &arg_def.validation {
Validation::Custom(msg) => msg.clone(),
_ => {
let opts = choices.join(", ");
format!("Invalid '{}'. Options: {opts}", arg_def.name)
}
});
}
}
Arg::Boolean => match token {
"true" => {
args.insert(arg_def.name.clone(), ArgValue::Boolean(true));
}
"false" => {
args.insert(arg_def.name.clone(), ArgValue::Boolean(false));
}
_ => {
return Err(match &arg_def.validation {
Validation::Custom(msg) => msg.clone(),
_ => format!("Expected true/false for '{}'", arg_def.name),
});
}
},
Arg::Vec3
| Arg::Vec2
| Arg::BlockPos
| Arg::ColumnPos
| Arg::Rotation
| Arg::Message => {
unreachable!()
}
}
}
Ok(args)
}
#[cfg(test)]
mod tests {
use super::*;
fn arg(name: &str, arg_type: Arg) -> CommandArg {
CommandArg {
name: name.to_string(),
arg_type,
validation: Validation::Auto,
required: true,
}
}
#[test]
fn parse_double_args() {
let schema = vec![
arg("x", Arg::Double),
arg("y", Arg::Double),
arg("z", Arg::Double),
];
let result = parse_args("10.5 64.0 -5.0", &schema).unwrap();
assert_eq!(result.get_double("x"), Some(10.5));
assert_eq!(result.get_double("y"), Some(64.0));
assert_eq!(result.get_double("z"), Some(-5.0));
}
#[test]
fn parse_integer_args() {
let schema = vec![arg("count", Arg::Integer)];
let result = parse_args("42", &schema).unwrap();
assert_eq!(result.get_integer("count"), Some(42));
}
#[test]
fn parse_string_arg() {
let schema = vec![arg("name", Arg::String)];
let result = parse_args("Steve", &schema).unwrap();
assert_eq!(result.get_string("name"), Some("Steve"));
}
#[test]
fn parse_options_valid() {
let schema = vec![arg(
"mode",
Arg::Options(vec!["survival".into(), "creative".into()]),
)];
let result = parse_args("creative", &schema).unwrap();
assert_eq!(result.get_string("mode"), Some("creative"));
}
#[test]
fn parse_options_invalid() {
let schema = vec![arg(
"mode",
Arg::Options(vec!["survival".into(), "creative".into()]),
)];
let err = parse_args("hardcore", &schema).unwrap_err();
assert!(err.contains("Invalid 'mode'"));
}
#[test]
fn parse_options_custom_error() {
let schema = vec![CommandArg {
name: "mode".into(),
arg_type: Arg::Options(vec!["survival".into(), "creative".into()]),
validation: Validation::Custom("Nope, bad mode".into()),
required: true,
}];
let err = parse_args("hardcore", &schema).unwrap_err();
assert_eq!(err, "Nope, bad mode");
}
#[test]
fn parse_double_invalid() {
let schema = vec![arg("x", Arg::Double)];
let err = parse_args("abc", &schema).unwrap_err();
assert!(err.contains("Expected a number"));
}
#[test]
fn parse_too_few_args() {
let schema = vec![
arg("x", Arg::Double),
arg("y", Arg::Double),
arg("z", Arg::Double),
];
let err = parse_args("10.5", &schema).unwrap_err();
assert!(err.contains("Usage:"));
}
#[test]
fn parse_validation_disabled() {
let schema = vec![CommandArg {
name: "value".into(),
arg_type: Arg::Double,
validation: Validation::Disabled,
required: true,
}];
let result = parse_args("abc", &schema).unwrap();
assert_eq!(result.get_string("value"), Some("abc"));
}
#[test]
fn parse_optional_arg_missing() {
let schema = vec![CommandArg {
name: "target".into(),
arg_type: Arg::String,
validation: Validation::Auto,
required: false,
}];
let result = parse_args("", &schema).unwrap();
assert_eq!(result.get_string("target"), None);
}
#[test]
fn parse_greedy_string() {
let schema = vec![arg("msg", Arg::Message)];
let result = parse_args("hello world foo", &schema).unwrap();
assert_eq!(result.get_string("msg"), Some("hello world foo"));
}
#[test]
fn parse_boolean_valid() {
let schema = vec![arg("flag", Arg::Boolean)];
let result = parse_args("true", &schema).unwrap();
assert_eq!(result.get_bool("flag"), Some(true));
let result = parse_args("false", &schema).unwrap();
assert_eq!(result.get_bool("flag"), Some(false));
}
#[test]
fn parse_boolean_invalid() {
let schema = vec![arg("flag", Arg::Boolean)];
let err = parse_args("maybe", &schema).unwrap_err();
assert!(err.contains("Expected true/false"));
}
#[test]
fn parse_player_arg() {
let schema = vec![arg("target", Arg::Player)];
let result = parse_args("Steve", &schema).unwrap();
assert_eq!(result.get_string("target"), Some("Steve"));
}
#[test]
fn parse_variants_first_match() {
let v1 = vec![arg("x", Arg::Double), arg("y", Arg::Double)];
let v2 = vec![arg("name", Arg::String)];
let result = parse_command_args("10.5 20.0", &[], &[v1, v2]).unwrap();
assert_eq!(result.get_double("x"), Some(10.5));
}
#[test]
fn parse_variants_second_match() {
let v1 = vec![arg("x", Arg::Double), arg("y", Arg::Double)];
let v2 = vec![arg("name", Arg::Player)];
let result = parse_command_args("Steve", &[], &[v1, v2]).unwrap();
assert_eq!(result.get_string("name"), Some("Steve"));
}
#[test]
fn raw_preserved() {
let schema = vec![arg("msg", Arg::String)];
let result = parse_args("hello world", &schema).unwrap();
assert_eq!(result.raw(), "hello world");
}
#[test]
fn parse_vec3_typed() {
let schema = vec![arg("pos", Arg::Vec3)];
let result = parse_args("10.5 64.0 -5.0", &schema).unwrap();
assert_eq!(result.get_vec3("pos"), Some((10.5, 64.0, -5.0)));
assert_eq!(result.get_string("pos"), None); }
#[test]
fn parse_vec3_invalid() {
let schema = vec![arg("pos", Arg::Vec3)];
let err = parse_args("10.5 abc -5.0", &schema).unwrap_err();
assert!(err.contains("Invalid coordinate"));
}
#[test]
fn parse_block_pos_typed() {
let schema = vec![arg("pos", Arg::BlockPos)];
let result = parse_args("10 64 -5", &schema).unwrap();
assert_eq!(result.get_block_pos("pos"), Some((10, 64, -5)));
}
#[test]
fn token_count_method() {
assert_eq!(Arg::Vec3.token_count(), 3);
assert_eq!(Arg::BlockPos.token_count(), 3);
assert_eq!(Arg::Vec2.token_count(), 2);
assert_eq!(Arg::Rotation.token_count(), 2);
assert_eq!(Arg::Message.token_count(), 0);
assert_eq!(Arg::String.token_count(), 1);
assert_eq!(Arg::Boolean.token_count(), 1);
}
}