Tusks
Tusks allows you to define CLIs easily and idiomatically through a Rust module and function structure.
If You just want a quick example head over to the Comprehensive Example section.
Table of Contents
- Motivation
- Installation
- Core Concepts
- Relationship with Clap
- Features and Examples
- 1. Simple Root Module Definition
- 2. Nested Modules (Subcommands)
- 3. Root Parameters with Parameters Struct
- 4. Module-Level Parameters
- 5. External Modules
- 6. Return Values and Exit Codes
- 7. Various Argument Types
- 8. Custom Value Parsers
- 9. Tasks Mode (Ruby Rake-Style)
- 10. Command Attributes for Documentation
- 11. Default Functions for Modules
- Comprehensive Example
- License
- Contributions
- Trivia
Motivation
Creating complex CLI applications with nested commands and shared parameters can quickly become unwieldy. Tusks solves this problem by providing a declarative syntax for CLI structures that:
- Naturally maps hierarchical command structures
- Automatically manages parameter chaining across multiple levels
- Uses Clap under the hood but eliminates boilerplate code
- Supports modular organization through external modules
- Guarantees type safety through Rust/Clap's type system
Instead of manually managing Clap subcommands and creating match statements, you simply define modules and functions – Tusks takes care of the rest.
Want to see it in action? Check out the comprehensive example at the end of this document.
Installation
[]
= "2.1"
Core Concepts
Tusks is based on four main concepts:
- Modules as Commands/Subcommands: Rust modules automatically become CLI commands. Modules serve to hierarchically group functions as subcommands.
- Functions as Commands/Subcommands: Public functions in modules become executable CLI commands/subcommands. Function arguments are automatically translated into CLI parameters.
- Parameters Struct for Command Arguments: The
Parametersstruct defines arguments at the module level that apply to the respective command/subcommand. These arguments are automatically available to all underlying subcommands. - External Modules: External modules, i.e., modules in other files, can be easily integrated into the current CLI structure, even recursively!
The CLI is started with cli::exec_cli(), which parses the command line arguments and executes the corresponding commands. The function always returns Option<u8>, which can be used as an exit code.
Relationship with Clap
Tusks is a high-level wrapper around Clap, the popular CLI parsing framework for Rust. Many of Clap's features are retained.
#[arg()]attributes for argument configuration#[command()]attributes for subcommand descriptions- Data types are parsed as in Clap
- Automatic help generation
- Type-safe parsing
Tusks generates Clap code internally.
Features and Examples
1. Simple Root Module Definition
The #[tusks(root)] attribute marks a module as the CLI entry point. The CLI is started with cli::exec_cli().
use tusks;
Usage:
Visibility and #[skip]
Only public (pub) modules and functions are used for CLI construction. Private functions are automatically ignored:
With the #[skip] attribute, you can also exclude public functions from CLI parsing:
The #[skip] attribute also works for modules:
2. Nested Modules (Subcommands)
Modules automatically become subcommands and serve to hierarchically group functions.
Usage:
)
Modules can be nested arbitrarily deep:
Usage:
With a Parameters struct, you can define common parameters that are available to all commands.
Usage:
)
Important Notes on Parameters
-
Optional: The
Parametersstruct is completely optional. You only need it if you want to define parameters at the current module level or access parent parameters. -
Lifetime required: If you define a
Parametersstruct, it must always have the lifetime<'a>: -
Automatic
super_field: Tusks automatically adds asuper_field that references the parent Parameters struct. You must not define this field yourself: -
Implicit Parameters structs: Even if you don't define a
Parametersstruct at a level, it exists in the background. This means thatsuper_.super_always works to access parameters two levels up: -
Parameters as function argument: The
Parametersstruct may only be specified as the first argument of a function and is optional. If you don't need it, you can omit it:// With Parameters (must be first argument) // Without Parameters // WRONG: Parameters not in first position ❌
3. Root Parameters with Parameters Struct
With a Parameters struct, you can define common parameters that are available to all commands.
Usage:
)
Modules automatically become subcommands with their own parameters.
Usage:
4. Module-Level Parameters
Each module can define its own Parameters struct to define specific arguments for the respective subcommand. These parameters are automatically available to all underlying subcommands.
Usage:
Parameters can be passed through an arbitrary number of levels.
Usage:
5. External Modules
External modules allow distributing CLI structures across multiple files. This is particularly useful for organizing large CLIs and promoting code reusability.
An external module differs from the root module in that it does not have the root flag in the #[tusks()] attribute. Instead, it must include a parent_ reference to its parent module to enable parameter chaining.
src/main.rs:
// Root module with 'root' flag
src/git.rs:
use tusks;
// External module WITHOUT 'root' flag
Usage:
Important Notes on External Modules
-
Parent reference required: External modules must always contain a
parent_reference to their parent module. The aliasparent_must be used.pub use cratecli as parent_; // Required!This reference also enables parameter chaining via
super_. -
No
rootflag: External modules use#[tusks()]without therootflag. Only the main module (the entry point for the CLI) uses#[tusks(root)]. -
Customize subcommand names: The name of the subcommand is determined by the name used during import:
// Subcommand is named "cli" (name of imported module) pub use cratecli; // Subcommand is named "git" (with alias) pub use cratecli as git; // Subcommand is named "vcs" (with different alias) pub use cratecli as vcs; // Customize subcommand name via attribute, alias is ignored pub use cratecli as git; -
Arbitrary nesting: External modules can themselves include external modules:
src/git.rs:
src/git_advanced.rs:
Invocation:
6. Return Values and Exit Codes
Commands can return values that are used as exit codes. Allowed return types are (), u8, and Option<u8>. The return value is always returned by cli::exec_cli() as Option<u8>.
Usage:
7. Various Argument Types
Tusks supports all Clap argument types.
Usage:
8. Custom Value Parsers
Clap's value parsers can be used to add custom validation.
Usage:
9. Tasks Mode (Ruby Rake-Style)
Tasks mode provides a simplified, flat CLI syntax in the style of Ruby Rake. Instead of nested subcommands, tasks can be invoked with a separator (default .).
Activation
Tasks mode is simply activated through the tasks attribute:
Display Task List
Without arguments, the CLI automatically shows all available tasks in a grouped overview:
Flat Task Syntax
Tasks can be invoked directly via their full path:
# Rake-style (with separator)
# Equivalent to traditional subcommand syntax
Both variants are fully interchangeable. The subcommand structure is preserved, so you can still use module-specific parameters:
Help for Tasks
Help can be accessed in multiple ways:
# With 'h' prefix
# With '-h' flag
# Traditional
All three variants display the same help:
Clone a repository
Usage: tasks git clone <URL> [PATH]
Arguments:
<URL> Repository URL
[PATH] Target path
Options:
-h, --help Print help
Configuring Task Grouping
The task overview display can be configured to control how tasks are organized and presented:
Parameters:
-
separator(default:".") - Character(s) used to separate module levels in command names# With separator="." # With separator=":" -
use_colors(default:true) Iftrue(which is the default) the overview over all tasks is colored
[!NOTE] The following part of this section is proably quite technical and not that important. If the task overview is fine for your needs, it is advised to skip it.
-
max_groupsize(default:5) - Threshold for creating subgroups in the task overviewThis parameter only affects the task overview output, not command execution.
When a group contains more than
max_groupsizevisible tasks, they are organized into subgroups based on their module hierarchy. Hidden tasks (marked with#[hidden]) are excluded from this count.Example with
max_groupsize=5:# Scenario: 4 visible tasks (+ 2 hidden tasks) # 4 ≤ 5, so no grouping occurs - all tasks shown at root level# Scenario: 6 visible tasks (+ 2 hidden tasks) # 6 > 5, so grouping by first module level occurs -
max_depth(default:20) - Maximum nesting depth for hierarchical groupingThis parameter only affects the task overview output, not command execution.
Controls how many levels deep the grouping can go. Each level corresponds to one module in the path hierarchy.
Example with
max_depth=1:# Deep module structure with 8 visible tasks # docker.container.list, docker.container.logs, docker.image.build, docker.image.pull, etc. # With max_depth=1, only first level grouping is allowed# Same structure with max_depth=2 and max_groupsize=3 # Second level grouping is allowed, creating subgroups
Interaction between max_groupsize and max_depth:
Both parameters work together to control grouping behavior. Both conditions must be met for grouping to occur:
- The number of visible tasks must exceed
max_groupsize(threshold condition) - The current depth must be less than
max_depth(depth limit)
max_depth takes precedence - once the depth limit is reached, no further grouping occurs regardless of group size.
Example showing the interaction:
# Configuration: max_groupsize=3, max_depth=2
# Module structure with 12 visible tasks:
# docker.container.alpine.create, docker.container.alpine.run, docker.container.alpine.stop
# docker.container.ubuntu.create, docker.container.ubuntu.run, docker.container.ubuntu.stop
# docker.image.alpine.build, docker.image.alpine.pull
# docker.image.ubuntu.build, docker.image.ubuntu.pull, docker.image.ubuntu.push, docker.image.ubuntu.tag
# Depth 2 reached - no further grouping even though each subgroup has >3 tasks
# Depth 2 reached - no further grouping
# Same structure with max_groupsize=3, max_depth=3
# Now depth 3 is allowed, enabling finer grouping
How the Grouping Algorithm Works:
- Check depth limit: If
max_depthis 0, stop grouping immediately - Count visible tasks: If visible tasks ≤
max_groupsize, no grouping occurs - all tasks are displayed directly - Group by module prefix: Tasks are grouped by their next module level (e.g., all
git.*tasks together) - Recursive grouping: Each group is evaluated recursively with
max_depth - 1 - Single-task optimization: Groups containing only one visible task are automatically flattened into their parent group
Tip: Start with the default values. Decrease max_groupsize for more granular grouping, or decrease max_depth to prevent overly nested displays with deeply nested module structures.
Complete Example
use tusks;
Usage:
# Task overview
# Execute tasks (both syntaxes work)
# Display help
10. Command Attributes for Documentation
Use #[command()] attributes to define CLI documentation. This is a standard Clap feature and is mentioned here only as a common use case.
Usage:
)
11. Default Functions for Modules
With the #[default] attribute, you can define a function that is executed when a module is invoked without a specific subcommand.
Usage:
# Without subcommand - executes the default function
)
# Equivalent to
Restrictions for Default Functions
-
Only Parameters allowed: Default functions may have at most the
Parametersstruct of the current level as an argument. Additional parameters are not permitted:// ✓ Allowed // ✓ Allowed (without Parameters) // ❌ Not allowed -
Exception: Allow External Subcommands: If
allow_external_subcommands = trueis set for the module, the default function may additionally receive aVec<String>argument containing all arguments of the external subcommand:Usage:
# External subcommand (not defined) - default function with args # Built-in subcommand
Comprehensive Example
Here's a complete example demonstrating most of Tusks' features in a single application:
src/main.rs:
use tusks;
src/deploy.rs:
use tusks;
// External module
Usage examples:
# Top-level command
# Default command (status is called automatically)
# Explicit subcommand
# Migration with various argument types
# Nested submodule command
# External module with custom value parser
# Custom parser validation
# Rollback command
Note: This example demonstrates how arguments can be defined directly in function signatures. You can also define module-level parameters using a Parameters struct which would then be available to all commands within that module and automatically passed down to nested submodules. The parameters would be specified before the subcommand name:
# Example if database module had Parameters with --connection and --verbose:
For more details on how to define and use module-level parameters, see Module-Level Parameters.
Equivalent with Raw Clap Syntax
Here's what the same CLI structure would look like using Clap's derive API directly (abbreviated for brevity):
// All the handler functions need to be manually implemented
As you can see, Tusks eliminates:
- Manual enum definitions for every command level
- Repetitive match/dispatch logic
- Manual parameter passing through the hierarchy
- Boilerplate for default command handling
License
MIT
Contributions
Contributions are welcome! Please create an issue or pull request on GitHub.
I'm still learning Rust. I always have an open ear for suggestions on best practices, code style, etc.
I will try to respond to contributions, but as I work full-time with a wife and child, this will not always be possible in a timely manner.
Trivia
This project originally came about because I've wanted to learn and use Rust for a long time, and also because I was looking for a replacement for Ruby Rake and Python Invoke that is idiomatic, future-oriented, and easy to use. The use of these tools is also always limited by the environment you're in. Often, the versions of interpreters and packages differ across various server environments. A compiled Rust application is much more flexible in this regard. However, there are also disadvantages. The effort and barrier to creating and extending a compiled application is higher than with a simple Python script. And of course, the appropriate toolchain must be available on the system where you develop. But Rust makes it quite easy with cargo.
During development, I worked extensively with various AIs. Otherwise, this would not have been possible within a week with my existing basic knowledge, especially not for a project that relies so heavily on source code parsing and generation, i.e., the creation of macros. In some places, I performed some refactoring. In other places, I only superficially adapted or reviewed the code. Little code was actually written 100% by myself. This is an interesting experience for me, and you can certainly view it critically. However, I think I still learned a lot, and I'm actually quite satisfied with the code quality. If I had actually done everything myself, it would probably have turned out significantly worse (or simply not finished). But one should always keep in mind that I'm a Rust beginner. The way to write code in Rust, to structure it, the patterns used - all of this is definitely very different in many ways from many programming languages I've used before. Accordingly, this is a beginner's project.