Execution Hooks Guide
The goopt v2 execution hooks feature provides pre- and post-execution hooks for command lifecycle management, enabling cross-cutting concerns such as logging, authentication, and cleanup.
Overview
Execution hooks allow you to:
- Run code before commands execute (pre-hooks)
- Run code after commands execute (post-hooks)
- Handle errors and cleanup consistently
- Implement cross-cutting concerns without modifying command logic
Hook Types
Pre-Execution Hooks
Run before command execution. If a pre-hook returns an error, the command is not executed.
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
// Runs before any command
log.Printf("Executing: %s", c.Path())
return nil
})
Post-Execution Hooks
Run after command execution, even if the command fails. Receive the command’s error (if any).
parser.AddGlobalPostHook(func(p *goopt.Parser, c *goopt.Command, cmdErr error) error {
// Always runs after command
if cmdErr != nil {
log.Printf("Command failed: %v", cmdErr)
}
return cleanup()
})
Scope
Global Hooks
Apply to all commands:
// Global pre-hook
parser.AddGlobalPreHook(authCheck)
// Global post-hook
parser.AddGlobalPostHook(logMetrics)
Command-Specific Hooks
Apply only to specific commands:
// Pre-hook for "db backup" command
parser.AddCommandPreHook("db backup", validateBackupConfig)
// Post-hook for "server start" command
parser.AddCommandPostHook("server start", notifyServerStarted)
Configuration
Using With Functions
parser, err := goopt.NewParserWith(
goopt.WithGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
return authenticate()
}),
goopt.WithGlobalPostHook(func(p *goopt.Parser, c *goopt.Command, err error) error {
return logExecution(c, err)
}),
goopt.WithCommandPreHook("deploy", validateDeployment),
goopt.WithHookOrder(goopt.OrderGlobalFirst),
)
Hook Execution Order
Control whether global or command-specific hooks run first:
// Global hooks run first (default)
parser.SetHookOrder(goopt.OrderGlobalFirst)
// Command hooks run first
parser.SetHookOrder(goopt.OrderCommandFirst)
For cleanup, post-hooks run in reverse order of pre-hooks.
Common Use Cases
1. Authentication/Authorization
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
// Skip auth for public commands
if isPublicCommand(c.Name) {
return nil
}
// Check authentication
if !isAuthenticated() {
return errors.New("authentication required")
}
// Check authorization
if !isAuthorized(c.Path()) {
return errors.New("permission denied")
}
return nil
})
2. Logging and Telemetry
// Track command execution
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
log.Printf("[START] Command: %s, User: %s", c.Path(), currentUser())
telemetry.StartSpan(c.Path())
return nil
})
parser.AddGlobalPostHook(func(p *goopt.Parser, c *goopt.Command, err error) error {
duration := telemetry.EndSpan(c.Path())
if err != nil {
log.Printf("[ERROR] Command: %s, Error: %v, Duration: %v",
c.Path(), err, duration)
metrics.IncrementErrors(c.Path())
} else {
log.Printf("[SUCCESS] Command: %s, Duration: %v",
c.Path(), duration)
metrics.IncrementSuccess(c.Path())
}
return nil
})
3. Resource Management
var dbConn *sql.DB
// Ensure database connection
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
if needsDatabase(c) {
conn, err := connectDB()
if err != nil {
return fmt.Errorf("database connection failed: %w", err)
}
dbConn = conn
}
return nil
})
// Cleanup resources
parser.AddGlobalPostHook(func(p *goopt.Parser, c *goopt.Command, err error) error {
if dbConn != nil {
dbConn.Close()
dbConn = nil
}
closeOpenFiles()
releaseLocks()
return nil
})
4. Transaction Management
// Start transaction for write operations
parser.AddCommandPreHook("db update", func(p *goopt.Parser, c *goopt.Command) error {
tx, err := db.Begin()
if err != nil {
return err
}
setTransaction(tx)
return nil
})
// Commit or rollback based on result
parser.AddCommandPostHook("db update", func(p *goopt.Parser, c *goopt.Command, err error) error {
tx := getTransaction()
if tx == nil {
return nil
}
if err != nil {
return tx.Rollback()
}
return tx.Commit()
})
5. Environment Setup
// Prepare environment
parser.AddCommandPreHook("test", func(p *goopt.Parser, c *goopt.Command) error {
// Set up test environment
os.Setenv("ENV", "test")
createTempDirs()
seedTestData()
return nil
})
// Clean up environment
parser.AddCommandPostHook("test", func(p *goopt.Parser, c *goopt.Command, err error) error {
// Always clean up
removeTempDirs()
clearTestData()
os.Unsetenv("ENV")
return nil
})
Hook Context
Hooks have access to:
- Parser state (flags, arguments)
- Command information
- Previous errors (in post-hooks)
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
// Access flags
verbose, _ := p.Get("verbose")
if verbose == "true" {
log.SetLevel(log.DebugLevel)
}
// Access command info
fmt.Printf("Executing: %s (%s)\n", c.Name, c.Description)
// Access positional args
args := p.GetPositionalArgs()
validateArgs(args)
return nil
})
Error Handling
Pre-Hook Errors
- Prevent command execution
- Post-hooks still run (for cleanup)
- Error is returned to caller
Post-Hook Errors
- Don’t affect command result (unless command succeeded)
- All post-hooks run regardless
- Last error is returned
Example
// Pre-hook error prevents execution
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
if !hasPermission(c) {
return errors.New("permission denied") // Command won't run
}
return nil
})
// Post-hook always runs
parser.AddGlobalPostHook(func(p *goopt.Parser, c *goopt.Command, err error) error {
// This runs even if pre-hook failed
audit.Log(c.Path(), err)
return nil
})
Best Practices
1. Keep Hooks Lightweight
// Good: Quick checks
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
if !isAuthenticated() {
return errors.New("not authenticated")
}
return nil
})
// Avoid: Heavy processing in hooks
parser.AddGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
// Don't do this - move to command logic
processLargeFile()
return nil
})
2. Use Post-Hooks for Cleanup
parser.AddGlobalPostHook(func(p *goopt.Parser, c *goopt.Command, err error) error {
// Always runs, perfect for cleanup
defer closeConnections()
defer releaseResources()
// Log regardless of success/failure
logCommandExecution(c, err)
return nil
})
3. Order Matters
// Authentication should run before authorization
parser.AddGlobalPreHook(authenticate)
parser.AddGlobalPreHook(authorize) // Runs second
// Cleanup in reverse order
parser.AddGlobalPostHook(closeDatabase) // Runs second
parser.AddGlobalPostHook(closeNetwork) // Runs first
4. Handle Hook Errors
if errs := parser.ExecuteCommands(); errs > 0 {
// Check specific command errors
for _, cmd := range executedCommands {
if err := parser.GetCommandExecutionError(cmd); err != nil {
handleError(cmd, err)
}
}
}
5. Document Hook Behavior
// Package auth provides authentication hooks for CLI commands.
//
// All commands except "login" and "help" require authentication.
// Set AUTH_TOKEN environment variable or login first.
package auth
import (
"github.com/napalu/goopt/v2"
)
func AuthenticationHook(p *goopt.Parser, c *goopt.Command) error {
// Skip auth for public commands
if c.Name == "login" || c.Name == "help" {
return nil
}
// ... authentication logic
}
Complete Example
package main
import (
"errors"
"os"
"fmt"
"log"
"time"
"github.com/napalu/goopt/v2"
)
func confirmProduction() bool {
// something happens
return true
}
func validateDeploymentConfig()error {
// something happens
return nil
}
func rollbackDeployment() {
// something happens
}
func notifyDeploymentSuccess() {
// something happens
}
func cleanupTempFiles() {
// something happens
}
func main() {
parser, err := goopt.NewParserWith(
// Global logging
goopt.WithGlobalPreHook(func(p *goopt.Parser, c *goopt.Command) error {
log.Printf("[%s] Starting: %s", time.Now().Format(time.RFC3339), c.Path())
return nil
}),
goopt.WithGlobalPostHook(func(p *goopt.Parser, c *goopt.Command, err error) error {
status := "SUCCESS"
if err != nil {
status = "FAILED"
}
log.Printf("[%s] %s: %s", time.Now().Format(time.RFC3339), status, c.Path())
return nil
}),
// Command-specific validation
goopt.WithCommandPreHook("deploy production", func(p *goopt.Parser, c *goopt.Command) error {
if !confirmProduction() {
return errors.New("production deployment cancelled")
}
return validateDeploymentConfig()
}),
// Cleanup hook
goopt.WithCommandPostHook("deploy production", func(p *goopt.Parser, c *goopt.Command, err error) error {
if err != nil {
rollbackDeployment()
} else {
notifyDeploymentSuccess()
}
cleanupTempFiles()
return nil
}),
)
if err != nil {
log.Fatal(err)
}
// Add commands...
if !parser.Parse(os.Args) {
parser.PrintHelp(os.Stderr)
os.Exit(1)
}
if errs := parser.ExecuteCommands(); errs > 0 {
os.Exit(1)
}
}
Integration with Other Features
Hooks work seamlessly with:
- Auto-help: Help display bypasses hooks
- Version: Version display bypasses hooks
- Struct tags: Hooks apply to struct-based commands
- Nested commands: Hooks see full command path
- Internationalization: Hook errors can use i18n
The execution hooks feature provides a powerful way to implement cross-cutting concerns in your CLI applications without cluttering command logic.