1
votes

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:

  1. I don't know how to implement the 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 arguments
  2. I don't know how to handle situations where there are no arguments at all, ideally that would be a simple enum variant without fields.

Does anyone know if there's a way to solve this on stable rust? Or should I just generate the code using a python script?

1
Note: an intermediate step between macros and external scripts is the build.rs file. When using cargo build, cargo will first compile and execute build.rs (if present) before compiling the rest of your crate, so you can easily use build.rs to generate Rust code without involving any 3rd party tooling/makefile.Matthieu M.

1 Answers

5
votes

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.