NewRootCommand creates a new root command.
Parameters:
- name: The name of the root command.
- usage: The usage of the root command.
- description: The description of the root command.
- version: The version of the application.
{
rc := &RootCommand{
Version: version,
Command: command.Command{
Name: name,
Usage: usage,
Description: description,
Flags: flag.NewFlagSet(name, flag.ExitOnError),
SubCommands: []*command.Command{},
ArgFlags: make(map[string]bool),
Logger: &log.DefaultLogger{ComponentName: name},
},
Commands: make(map[string]*command.Command),
}
// Adds the built-in commands (version and completion)
rc.addBuiltInCommands()
return rc
}
RootCommand represents the root command. Can be assigned while creating
a new root command with NewRootCommand.
AddCommand adds a subcommand.
Parameters:
- cmd: The subcommand to add.
{
cmd.Logger = &log.DefaultLogger{ComponentName: cmd.Name}
rc.SubCommands = append(rc.SubCommands, cmd)
rc.Commands[cmd.Name] = cmd
}
Execute runs the main command.
{
// Required flags check
setFlags := make(map[string]bool)
var requiredFlags []string
// Parse root flags
if len(os.Args) > 1 {
if err := rc.Flags.Parse(os.Args[1:]); err != nil {
return err
}
// Check for required flags on the root command
rc.Flags.Visit(func(f *flag.Flag) {
setFlags[f.Name] = true
})
for _, requiredFlag := range rc.RequiredFlags {
if _, isSet := setFlags[requiredFlag]; !isSet {
requiredFlags = append(requiredFlags, "'-"+requiredFlag+"'")
}
}
}
args := rc.Flags.Args()
if len(args) == 0 {
rc.PrintHelp()
return nil
}
var cmd *command.Command
currentLevelCmds := rc.SubCommands
var commandsTraversed int
// Find the command to execute by traversing the command tree
for i, arg := range args {
var foundCmd *command.Command
for _, c := range currentLevelCmds {
if c.Name == arg {
foundCmd = c
break
}
}
if foundCmd != nil {
cmd = foundCmd
currentLevelCmds = cmd.SubCommands
commandsTraversed = i + 1
} else {
break
}
}
if cmd == nil {
rc.PrintHelp()
return nil
}
cmdArgs := args[commandsTraversed:]
// If the command has subcommands and no arguments are provided, show help
if len(cmd.SubCommands) > 0 && len(cmdArgs) == 0 {
cmd.PrintCommandHelp()
return nil
}
// Handle help flag for the found subcommand
for _, arg := range cmdArgs {
if arg == "-h" || arg == "--help" {
cmd.PrintCommandHelp()
return nil
}
}
var remainingArgs []string
// Parse flags for the subcommand
if cmd.Flags != nil {
expandedArgs := make([]string, 0, len(cmdArgs))
for _, arg := range cmdArgs {
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && len(arg) == 2 {
shortName := strings.TrimPrefix(arg, "-")
if longName, ok := cmd.ShortFlagMap[shortName]; ok {
expandedArgs = append(expandedArgs, fmt.Sprintf("--%s", longName))
} else {
expandedArgs = append(expandedArgs, arg)
}
} else {
expandedArgs = append(expandedArgs, arg)
}
}
if err := cmd.Flags.Parse(expandedArgs); err != nil {
return err
}
// Check for required flags on the subcommand
cmd.Flags.Visit(func(f *flag.Flag) {
setFlags[f.Name] = true
})
for _, requiredFlag := range cmd.RequiredFlags {
if _, isSet := setFlags[requiredFlag]; !isSet {
requiredFlags = append(requiredFlags, "'-"+requiredFlag+"'")
}
}
remainingArgs = cmd.Flags.Args()
} else {
remainingArgs = cmdArgs
}
// Check for any missing required flags (both root and subcommand)
// add a subcommand to a subcommand
if len(requiredFlags) > 0 {
cmd.Logger.Error("Missing required flags: %s", strings.Join(requiredFlags, ", "))
cmd.PrintCommandHelp()
return nil
}
if cmd.Run == nil {
err := fmt.Errorf("no main `Run` function defined for command, can't call any hooks (BeforeRun, Run, AfterRun) '%s'", cmd.Name)
cmd.Logger.Error("%v", err)
return err
}
parsedRootFlags := &command.RootFlags{FlagSet: rc.Flags}
if cmd.BeforeRun != nil {
if err := cmd.BeforeRun(cmd, parsedRootFlags, remainingArgs); err != nil {
cmd.Logger.Error("Error running before main command: %v", err)
return err
}
}
if err := cmd.Run(cmd, parsedRootFlags, remainingArgs); err != nil {
cmd.Logger.Error("Error running main command: %v", err)
return err
}
if cmd.AfterRun != nil {
if err := cmd.AfterRun(cmd, parsedRootFlags, remainingArgs); err != nil {
cmd.Logger.Error("Error running after main command: %v", err)
return err
}
}
return nil
}
PrintHelp prints the help message for the root command.
{
fmt.Printf("%s - %s\n\n", rc.Name, rc.Description)
if rc.Version != "" {
fmt.Printf("Version: %s\n\n", rc.Version)
}
fmt.Printf("Usage: %s\n\n", rc.Usage)
fmt.Println("Commands:")
maxNameLength := 0
for _, cmd := range rc.SubCommands {
if len(cmd.Name) > maxNameLength {
maxNameLength = len(cmd.Name)
}
}
for _, cmd := range rc.SubCommands {
fmt.Printf(" %-*s %s\n", maxNameLength, cmd.Name, cmd.Description)
}
if rc.Flags != nil {
fmt.Println("\nFlags:")
rc.Flags.VisitAll(func(f *flag.Flag) {
fmt.Printf(" -%s: %s\n", f.Name, f.Usage)
})
}
}
AddAlias adds an alias for a command or subcommand chain of any depth.
Parameters:
- alias: The name of the alias.
- targets: The target command and optional subcommands in sequence (e.g., "cmd", "subcmd", "subsubcmd").
{
if len(targets) == 0 {
fmt.Fprintf(os.Stderr, "No target command specified for alias '%s'\n", alias)
return
}
// Check if the first command exists
if _, ok := rc.Commands[targets[0]]; !ok {
fmt.Fprintf(os.Stderr, "Target command '%s' not found for alias '%s'\n", targets[0], alias)
return
}
// Build description based on targets depth
var description string
if len(targets) == 1 {
description = fmt.Sprintf("Alias for '%s'", targets[0])
} else {
description = fmt.Sprintf("Alias for '%s'", strings.Join(targets, " "))
}
// Create alias command
aliasCmd := &command.Command{
Name: alias,
Usage: alias,
Description: description,
Run: func(cmd *command.Command, rootFlags *command.RootFlags, args []string) error {
newArgs := append([]string{}, targets...)
newArgs = append(newArgs, args...)
return utils.Reexec(newArgs)
},
}
aliasCmd.SetupLogger(alias)
rc.AddCommand(aliasCmd)
}
AddAliasSubCommand adds an alias for a subcommand of a command.
Parameters:
- alias: The name of the alias.
- targetCmd: The name of the target command.
- targetSubCmd: The name of the target subcommand.
- targets: Optional additional targets for deeper nesting.
{
allTargets := append([]string{targetCmd, targetSubCmd}, targets...)
rc.AddAlias(alias, allTargets...)
}
AddAliasCommand adds an alias for a command.
Parameters:
- alias: The name of the alias.
- targetCmd: The name of the target command.
{
rc.AddAlias(alias, targetCmd)
}
addBuiltInCommands add all the built-in commands to the root command.
{
rc.addVersionCommand()
rc.addCompletionCommand()
}
getCommandNames returns a slice with the names of all commands.
{
var names []string
for _, cmd := range rc.SubCommands {
names = append(names, cmd.Name)
}
return names
}
addVersionCommand adds the version command to the root command.
{
versionCmd := &command.Command{
Name: "version",
Usage: "version",
Description: "Print the application version",
Run: func(cmd *command.Command, rootFlags *command.RootFlags, args []string) error {
fmt.Println(rc.Version)
return nil
},
}
versionCmd.SetupLogger("version")
rc.AddCommand(versionCmd)
}
addCompletionCommand adds the 'completion' command to the root command.
{
completionCmd := &command.Command{
Name: "completion",
Usage: "completion [bash|zsh|fish]",
Description: "Generates shell completion scripts",
Run: rc.runCompletion,
}
completionCmd.SetupLogger("completion")
rc.AddCommand(completionCmd)
}
runCompletion generates the completion script for the specified shell.
{
if len(args) < 1 {
return fmt.Errorf("shell type required (bash, zsh, fish)")
}
shell := args[0]
switch shell {
case "bash":
return rc.genBashCompletion()
case "zsh":
return rc.genZshCompletion()
case "fish":
return rc.genFishCompletion()
default:
return fmt.Errorf("unsupported shell type: %s", shell)
}
}
genBashCompletion generates the bash completion script.
{
fmt.Printf(`
_%s_completion()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_PREV_CWORD]}"
opts="%s"
if [[ "${cur}" == "-" ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
local completions=()
for opt in ${opts}; do
if [[ "${opt}" == "${cur}"* ]]; then
completions+=("${opt}")
fi
done
COMPREPLY=("${completions[@]}")
return 0
}
complete -F _%s_completion %s
`, rc.Name, strings.Join(rc.getCommandNames(), " "), rc.Name, rc.Name)
return nil
}
genZshCompletion generates the zsh completion script.
{
fmt.Printf(`
#compdef %s
_%s_completion() {
local line
read -L line
local commands="%s"
local words=("${(s/ /)line}")
local cur="${words[-1]}"
if [[ $words[1] == "-*" ]]; then
reply=("${(@(|)${(M)commands:#$cur} )}")
else
reply=("${(@(|)${(M)commands:#$cur} )}")
fi
}
_%s_completion
`, rc.Name, rc.Name, strings.Join(rc.getCommandNames(), " "), rc.Name)
return nil
}
genFishCompletion generates the fish completion script.
{
fmt.Printf(`
function __fish_%s_complete
set -lx COMP_WORDS (commandline -o | string split -s ' ')
set -lx COMP_CWORD (math (contains -i -- (commandline -o | string split -s ' ') -- (commandline -o | string cursor)) - 1)
set -lx COMP_LINE (commandline -o)
set -lx cmd (string split -s ' ' -- (commandline -o))[1]
for word in (%s)
if string match -q $word $COMP_WORDS[$COMP_CWORD]
echo $word
end
end
end
complete -f -c %s -a "(__fish_%s_complete)"
`, rc.Name, strings.Join(rc.getCommandNames(), " "), rc.Name, rc.Name)
return nil
}
import "flag"
import "fmt"
import "os"
import "strings"
import "github.com/mirkobrombin/go-cli-builder/v1/command"
import "github.com/mirkobrombin/go-cli-builder/v1/log"
import "github.com/mirkobrombin/go-cli-builder/v1/utils"
import "github.com/mirkobrombin/go-cli-builder/v1/command"
import "fmt"
import "github.com/mirkobrombin/go-cli-builder/v1/command"
import "fmt"
import "strings"
import "github.com/mirkobrombin/go-cli-builder/v1/command"