Complete reference for creating, configuring, and running commands in Glazed
The Glazed command system provides a structured approach to building CLI applications that handle multiple output formats, complex field validation, and reusable components. This reference covers the complete command system architecture, interfaces, and implementation patterns.
Building CLI tools typically involves handling field parsing, validation, output formatting, and configuration management. Glazed addresses these concerns through a composed architecture that separates command logic from presentation and field management.
The core principle is separation of concerns: commands focus on business logic while Glazed handles field parsing, validation, and output formatting. This approach enables automatic support for multiple output formats, consistent field handling across commands, and reusable field groups.
βββββββββββββββββββββββββββββββββββββββββββββββ
β Command Interface β
ββββββββββββββ¬ββββββββββββββββββ¬βββββββββββββββ€
βBareCommand β WriterCommand β GlazeCommand β
ββββββββββββββ΄ββββββββββββββββββ΄βββββββββββββββ
β
βββββββββββΌββββββββββ
βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β CommandDescription β
β (name, flags, arguments, sections, etc.) β
βββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Field Sections β
β βββββββββββββ¬βββββββββββββ¬βββββββββ β
β βDefault βGlazed βCustom β β
β β(flags/args)β(output fmt)β(any) β β
β βββββββββββββ΄βββββββββββββ΄βββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββ
β β
βββββββββββ ββββββββββ
βΌ βΌ
ββββββββββββββββ βββββββββββββββββββ
β Fields β β Values β
β (definitions)βββββββββββΆβ(runtime values) β
ββββββββββββββββ βββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Execution (via Run methods or runner) β
βββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββΌβββββββββββββ
βΌ βΌ βΌ
ββββββββββ ββββββββββββ ββββββββββββββββ
βDirect β βWriter β βGlazeProcessorβ
βOutput β βOutput β β(structured) β
ββββββββββ ββββββββββββ ββββββββββββββββ
β β
βββββββ¬βββββββ
βΌ
βββββββββββββββββββββββ
β Dual Commands β
β (can run in both β
β classic & glaze) β
βββββββββββββββββββββββ
Key components:
The Glazed framework is organized into distinct packages to separate concerns like command definition, field handling, and output processing. This modular design makes the system extensible and easier to maintain. Key packages handle command interfaces (cmds), field definitions (fields), and integration with CLI libraries like Cobra (cli).
github.com/go-go-golems/glazed/pkg/cmds: Core command interfaces and descriptionsgithub.com/go-go-golems/glazed/pkg/cmds/fields: Field types and definitionsgithub.com/go-go-golems/glazed/pkg/cmds/schema: Field schema systemgithub.com/go-go-golems/glazed/pkg/cmds/runner: Programmatic command executiongithub.com/go-go-golems/glazed/pkg/middlewares: Output processinggithub.com/go-go-golems/glazed/pkg/types: Structured data typesgithub.com/go-go-golems/glazed/pkg/cli: Helpers for integrating with Cobragithub.com/go-go-golems/glazed/pkg/settings: Standard Glazed field sectionsA command's output contract is defined by the interface it implements. Glazed offers three primary interfaces to support different use cases: BareCommand for direct stdout control, WriterCommand for sending text to any io.Writer, and GlazeCommand for producing structured data that can be automatically formatted. This design allows a command's business logic to be decoupled from its final output format.
Glazed provides three main command interfaces, each designed for different output scenarios:
Additionally, Dual Commands can implement multiple interfaces and switch between output modes at runtime based on flags.
BareCommand provides direct control over output. Commands implementing this interface handle their own output formatting and presentation.
// BareCommand interface definition
type BareCommand interface {
Command
Run(ctx context.Context, parsedSections *values.Values) error
}
Use cases:
Example implementation:
type CleanupCommand struct {
*cmds.CommandDescription
}
func (c *CleanupCommand) Run(ctx context.Context, parsedSections *values.Values) error {
s := &CleanupSettings{}
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, s); err != nil {
return err
}
fmt.Printf("Starting cleanup in %s...\n", s.Directory)
files, err := findOldFiles(s.Directory, s.OlderThan)
if err != nil {
return fmt.Errorf("failed to scan directory: %w", err)
}
if len(files) == 0 {
fmt.Printf("Directory is clean - no files older than %s found.\n", s.OlderThan)
return nil
}
fmt.Printf("Found %d files to clean up:\n", len(files))
for i, file := range files {
fmt.Printf(" %d. %s\n", i+1, file)
if !s.DryRun {
if err := os.Remove(file); err != nil {
fmt.Printf(" Failed to remove: %s\n", err)
} else {
fmt.Printf(" Removed\n")
}
}
}
if s.DryRun {
fmt.Printf("Dry run completed. Use --execute to actually remove files.\n")
} else {
fmt.Printf("Cleanup completed successfully.\n")
}
return nil
}
WriterCommand allows commands to write text output to any destination (files, stdout, network connections) without knowing the specific target.
// WriterCommand interface definition
type WriterCommand interface {
Command
RunIntoWriter(ctx context.Context, parsedSections *values.Values, w io.Writer) error
}
This interface separates content generation from output destination, improving testability and reusability.
Use cases:
Example implementation:
type HealthReportCommand struct {
*cmds.CommandDescription
}
func (c *HealthReportCommand) RunIntoWriter(ctx context.Context, parsedSections *values.Values, w io.Writer) error {
s := &HealthReportSettings{}
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, s); err != nil {
return err
}
// Generate a comprehensive system health report
fmt.Fprintf(w, "System Health Report\n")
fmt.Fprintf(w, "Generated: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(w, "Host: %s\n\n", s.Hostname)
// Check various system components
components := []string{"CPU", "Memory", "Disk", "Network"}
for _, component := range components {
status, details := checkComponentHealth(component)
fmt.Fprintf(w, "%s Status: %s\n", component, status)
if s.Verbose {
fmt.Fprintf(w, " Details: %s\n", details)
}
}
// Add recommendations if any issues found
if recommendations := generateRecommendations(); len(recommendations) > 0 {
fmt.Fprintf(w, "\nRecommendations:\n")
for i, rec := range recommendations {
fmt.Fprintf(w, "%d. %s\n", i+1, rec)
}
}
return nil
}
This command can write its report to a file for archival, to stdout for immediate viewing, or even to a network connection for monitoring systems. The command logic doesn't change - only the destination.
GlazeCommand produces structured data that Glazed processes into various output formats. Commands generate data rows instead of formatted text, enabling automatic format conversion and data processing.
// GlazeCommand interface definition
type GlazeCommand interface {
Command
RunIntoGlazeProcessor(ctx context.Context, parsedSections *values.Values, gp middlewares.Processor) error
}
GlazeCommand generates structured data events that can be transformed, filtered, sorted, and formatted automatically.
Key capabilities:
Real-world example - A server monitoring command:
type MonitorServersCommand struct {
*cmds.CommandDescription
}
func (c *MonitorServersCommand) RunIntoGlazeProcessor(
ctx context.Context,
parsedSections *values.Values,
gp middlewares.Processor,
) error {
s := &MonitorSettings{}
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, s); err != nil {
return err
}
// Get server data from various sources
servers := getServersFromInventory(s.Environment)
for _, server := range servers {
// Check server health
health := checkServerHealth(server.Hostname)
// Produce a rich data row with nested information
row := types.NewRow(
types.MRP("hostname", server.Hostname),
types.MRP("environment", server.Environment),
types.MRP("cpu_percent", health.CPUPercent),
types.MRP("memory_used_gb", health.MemoryUsedGB),
types.MRP("memory_total_gb", health.MemoryTotalGB),
types.MRP("disk_used_percent", health.DiskUsedPercent),
types.MRP("status", health.Status),
types.MRP("last_seen", health.LastSeen),
types.MRP("alerts", health.ActiveAlerts), // Can be an array
types.MRP("metadata", map[string]interface{}{ // Nested objects work too
"os_version": server.OSVersion,
"kernel": server.KernelVersion,
"uptime_days": health.UptimeDays,
}),
)
if err := gp.AddRow(ctx, row); err != nil {
return err
}
}
return nil
}
Now your users can run:
monitor --output table for a human-readable overviewmonitor --output json | jq '.[] | select(.status != "healthy")' to find problem serversmonitor --output csv > servers.csv to import into spreadsheetsmonitor --filter 'cpu_percent > 80' --sort cpu_percent to find CPU hotspotsmonitor --template custom.tmpl to generate custom reportsAll from the same command implementation.
Dual commands implement multiple interfaces and switch between output modes based on runtime flags. For the complete guide, see Dual Commands.
Quick reference:
// Implement both interfaces
var _ cmds.BareCommand = &StatusCommand{}
var _ cmds.GlazeCommand = &StatusCommand{}
// Build with dual mode
cli.BuildCobraCommand(cmd,
cli.WithDualMode(true),
cli.WithGlazeToggleFlag("with-glaze-output"),
)
See Dual Commands for advanced options (default output format, hidden flags, callback patterns).
A well-structured Glazed command separates its identity and logic. The recommended pattern involves a Command struct embedding a CommandDescription for metadata, a separate Settings struct for type-safe field access via glazed tags, and a Run method containing the business logic. This separation is bridged at runtime by DecodeSectionInto, which populates the Settings struct from parsed command-line values.
Glazed commands follow a consistent structure with four key components:
Command Struct: Contains the command's identity and embeds CommandDescription which holds metadata (name, flags, help text) separately from business logic.
Settings Struct: Provides type safety by defining a struct that mirrors command inputs. Glazed automatically maps fields to struct fields through glazed tags.
Run Method: Contains business logic. The method signature depends on the implemented interface, but the pattern is consistent: extract settings using DecodeSectionInto, execute logic, return results.
Constructor Function: Creates the command description with its fields and sections.
Settings structs provide type-safe access to parsed command fields. Each field uses a glazed tag that must match the field name defined in the command description:
// Settings struct with glazed tags
type MyCommandSettings struct {
Count int `glazed:"count"` // Maps to "count" field
Format string `glazed:"format"` // Maps to "format" field
Verbose bool `glazed:"verbose"` // Maps to "verbose" field
DryRun bool `glazed:"dry-run"` // Maps to "dry-run" field
}
The DecodeSectionInto method populates the settings struct from parsed sections. Always specify the correct section slug (use schema.DefaultSlug for command-specific fields):
func (c *MyCommand) RunIntoGlazeProcessor(
ctx context.Context,
parsedSections *values.Values,
gp middlewares.Processor,
) error {
// Create settings struct instance
s := &MyCommandSettings{}
// Extract values from the "default" section into the struct
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, s); err != nil {
return fmt.Errorf("failed to initialize settings: %w", err)
}
// Now use the populated struct fields
for i := 0; i < s.Count; i++ {
if s.Verbose {
log.Printf("Processing item %d with format %s", i, s.Format)
}
if s.DryRun {
fmt.Printf("Would process item %d\n", i)
continue
}
row := types.NewRow(
types.MRP("id", i),
types.MRP("format", s.Format),
)
if err := gp.AddRow(ctx, row); err != nil {
return err
}
}
return nil
}
Commands often use multiple field sections. Extract settings from each section separately:
type DatabaseSettings struct {
Host string `glazed:"db-host"`
Port int `glazed:"db-port"`
Database string `glazed:"db-name"`
}
type LoggingSettings struct {
Level string `glazed:"log-level"`
File string `glazed:"log-file"`
}
func (c *MyCommand) RunIntoGlazeProcessor(
ctx context.Context,
parsedSections *values.Values,
gp middlewares.Processor,
) error {
// Extract command-specific settings
cmdSettings := &MyCommandSettings{}
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, cmdSettings); err != nil {
return err
}
// Extract database section settings
dbSettings := &DatabaseSettings{}
if err := parsedSections.DecodeSectionInto("database", dbSettings); err != nil {
return err
}
// Extract logging section settings
logSettings := &LoggingSettings{}
if err := parsedSections.DecodeSectionInto("logging", logSettings); err != nil {
return err
}
// Use all settings together
fmt.Printf("Connecting to %s:%d/%s with log level %s\n",
dbSettings.Host, dbSettings.Port, dbSettings.Database, logSettings.Level)
// ... rest of command logic
return nil
}
Pattern 1: Inline struct definition (for simple cases)
func (c *ExampleCommand) Run(ctx context.Context, parsedSections *values.Values) error {
s := struct {
Message string `glazed:"message"`
Count int `glazed:"count"`
}{}
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, &s); err != nil {
return err
}
// Use s.Message and s.Count
return nil
}
Pattern 2: Named settings struct (recommended for complex commands)
type ExampleSettings struct {
Message string `glazed:"message"`
Count int `glazed:"count"`
}
func (c *ExampleCommand) Run(ctx context.Context, parsedSections *values.Values) error {
s := &ExampleSettings{}
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, s); err != nil {
return err
}
// Use s.Message and s.Count
return nil
}
Pattern 3: Helper function for reusable settings
func GetDatabaseSettings(parsedSections *values.Values) (*DatabaseSettings, error) {
settings := &DatabaseSettings{}
err := parsedSections.DecodeSectionInto("database", settings)
return settings, err
}
func (c *MyCommand) RunIntoGlazeProcessor(...) error {
dbSettings, err := GetDatabaseSettings(parsedSections)
if err != nil {
return err
}
// Use dbSettings
return nil
}
While Glazed excels at building standard CLI tools, its architecture also supports more advanced use cases. Commands can be executed programmatically for testing or integration into other Go applications, and the field system can load values from multiple sources like environment variables and config files, not just CLI flags. These patterns allow you to build commands that are not just standalone tools, but reusable components in a larger software ecosystem.
To run a command programmatically without Cobra:
// Create command instance
cmd, err := NewMyCommand()
if err != nil {
log.Fatalf("Error creating command: %v", err)
}
// Set up execution context
ctx := context.Background()
// Define field values
parseOptions := []runner.ParseOption{
runner.WithValuesForSections(map[string]map[string]interface{}{
"default": {
"count": 20,
"format": "json",
},
}),
runner.WithEnvMiddleware("MYAPP_"),
}
// Configure output
runOptions := []runner.RunOption{
runner.WithWriter(os.Stdout),
}
// Run the command
err = runner.ParseAndRun(ctx, cmd, parseOptions, runOptions)
if err != nil {
log.Fatalf("Error running command: %v", err)
}
Fields can be loaded from multiple sources (in priority order):
parseOptions := []runner.ParseOption{
// Load from environment with prefix
runner.WithEnvMiddleware("MYAPP_"),
// Load from configuration file
runner.WithViper(),
// Set explicit values
runner.WithValuesForSections(map[string]map[string]interface{}{
"default": {"count": 10},
}),
// Add custom middleware
runner.WithAdditionalMiddlewares(customMiddleware),
}
For GlazeCommands, rows form the structured output:
row := types.NewRow(
types.MRP("id", 1),
types.MRP("name", "John Doe"),
types.MRP("email", "john@example.com"),
types.MRP("active", true),
)
data := map[string]interface{}{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"active": true,
}
row := types.NewRowFromMap(data)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Active bool `json:"active"`
}
user := User{ID: 1, Name: "John Doe", Email: "john@example.com", Active: true}
row := types.NewRowFromStruct(&user, true) // true = lowercase field names
func (c *MyCommand) RunIntoGlazeProcessor(
ctx context.Context,
parsedSections *values.Values,
gp middlewares.Processor,
) error {
// Process data and create rows
for _, item := range data {
row := types.NewRow(
types.MRP("field1", item.Value1),
types.MRP("field2", item.Value2),
)
if err := gp.AddRow(ctx, row); err != nil {
return err
}
}
return nil
}
func (c *MyCommand) RunIntoGlazeProcessor(
ctx context.Context,
parsedSections *values.Values,
gp middlewares.Processor,
) error {
s := &MyCommandSettings{}
if err := parsedSections.DecodeSectionInto(schema.DefaultSlug, s); err != nil {
return fmt.Errorf("failed to initialize settings: %w", err)
}
// Validate settings
if err := c.validateSettings(s); err != nil {
return fmt.Errorf("invalid settings: %w", err)
}
// Process with context cancellation support
for i := 0; i < s.Count; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
row := types.NewRow(types.MRP("id", i))
if err := gp.AddRow(ctx, row); err != nil {
return fmt.Errorf("failed to add row %d: %w", i, err)
}
}
}
return nil
}
func (c *MyCommand) validateSettings(s *MyCommandSettings) error {
if s.Count < 0 {
return errors.New("count must be non-negative")
}
if s.Count > 1000 {
return errors.New("count cannot exceed 1000")
}
return nil
}
Commands can control application exit behavior:
func (c *MyCommand) Run(ctx context.Context, parsedSections *values.Values) error {
// For early exit without error
if shouldExit {
return &cmds.ExitWithoutGlazeError{}
}
// Normal processing
return nil
}
For large datasets, optimize row creation:
// Good: Processes data as it arrives
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
row := processLine(scanner.Text())
if err := gp.AddRow(ctx, row); err != nil {
return err
}
}
// Problematic: Loads everything into memory first
allData := loadAllDataIntoMemory() // What if this is 10GB?
for _, item := range allData {
// Process items...
}
For commands processing large amounts of data:
// Good: Processes data as it arrives
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
row := processLine(scanner.Text())
if err := gp.AddRow(ctx, row); err != nil {
return err
}
}
// Problematic: Loads everything into memory first
allData := loadAllDataIntoMemory() // What if this is 10GB?
for _, item := range allData {
// Process items...
}
Glazed treats command-line fields as more than just strings. They are typed objects with built-in validation, default values, and help text. This approach shifts the burden of parsing and validation from the command's business logic to the framework itself. By defining a field's type (e.g., TypeInteger, TypeDate, TypeFile), you get automatic error handling and a more robust and user-friendly CLI.
Glazed fields are typed objects with validation rules and behavior, unlike traditional CLI libraries that treat fields as simple strings requiring manual parsing and validation. This enables automatic validation, help generation, and multi-source value loading.
Field types define data structure, parsing behavior, and validation rules. Each type handles string parsing, validation, and help text generation.
TypeString: The workhorse for text inputs - names, descriptions, URLs
TypeSecret: Like strings, but values are masked in help and logs (perfect for passwords, API keys)
TypeInteger: Whole numbers with automatic range validation
TypeFloat: Decimal numbers for measurements, percentages, ratios
TypeBool: True/false flags that work with --flag and --no-flag patterns
TypeDate: Intelligent date parsing that handles multiple formats
TypeStringList: Multiple values like --tag web --tag api --tag production
TypeIntegerList: Lists of numbers for ports, IDs, quantities
TypeFloatList: Multiple decimal values for coordinates, measurements
TypeChoice: Single selection from predefined options (with tab completion!)
TypeChoiceList: Multiple selections from predefined options
TypeFile: File paths with existence validation and tab completion
TypeFileList: Multiple file paths
TypeStringFromFile: Read text content from a file (useful for large inputs)
TypeStringListFromFile: Read line-separated lists from files
TypeKeyValue: Map-like inputs: --env DATABASE_URL=postgres://... --env DEBUG=true
fields.New(
"field-name", // Required: field name
fields.TypeString, // Required: field type
// Optional configuration
fields.WithDefault("default"), // Default value
fields.WithHelp("Description"), // Help text
fields.WithRequired(true), // Mark as required
fields.WithShortFlag("n"), // Short flag (-n)
// For choice types
fields.WithChoices("opt1", "opt2", "opt3"),
// For file types
fields.WithFileExtensions(".txt", ".md"),
)
Arguments are positional fields that don't use flags:
cmds.WithArguments(
fields.New(
"input-file",
fields.TypeFile,
fields.WithHelp("Input file to process"),
fields.WithRequired(true),
),
fields.New(
"output-file",
fields.TypeString,
fields.WithHelp("Output file path"),
fields.WithRequired(false),
),
)
Glazed commands are defined independently of any specific CLI library, but they are most commonly used with Cobra. The pkg/cli package provides a bridge to convert a cmds.Command interface into a cobra.Command. This bridge automatically sets up flags, argument handling, and the execution flow, allowing you to benefit from Glazed's features within a standard Cobra application structure.
Glazed provides several ways to convert commands to Cobra commands:
// Automatically selects the appropriate builder based on command type
cobraCmd, err := cli.BuildCobraCommand(myCmd)
if err != nil {
log.Fatalf("Error building Cobra command: %v", err)
}
// Use specific builder invocation (unified detection available)
if glazeCmd, ok := myCmd.(cmds.GlazeCommand); ok {
cobraCmd, err = cli.BuildCobraCommand(glazeCmd)
} else if writerCmd, ok := myCmd.(cmds.WriterCommand); ok {
cobraCmd, err = cli.BuildCobraCommand(writerCmd)
} else if bareCmd, ok := myCmd.(cmds.BareCommand); ok {
cobraCmd, err = cli.BuildCobraCommand(bareCmd)
}
// For commands that implement multiple interfaces
cobraCmd, err := cli.BuildCobraCommand(myCmd,
cli.WithDualMode(true), // enable dual-mode
cli.WithGlazeToggleFlag("with-glaze-output"),
)
if err != nil {
log.Fatalf("Error building Cobra command: %v", err)
}
See Dual Commands for complete documentation of all builder options.
Building effective command-line tools involves more than just making them work. A great CLI is maintainable, performant, and user-friendly. The following guidelines represent key design principles and patterns for building high-quality Glazed applications, from choosing the right command interface to writing clear documentation and handling errors gracefully.
Choose interfaces based on user requirements:
Example: A backup command might start as BareCommand for user feedback (Backing up 1,247 files...), but users eventually want structured output for monitoring scripts. A dual command serves both needs.
Use settings structs with glazed tags to prevent type conversion errors:
// Good: Type-safe and clear
type BackupSettings struct {
Source string `glazed:"source"`
Destination string `glazed:"destination"`
MaxAge time.Duration `glazed:"max-age"`
DryRun bool `glazed:"dry-run"`
}
// Avoid: Manual field extraction
source, _ := parsedSections.GetString("source")
maxAge, _ := parsedSections.GetString("max-age") // Bug waiting to happen!
Provide sensible defaults so commands work with minimal flags. If your command requires multiple flags to be useful, reconsider the design.
Write clear help text with examples for complex fields:
fields.New(
"filter",
fields.TypeString,
fields.WithHelp("Filter results using SQL-like syntax. Examples: 'status = \"active\"', 'created_at > \"2023-01-01\"'"),
)
Provide specific, actionable error messages:
// Good: Specific and actionable
if s.Port < 1 || s.Port > 65535 {
return fmt.Errorf("port %d is invalid; must be between 1 and 65535", s.Port)
}
// Poor: Vague and frustrating
if !isValidPort(s.Port) {
return errors.New("invalid port")
}
Validate fields before expensive operations. Always check context cancellation in loops and long operations.
Design for streaming to handle large datasets:
// Good: Processes data as it arrives
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
row := processLine(scanner.Text())
if err := gp.AddRow(ctx, row); err != nil {
return err
}
}
// Problematic: Loads everything into memory first
allData := loadAllDataIntoMemory() // What if this is 10GB?
for _, item := range allData {
// Process items...
}
Command help text is often the primary documentation users read:
For GlazeCommands, document the output schema. Users building scripts need to know what fields are available and what they contain.
glaze help build-first-command
glaze help sections-guide