use std::fmt;
#[derive(Debug, Clone)]
pub enum Param {
Numeric(f64),
Bool(bool),
String(String),
}
#[derive(Debug)]
pub struct Command {
pub index: usize,
pub params: Vec<Param>,
pub suffixes: Vec<u32>,
}
pub type Handler = fn(&Command);
pub struct CommandSet {
entries: Vec<Entry>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ParamKind {
Num,
Bool,
Str,
}
#[derive(Debug, Clone)]
enum Segment {
Keyword { short: String, long: String },
NumericSuffix,
}
struct Entry {
segments: Vec<Segment>,
optional_groups: Vec<OptGroup>,
is_query: bool,
param: Option<ParamKind>,
handler: Handler,
}
#[derive(Debug, Clone)]
struct OptGroup {
start: usize,
end: usize, }
#[derive(Debug)]
pub struct ParseError(String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ParseError {}
fn short_long(token: &str) -> (String, String) {
let long = token.to_ascii_uppercase();
let short: String = token
.chars()
.take_while(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || *c == '+' || *c == '-')
.collect::<String>()
.to_ascii_uppercase();
(short, long)
}
fn parse_keyword_segments(chars: &[char], pos: &mut usize) -> Vec<Segment> {
if chars[*pos] == '#' {
*pos += 1;
return vec![Segment::NumericSuffix];
}
let start = *pos;
while *pos < chars.len() && !matches!(chars[*pos], ':' | '[' | ']' | '#') {
*pos += 1;
}
let token: String = chars[start..*pos].iter().collect();
let (short, long) = short_long(&token);
let kw = Segment::Keyword {
short: short.to_ascii_uppercase(),
long: long.to_ascii_uppercase(),
};
if *pos < chars.len() && chars[*pos] == '#' {
*pos += 1;
vec![kw, Segment::NumericSuffix]
} else {
vec![kw]
}
}
fn compile(
pattern: &str,
) -> Result<(Vec<Segment>, Vec<OptGroup>, bool, Option<ParamKind>), ParseError> {
let (kw_part, param) = if pattern.ends_with(" num") {
(&pattern[..pattern.len() - 4], Some(ParamKind::Num))
} else if pattern.ends_with(" bool") {
(&pattern[..pattern.len() - 5], Some(ParamKind::Bool))
} else if pattern.ends_with(" str") {
(&pattern[..pattern.len() - 4], Some(ParamKind::Str))
} else {
(pattern, None)
};
let is_query = kw_part.ends_with('?');
let kw_part = if is_query {
&kw_part[..kw_part.len() - 1]
} else {
kw_part
};
let mut segments: Vec<Segment> = Vec::new();
let mut opt_groups: Vec<OptGroup> = Vec::new();
let chars: Vec<char> = kw_part.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == ':' {
i += 1;
continue;
}
if chars[i] == '[' {
let group_start = segments.len();
i += 1;
if i < chars.len() && chars[i] == ':' {
i += 1;
}
while i < chars.len() && chars[i] != ']' {
if chars[i] == ':' {
i += 1;
continue;
}
let segs = parse_keyword_segments(&chars, &mut i);
segments.extend(segs);
}
if i < chars.len() && chars[i] == ']' {
i += 1;
}
opt_groups.push(OptGroup {
start: group_start,
end: segments.len(),
});
} else {
let segs = parse_keyword_segments(&chars, &mut i);
segments.extend(segs);
}
}
Ok((segments, opt_groups, is_query, param))
}
fn keyword_matches(short: &str, long: &str, input: &str) -> bool {
let ilen = input.len();
if ilen < short.len() || ilen > long.len() {
return false;
}
long[..ilen] == *input
}
fn tokenise_command(input: &str) -> (Vec<String>, Option<String>) {
let input = input.trim();
if input.is_empty() {
return (vec![], None);
}
let bytes = input.as_bytes();
let mut kw_end = input.len();
for (j, &b) in bytes.iter().enumerate() {
if b == b'\'' || b == b'"' {
kw_end = j;
break;
}
if b == b' ' || b == b'\t' {
kw_end = j;
break;
}
}
let kw_str = &input[..kw_end];
let param_str = input[kw_end..].trim();
let param = if param_str.is_empty() {
None
} else {
Some(param_str.to_string())
};
let tokens: Vec<String> = if kw_str.starts_with('*') {
vec![kw_str.to_ascii_uppercase()]
} else {
kw_str
.split(':')
.filter(|s| !s.is_empty())
.map(|s| s.to_ascii_uppercase())
.collect()
};
(tokens, param)
}
fn extract_suffix(token: &str, short: &str, long: &str) -> Option<(String, u32)> {
for kw in &[long, short] {
if token.len() >= kw.len() && token[..kw.len()] == **kw {
let rest = &token[kw.len()..];
if rest.is_empty() {
return Some((kw.to_string(), 1));
}
if let Ok(n) = rest.parse::<u32>() {
return Some((kw.to_string(), n));
}
}
}
None
}
fn try_match(
entry: &Entry,
tokens: &[String],
input_is_query: bool,
) -> Option<Vec<u32>> {
if input_is_query != entry.is_query {
return None;
}
let n_opt = entry.optional_groups.len();
let combos = 1u32 << n_opt;
for combo in 0..combos {
let mut active_segments: Vec<&Segment> = Vec::new();
for (idx, seg) in entry.segments.iter().enumerate() {
let mut skipped = false;
for (g, grp) in entry.optional_groups.iter().enumerate() {
if idx >= grp.start && idx < grp.end && (combo >> g) & 1 == 0 {
skipped = true;
break;
}
}
if !skipped {
active_segments.push(seg);
}
}
if let Some(suffixes) = match_segments(&active_segments, tokens) {
return Some(suffixes);
}
}
None
}
fn match_segments(segments: &[&Segment], tokens: &[String]) -> Option<Vec<u32>> {
let mut suffixes = Vec::new();
let mut ti = 0; let mut si = 0;
while si < segments.len() {
match &segments[si] {
Segment::Keyword { short, long } => {
if ti >= tokens.len() {
return None;
}
let token = &tokens[ti];
let next_is_suffix =
si + 1 < segments.len() && matches!(segments[si + 1], Segment::NumericSuffix);
if next_is_suffix {
let (_, suf) = extract_suffix(token, short, long)?;
suffixes.push(suf);
si += 2; ti += 1;
} else {
if !keyword_matches(short, long, token) {
return None;
}
si += 1;
ti += 1;
}
}
Segment::NumericSuffix => {
if ti >= tokens.len() {
return None;
}
if let Ok(n) = tokens[ti].parse::<u32>() {
suffixes.push(n);
ti += 1;
si += 1;
} else {
return None;
}
}
}
}
if ti == tokens.len() {
Some(suffixes)
} else {
None
}
}
fn strip_unit_suffix(raw: &str) -> &str {
let bytes = raw.as_bytes();
let mut end = bytes.len();
while end > 0 && bytes[end - 1].is_ascii_alphabetic() {
end -= 1;
}
if end == bytes.len() {
return raw; }
if end == 0 {
return raw; }
let suffix = &raw[end..];
if (suffix.starts_with('e') || suffix.starts_with('E'))
&& bytes[end - 1].is_ascii_digit()
&& suffix[1..].chars().all(|c| c.is_ascii_digit() || c == '+' || c == '-')
{
return raw;
}
raw[..end].trim_end()
}
fn parse_param(kind: ParamKind, raw: &str) -> Result<Param, ParseError> {
let raw = raw.trim();
match kind {
ParamKind::Num => {
let num_str = strip_unit_suffix(raw);
if num_str.starts_with("#H") || num_str.starts_with("#h") {
let val =
u64::from_str_radix(&num_str[2..], 16).map_err(|e| ParseError(e.to_string()))?;
Ok(Param::Numeric(val as f64))
} else if num_str.starts_with("#Q") || num_str.starts_with("#q") {
let val =
u64::from_str_radix(&num_str[2..], 8).map_err(|e| ParseError(e.to_string()))?;
Ok(Param::Numeric(val as f64))
} else if num_str.starts_with("#B") || num_str.starts_with("#b") {
let val =
u64::from_str_radix(&num_str[2..], 2).map_err(|e| ParseError(e.to_string()))?;
Ok(Param::Numeric(val as f64))
} else {
let val: f64 = num_str
.parse()
.map_err(|e: std::num::ParseFloatError| ParseError(e.to_string()))?;
Ok(Param::Numeric(val))
}
}
ParamKind::Bool => {
let upper = raw.to_ascii_uppercase();
match upper.as_str() {
"ON" | "1" => Ok(Param::Bool(true)),
"OFF" | "0" => Ok(Param::Bool(false)),
_ => Err(ParseError(format!("invalid boolean: {raw}"))),
}
}
ParamKind::Str => {
if (raw.starts_with('"') && raw.ends_with('"'))
|| (raw.starts_with('\'') && raw.ends_with('\''))
{
Ok(Param::String(raw[1..raw.len() - 1].to_string()))
} else {
Ok(Param::String(raw.to_string()))
}
}
}
}
impl CommandSet {
pub fn from_table(table: &[(&str, Handler)]) -> Result<Self, ParseError> {
let mut entries = Vec::with_capacity(table.len());
for (pattern, handler) in table {
let (segments, optional_groups, is_query, param) = compile(pattern)?;
entries.push(Entry {
segments,
optional_groups,
is_query,
param,
handler: *handler,
});
}
Ok(CommandSet { entries })
}
pub fn parse(&self, line: &str) -> Result<Vec<Command>, ParseError> {
let line = line.trim();
if line.is_empty() {
return Ok(vec![]);
}
let raw_cmds = split_commands(line);
let mut result = Vec::new();
for raw in &raw_cmds {
let raw = raw.trim();
if raw.is_empty() {
continue;
}
let cmd = self.parse_single(raw)?;
result.push(cmd);
}
Ok(result)
}
fn parse_single(&self, input: &str) -> Result<Command, ParseError> {
let (tokens, param_str) = tokenise_command(input);
if tokens.is_empty() {
return Err(ParseError("empty command".into()));
}
let mut tokens = tokens;
let mut is_query = false;
if let Some(last) = tokens.last_mut() {
if last.ends_with('?') {
is_query = true;
last.truncate(last.len() - 1);
if last.is_empty() {
tokens.pop();
}
}
}
for (idx, entry) in self.entries.iter().enumerate() {
if let Some(suffixes) = try_match(entry, &tokens, is_query) {
let params = if let Some(kind) = entry.param {
let raw = param_str.as_deref().unwrap_or("");
if raw.is_empty() {
return Err(ParseError(format!(
"command expects a parameter but none given"
)));
}
vec![parse_param(kind, raw)?]
} else {
vec![]
};
return Ok(Command {
index: idx,
params,
suffixes,
});
}
}
Err(ParseError(format!("unrecognised command: {input}")))
}
pub fn dispatch(&self, cmd: &Command) {
(self.entries[cmd.index].handler)(cmd);
}
}
fn split_commands(line: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut quote_char = ' ';
for ch in line.chars() {
if in_quotes {
current.push(ch);
if ch == quote_char {
in_quotes = false;
}
} else if ch == '\'' || ch == '"' {
in_quotes = true;
quote_char = ch;
current.push(ch);
} else if ch == ';' {
parts.push(std::mem::take(&mut current));
} else {
current.push(ch);
}
}
if !current.is_empty() {
parts.push(current);
}
parts
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy(_: &Command) {}
#[test]
fn common_command() {
let table: &[(&str, Handler)] = &[("*IDN?", dummy), ("*RST", dummy)];
let set = CommandSet::from_table(table).unwrap();
let cmds = set.parse("*IDN?").unwrap();
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].index, 0);
assert!(cmds[0].params.is_empty());
let cmds = set.parse("*RST").unwrap();
assert_eq!(cmds[0].index, 1);
}
#[test]
fn common_with_param() {
let table: &[(&str, Handler)] = &[("*ESE num", dummy)];
let set = CommandSet::from_table(table).unwrap();
let cmds = set.parse("*ESE 42").unwrap();
assert_eq!(cmds[0].index, 0);
assert!(matches!(cmds[0].params[0], Param::Numeric(v) if v == 42.0));
}
#[test]
fn hierarchical_short_long() {
let table: &[(&str, Handler)] = &[("SYSTem:VERSion?", dummy)];
let set = CommandSet::from_table(table).unwrap();
assert!(set.parse("SYST:VERS?").is_ok());
assert!(set.parse("system:version?").is_ok());
assert!(set.parse("System:Version?").is_ok());
}
#[test]
fn optional_node() {
let table: &[(&str, Handler)] = &[("SYSTem:ERRor[:NEXT]?", dummy)];
let set = CommandSet::from_table(table).unwrap();
assert!(set.parse("SYST:ERR:NEXT?").is_ok());
assert!(set.parse("SYST:ERR?").is_ok());
}
#[test]
fn numeric_suffix() {
let table: &[(&str, Handler)] = &[("SOURce#:FREQuency num", dummy)];
let set = CommandSet::from_table(table).unwrap();
let cmds = set.parse("SOUR2:FREQ 1e6").unwrap();
assert_eq!(cmds[0].suffixes[0], 2);
assert!(matches!(cmds[0].params[0], Param::Numeric(v) if v == 1e6));
let cmds = set.parse("SOURCE:FREQ 500").unwrap();
assert_eq!(cmds[0].suffixes[0], 1);
}
#[test]
fn bool_param() {
let table: &[(&str, Handler)] = &[("OUTPut#:STATe bool", dummy)];
let set = CommandSet::from_table(table).unwrap();
let cmds = set.parse("OUTP1:STAT ON").unwrap();
assert!(matches!(cmds[0].params[0], Param::Bool(true)));
let cmds = set.parse("OUTPUT2:STATE 0").unwrap();
assert!(matches!(cmds[0].params[0], Param::Bool(false)));
assert_eq!(cmds[0].suffixes[0], 2);
}
#[test]
fn string_param() {
let table: &[(&str, Handler)] = &[("DISPlay:TEXT str", dummy)];
let set = CommandSet::from_table(table).unwrap();
let cmds = set.parse("DISP:TEXT \"hello world\"").unwrap();
assert!(matches!(&cmds[0].params[0], Param::String(s) if s == "hello world"));
}
#[test]
fn multi_command_line() {
let table: &[(&str, Handler)] = &[("*RST", dummy), ("*IDN?", dummy)];
let set = CommandSet::from_table(table).unwrap();
let cmds = set.parse("*RST;*IDN?").unwrap();
assert_eq!(cmds.len(), 2);
assert_eq!(cmds[0].index, 0);
assert_eq!(cmds[1].index, 1);
}
#[test]
fn optional_voltage_level() {
let table: &[(&str, Handler)] = &[
("SOURce#:VOLTage[:LEVel] num", dummy),
("SOURce#:VOLTage[:LEVel]?", dummy),
];
let set = CommandSet::from_table(table).unwrap();
assert!(set.parse("SOUR1:VOLT:LEV 3.3").is_ok());
assert!(set.parse("SOUR1:VOLT 3.3").is_ok());
assert!(set.parse("SOUR1:VOLT:LEVEL?").is_ok());
assert!(set.parse("SOUR1:VOLT?").is_ok());
}
}