cache-apt-pkgs-action/cmd/cache_apt_pkgs/cmdflags/cmd.go
2026-01-05 16:36:23 -08:00

118 lines
4.3 KiB
Go

// Package cmdflags provides types and utilities for parsing command-line flags
// and managing subcommands in the cache-apt-pkgs CLI tool.
package cmdflags
import (
"flag"
"fmt"
"os"
"strings"
"awalsh128.com/cache-apt-pkgs-action/internal/cio"
"awalsh128.com/cache-apt-pkgs-action/internal/logging"
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
)
// Cmd represents a command-line subcommand with its associated flags and behavior.
// Each command has a name, description, set of flags, and a function to execute the command.
type Cmd struct {
// Name is the command identifier used in CLI arguments
Name string
// Description explains what the command does
Description string
// Flags contains the command-specific command-line flags
Flags *flag.FlagSet
// Run executes the command with the given packages and returns any errors
Run func(cmd *Cmd, pkgArgs pkgs.Packages) error
// GhioPrinter provides an interface for GitHub Actions output printing that supports testing
// locally and in Action workflows
GhioPrinter cio.GhPrinter
// Examples provides example usage strings for the command
Examples []string
// ExamplePackages provides example package arguments for documentation and testing
ExamplePackages pkgs.Packages
}
// NewCmd creates a new command with the given name, description, examples, and run function.
// It automatically includes global flags and sets up the usage documentation.
// The returned Cmd is ready to be used as a subcommand in the CLI.
func NewCmd(name, description string, examples []string, runFunc func(cmd *Cmd, pkgArgs pkgs.Packages) error) *Cmd {
flags := flag.NewFlagSet(name, flag.ExitOnError)
globalFlags.VisitAll(func(f *flag.Flag) {
flags.Var(f.Value, f.Name, f.Usage)
})
flags.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: %s %s [flags] [packages]\n\n%s\n\n", binaryName, name, description)
fmt.Fprintf(os.Stderr, "flags:\n")
flags.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(os.Stderr, " -%s: %s\n", f.Name, f.Usage)
})
fmt.Fprintf(os.Stderr, "\nexamples:\n")
for _, example := range examples {
fmt.Fprintf(os.Stderr, " %s %s %s\n", binaryName, name, example)
}
}
wrappedRunFunc := func(cmd *Cmd, pkgArgs pkgs.Packages) error {
// If any command args include help flag, print usage and don't execute command.
if helpFlagSet(cmd.Flags) {
cmd.Flags.Usage()
return nil
}
return runFunc(cmd, pkgArgs)
}
return &Cmd{
Name: name,
Description: description,
Flags: flags,
Run: wrappedRunFunc,
GhioPrinter: cio.NewGhPrinter(),
Examples: examples,
ExamplePackages: ExamplePackages,
}
}
// StringFlag returns the string value of a flag by name.
// It panics if the flag does not exist, so ensure the flag exists before calling.
func (c *Cmd) StringFlag(name string) string {
return c.Flags.Lookup(name).Value.String()
}
// parseFlags processes command line arguments for the command.
// It validates required flags and parses package arguments.
// Returns the parsed package arguments or exits with an error if validation fails.
func (c *Cmd) parseFlags() (pkgs.Packages, error) {
logging.Debug("Parsing flags for command %q with args: %v", c.Name, os.Args[2:])
if len(os.Args) < 3 {
return nil, fmt.Errorf("command %q requires arguments", c.Name)
}
// Parse the command line flags
if err := c.Flags.Parse(os.Args[2:]); err != nil {
return nil, fmt.Errorf("unable to parse flags for command %q: %v", c.Name, err)
}
// Check for missing required flags
missingFlagNames := []string{}
c.Flags.VisitAll(func(f *flag.Flag) {
// Skip all global flags since they are considered optional
if gf := globalFlags.Lookup(f.Name); gf != nil {
return
}
if f.DefValue == "" && f.Value.String() == "" {
logging.Info("Missing required flag: %s", f.Name)
missingFlagNames = append(missingFlagNames, f.Name)
}
})
if len(missingFlagNames) > 0 {
return nil, fmt.Errorf("missing required flags for command %q: %s", c.Name, missingFlagNames)
}
logging.Debug("Parsed flags successfully")
// Parse the remaining arguments as package arguments
pkgArgs, err := pkgs.ParsePackageArgs(c.Flags.Args())
if err != nil {
return nil, fmt.Errorf("failed to parse package arguments for command %q: %v", c.Name, err)
}
logging.Debug("Parsed package arguments:\n%s", strings.Join(c.Flags.Args(), "\n "))
return pkgArgs, nil
}