Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Flaggy Contribution Guidelines for Agents

This repository provides a zero-dependency command-line parsing library that must always rely exclusively on the Go standard library for runtime and test code. The core principles that follow preserve the project's lightweight nature and its focus on easily understandable, flat code.

## Core Principles
- Keep the codebase dependency-free beyond the Go standard library. Adding third-party modules for any purpose, including testing, is not permitted.
- Prefer flat, straightforward control flow. Avoid `else` statements when possible and limit indentation depth so examples remain approachable for beginners.
- Optimize every change for readability. Favor descriptive names, small logical blocks, and explanatory comments that teach users how the parser behaves.

## Documentation Expectations
- Maintain clear, beginner-friendly explanations throughout the codebase. Add comments to **every** function and test describing what they do and why they matter to the overall library.
- Annotate each stanza of code with concise comments, even when the logic appears self-explanatory.
- Keep primary documentation accurate. Update `README.md` and `CONTRIBUTING.md` whenever your modifications alter usage instructions, contribution workflows, or observable behavior.

## Tooling Requirements
- Always run `go fmt`, `go vet`, and `goimports` over affected packages before committing.
- Favor consistent formatting and import organization that highlight the minimal surface area of each example.

## Testing Guidance
- When writing tests, ensure the accompanying comment explains exactly what is being verified.
- Leave benchmarks and examples with clarifying comments so readers immediately understand the intent and scope of each scenario.

Following these guidelines keeps Flaggy's codebase welcoming to newcomers and aligned with its lightweight philosophy.
174 changes: 174 additions & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package flaggy

import (
"testing"
"time"
)

// BenchmarkParseSingleFlag measures parsing a single string flag so we can ensure
// the simplest CLI scenario stays efficient for users building tiny utilities.
func BenchmarkParseSingleFlag(b *testing.B) {
// Track allocations to verify the parser remains lightweight.
b.ReportAllocs()

// Provide a minimal argument slice that exercises a lone string flag.
args := []string{"--name", "benchmark"}

// Execute the parse flow repeatedly to gather benchmark statistics.
for i := 0; i < b.N; i++ {
// Reset the parser so each iteration starts from a clean state.
ResetParser()

// Capture the string flag value during parsing.
var name string

// Declare the flag that the benchmark will parse.
String(&name, "n", "name", "collects a name for benchmarking")

// Parse the synthetic arguments.
ParseArgs(args)

// Fail fast if the parsed value is not the expected string.
if name != "benchmark" {
b.Fatalf("expected name to equal benchmark, got %q", name)
}
}
}

// BenchmarkParseSubcommandWithTwoFlags tracks the performance of parsing a
// subcommand that defines two flags so teams can gauge realistic CLI workloads.
func BenchmarkParseSubcommandWithTwoFlags(b *testing.B) {
// Record allocations during the benchmark to detect regressions.
b.ReportAllocs()

// Arrange a subcommand invocation with host and port flags populated.
args := []string{"serve", "--host", "localhost", "--port", "8080"}

// Drive the parser repeatedly to gather benchmark metrics.
for i := 0; i < b.N; i++ {
// Reset state so each run configures fresh flag bindings.
ResetParser()

// Create the subcommand and variables that store parsed values.
serve := NewSubcommand("serve")
var host string
var port int

// Declare the host and port flags on the subcommand.
serve.String(&host, "h", "host", "host name to bind to")
serve.Int(&port, "p", "port", "port to listen on")

// Attach the subcommand so the parser can discover it.
AttachSubcommand(serve, 1)

// Parse the prepared argument slice.
ParseArgs(args)

// Ensure the subcommand was detected and values were populated.
if !serve.Used {
b.Fatal("expected serve subcommand to be used")
}
if host != "localhost" || port != 8080 {
b.Fatalf("unexpected host %q or port %d", host, port)
}
}
}

// BenchmarkParseNestedSubcommandsWithTrailingArgs covers a complex hierarchy of
// three subcommands, mixed flag types, and trailing arguments to validate that
// rich CLIs remain performant.
func BenchmarkParseNestedSubcommandsWithTrailingArgs(b *testing.B) {
// Track allocation counts while exercising the full parser feature set.
b.ReportAllocs()

// Define arguments that walk through alpha -> beta -> gamma subcommands
// and include trailing values after a -- separator.
args := []string{
"alpha", "--name", "alpha-task", "--count", "3", "--enabled", "--speed", "1.5", "--timeout", "500ms",
"beta", "--label", "release", "--label", "candidate", "--level", "9", "--active", "--ratio", "1.75", "--interval", "45s",
"gamma", "--mode", "auto", "--max", "100", "--debug", "--threshold", "0.8", "--window", "1m30s",
"--", "artifact-one", "artifact-two", "artifact-three",
}

// Loop to exercise the parser repeatedly during the benchmark.
for i := 0; i < b.N; i++ {
// Reset state before wiring up the subcommand hierarchy again.
ResetParser()

// Prepare variables that will hold parsed values from each layer.
var (
alphaName string
alphaCount int
alphaEnabled bool
alphaSpeed float64
alphaTimeout time.Duration

betaLabels []string
betaLevel int
betaActive bool
betaRatio float32
betaInterval time.Duration

gammaMode string
gammaMax uint
gammaDebug bool
gammaThreshold float64
gammaWindow time.Duration
)

// Configure the alpha subcommand with mixed flag types.
alpha := NewSubcommand("alpha")
alpha.String(&alphaName, "n", "name", "name for the alpha subcommand")
alpha.Int(&alphaCount, "c", "count", "number of repetitions")
alpha.Bool(&alphaEnabled, "e", "enabled", "whether alpha is enabled")
alpha.Float64(&alphaSpeed, "s", "speed", "speed multiplier")
alpha.Duration(&alphaTimeout, "t", "timeout", "how long alpha should wait")
AttachSubcommand(alpha, 1)

// Configure the beta subcommand, including slice and duration flags.
beta := NewSubcommand("beta")
beta.StringSlice(&betaLabels, "l", "label", "labels to apply")
beta.Int(&betaLevel, "v", "level", "an integer setting")
beta.Bool(&betaActive, "a", "active", "whether beta is active")
beta.Float32(&betaRatio, "r", "ratio", "ratio value for calculations")
beta.Duration(&betaInterval, "i", "interval", "time between runs")
alpha.AttachSubcommand(beta, 1)

// Configure the gamma subcommand to complete the hierarchy.
gamma := NewSubcommand("gamma")
gamma.String(&gammaMode, "m", "mode", "mode of operation")
gamma.UInt(&gammaMax, "x", "max", "maximum allowed value")
gamma.Bool(&gammaDebug, "d", "debug", "enable debug logging")
gamma.Float64(&gammaThreshold, "h", "threshold", "threshold used for alerts")
gamma.Duration(&gammaWindow, "w", "window", "time window to inspect")
beta.AttachSubcommand(gamma, 1)

// Parse the synthetic arguments that exercise the full tree.
ParseArgs(args)

// Assert that each subcommand was used during parsing.
if !alpha.Used || !beta.Used || !gamma.Used {
b.Fatalf("expected alpha, beta, gamma to be used but got alpha=%t beta=%t gamma=%t", alpha.Used, beta.Used, gamma.Used)
}

// Confirm the beta subcommand gathered labels and the active flag.
if len(betaLabels) != 2 || !betaActive {
b.Fatalf("expected beta labels and active flag to be parsed correctly")
}

// Confirm gamma parsed every value correctly, including numeric caps.
if !gammaDebug || gammaMode == "" || gammaMax != 100 {
b.Fatalf("gamma values not parsed as expected: debug=%t mode=%q max=%d", gammaDebug, gammaMode, gammaMax)
}

// Confirm trailing arguments flowed through after the -- separator.
if len(TrailingArguments) != 3 {
b.Fatalf("expected 3 trailing arguments, got %d", len(TrailingArguments))
}

// Validate every alpha flag to ensure full coverage of parsed values.
if alphaName == "" || alphaCount != 3 || !alphaEnabled || alphaTimeout != 500*time.Millisecond {
b.Fatalf("alpha values not parsed correctly")
}
}
}
62 changes: 62 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,65 @@ func ExampleSubcommand() {
// Subcommand Positional: subcommandPositional
// Trailing variable 1: trailingVar
}

// ExampleTrailingArguments shows how to read values that appear after the
// double-dash (--) separator. Any arguments after -- are stored as trailing
// arguments so you can forward them to another command or treat them as raw
// input.
func ExampleTrailingArguments() {

// Reset the parser and provide some beginner-friendly help text so anyone
// running `./app --help` understands what this example demonstrates.
flaggy.ResetParser()
flaggy.DefaultParser.Description = "Collects file names that appear after -- as trailing arguments."
flaggy.DefaultParser.AdditionalHelpPrepend = "Usage: ./app -- <file> [<file> ...]"
flaggy.DefaultParser.AdditionalHelpAppend = "Example: ./app -- notes.txt todo.md"

// Pretend the CLI input looks like `./app -- notes.txt todo.md`.
os.Args = []string{"app", "--", "notes.txt", "todo.md"}

// Parse the input arguments from the OS (os.Args).
flaggy.Parse()

// Trailing arguments are available even if you did not define explicit
// positional values or flags for them.
fmt.Printf("Trailing arguments (%d):\n", len(flaggy.TrailingArguments))
for index, argument := range flaggy.TrailingArguments {
fmt.Printf(" %d. %s\n", index+1, argument)
}
// Output:
// Trailing arguments (2):
// 1. notes.txt
// 2. todo.md
}

// ExampleStringSlice shows how to gather repeated flag values into a slice of
// strings. This example fulfills the "ExampleSliceString" request by showing
// how repeated string flags build a slice of values.
func ExampleStringSlice() {

// Reset the parser and describe the command so that --help output is clear
// for new users experimenting with the example.
flaggy.ResetParser()
flaggy.DefaultParser.Description = "Stores every -a/--add value in a slice."
flaggy.DefaultParser.AdditionalHelpPrepend = "Usage: ./app -a <value> [-a <value> ...]"
flaggy.DefaultParser.AdditionalHelpAppend = "Example: ./app -a test -a another -a again"

// Simulate invoking the binary as `./app -a test -a another -a again`.
os.Args = []string{"app", "-a", "test", "-a", "another", "-a", "again"}

var sliceFlag []string
flaggy.StringSlice(&sliceFlag, "a", "add", "Collect values for this slice")

flaggy.Parse()

fmt.Printf("String slice has %d items:\n", len(sliceFlag))
for index, value := range sliceFlag {
fmt.Printf(" %d. %s\n", index+1, value)
}
// Output:
// String slice has 3 items:
// 1. test
// 2. another
// 3. again
}
Loading