Introduction
Cobra, the popular Go CLI library, provides MarkFlagRequired() to enforce that certain flags are provided by the user. However, the error messages, validation timing, and interaction with subcommands can be confusing. When required flag validation is not configured correctly, commands execute with missing configuration values, leading to cryptic downstream errors. Proper validation requires understanding when Cobra checks required flags (between PersistentPreRun and Run), how to add custom validation beyond presence checks, and how to provide helpful error messages.
Symptoms
Command runs with missing flags and fails with a cryptic error:
$ myapp deploy
Error: database connection string is emptyInstead of the expected:
$ myapp deploy
Error: required flag(s) "database-url" not setOr validation runs too late, after expensive setup:
$ myapp deploy --env production
Setting up infrastructure...
Creating resources...
Error: required flag "region" not setCommon Causes
- MarkFlagRequired not called: The flag is defined but not marked as required
- Required flag check happens after PreRun: Custom PreRun runs before required flag validation
- Persistent flags vs local flags confusion:
MarkFlagRequiredon a persistent flag must be called on the root command - Custom validation not integrated: Required check only verifies presence, not value validity
- Subcommand flag inheritance: Flags defined on parent command not automatically required on child
- Flag default value bypasses required check: A default value satisfies the required check even when the user did not provide one
Step-by-Step Fix
Step 1: Use MarkFlagRequired correctly
```go var rootCmd = &cobra.Command{ Use: "myapp", Short: "My CLI application", }
var deployCmd = &cobra.Command{ Use: "deploy", Short: "Deploy the application", RunE: runDeploy, }
func init() { deployCmd.Flags().String("database-url", "", "Database connection URL") deployCmd.Flags().String("region", "", "Deployment region") deployCmd.Flags().String("env", "", "Environment name")
// Mark flags as required deployCmd.MarkFlagRequired("database-url") deployCmd.MarkFlagRequired("region") deployCmd.MarkFlagRequired("env")
rootCmd.AddCommand(deployCmd) } ```
Now the error is clear:
$ myapp deploy
Error: required flag(s) "database-url", "env", "region" not setStep 2: Add custom validation with PreRunE
```go var deployCmd = &cobra.Command{ Use: "deploy", Short: "Deploy the application", PreRunE: func(cmd *cobra.Command, args []string) error { // Validate flag values beyond presence region, _ := cmd.Flags().GetString("region") validRegions := map[string]bool{ "us-east-1": true, "us-west-2": true, "eu-west-1": true, } if !validRegions[region] { return fmt.Errorf("invalid region %q, must be one of: us-east-1, us-west-2, eu-west-1", region) }
env, _ := cmd.Flags().GetString("env") if env != "staging" && env != "production" { return fmt.Errorf("invalid environment %q, must be staging or production", env) }
return nil }, RunE: runDeploy, } ```
Cobra runs required flag checks before PreRunE, so missing flags are caught first.
Step 3: Use PersistentPreRunE for shared validation
```go var rootCmd = &cobra.Command{ Use: "myapp", Short: "My CLI application", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // Validate flags shared across all subcommands configPath, _ := cmd.Flags().GetString("config") if configPath != "" { if _, err := os.Stat(configPath); os.IsNotExist(err) { return fmt.Errorf("config file not found: %s", configPath) } } return nil }, }
func init() { rootCmd.PersistentFlags().String("config", "", "Path to config file") } ```
Prevention
- Always pair
Flags().String()withMarkFlagRequired()for mandatory flags - Use
RunEinstead ofRunto return errors that Cobra formats properly - Add
PreRunEfor value validation (format, range, existence) beyond presence checks - Use
MarkFlagFilename("config", "yaml", "yml")for file path flags - Test CLI commands in your test suite using
ExecuteCwith missing flags - Document required flags in the command's
Longdescription and--helpoutput