Skip to main content

Organize Large CLI Applications

Learn how to structure your Clawless project as it grows, separating CLI interface from business logic.

Commands are your interface

The src/commands/ directory defines your CLI's interface - the commands users can run. Keep command functions thin, using them to parse arguments and call your application logic:

// src/commands/deploy.rs
use clawless::prelude::*;
use crate::deployment; // Business logic in separate module

#[derive(Debug, Args)]
pub struct DeployArgs {
environment: String,
}

/// Deploy to an environment
#[command]
pub async fn deploy(args: DeployArgs, context: Context) -> CommandResult {
// Command function just handles CLI concerns
deployment::run(&args.environment, context.current_working_directory()).await?;
Ok(())
}

Put business logic in separate modules

Create modules outside src/commands/ for your actual application logic:

src/
├── main.rs
├── commands.rs
├── commands/
│ ├── deploy.rs // CLI interface
│ ├── build.rs // CLI interface
│ └── test.rs // CLI interface
├── deployment/ // Business logic
│ ├── mod.rs
│ ├── validator.rs
│ └── runner.rs
├── builder/ // Business logic
│ ├── mod.rs
│ └── compiler.rs
└── testing/ // Business logic
├── mod.rs
└── runner.rs

The command functions become thin wrappers:

// src/commands/build.rs
use crate::builder;

#[command]
pub async fn build(args: BuildArgs, context: Context) -> CommandResult {
let config = builder::Config::from_args(&args)?;
builder::compile(config).await?;
Ok(())
}

This separation means:

  • Business logic can be tested without CLI dependencies
  • Logic can be reused across different commands
  • Command structure stays clean and focused on interface

Grouping commands

As you add commands, group related ones together using nested modules:

src/commands/
├── build.rs
├── test.rs
├── config.rs
└── config/
├── get.rs
├── set.rs
└── list.rs

This creates the command structure:

myapp build
myapp test
myapp config get
myapp config set
myapp config list

Each command is still thin, calling into your business logic modules in src/:

// src/commands/config/get.rs
use crate::config_manager; // Business logic

#[command]
pub async fn get(args: GetArgs, context: Context) -> CommandResult {
let value = config_manager::get_value(&args.key)?;
println!("{}", value);
Ok(())
}

Example structure for a large CLI

A deployment tool with multiple domains:

src/
├── main.rs
├── commands.rs
├── commands/ # CLI interface layer
│ ├── init.rs
│ ├── project.rs
│ ├── project/
│ │ ├── create.rs
│ │ └── delete.rs
│ ├── deploy.rs
│ └── deploy/
│ ├── staging.rs
│ └── production.rs

├── projects/ # Business logic
│ ├── mod.rs
│ ├── creator.rs
│ ├── deleter.rs
│ └── validator.rs

├── deployment/ # Business logic
│ ├── mod.rs
│ ├── pipeline.rs
│ ├── health_check.rs
│ └── rollback.rs

├── config/ # Business logic
│ ├── mod.rs
│ └── loader.rs

└── api/ # Business logic
├── mod.rs
└── client.rs

The commands just wire up the interface:

// src/commands/project/create.rs
use crate::projects;

#[command]
pub async fn create(args: CreateArgs, context: Context) -> CommandResult {
projects::Creator::new(&args.name)
.in_directory(context.current_working_directory())
.create()
.await?;
Ok(())
}

Shared code within command groups

If you need to share code between commands in the same group, create a non-command module:

src/commands/
├── database.rs
└── database/
├── migrate.rs # Command
├── seed.rs # Command
├── reset.rs # Command
└── helpers.rs # NOT a command - just shared utilities

In database/helpers.rs:

// This is NOT a command - it doesn't have #[command]
// Just regular Rust code shared by commands in this group

pub fn parse_connection_string(s: &str) -> Result<DbConfig, Error> {
// Parsing logic used by multiple database commands
}

Use from commands:

// src/commands/database/migrate.rs
use super::helpers; // Import from the same module

#[command]
pub async fn migrate(args: MigrateArgs, context: Context) -> CommandResult {
let config = helpers::parse_connection_string(&args.connection)?;
// ...
}

Note: Don't forget to declare the module in the parent:

// src/commands/database.rs
mod migrate;
mod seed;
mod reset;
mod helpers; // Declare non-command modules too

// ... rest of database.rs

When to create command groups

Create nested command structures when you have multiple related commands that share a conceptual grouping. There's no hard rule - use groups when they make the CLI more intuitive for your users.

The file structure flexibility is there when you need it. Start with flat structure, add grouping when it improves clarity.

See also