UP | HOME

Cleopatry Binary

Table of Contents

In this document, we explain the implementation of the cleopatra CLI tool. We do not try to parse command-line arguments ourselves, but we rather rely on clap, a powerful Rust crate that can do it for us as long as we provide a specification for the desired CLI. clap encodes a CLI specification as a Rust structure called App which follows the builder pattern. This quickly becomes cumbersome and verbose to use, and this is why over time clap has implemented more high-level way to define a CLI.

We proceed as follows: we use the clap_app! macro to build the App type using a S-Expression. Rather than defining the S-Expression at one place, and then provide an implementation for the related CLI, we enjoy the facilities provided by literate programming and the noweb feature of Babel to provide the specification of a given subcommand and its implementation in the same section.

We first give the general-purpose information about cleopatra: its name, author and current version (we recall the version number is specify once in the commons.org file).

(version: "1.0.0-dev")
(author: "Thomas Letan <lthms@soap.coffee")
(@setting ArgsNegateSubcommands)
(about: "An extensible toolchain with facilities for literate programming")

1 The CleopatraCommand Trait

From a high-level perspective, the execution of cleopatra can be divided into three steps:

  1. Command-line arguments parsing
  2. Command-line interpretation
  3. Actual Business logic

The first step is achieved with clap, but we still need to take care of the rest.

To that end, we first introduce the trait CleopatraCommand. Each cleopatra command will have a dedicated type equipped with two methods: with_args (step 2), and run (step 3). Besides, the go method provided by CleopatraCommand binds the two methods together.

pub trait CleopatraCommand<'a>
where Self : Sized {
    fn of_args(args : &'a ArgMatches<'static>) -> Self;

    fn run(self) -> Result<(), Error>;

    fn go(args : &'a ArgMatches<'static>) -> Result<(), Error> {
        Self::of_args(args).run()
    }
}

CleopatraCommand takes a lifetime as a parameter due to an implementation details of clap. When fetching arguments value from a &'a ArgMatches reference, the result is a reference with 'a as its lifetime. For the “command types” of cleopatra to use these reference as-is (and therefore avoid unnecessary copies), they need to be parameterized with the same lifetime as ArgMatches.

A typical implementationg of the CleopatraCommand trait will therefore be declared as

impl<'a> CleopatraCommand<'a> for FooCommand<'a> {
    // ...
}

2 Base Command

Calling cleopatra without any particular subcommand will run make with the appropriate Makefile.

(@arg parallel: -j "Enable parallel build")
(@arg RECIPE: "The recipe to run (default to `postbuild')")

We introduce the structure Make, and provide the necessary implementation of the trait CleopatraCommand.

recipe &'a str The recipe to execute  
parallel bool Set to `true` to allow for parallel build by Make  

Note that the run method will first prepare the workspace (the .cleopatra/ directory), then we call make init in order to ensure that generation processes are up-to-date (see the related chapter if interested), and only after that we call make with the supplied recipe.

impl<'a> CleopatraCommand<'a> for Make<'a> {
    fn of_args(args : &'a ArgMatches<'static>) -> Make<'a> {
        let recipe = args.value_of("RECIPE").unwrap_or("postbuild");
        let jobs = args.is_present("parallel");

        Make { recipe : recipe, parallel : jobs }
    }

    fn run(self) -> Result<(), Error> {
        Config::find_project_then(|project| {
            project.prepare_workspace()?;

            exec(&vec!["make", "-f", ".cleopatra/boot.mk", "init"])?;

            let mut cmd = vec!["make", "-f", ".cleopatra/boot.mk", self.recipe];

            if self.parallel {
                cmd.push("-j");
            }

            exec(&cmd)
        })
    }
}

3 Subcommands

The following table lists the subcommands supported by cleopatra, with their dedicated types.

exec Exec
echo Echo

Interestingly, we can —and we do— generate the match statement with the following Emacs lisp routine.

(mapconcat
 (lambda (cmd)
   (format "(\"%s\", Some(args)) => %s::go(args),"
           (nth 0 cmd)
           (nth 1 cmd)))
 cmds
 "\n")

3.1 cleopatra exec

cleopatra exec CMD will execute CMD from the root of the current cleopatra project, with the environment variables defined in the cleopatra.toml set as expected.

(@subcommand exec =>
  (about: "Execute a command from the root of the current project")
  (@setting TrailingVarArg)
  (@arg CMD: +required +takes_value +multiple "The command to run"))

So, for instance cleopatra exec printenv ROOT will print the root of the project, that is the directory which contains cleopatra.toml.

The TrailingVarArg settings is used to tell to clap not to parse the arguments of exec. That is, if we call cleopatra exec echo -n hi, the default behavior of clap would be to try to parse n as a flag. With TrailingVarArg, it does not, and -n is just parsed as the string "-n".

The following table lists the fields of the Exec structure

command Vec<&'a str> A list of strings which together form the command to execute  

As one might expect, implementing CleopatraCommand does not pose any fundamental challenge. In particular, the run method is simply the composition of Config::find_project_then with the exec helper.

impl<'a> CleopatraCommand<'a> for Exec<'a> {
    fn of_args(args : &'a ArgMatches<'static>) -> Exec<'a> {
        let cmd = args.values_of("CMD")
            .unwrap()
            .collect();

        Exec { command : cmd }
    }

    fn run(self) -> Result<(), Error> {
        Config::find_project_then(|_|  exec(&self.command))
    }
}

3.2 cleopatra echo

cleopatra echo CATEGORY DESCRIPTION will echo a formatted message a la cargo.

(@subcommand echo =>
  (about: "Echo a la cargo")
  (@arg CATEGORY: +required "")
  (@arg DESCRIPTION: +required ""))

The following table lists the fields of the Echo structure

cat &'a str    
descr &'a str    
impl<'a> CleopatraCommand<'a> for Echo<'a> {
    fn of_args(args : &'a ArgMatches<'static>) -> Echo<'a> {
        let cat = args.value_of("CATEGORY").unwrap();
        let descr = args.value_of("DESCRIPTION").unwrap();

        Echo { cat : cat, descr : descr }
    }

    fn run(self) -> Result<(), Error> {
        println!("{:>12} {}", self.cat.green(), self.descr);
        Ok(())
    }
}

4 Helpers

4.1 exec

fn exec(cmd : &[&str]) -> Result<(), Error> {
    Command::new(cmd[0])
        .args(cmd.split_at(1).1)
        .status()
        .or_raise("Could not execute submitted command")
        .and_then(|status| {
            if status.success() {
                Ok(())
            } else {
                Err(Error::Anomaly(format!("The command `{}' failed", cmd.join(" "))))
            }
        })
}

5 main

fn main() -> () {
    match run(args()) {
        Err(err) => {
            let msg = err.message();
            eprintln!("{} {}\n{}", "Error:".red().bold(), msg.title, msg.description);
            std::process::exit(1);
        },
        _ => (),
    }
}
fn run(matches : ArgMatches<'static>) -> Result<(), Error> {
    match matches.subcommand() {
        ("", _) => Make::go(&matches),
        ("exec", Some(args)) => Exec::go(args),
        ("echo", Some(args)) => Echo::go(args),
        (cmd, _) => Err(Error::UnknownSubcommand(String::from(cmd))),
    }?;

    Ok(())
}

Author: Thomas Letan

Created: 2020-12-20 Sun 12:56

Validate