282 lines
8.0 KiB
Markdown
282 lines
8.0 KiB
Markdown
# 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)
|
|
|
|
```rust
|
|
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
|
|
|
|
```zig
|
|
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
|
|
|
|
```java
|
|
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
|
|
|
|
```cpp
|
|
#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:
|
|
```zig
|
|
var parser = ArgParser.init(allocator);
|
|
parser = parser.name("mytool");
|
|
parser = parser.version("1.0");
|
|
// ... etc
|
|
```
|
|
|
|
### Nested Builders
|
|
Often there's a hierarchy:
|
|
```zig
|
|
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
|
|
|
|
1. **No macros needed** - Pure runtime construction
|
|
2. **Conditional arguments** - Easy to add args based on runtime conditions:
|
|
```zig
|
|
var parser = ArgParser.init(allocator);
|
|
if (enable_debug_features) {
|
|
parser = parser.flag("trace").help("Enable tracing").done();
|
|
}
|
|
```
|
|
3. **Type-safe** - Compiler checks method calls
|
|
4. **Readable** - Sequential, easy to follow
|
|
5. **Still generates help** - All metadata collected during building
|
|
|
|
## Disadvantages
|
|
|
|
1. **Verbose** - More code than declarative style
|
|
2. **Boilerplate** - Lots of repeated method calls
|
|
3. **No compile-time validation** - Errors happen at runtime
|
|
4. **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.
|