use std::{collections::HashMap, fmt};
use reovim_driver_command_types::{ArgKind, ArgSpec, ArgValue};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCmdline {
pub name: String,
pub bang: bool,
pub args: Vec<String>,
pub raw_args: String,
}
#[must_use]
pub fn parse_cmdline(input: &str) -> Option<ParsedCmdline> {
let input = input.trim();
if input.is_empty() {
return None;
}
let name_end = input
.find(|c: char| !c.is_ascii_alphabetic())
.unwrap_or(input.len());
let cmd_part = &input[..name_end];
let rest = &input[name_end..];
let (bang, args_part) = rest
.strip_prefix('!')
.map_or_else(|| (false, rest.trim_start()), |after| (true, after.trim_start()));
let name = cmd_part;
let args = if args_part.is_empty() {
vec![]
} else {
args_part.split_whitespace().map(String::from).collect()
};
Some(ParsedCmdline {
name: name.to_string(),
bang,
args,
raw_args: args_part.to_string(),
})
}
#[must_use]
pub fn tokenize_args(input: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut chars = input.chars().peekable();
let mut has_content = false;
while let Some(&ch) = chars.peek() {
match ch {
' ' | '\t' => {
if has_content {
tokens.push(std::mem::take(&mut current));
has_content = false;
}
chars.next();
}
'"' | '\'' => {
has_content = true;
let quote = ch;
chars.next(); loop {
match chars.next() {
Some(c) if c == quote => break,
Some('\\') if quote == '"' => {
if let Some(escaped) = chars.next() {
current.push(escaped);
}
}
Some(c) => current.push(c),
None => break, }
}
}
'\\' => {
has_content = true;
chars.next(); if let Some(escaped) = chars.next() {
current.push(escaped);
}
}
_ => {
has_content = true;
current.push(ch);
chars.next();
}
}
}
if has_content {
tokens.push(current);
}
tokens
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgError {
MissingRequired {
name: &'static str,
kind: ArgKind,
},
TooManyArgs {
expected: usize,
got: usize,
},
InvalidValue {
name: &'static str,
kind: ArgKind,
value: String,
},
}
impl fmt::Display for ArgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingRequired { name, kind } => {
write!(f, "E471: Missing required argument: {name} ({kind:?})")
}
Self::TooManyArgs { expected, got } => {
write!(f, "E488: Too many arguments (expected {expected}, got {got})")
}
Self::InvalidValue { name, kind, value } => {
write!(f, "E474: Invalid value for {name} ({kind:?}): \"{value}\"")
}
}
}
}
pub fn bind_args(
specs: &[ArgSpec],
raw_args: &str,
bang: bool,
) -> Result<HashMap<String, ArgValue>, ArgError> {
let tokens = tokenize_args(raw_args);
let mut result = HashMap::new();
let mut token_idx = 0;
let positional_count = specs.iter().filter(|s| s.kind != ArgKind::Bang).count();
let mut consumed_rest = false;
for spec in specs {
match spec.kind {
ArgKind::Bang => {
if bang {
result.insert(spec.name.to_string(), ArgValue::Bang(true));
}
}
ArgKind::Rest => {
let remaining = remaining_raw(raw_args, token_idx);
if remaining.is_empty() {
if spec.required {
return Err(ArgError::MissingRequired {
name: spec.name,
kind: spec.kind,
});
}
} else {
result.insert(spec.name.to_string(), ArgValue::String(remaining));
token_idx = tokens.len(); }
consumed_rest = true;
}
_ => {
if token_idx >= tokens.len() {
if spec.required {
return Err(ArgError::MissingRequired {
name: spec.name,
kind: spec.kind,
});
}
continue;
}
let token = &tokens[token_idx];
token_idx += 1;
let value = parse_token(spec.name, spec.kind, token)?;
result.insert(spec.name.to_string(), value);
}
}
}
if !consumed_rest && token_idx < tokens.len() {
return Err(ArgError::TooManyArgs {
expected: positional_count,
got: positional_count + (tokens.len() - token_idx),
});
}
Ok(result)
}
fn parse_token(name: &'static str, kind: ArgKind, token: &str) -> Result<ArgValue, ArgError> {
match kind {
ArgKind::FilePath => Ok(ArgValue::FilePath(token.to_string())),
ArgKind::String => Ok(ArgValue::String(token.to_string())),
ArgKind::Count => {
token
.parse::<usize>()
.map(ArgValue::Count)
.map_err(|_| ArgError::InvalidValue {
name,
kind,
value: token.to_string(),
})
}
ArgKind::Bool => match token {
"true" => Ok(ArgValue::Bool(true)),
"false" => Ok(ArgValue::Bool(false)),
_ => Err(ArgError::InvalidValue {
name,
kind,
value: token.to_string(),
}),
},
ArgKind::Char => {
let mut chars = token.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => Ok(ArgValue::Char(c)),
_ => Err(ArgError::InvalidValue {
name,
kind,
value: token.to_string(),
}),
}
}
ArgKind::Register => {
let mut chars = token.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => Ok(ArgValue::Register(c)),
_ => Err(ArgError::InvalidValue {
name,
kind,
value: token.to_string(),
}),
}
}
ArgKind::Bang | ArgKind::Rest | ArgKind::Motion | ArgKind::Range | ArgKind::BufferId => {
Err(ArgError::InvalidValue {
name,
kind,
value: token.to_string(),
})
}
}
}
fn remaining_raw(raw_args: &str, consumed: usize) -> String {
if consumed == 0 {
return raw_args.trim().to_string();
}
let mut pos = 0;
let bytes = raw_args.as_bytes();
for _ in 0..consumed {
while pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
pos += 1;
}
if pos >= bytes.len() {
return String::new();
}
match bytes[pos] {
b'"' | b'\'' => {
let quote = bytes[pos];
pos += 1;
while pos < bytes.len() && bytes[pos] != quote {
if bytes[pos] == b'\\' && quote == b'"' {
pos += 1; }
pos += 1;
}
if pos < bytes.len() {
pos += 1; }
}
_ => {
while pos < bytes.len() && bytes[pos] != b' ' && bytes[pos] != b'\t' {
if bytes[pos] == b'\\' {
pos += 1; }
pos += 1;
}
}
}
}
raw_args[pos..].trim().to_string()
}
#[cfg(test)]
#[path = "parse_tests.rs"]
mod tests;