From 96d546a5873bcee176a284e42006814e6892f7fc Mon Sep 17 00:00:00 2001 From: Eric Greer Date: Fri, 19 Sep 2025 00:44:26 -0700 Subject: [PATCH] Document benchmarks and add agent guidelines --- AGENTS.md | 23 ++++++ benchmark_test.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++ examples_test.go | 62 +++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 AGENTS.md create mode 100644 benchmark_test.go diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e521055 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000..eb4838f --- /dev/null +++ b/benchmark_test.go @@ -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") + } + } +} diff --git a/examples_test.go b/examples_test.go index 8e5a4f1..effb272 100644 --- a/examples_test.go +++ b/examples_test.go @@ -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 -- [ ...]" + 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 [-a ...]" + 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 +}