Skip to content

Repository files navigation

FLI

A powerful, type-safe CLI library for Rust inspired by commander.js, designed to make building command-line applications intuitive and ergonomic.

Crates.ioDocumentationLicense: MIT

⚠️ BREAKING CHANGE in v1.2.0: ValueTypes::None has been removed! Use ValueTypes::OptionalSingle(Some(Value::Bool(false))) for flag options instead. See the Migration Guide for details.

Note: Version 1.0.0 included breaking changes. See the Migration Guide for upgrading from v0.x.

Features

  • 🎯 Type-safe value parsing with compile-time guarantees
  • 🌲 Hierarchical commands with subcommand support
  • 🎨 Beautiful help output with automatic formatting
  • 🔧 Flexible option types: flags, required/optional values, single/multiple values
  • 🚀 Zero-cost abstractions with minimal overhead
  • 📦 Cargo.toml integration for automatic metadata
  • Error handling with detailed, actionable messages

Quick Start

[dependencies]
fli = "1.0"
use fli::{Fli,ValueTypes,Value};fnmain(){letmut app = Fli::new("myapp","1.0.0","A sample CLI app");// Add a simple flag
app.add_option("verbose","Enable verbose output","-v","--verbose",ValueTypes::OptionalSingle(Some(Value::Bool(false))));// Add an option that requires a value
app.add_option("name","Your name","-n","--name",ValueTypes::RequiredSingle(Value::Str(String::new())));// Set the callback
app.set_callback(|data| {let name = data.get_option_value("name").and_then(|v| v.as_str()).unwrap_or("World");let verbose = data.get_option_value("verbose").is_some();println!("Hello, {}!", name);if verbose {println!("Verbose mode enabled!");}});
app.run();}

Run it:

$ cargo run -- -n Alice -v
Hello, Alice!
Verbose mode enabled!

Core Concepts

Value Types

Fli provides explicit value types for type-safe option parsing:

use fli::{ValueTypes,Value};// Flag option (no value)ValueTypes::OptionalSingle(Some(Value::Bool(false)))// Required single valueValueTypes::RequiredSingle(Value::Str(String::new()))ValueTypes::RequiredSingle(Value::Int(0))// Optional single value with defaultValueTypes::OptionalSingle(Some(Value::Int(8080)))ValueTypes::OptionalSingle(None)// Required multiple values (at least 1)ValueTypes::RequiredMultiple(vec![],None)// Exactly N values requiredValueTypes::RequiredMultiple(vec![],Some(3))// Optional multiple valuesValueTypes::OptionalMultiple(None,None)// Up to N valuesValueTypes::OptionalMultiple(None,Some(5))

Commands and Subcommands

Create hierarchical command structures:

use fli::{Fli,ValueTypes,Value};fnmain(){letmut app = Fli::new("git","2.0.0","Version control system");// Create a commandlet commit_cmd = app.command("commit","Record changes").unwrap();// Add options to the command
commit_cmd.add_option("message","Commit message","-m","--message",ValueTypes::RequiredSingle(Value::Str(String::new())));
commit_cmd.add_option("all","Commit all changes","-a","--all",ValueTypes::OptionalSingle(Some(Value::Bool(false))));// Set the command callback
commit_cmd.set_callback(|data| {let message = data.get_option_value("message").and_then(|v| v.as_str()).unwrap_or("No message");let all = data.get_option_value("all").is_some();println!("Committing with message: {}", message);if all {println!("Including all changes");}});
app.run();}

Accessing Values in Callbacks

The FliCallbackData provides convenient methods for accessing parsed values:

use fli::option_parser::{Value,ValueTypes};fnmy_callback(data:&FliCallbackData){// Get a single string valuelet name = data.get_option_value("name").and_then(|v| v.as_str()).unwrap_or("default");// Get multiple string valueslet files = data.get_option_value("files").and_then(|v| v.as_strings()).unwrap_or_default();// Check if a flag was passed (NEW in v1.2.0)// Flags use Bool values: false = not passed, true = passedlet verbose = data.get_option_value("verbose").map(|v| matches!(v,ValueTypes::OptionalSingle(Some(Value::Bool(true))))).unwrap_or(false);// Shorter alternative for flags:let is_flag_set = data.get_option_value("verbose").is_some();// Get positional argumentslet first_arg = data.get_argument_at(0);let all_args = data.get_arguments();// Access the commandlet cmd_name = data.get_command().get_name();}

Examples

Basic Calculator

use fli::{Fli,ValueTypes,Value};fnmain(){letmut app = Fli::new("calc","1.0.0","Simple calculator");let calc_cmd = app.command("calculate","Perform calculation").unwrap();
calc_cmd.add_option("operation","Operation to perform (add, sub, mul, div)","-o","--operation",ValueTypes::RequiredSingle(Value::Str(String::new())));
calc_cmd.set_expected_positional_args(2);
calc_cmd.set_callback(|data| {let op = data.get_option_value("operation").and_then(|v| v.as_str()).unwrap_or("add");let args = data.get_arguments();if args.len() < 2{eprintln!("Error: Need two numbers");return;}let a:f64 = args[0].parse().unwrap_or(0.0);let b:f64 = args[1].parse().unwrap_or(0.0);let result = match op {"add" => a + b,"sub" => a - b,"mul" => a * b,"div" => a / b,
_ => {eprintln!("Unknown operation: {}", op);return;}};println!("{} {} {} = {}", a, op, b, result);});
app.run();}

Usage:

$ calc calculate -o add 10 20
10 add 20 = 30

File Operations with Subcommands

use fli::{Fli,ValueTypes,Value};fnmain(){letmut app = Fli::new("file-tool","1.0.0","File operations");// File command with subcommandslet file_cmd = app.command("file","File operations").unwrap();// Copy subcommand
file_cmd.subcommand("copy","Copy files").add_option("source","Source files","-s","--source",ValueTypes::RequiredMultiple(vec![],None)).add_option("dest","Destination directory","-d","--dest",ValueTypes::RequiredSingle(Value::Str(String::new()))).set_callback(|data| {let sources = data.get_option_value("source").and_then(|v| v.as_strings()).unwrap_or_default();let dest = data.get_option_value("dest").and_then(|v| v.as_str()).unwrap_or(".");println!("Copying {:?} to {}", sources, dest);});// Move subcommand
file_cmd.subcommand("move","Move files").add_option("path","Files to move","-p","--path",ValueTypes::RequiredMultiple(vec![],None)).set_callback(|data| {let paths = data.get_option_value("path").and_then(|v| v.as_strings()).unwrap_or_default();println!("Moving: {:?}", paths);});
app.run();}

Usage:

$ file-tool file copy -s file1.txt file2.txt -d /backup
Copying ["file1.txt", "file2.txt"] to /backup
$ file-tool file move -p old1.txt old2.txt
Moving: ["old1.txt", "old2.txt"]

Greeting App with Options

use fli::{Fli,ValueTypes,Value};fnmain(){letmut app = Fli::new("greet","1.0.0","Greeting application");let greet_cmd = app.command("greet","Greet someone").unwrap();
greet_cmd.add_option("name","Name to greet","-n","--name",ValueTypes::RequiredSingle(Value::Str(String::new())));
greet_cmd.add_option("time","Time of day (morning, afternoon, evening)","-t","--time",ValueTypes::OptionalSingle(None));
greet_cmd.add_option("repeat","Number of times to repeat","-r","--repeat",ValueTypes::OptionalSingle(Some(Value::Int(1))));
greet_cmd.set_callback(|data| {let name = data.get_option_value("name").and_then(|v| v.as_str()).unwrap_or("friend");let time = data.get_option_value("time").and_then(|v| v.as_str()).unwrap_or("Hello");let greeting = match time {"morning" => "Good morning","afternoon" => "Good afternoon","evening" => "Good evening",
_ => "Hello",};let repeat = data.get_option_value("repeat").and_then(|v| match v {ValueTypes::OptionalSingle(Some(Value::Int(n))) => Some(*n),
_ => None,}).unwrap_or(1);for _ in0..repeat {println!("{}, {}!", greeting, name);}});
app.run();}

Usage:

$ greet greet -n Alice -t morning -r 2
Good morning, Alice!
Good morning, Alice!

Advanced Features

Custom Help Messages

Fli automatically generates beautiful help messages:

$ myapp --help
Command: myapp
A sample CLI application
Usage
myapp [SUBCOMMANDS] [ARGUMENT] [ARGUMENT] [OPTIONS]
[SUBCOMMANDS] [OPTIONS] -- [ARGUMENT] [ARGUMENT]
Options
┌───────┬────────────┬──────────────────────┬─────────────────────┐
│ Flag │ Long Form │ Value Type │ Description │
├───────┼────────────┼──────────────────────┼─────────────────────┤
│ -v │ --verbose │ none │ Enable verbose │
│ -n │ --name │ single (required) │ Your name │
└───────┴────────────┴──────────────────────┴─────────────────────┘

Debug Mode

Enable debug output to see internal parsing details:

let app = Fli::new("myapp","1.0.0","Description").with_debug();// Or add a debug flag
app.add_debug_option();

Error Handling

Commands return Result<&mut FliCommand> for better error handling:

use fli::Result;fnmain() -> Result<()>{letmut app = Fli::new("myapp","1.0.0","Description");let cmd = app.command("serve","Start server")?;
cmd.add_option("port","Port to bind","-p","--port",ValueTypes::RequiredSingle(Value::Int(8080)));
app.run();Ok(())}

Cargo.toml Integration

Initialize from your Cargo.toml metadata:

use fli::init_fli_from_toml;fnmain(){letmut app = init_fli_from_toml!();// App name, version, and description are automatically loaded// from your Cargo.toml file
app.run();}

Migration from 0.x

Version 1.0.0 includes significant improvements but breaks backwards compatibility. Key changes:

Option Syntax

Before (v0.x):

app.option("-n --name, <>","Your name", callback);

After (v1.0):

app.add_option("name","Your name","-n","--name",ValueTypes::RequiredSingle(Value::Str(String::new())));
app.set_callback(callback);

Value Types

v0.xv1.0
(no symbol)ValueTypes::OptionalSingle(Some(Value::Bool(false)))
<>ValueTypes::RequiredSingle(_)
[]ValueTypes::OptionalSingle(_)
<...>ValueTypes::RequiredMultiple(vec![], None)
[...]ValueTypes::OptionalMultiple(None, None)

Callbacks

Before (v0.x):

fncallback(app:&Fli){let name = app.get_values("name".to_owned()).unwrap()[0];}

After (v1.0):

fncallback(data:&FliCallbackData){let name = data.get_option_value("name").and_then(|v| v.as_str()).unwrap_or("default");}

Commands

Before (v0.x):

let cmd = app.command("serve","Start server");

After (v1.0):

let cmd = app.command("serve","Start server")?;// Returns Result

For a complete migration guide, see MIGRATION.md.

Migration from ValueTypes::None

⚠️ Breaking Change in v1.2.0

ValueTypes::None has been removed to fix a critical design flaw where you couldn't distinguish between "flag was defined" and "flag was passed".

Quick Fix

Before (v1.1 and earlier):

app.add_option("verbose","Enable verbose output","-v","--verbose",ValueTypes::None// ❌ Removed in v1.2.0);

After (v1.2.0+):

use fli::option_parser::{Value,ValueTypes};
app.add_option("verbose","Enable verbose output","-v","--verbose",ValueTypes::OptionalSingle(Some(Value::Bool(false)))// ✅ Use Bool(false) as default);

Checking if a Flag was Passed

Before (v1.1 - didn't work correctly):

// This would always be true if the option was defined!let verbose = data.get_option_value("verbose").is_some();

After (v1.2.0 - works correctly):

// Method 1: Check the boolean valuelet verbose = data.get_option_value("verbose").map(|v| matches!(v,ValueTypes::OptionalSingle(Some(Value::Bool(true))))).unwrap_or(false);// Method 2: Simpler (still works, checks if value was updated)let verbose = data.get_option_value("verbose").is_some();

Why the Change?

With ValueTypes::None:

  • ❌ Could not tell if -v was passed or not
  • get_option_value() always returned Some for defined options
  • ❌ Had to search command chain to check flag usage

With ValueTypes::OptionalSingle(Some(Value::Bool(...))):

  • Bool(false) = flag not passed (default)
  • Bool(true) = flag was passed
  • ✅ Can query option parser directly
  • ✅ Clear, idiomatic Rust

Documentation

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

About

A library for creating CLI apps in rust inspired by the like of commander.js

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages