How to Parse Arguments for Commands in gastownhall/gastown
Gastown uses the Cobra library to parse command-line arguments through structured command definitions, flag registration, and positional argument validators in internal/cmd/*.go files.
The gastownhall/gastown repository implements a hierarchical CLI (gt) built on the Cobra framework, which provides a robust pattern for parsing arguments for commands. Understanding how this architecture handles flags, positional arguments, and external command forwarding is essential for extending the tool or debugging command execution.
Cobra-Based Command Architecture
Gastown’s CLI relies on the standard Cobra pattern where each command is defined as a *cobra.Command struct. This structure determines how to parse arguments for commands by declaring four key elements: Use for syntax documentation, Args for positional argument validation, Flags() for named options, and Run for execution logic.
Command Structure and File Locations
In internal/cmd/witness.go, the witness start command demonstrates the standard declaration pattern:
var witnessStartCmd = &cobra.Command{
Use: "witness start",
Short: "Start the witness agent",
Args: cobra.NoArgs, // Validates zero positional arguments
Run: func(cmd *cobra.Command, args []string) error {
return runWitnessStart(cmd, args)
},
}
The entry point in cmd/gt/main.go initializes the command tree and executes the parser, while individual command definitions reside in files like internal/cmd/witness.go, internal/cmd/version.go, and others within the internal/cmd/ directory.
Registering and Retrieving Flags
Flags are registered before command execution and automatically populated by Cobra during the parsing phase. This separates definition from consumption and ensures type safety.
Boolean and String Flags
In internal/cmd/witness.go, flags attach directly to Go variables through pointer binding:
var witnessForeground bool
var witnessAgentOverride string
func init() {
witnessStartCmd.Flags().BoolVar(&witnessForeground, "foreground", false,
"Run in foreground (default: background)")
witnessStartCmd.Flags().StringVar(&witnessAgentOverride, "agent", "",
"Agent alias to run the Witness with (overrides town default)")
}
When runWitnessStart executes, these variables are already populated with parsed values from the command line.
String Array Flags for Repeated Values
For flags that accept multiple values, such as environment variable overrides, Gastown uses StringArrayVar:
var witnessEnvOverrides []string
witnessStartCmd.Flags().StringArrayVar(&witnessEnvOverrides, "env", nil,
"Environment variable override (KEY=VALUE, can be repeated)")
Users can invoke this with multiple --env flags, and Cobra aggregates them into the slice before the handler runs.
Handling Positional Arguments
Positional arguments are collected after flags and validated according to the Args field configuration.
Argument Validation
Cobra provides built-in validators that enforce argument count during the parsing phase:
- cobra.ExactArgs(n) – Requires exactly n arguments
- cobra.NoArgs – Requires zero arguments
- cobra.MinimumNArgs(n) – Requires at least n arguments
The validation occurs automatically before the Run function executes, ensuring handlers receive the expected argument slice length.
Forwarding Arguments to External Tools
Gastown frequently forwards flags to downstream tools like bd or git, requiring careful argument construction to prevent flag misinterpretation.
The Flags-First Convention
According to the source code in internal/web/setup.go (lines 147-150), Gastown follows a "flags-first, then --, then positional path" pattern. This mirrors the POSIX -- separator that stops flag parsing.
The comment explicitly states: "Build gt install command. Flags go first, then -- to end flag parsing, then the positional path (prevents paths like --help being parsed as flags)."
Building External Commands
When constructing commands for external execution, Gastown assembles slices in the required order:
func buildInstallCmd(flags []string, path string) []string {
// flags (e.g. --json) must precede the `--` separator
// after `--` everything is treated as a literal argument.
return append(append([]string{"gt", "install"}, flags...), "--", path)
}
This pattern ensures that paths starting with hyphens (like --help) are treated as literal arguments rather than flags. The assembled slice is then passed to exec.Command via utilities in internal/util/exec.go.
Wrapper Binary Handling
For agents launched through wrappers like env, sudo, or nohup, Gastown must extract the real binary name from the wrapper's argument list.
Parsing Wrapped Arguments
The function extractWrappedBinary in internal/config/agents.go implements custom flag parsing logic that mirrors Cobra's behavior:
func extractWrappedBinary(wrapper string, args []string) string {
flagsTakeValue := wrapperFlagsTakeValue(wrapper)
for i := 0; i < len(args); i++ {
a := args[i]
if a == "--" { // end-of-options marker
if i+1 < len(args) {
return filepath.Base(args[i+1])
}
return ""
}
if strings.HasPrefix(a, "-") && a != "-" {
if flagsTakeValue[a] && i+1 < len(args) {
i++ // skip the flag's argument
}
continue
}
return filepath.Base(a) // first non-flag token = real binary
}
return ""
}
This logic handles short flags that take values (skipping the next token), respects the -- separator, and identifies the first non-flag argument as the actual binary name.
Summary
- Cobra command structs in
internal/cmd/*.godeclare command syntax, validators, and flag bindings. - Flag registration via
Flags().*Varmethods automatically populates Go variables during parsing. - Argument validation uses
cobra.ExactArgs,cobra.NoArgs, and similar validators to enforce positional argument requirements. - External command builders in
internal/web/setup.gouse the "flags--path" pattern to safely forward arguments to sub-tools. - Wrapper handling in
internal/config/agents.goparses complex argument lists to identify real binary names behind wrapper commands.
Frequently Asked Questions
How does Gastown handle flags that can be repeated multiple times?
Gastown uses StringArrayVar for flags that accept multiple values, such as the --env flag in witness start. This binds repeated flag instances to a []string slice, allowing users to specify --env KEY1=VAL1 --env KEY2=VAL2. The Cobra library automatically aggregates these into the bound variable before the command handler executes.
What prevents file paths from being parsed as flags when forwarding commands?
Gastown inserts a -- separator between flags and positional arguments when building external commands (as seen in internal/web/setup.go). This POSIX-compliant separator forces the parser to treat all subsequent tokens as positional arguments, ensuring paths like --help or --config are passed literally to the downstream tool rather than being interpreted as flags.
Where does argument validation occur in the Gastown CLI?
Argument validation occurs in the Args field of the cobra.Command struct definition within files like internal/cmd/witness.go. Validators such as cobra.ExactArgs(1) or cobra.NoArgs execute automatically after Cobra parses the command line but before the Run function begins, ensuring handlers receive the correct argument count.
How does Gastown extract the real binary when using wrapper commands like sudo?
The extractWrappedBinary function in internal/config/agents.go scans the wrapper's argument list, skips any flags (and their values if applicable), stops processing if it encounters a -- separator, and returns the first non-flag argument as the actual binary name. This allows process monitoring to identify the real application regardless of wrapper invocations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →