Sling Academy
Home/Rust/Building Interactive CLI Tools in Rust Using OOP-Like Patterns

Building Interactive CLI Tools in Rust Using OOP-Like Patterns

Last updated: January 06, 2025

Building Command Line Interface (CLI) tools is a popular use of the Rust programming language, thanks to its performance and safety guarantees. Rust supports a variety of programming paradigms, including object-oriented programming (OOP) via traits and structs. This article will guide you through creating interactive CLI tools in Rust by leveraging OOP-like patterns.

Setting Up Your Rust Project

Before we begin coding, make sure you have Rust installed on your machine. You can do this by following the instructions at rustup.rs. Once Rust is installed, you can create a new project:

cargo new cli_tool

This will set up a new Rust project named cli_tool.

Defining the CLI Tool Structure

In OOP, we typically start by designing classes. In Rust, we use structs and traits to achieve similar encapsulation and abstraction. Below is a simple definition of a Command struct that we'll use to represent our CLI commands.


// src/main.rs

struct Command {
    name: String,
    description: String,
    execute: fn(),
}

impl Command {
    fn new(name: &str, description: &str, execute: fn()) -> Command {
        Command {
            name: name.to_string(),
            description: description.to_string(),
            execute,
        }
    }
}

Here we defined a Command struct with fields for the command name, description, and a function pointer to handle execution.

Implementing a Trait for Execution

Let's enforce an interface for command execution using traits. This provides similar functionality to interfaces in OOP languages.


trait Executable {
    fn execute(&self);
}

impl Executable for Command {
    fn execute(&self) {
        (self.execute)();
    }
}

Here, the Executable trait defines a method, execute(), which we must implement for any struct that implements this trait.

Designing Interactive CLI Input

Now let’s give some interactivity to our CLI tool by incorporating user input. We'll update our main function to interactively handle user commands.


use std::io::{self, Write};

fn main() {
    let hello_command = Command::new(
        "hello", 
        "Prints 'Hello, world!'", 
        || println!("Hello, world!"),
    );
    
    let commands: Vec> = vec![Box::new(hello_command)];

    loop {
        print!("Enter a command: ");
        io::stdout().flush().unwrap();
        let mut input = String::new();
        io::stdin().read_line(&mut input).unwrap();
        let input = input.trim();
        
        for command in &commands {
            if command.name == input {
                command.execute();
                break;
            }
        }

        if input == "exit" {
            break;
        }
    }
}

In this example, we've created an interactive loop that accepts user input and tries to match it to one of our commands. We also handle an "exit" command to break the loop.

Expanding Your Toolset

To expand your CLI tool, you can define more commands by following the Command struct pattern and adding them to the commands list. Additionally, consider adding error handling and user feedback for better interaction and debugging.

This article has demonstrated how to use Rust's struct and trait system to create an interactive CLI tool with OOP-like flexibility and reusability. By continuing to apply these patterns, you can build robust command-line applications suited for a variety of tasks, from quick automation scripts to comprehensive command suites.

Next Article: Avoiding Over-Engineering in Rust: Simpler Alternatives to Traditional OOP

Previous Article: When Not to Use OOP Concepts in Rust: Embracing Enums and Functions

Series: Object-Oriented Programming in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior