Writing a Static Analyser for PHP in Rust - Setup
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.