Reputation: 1005
I'm writing a little library for parsing OVPN config files. OVPN config files have this format
command arg1 arg2
othercommand arg1 arg2
There's a fixed set of commands, some of them have optional arguments. I want to represent the parsed commands as an enum. So the above might end up being represented like this:
enum ConfigDirective{
Command{arg1: String},
OtherCommand{arg1: String, optinal_arg1: Option<String>},
}
fn parse_line(command: String, args: Vec<String>) -> ConfigDirective {
match command {
"command" => ConfigDirective::Command{arg1: args[0]},
"other_command" => ConfigDirective:OtherCommand{arg1: args[0], optional_arg1: args.get(1),
}
}
I like this structure but there are a lot of possible commands (somewhere in the region of 280). So I want to write a macro to generate most of the boilerplate. Ideally I would write something like the following:
define_config_directive!{
{command => "command1", rust_name => CommandOne, args => [arg1], optional_args => []},
{command => "other_command", rust_name => OtherCommand, args => [arg1], optional_args => [optional_arg1]},
}
The closest I've been able to get so far is this:
macro_rules! define_config_directives {
($({
rust_name => $rust_name:ident,
required => [$($required:ident),*],
optional => [$($optional:ident),*]
}),*) => {
#[derive(PartialEq, Eq, Debug)]
pub enum ConfigDirective {
$($rust_name{
$($required: String),*,
$($optional: Option<String>),*,
}),*
}
};
}
So I have a few problems:
parse_line
function in this macro, I need to iterate over each required argument in order writing some code to pull the corresponding argument out of the line and the same for optional argumentsDoes anyone know if there's a way to solve this on stable rust? Or should I just generate the code using a python script?
Upvotes: 4
Views: 3269
Reputation: 59155
This is a somewhat pathological case. Firstly, you want to process parts of the input differently, which macros aren't good at. Worse, you want to do this in conjunction with generating enum variants, which macros are also bad at. Taken together leaves only one approach, so far as I can see: full on push-down generation.
The short version is: break it down into simple matching steps, where each step processes one thing, and adds the output for that one thing to an accumulator (in this case, $eout
and $pout
). When you're out of input, dump the accumulators into your output.
macro_rules! define_config_directive {
// Start rule.
// Note: `$(,)*` is a trick to eat any number of trailing commas.
( $( {$($cmd:tt)*} ),* $(,)*) => {
// This starts the parse, giving the initial state of the output
// (i.e. empty). Note that the commands come after the semicolon.
define_config_directive! { @parse {}, (args){}; $({$($cmd)*},)* }
};
// Termination rule: no more input.
(
@parse
// $eout will be the body of the enum.
{$($eout:tt)*},
// $pout will be the body of the `parse_line` match.
// We pass `args` explicitly to make sure all stages are using the
// *same* `args` (due to identifier hygiene).
($args:ident){$($pout:tt)*};
// See, nothing here?
) => {
#[derive(PartialEq, Eq, Debug)]
enum ConfigDirective {
$($eout)*
}
fn parse_line(command: &str, $args: &[&str]) -> ConfigDirective {
match command {
$($pout)*
_ => panic!("unknown command: {:?}", command)
}
}
};
// Rule for command with no arguments.
(
@parse {$($eout:tt)*}, ($pargs:ident){$($pout:tt)*};
{
command: $sname:expr,
rust_name: $rname:ident,
args: [],
optional_args: [] $(,)*
},
$($tail:tt)*
) => {
define_config_directive! {
@parse
{
$($eout)*
$rname,
},
($pargs){
$($pout)*
$sname => ConfigDirective::$rname,
};
$($tail)*
}
};
// Rule for other commands.
(
@parse {$($eout:tt)*}, ($pargs:ident){$($pout:tt)*};
{
command: $sname:expr,
rust_name: $rname:ident,
args: [$($args:ident),* $(,)*],
optional_args: [$($oargs:ident),* $(,)*] $(,)*
},
$($tail:tt)*
) => {
define_config_directive! {
@parse
{
$($eout)*
$rname { $( $args: String, )* $( $oargs: Option<String>, )* },
},
($pargs){
$($pout)*
$sname => {
// This trickery is because macros can't count with
// regular integers. We'll just use a mutable index
// instead.
let mut i = 0;
$(let $args = $pargs[i].into(); i += 1;)*
$(let $oargs = $pargs.get(i).map(|&s| s.into()); i += 1;)*
let _ = i; // avoid unused assignment warnings.
ConfigDirective::$rname {
$($args: $args,)*
$($oargs: $oargs,)*
}
},
};
$($tail)*
}
};
}
define_config_directive! {
{command: "command1", rust_name: CommandOne, args: [arg1], optional_args: []},
{command: "other_command", rust_name: OtherCommand, args: [arg1], optional_args: [optional_arg1]},
}
fn main() {
println!("{:?}", parse_line("command1", &["foo"]));
println!("{:?}", parse_line("other_command", &["foo"]));
println!("{:?}", parse_line("other_command", &["foo", "bar"]));
}
No, you can't avoid the accumulator thing because macros can't directly expand to enum variants. As such, you have to expand to the entire enum definition in a single step.
Upvotes: 11