533 lines
17 KiB
Rust
533 lines
17 KiB
Rust
use std::{collections::VecDeque, fmt::Display};
|
|
|
|
use nom::{
|
|
branch::alt,
|
|
character::complete::{anychar, char, none_of},
|
|
combinator::{eof, map, recognize, value},
|
|
multi::{many0_count, separated_list0},
|
|
sequence::{delimited, preceded, separated_pair},
|
|
IResult,
|
|
};
|
|
|
|
#[derive(PartialEq, Eq, Debug)]
|
|
pub struct ParseResult {
|
|
pub commands: Vec<ParsedCommand>,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub enum ArgumentGuard {
|
|
Paren, // {}
|
|
DoubleQuote, // ""
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub struct ParsedArgument {
|
|
pub guard: Option<ArgumentGuard>,
|
|
pub text: String,
|
|
pub quoted_text: String,
|
|
}
|
|
|
|
impl ParsedArgument {
|
|
pub fn forget_guard(&self) -> Self {
|
|
Self {
|
|
guard: None,
|
|
..self.clone()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for ParsedArgument {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self.guard {
|
|
None => {}
|
|
Some(ArgumentGuard::Paren) => {
|
|
f.write_str("{")?;
|
|
}
|
|
Some(ArgumentGuard::DoubleQuote) => {
|
|
f.write_str("\"")?;
|
|
}
|
|
}
|
|
f.write_str(&self.quoted_text)?;
|
|
match self.guard {
|
|
None => {}
|
|
Some(ArgumentGuard::Paren) => {
|
|
f.write_str("}")?;
|
|
}
|
|
Some(ArgumentGuard::DoubleQuote) => {
|
|
f.write_str("\"")?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub struct ParsedCommand {
|
|
pub arguments: VecDeque<ParsedArgument>,
|
|
}
|
|
|
|
impl ParsedCommand {
|
|
pub fn forget_guards(&self) -> Self {
|
|
Self {
|
|
arguments: self.arguments.iter().map(|v| v.forget_guard()).collect(),
|
|
..self.clone()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for ParsedCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let mut it = self.arguments.iter();
|
|
match it.next() {
|
|
None => {}
|
|
Some(head) => {
|
|
head.fmt(f)?;
|
|
|
|
for v in it {
|
|
f.write_str(" ")?;
|
|
v.fmt(f)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl ParsedCommand {
|
|
pub fn split_out_command(&self) -> Option<(String, Self)> {
|
|
let mut tmp_arguments = self.arguments.clone();
|
|
loop {
|
|
match tmp_arguments.pop_front() {
|
|
None => return None,
|
|
Some(head) => {
|
|
let trimmed_cmd = head.text.trim();
|
|
if !trimmed_cmd.is_empty() {
|
|
return Some((
|
|
trimmed_cmd.to_owned(),
|
|
Self {
|
|
arguments: tmp_arguments,
|
|
},
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unquote_string(input: &str) -> String {
|
|
let mut buf: String = String::new();
|
|
let mut iter = input.chars();
|
|
loop {
|
|
match iter.next() {
|
|
None => return buf,
|
|
Some('\\') => match iter.next() {
|
|
None => {
|
|
buf.push('\\');
|
|
return buf;
|
|
}
|
|
Some('\\') => buf.push('\\'),
|
|
Some('{') => buf.push('{'),
|
|
Some('}') => buf.push('}'),
|
|
Some('"') => buf.push('"'),
|
|
Some(c) => {
|
|
buf.push('\\');
|
|
buf.push(c);
|
|
}
|
|
},
|
|
Some(c) => buf.push(c),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn quote_string(input: &str) -> String {
|
|
let mut buf: String = String::new();
|
|
for c in input.chars() {
|
|
match c {
|
|
'\\' => buf.push_str("\\\\"),
|
|
'{' => buf.push_str("\\{"),
|
|
'}' => buf.push_str("\\}"),
|
|
'"' => buf.push_str("\\\""),
|
|
c => buf.push(c),
|
|
}
|
|
}
|
|
buf
|
|
}
|
|
|
|
fn parse_string(input: &str) -> IResult<&str, ()> {
|
|
value(
|
|
(),
|
|
many0_count(alt((
|
|
value((), preceded(char('\\'), anychar)),
|
|
value((), none_of("\"")),
|
|
))),
|
|
)(input)
|
|
}
|
|
|
|
fn parse_parenthetical(input: &str) -> IResult<&str, ()> {
|
|
value(
|
|
(),
|
|
many0_count(alt((
|
|
value((), preceded(char('\\'), anychar)),
|
|
value(
|
|
(),
|
|
separated_pair(
|
|
char('{'),
|
|
parse_parenthetical,
|
|
alt((value((), eof), value((), char('}')))),
|
|
),
|
|
),
|
|
value(
|
|
(),
|
|
separated_pair(
|
|
char('"'),
|
|
parse_string,
|
|
alt((value((), eof), value((), char('"')))),
|
|
),
|
|
),
|
|
value((), none_of("}")),
|
|
))),
|
|
)(input)
|
|
}
|
|
|
|
fn parse_argument(input: &str) -> IResult<&str, ParsedArgument> {
|
|
alt((
|
|
map(
|
|
delimited(char('{'), recognize(parse_parenthetical), char('}')),
|
|
|v| ParsedArgument {
|
|
text: unquote_string(v),
|
|
quoted_text: v.to_owned(),
|
|
guard: Some(ArgumentGuard::Paren),
|
|
},
|
|
),
|
|
map(
|
|
delimited(char('"'), recognize(parse_string), char('"')),
|
|
|v| ParsedArgument {
|
|
text: unquote_string(v),
|
|
quoted_text: v.to_owned(),
|
|
guard: Some(ArgumentGuard::DoubleQuote),
|
|
},
|
|
),
|
|
map(
|
|
recognize(value(
|
|
(),
|
|
many0_count(alt((
|
|
value((), preceded(char('\\'), anychar)),
|
|
value(
|
|
(),
|
|
separated_pair(
|
|
char('{'),
|
|
parse_parenthetical,
|
|
alt((value((), eof), value((), char('}')))),
|
|
),
|
|
),
|
|
value(
|
|
(),
|
|
separated_pair(
|
|
char('"'),
|
|
parse_string,
|
|
alt((value((), eof), value((), char('"')))),
|
|
),
|
|
),
|
|
value((), none_of(" ;")),
|
|
))),
|
|
)),
|
|
|v| ParsedArgument {
|
|
text: unquote_string(v),
|
|
quoted_text: v.to_owned(),
|
|
guard: None,
|
|
},
|
|
),
|
|
))(input)
|
|
}
|
|
|
|
fn parse_command(input: &str) -> IResult<&str, ParsedCommand> {
|
|
map(separated_list0(char(' '), parse_argument), |arguments| {
|
|
ParsedCommand {
|
|
arguments: arguments.into(),
|
|
}
|
|
})(input)
|
|
}
|
|
|
|
pub fn parse_commands(input: &str) -> ParseResult {
|
|
// Note that the core parser doesn't do things like skipping multiple whitespace,
|
|
separated_list0(preceded(char(';'), many0_count(char(' '))), parse_command)(input)
|
|
.map(|(_, commands)| ParseResult { commands })
|
|
.unwrap_or_else(|_| ParseResult { commands: vec![] })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
#[test]
|
|
fn test_parse_commands() {
|
|
assert_eq!(
|
|
parse_commands(""),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![ParsedArgument {
|
|
text: "".to_owned(),
|
|
quoted_text: "".to_owned(),
|
|
guard: None
|
|
}]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("north"),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![ParsedArgument {
|
|
text: "north".to_owned(),
|
|
quoted_text: "north".to_owned(),
|
|
guard: None
|
|
}]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("north "),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
// This is deliberate, ensures we can reconstruct the input.
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "north".to_owned(),
|
|
quoted_text: "north".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "".to_owned(),
|
|
quoted_text: "".to_owned(),
|
|
guard: None
|
|
}
|
|
]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah {x = 1 + 2; y = 3}; #home"),
|
|
ParseResult {
|
|
commands: vec![
|
|
ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "x = 1 + 2; y = 3".to_owned(),
|
|
quoted_text: "x = 1 + 2; y = 3".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
}
|
|
]
|
|
.into()
|
|
},
|
|
ParsedCommand {
|
|
arguments: vec![ParsedArgument {
|
|
text: "#home".to_owned(),
|
|
quoted_text: "#home".to_owned(),
|
|
guard: None
|
|
}]
|
|
.into()
|
|
}
|
|
]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah {x = 1 + 2"),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "{x = 1 + 2".to_owned(),
|
|
quoted_text: "{x = 1 + 2".to_owned(),
|
|
guard: None
|
|
}
|
|
]
|
|
.into()
|
|
},]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah {x = 1} {y = 1}"),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "x = 1".to_owned(),
|
|
quoted_text: "x = 1".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
},
|
|
ParsedArgument {
|
|
text: "y = 1".to_owned(),
|
|
quoted_text: "y = 1".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
}
|
|
]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah \"hello\" \"world\""),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "hello".to_owned(),
|
|
quoted_text: "hello".to_owned(),
|
|
guard: Some(ArgumentGuard::DoubleQuote)
|
|
},
|
|
ParsedArgument {
|
|
text: "world".to_owned(),
|
|
quoted_text: "world".to_owned(),
|
|
guard: Some(ArgumentGuard::DoubleQuote)
|
|
},
|
|
]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah {x = \"}\"} {y = 1}"),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "x = \"}\"".to_owned(),
|
|
quoted_text: "x = \"}\"".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
},
|
|
ParsedArgument {
|
|
text: "y = 1".to_owned(),
|
|
quoted_text: "y = 1".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
}
|
|
]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah {x = \"}\"; a = \"{\"; y = {}; z = 1;} { q = 5 };"),
|
|
ParseResult {
|
|
commands: vec![
|
|
ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "x = \"}\"; a = \"{\"; y = {}; z = 1;".to_owned(),
|
|
quoted_text: "x = \"}\"; a = \"{\"; y = {}; z = 1;".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
},
|
|
ParsedArgument {
|
|
text: " q = 5 ".to_owned(),
|
|
quoted_text: " q = 5 ".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
}
|
|
]
|
|
.into()
|
|
},
|
|
ParsedCommand {
|
|
arguments: vec![ParsedArgument {
|
|
text: "".to_owned(),
|
|
quoted_text: "".to_owned(),
|
|
guard: None
|
|
}]
|
|
.into()
|
|
}
|
|
]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah {\\}\\}\\}} {y = 1}"),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "}}}".to_owned(),
|
|
quoted_text: "\\}\\}\\}".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
},
|
|
ParsedArgument {
|
|
text: "y = 1".to_owned(),
|
|
quoted_text: "y = 1".to_owned(),
|
|
guard: Some(ArgumentGuard::Paren)
|
|
}
|
|
]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse_commands("#blah \"This is a \\\"test\\\"\""),
|
|
ParseResult {
|
|
commands: vec![ParsedCommand {
|
|
arguments: vec![
|
|
ParsedArgument {
|
|
text: "#blah".to_owned(),
|
|
quoted_text: "#blah".to_owned(),
|
|
guard: None
|
|
},
|
|
ParsedArgument {
|
|
text: "This is a \"test\"".to_owned(),
|
|
quoted_text: "This is a \\\"test\\\"".to_owned(),
|
|
guard: Some(ArgumentGuard::DoubleQuote)
|
|
}
|
|
]
|
|
.into()
|
|
}]
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_roundtrip() {
|
|
let input = "#hello {to the} \"world\" I say! ";
|
|
assert_eq!(
|
|
parse_command(input).map(|c| c.1.to_string()),
|
|
Ok(input.to_owned())
|
|
);
|
|
|
|
let input = "{{{\"Foo \\ Bar\"}}}";
|
|
assert_eq!(
|
|
parse_command(input).map(|c| c.1.to_string()),
|
|
Ok(input.to_owned())
|
|
);
|
|
}
|
|
}
|