8.0 KiB
8.0 KiB
Builder Pattern for Argument Parsing
Summary
The builder pattern uses method chaining to programmatically construct the argument parser configuration. Instead of declaring everything in a static schema or struct, you call a series of methods that each add one piece of configuration, returning the builder object so you can chain the next call.
Think of it like building with LEGO blocks - you start with a base and keep adding pieces one at a time.
Core Concept
parser = new Parser()
.addArg(...)
.addArg(...)
.addArg(...)
.parse()
Each .addArg() returns the parser object, so you can keep chaining.
Concrete Examples
Example 1: Simple CLI Tool (Rust-style with clap)
use clap::{App, Arg};
fn main() {
let matches = App::new("MyApp")
.version("1.0")
.author("John Doe")
.about("Does awesome things")
.arg(Arg::new("verbose")
.short('v')
.long("verbose")
.help("Enable verbose output"))
.arg(Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.help("Output file path")
.takes_value(true)
.required(false))
.arg(Arg::new("count")
.short('n')
.long("count")
.value_name("NUM")
.help("Number of iterations")
.takes_value(true)
.default_value("1")
.validator(|s| s.parse::<u32>().map(|_| ()).map_err(|_| "Must be a number")))
.arg(Arg::new("config")
.short('c')
.long("config")
.value_name("PATH")
.help("Config file path")
.takes_value(true)
.conflicts_with("output"))
.get_matches();
// Use the parsed arguments
let verbose = matches.is_present("verbose");
let output = matches.value_of("output");
let count: u32 = matches.value_of_t("count").unwrap();
}
Example 2: Hypothetical Zig Builder Style
const std = @import("std");
const ArgParser = @import("zargs").ArgParser;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Build the parser with chained calls
var parser = ArgParser.init(allocator)
.name("mytool")
.version("1.0.0")
.description("Does awesome things")
.flag("verbose")
.short('v')
.long("verbose")
.help("Enable verbose output")
.done()
.option("output")
.short('o')
.long("output")
.help("Output file path")
.value_name("FILE")
.required(false)
.done()
.option("count")
.short('n')
.long("count")
.help("Number of iterations")
.value_name("NUM")
.default_value("1")
.value_parser(parseU32)
.done()
.option("config")
.short('c')
.long("config")
.help("Config file path")
.value_name("PATH")
.conflicts_with(&.{"output"})
.done();
// Parse the arguments
const args = try parser.parse();
// Access the results
const verbose = args.getFlag("verbose");
const output = args.getString("output");
const count = args.getInt("count") orelse 1;
}
fn parseU32(s: []const u8) !u32 {
return std.fmt.parseInt(u32, s, 10);
}
Example 3: Java-style with JCommander
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
public class MyApp {
@Parameter(names = {"-v", "--verbose"}, description = "Enable verbose output")
private boolean verbose = false;
@Parameter(names = {"-o", "--output"}, description = "Output file path")
private String output;
@Parameter(names = {"-n", "--count"}, description = "Number of iterations")
private int count = 1;
public static void main(String[] args) {
MyApp app = new MyApp();
// Builder pattern for the parser itself
JCommander commander = JCommander.newBuilder()
.addObject(app)
.programName("myapp")
.build();
commander.parse(args);
// Use the parsed values
System.out.println("Verbose: " + app.verbose);
System.out.println("Output: " + app.output);
System.out.println("Count: " + app.count);
}
}
Example 4: C++ with cxxopts
#include <cxxopts.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
cxxopts::Options options("MyApp", "Does awesome things");
// Builder pattern for adding options
options
.add_options()
("v,verbose", "Enable verbose output")
("o,output", "Output file path",
cxxopts::value<std::string>())
("n,count", "Number of iterations",
cxxopts::value<int>()->default_value("1"))
("c,config", "Config file path",
cxxopts::value<std::string>())
("h,help", "Print help");
auto result = options.parse(argc, argv);
if (result.count("help")) {
std::cout << options.help() << std::endl;
return 0;
}
bool verbose = result["verbose"].as<bool>();
std::string output = result["output"].as<std::string>();
int count = result["count"].as<int>();
}
Key Characteristics
Fluent Interface
Each method returns self (or the builder) so you can chain:
builder.method1().method2().method3()
Incremental Construction
Build up the configuration step by step:
var parser = ArgParser.init(allocator);
parser = parser.name("mytool");
parser = parser.version("1.0");
// ... etc
Nested Builders
Often there's a hierarchy:
parser
.option("output") // Start building an option
.short('o') // Configure the option
.long("output") // More config
.help("...") // More config
.done() // Return to parent parser
.option("count") // Start next option
.short('n')
.done()
Advantages for Zig
- No macros needed - Pure runtime construction
- Conditional arguments - Easy to add args based on runtime conditions:
var parser = ArgParser.init(allocator); if (enable_debug_features) { parser = parser.flag("trace").help("Enable tracing").done(); } - Type-safe - Compiler checks method calls
- Readable - Sequential, easy to follow
- Still generates help - All metadata collected during building
Disadvantages
- Verbose - More code than declarative style
- Boilerplate - Lots of repeated method calls
- No compile-time validation - Errors happen at runtime
- Memory overhead - Must allocate storage for builder state
When to Use
- When you need runtime flexibility in argument definition
- When you want good help generation but can't use macros/comptime
- When arguments depend on configuration or conditional compilation
- When you prefer explicit, procedural code over declarative schemas
Comparison to Other Styles
| Feature | Builder | Declarative | Ad-hoc |
|---|---|---|---|
| Help generation | ✅ Good | ✅ Excellent | ❌ Poor |
| Flexibility | ✅ Good | ❌ Poor | ✅ Excellent |
| Verbosity | ⚠️ Moderate | ✅ Low | ✅ Very Low |
| Runtime overhead | ⚠️ Moderate | ⚠️ Moderate | ✅ Minimal |
| Type safety | ✅ Good | ✅ Excellent | ❌ Poor |
Builder Pattern in Zig Context
Zig could make this pattern very clean with:
- Method chaining (returning
*Self) - Comptime validation of method call sequences
- Tagged unions for storing different arg types
- Allocator control for builder state
The sweet spot might be a builder pattern that's mostly runtime but validates at comptime when possible.