Writing a Static Analyser for PHP in Rust - Setup

php
Table of Contents

Welcome back to the series. This post is going to be a short one focused on setting up the Rust application and the command-line interface.

The goal is to get a boilerplate Rust project created, install clap and scaffold out a couple of commands.

In typical developer fashion, we need to the most important thing first and think of a name for the project. Let's go with Statan.

cargo new statan

This will generate a statan folder with Cargo setup and a basic main.rs file.

There are quite a few options when it comes to argument parsing and command-line libraries in Rust. The most popular one is Clap, so we'll be using that. It can be installed with Cargo:

cargo add clap --features derive

We'll want the derive feature enabled so that we can use Clap's derive macros, dramatically simplifying the setup process.

The first step in setting up Clap is creating a struct to hold the command-line arguments.

use clap::Parser;

#[derive(Debug, Parser)]
struct Arguments {

}

fn main() {
    let arguments = Arguments::parse();
}

The CLI will be subcommand based. Thankfully, Clap provides a Subcommand macro that we can attach to an enumeration and it'll handle the rest.

use clap::{Parser, Subcommand};

#[derive(Debug, Parser)]
struct Arguments {
    #[clap(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    #[clap(about = "Analyse a file.")]
    Analyse,
}

fn main() {
    let arguments = Arguments::parse();
}

If we run the cargo run -- --help command, the analyse command should show up in the output.

Usage: statan <COMMAND>

Commands:
  analyse  Analyse a file.
  help     Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

And if we run cargo run -- analyse --help, there will be some basic output too:

Analyse a file.

Usage: statan analyse

Options:
  -h, --help  Print help

The first iteration of the analyse command will only support analysing a single file. To add the argument to the command, all we need to do is add a field to the Command::Analyse member with a macro.

#[derive(Debug, Subcommand)]
enum Command {
    #[clap(about = "Analyse a file.")]
    Analyse(AnalyseCommand)
}

#[derive(Debug, Parser)]
pub struct AnalyseCommand {
    #[clap(help = "The file to analyse.")]
    file: String,
}

Each command will have it's own module in the application exposing a single run function. I typically like to place them under src/cmd/[cmd].rs.

The main function needs to be updated to defer handling to the correct function.

use clap::{Parser, Subcommand};

mod cmd;

// ...

fn main() {
    let arguments = Arguments::parse();

    match arguments.command {
        Command::Analyse(args) => cmd::analyse::run(args),
    }
}

The src/cmd/analyse.rs file contains the function itself:

use crate::AnalyseCommand;

pub fn run(command: AnalyseCommand) {
    
}

And the help output for the subcommand reflects the new argument:

Analyse a file.

Usage: statan analyse <FILE>

Arguments:
  <FILE>  The file to analyse.

Options:
  -h, --help  Print help

Just like that, we've got a simple command-line interface to our new program. In the next part, we can start writing the business logic behind the analyser.

The code for this part is public and can be found on GitHub.

Enjoyed this post or found it useful? Please consider sharing it on Twitter.