diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..234b889 --- /dev/null +++ b/cli.go @@ -0,0 +1,150 @@ +package kli + +import ( + "fmt" + "log" + "os" +) + +// Cli - The main application object +type Cli struct { + version string + rootCommand *Command + defaultCommand *Command + preRunCommand func(*Cli) error + bannerFunction func(*Cli) string +} + +// Action represents a function that gets called when the command is executed +type Action func() error + +// NewCli - Creates a new Cli application object +func NewCli(name, description, version string) *Cli { + result := &Cli{ + version: version, + bannerFunction: defaultBannerFunction, + } + result.rootCommand = NewCommand(name, description) + result.rootCommand.setApp(result) + result.rootCommand.setParentCommandPath("") + return result +} + +// Version - Get the Application version string +func (c *Cli) Version() string { + return c.version +} + +// Name - Get the Application Name +func (c *Cli) Name() string { + return c.rootCommand.name +} + +// ShortDescription - Get the Application short description +func (c *Cli) ShortDescription() string { + return c.rootCommand.shortdescription +} + +// SetBannerFunction is used to set the function that is called +// to get the banner string. +func (c *Cli) SetBannerFunction(fn func(*Cli) string) { + c.bannerFunction = fn +} + +// Abort prints the given error and terminates the application +func (c *Cli) Abort(err error) { + log.Fatal(err) + os.Exit(1) +} + +// AddCommand - Adds a command to the application +func (c *Cli) AddCommand(command *Command) { + c.rootCommand.AddCommand(command) +} + +// PrintBanner prints the application banner! +func (c *Cli) PrintBanner() { + fmt.Println(c.bannerFunction(c)) + fmt.Println("") +} + +// PrintHelp - Prints the application's help +func (c *Cli) PrintHelp() { + c.rootCommand.PrintHelp() +} + +// Run - Runs the application with the given arguments +func (c *Cli) Run(args ...string) error { + if c.preRunCommand != nil { + err := c.preRunCommand(c) + if err != nil { + return err + } + } + if len(args) == 0 { + args = os.Args[1:] + } + return c.rootCommand.run(args) +} + +// DefaultCommand - Sets the given command as the command to run when +// no other commands given +func (c *Cli) DefaultCommand(defaultCommand *Command) *Cli { + c.defaultCommand = defaultCommand + return c +} + +// NewSubCommand - Creates a new SubCommand for the application +func (c *Cli) NewSubCommand(name, description string) *Command { + return c.rootCommand.NewSubCommand(name, description) +} + +// PreRun - Calls the given function before running the specific command +func (c *Cli) PreRun(callback func(*Cli) error) { + c.preRunCommand = callback +} + +// BoolFlag - Adds a boolean flag to the root command +func (c *Cli) BoolFlag(name, description string, variable *bool) *Cli { + c.rootCommand.BoolFlag(name, description, variable) + return c +} + +// StringFlag - Adds a string flag to the root command +func (c *Cli) StringFlag(name, description string, variable *string) *Cli { + c.rootCommand.StringFlag(name, description, variable) + return c +} + +// IntFlag - Adds an int flag to the root command +func (c *Cli) IntFlag(name, description string, variable *int) *Cli { + c.rootCommand.IntFlag(name, description, variable) + return c +} + +// Action - Define an action from this command +func (c *Cli) Action(callback Action) *Cli { + c.rootCommand.Action(callback) + return c +} + +// LongDescription - Sets the long description for the command +func (c *Cli) LongDescription(longdescription string) *Cli { + c.rootCommand.LongDescription(longdescription) + return c +} + +// OtherArgs - Returns the non-flag arguments passed to the cli. NOTE: This should only be called within the context of an action. +func (c *Cli) OtherArgs() []string { + return c.rootCommand.flags.Args() +} + +// defaultBannerFunction prints a banner for the application. +// If version is a blank string, it is ignored. +func defaultBannerFunction(c *Cli) string { + version := "" + if len(c.Version()) > 0 { + version = " " + c.Version() + } + return fmt.Sprintf("%s%s - %s", c.Name(), version, c.ShortDescription()) +} diff --git a/cli_test.go b/cli_test.go new file mode 100644 index 0000000..8271cce --- /dev/null +++ b/cli_test.go @@ -0,0 +1,167 @@ +package kli + +import ( + "errors" + "testing" +) + +func TestCli(t *testing.T) { + c := NewCli("test", "description", "0") + + t.Run("Run SetBannerFunction()", func(t *testing.T) { + c.SetBannerFunction(func(*Cli) string { return "" }) + }) + + // t.Run("Run Abort()", func(t *testing.T) { + // cl := NewCli("test", "description", "0") + // cl.Abort(errors.New("test error")) + // }) + + t.Run("Run AddCommand()", func(t *testing.T) { + c.AddCommand(&Command{name: "test"}) + }) + + t.Run("Run PrintBanner()", func(t *testing.T) { + c.PrintBanner() + }) + + t.Run("Run Run()", func(t *testing.T) { + c.Run("test") + c.Run() + + c.preRunCommand = func(*Cli) error { return errors.New("testing coverage") } + c.Run("test") + }) + + t.Run("Run DefaultCommand()", func(t *testing.T) { + c.DefaultCommand(&Command{}) + }) + + t.Run("Run NewSubCommand()", func(t *testing.T) { + c.NewSubCommand("name", "description") + }) + + t.Run("Run PreRun()", func(t *testing.T) { + c.PreRun(func(*Cli) error { return nil }) + }) + + t.Run("Run BoolFlag()", func(t *testing.T) { + var variable bool + c.BoolFlag("bool", "description", &variable) + }) + + t.Run("Run StringFlag()", func(t *testing.T) { + var variable string + c.StringFlag("string", "description", &variable) + }) + + t.Run("Run IntFlag()", func(t *testing.T) { + var variable int + c.IntFlag("int", "description", &variable) + }) + + t.Run("Run Action()", func(t *testing.T) { + c.Action(func() error { return nil }) + }) + + t.Run("Run LongDescription()", func(t *testing.T) { + c.LongDescription("long description") + }) +} + +func TestCliEmpty(t *testing.T) { + var mockCli *Cli + t.Run("Run NewCli()", func(t *testing.T) { + mockCli = NewCli("name", "description", "version") + t.Log(mockCli) + }) + + t.Run("Run defaultBannerFunction()", func(t *testing.T) { + err := defaultBannerFunction(mockCli) + t.Log(err) + }) +} + +func TestCliGlobalFlag(t *testing.T) { + var rootCli *Cli + + t.Run("rcli -config app.yml create", func(t *testing.T) { + var cnf string + + rootCli = NewCli("rcli", "description", "version") + rootCli.StringFlag("config", "Application yaml configuration file", &cnf) + rootCli.Action(func() error { + t.Fail() + return nil + }) + create := rootCli.NewSubCommand("create", "Create a sonic application") + create.Action(func() error { + t.Log("Running Create") + return nil + }) + rootCli.Run("-config", "app.yml", "create") + t.Logf("After Execution cnf: %s", cnf) + }) + + t.Run("rcli -config app.yml create -v", func(t *testing.T) { + var verb bool + var cnf string + + rootCli = NewCli("rcli", "description", "version") + rootCli.StringFlag("config", "Application yaml configuration file", &cnf) + rootCli.Action(func() error { + t.Fail() + return nil + }) + create := rootCli.NewSubCommand("create", "Create a sonic application") + create.BoolFlag("v", "Activate verbose", &verb) + create.Action(func() error { + if verb == false { + t.Fail() + } else { + t.Log("Running Create with verbose") + } + return nil + }) + rootCli.Run("-config", "app.yml", "create", "-v") + t.Logf("After Execution") + }) + + t.Run("rcli create -f file.txt", func(t *testing.T) { + var file string + + rootCli = NewCli("rcli", "description", "version") + create := rootCli.NewSubCommand("create", "Create a sonic application") + create.StringFlag("f", "Pass file", &file) + create.Action(func() error { + if file == "file.txt" { + t.Log("Running Create with -f params") + } else { + t.Fail() + } + return nil + }) + rootCli.Run("create", "-f", "file.txt") + t.Logf("After Execution") + }) + + t.Run("rcli -c app.yml create -f file.txt", func(t *testing.T) { + var cnf string + var file string + + rootCli = NewCli("rcli", "description", "version") + rootCli.StringFlag("c", "Pass file", &cnf) + create := rootCli.NewSubCommand("create", "Create a sonic application") + create.StringFlag("f", "Pass file", &file) + create.Action(func() error { + if file == "file.txt" && cnf == "app.yml" { + t.Log("Running Create with -f -c flags") + } else { + t.Fail() + } + return nil + }) + rootCli.Run("-c", "app.yml", "create", "-f", "file.txt") + t.Logf("After Execution") + }) +} diff --git a/command.go b/command.go new file mode 100644 index 0000000..c45d103 --- /dev/null +++ b/command.go @@ -0,0 +1,231 @@ +package kli + +import ( + "flag" + "fmt" + "os" + "strings" +) + +// Command represents a command that may be run by the user +type Command struct { + name string + commandPath string + shortdescription string + longdescription string + subCommands []*Command + subCommandsMap map[string]*Command + longestSubcommand int + actionCallback Action + app *Cli + flags *flag.FlagSet + flagCount int + helpFlag bool + hidden bool +} + +// NewCommand creates a new Command +// func NewCommand(name string, description string, app *Cli, parentCommandPath string) *Command { +func NewCommand(name string, description string) *Command { + result := &Command{ + name: name, + shortdescription: description, + subCommandsMap: make(map[string]*Command), + hidden: false, + } + + return result +} + +func (c *Command) setParentCommandPath(parentCommandPath string) { + // Set up command path + if parentCommandPath != "" { + c.commandPath += parentCommandPath + " " + } + c.commandPath += c.name + + // Set up flag set + c.flags = flag.NewFlagSet(c.commandPath, flag.ContinueOnError) + c.BoolFlag("help", "Get help on the '"+strings.ToLower(c.commandPath)+"' command.", &c.helpFlag) +} + +func (c *Command) setApp(app *Cli) { + c.app = app +} + +// parseFlags parses the given flags +func (c *Command) parseFlags(args []string) ([]string, error) { + if len(args) == 0 { + return args, nil + } + tmp := os.Stderr + os.Stderr = nil + err := c.flags.Parse(args) + os.Stderr = tmp + return c.flags.Args(), err +} + +// Run - Runs the Command with the given arguments +func (c *Command) run(args []string) error { + + // Parse flags + args, err := c.parseFlags(args) + if err != nil { + fmt.Printf("Error: %s\n\n", err.Error()) + c.PrintHelp() + return err + } + + // Help takes precedence + if c.helpFlag { + c.PrintHelp() + return nil + } + + // Check for subcommand + if len(args) > 0 { + subcommand := c.subCommandsMap[args[0]] + if subcommand != nil { + return subcommand.run(args[1:]) + } + } + + // Do we have an action? + if c.actionCallback != nil { + return c.actionCallback() + } + + // If we haven't specified a subcommand + // check for an app level default command + if c.app.defaultCommand != nil { + // Prevent recursion! + if c.app.defaultCommand != c { + // only run default command if no args passed + if len(args) == 0 { + return c.app.defaultCommand.run(args) + } + } + } + + // Nothing left we can do + c.PrintHelp() + return nil +} + +// Action - Define an action from this command +func (c *Command) Action(callback Action) *Command { + c.actionCallback = callback + return c +} + +// PrintHelp - Output the help text for this command +func (c *Command) PrintHelp() { + if c.app != nil { + c.app.PrintBanner() + } + + commandTitle := c.commandPath + if c.shortdescription != "" { + commandTitle += " - " + c.shortdescription + } + // Ignore root command + if c.commandPath != c.name { + fmt.Println(commandTitle) + } + if c.longdescription != "" { + fmt.Println(c.longdescription + "\n") + } + if len(c.subCommands) > 0 { + fmt.Println("Available commands:") + fmt.Println("") + for _, subcommand := range c.subCommands { + if subcommand.isHidden() { + continue + } + spacer := strings.Repeat(" ", 3+c.longestSubcommand-len(subcommand.name)) + isDefault := "" + if subcommand.isDefaultCommand() { + isDefault = "[default]" + } + fmt.Printf(" %s%s%s %s\n", subcommand.name, spacer, subcommand.shortdescription, isDefault) + } + fmt.Println("") + } + if c.flagCount > 0 { + fmt.Print("Flags:\n\n") + c.flags.SetOutput(os.Stdout) + c.flags.PrintDefaults() + c.flags.SetOutput(os.Stderr) + + } + fmt.Println() +} + +// isDefaultCommand returns true if called on the default command +func (c *Command) isDefaultCommand() bool { + if c.app == nil { + return false + } + return c.app.defaultCommand == c +} + +// isHidden returns true if the command is a hidden command +func (c *Command) isHidden() bool { + return c.hidden +} + +// Hidden hides the command from the Help system +func (c *Command) Hidden() { + c.hidden = true +} + +// NewSubCommand - Creates a new subcommand +func (c *Command) NewSubCommand(name, description string) *Command { + result := NewCommand(name, description) + c.AddCommand(result) + return result +} + +// AddCommand - Adds a subcommand +func (c *Command) AddCommand(command *Command) { + command.setApp(c.app) + command.setParentCommandPath(c.commandPath) + name := command.name + c.subCommands = append(c.subCommands, command) + c.subCommandsMap[name] = command + if len(name) > c.longestSubcommand { + c.longestSubcommand = len(name) + } +} + +// BoolFlag - Adds a boolean flag to the command +func (c *Command) BoolFlag(name, description string, variable *bool) *Command { + c.flags.BoolVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// StringFlag - Adds a string flag to the command +func (c *Command) StringFlag(name, description string, variable *string) *Command { + c.flags.StringVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// IntFlag - Adds an int flag to the command +func (c *Command) IntFlag(name, description string, variable *int) *Command { + c.flags.IntVar(variable, name, *variable, description) + c.flagCount++ + return c +} + +// LongDescription - Sets the long description for the command +func (c *Command) LongDescription(longdescription string) *Command { + c.longdescription = longdescription + return c +} + +// OtherArgs - Returns the non-flag arguments passed to the subcommand. NOTE: This should only be called within the context of an action. +func (c *Command) OtherArgs() []string { + return c.flags.Args() +} diff --git a/command_test.go b/command_test.go new file mode 100644 index 0000000..59d3374 --- /dev/null +++ b/command_test.go @@ -0,0 +1,154 @@ +package kli + +import ( + "testing" +) + +func TestCommand(t *testing.T) { + c := &Command{} + + t.Run("Run NewCommand()", func(t *testing.T) { + c = NewCommand("test", "test description") + }) + + t.Run("Run setParentCommandPath()", func(t *testing.T) { + c.setParentCommandPath("path") + }) + + t.Run("Run setApp()", func(t *testing.T) { + c.setApp(&Cli{}) + }) + + t.Run("Run parseFlags()", func(t *testing.T) { + _, err := c.parseFlags([]string{"test", "flags"}) + t.Log(err) + }) + + t.Run("Run run()", func(t *testing.T) { + cl := NewCli("test", "description", "0") + cl.rootCommand.run([]string{"test"}) + + cl.rootCommand.subCommandsMap["test"] = &Command{ + name: "subcom", + shortdescription: "short description", + hidden: false, + app: cl, + } + cl.rootCommand.run([]string{"test"}) + + cl.rootCommand.run([]string{"---"}) + + cl.rootCommand.run([]string{"-help"}) + + // cl.rootCommand.actionCallback = func() error { + // println("Hello World!") + // return nil + // } + // cl.rootCommand.run([]string{"test"}) + + cl.rootCommand.app.defaultCommand = &Command{ + name: "subcom", + shortdescription: "short description", + hidden: false, + app: cl, + } + cl.rootCommand.run([]string{"test"}) + }) + + t.Run("Run Action()", func(t *testing.T) { + c.Action(func() error { return nil }) + + }) + + t.Run("Run PrintHelp()", func(t *testing.T) { + cl := NewCli("test", "description", "0") + + // co.shortdescription = "" + cl.PrintHelp() + + cl.rootCommand.shortdescription = "test" + cl.PrintHelp() + + cl.rootCommand.commandPath = "notTest" + cl.PrintHelp() + + cl.rootCommand.longdescription = "" + cl.PrintHelp() + + cl.rootCommand.longdescription = "test" + cl.PrintHelp() + + mockCommand := &Command{ + name: "test", + shortdescription: "short description", + hidden: true, + longestSubcommand: len("test"), + } + cl.rootCommand.subCommands = append(cl.rootCommand.subCommands, mockCommand) + cl.PrintHelp() + + mockCommand = &Command{ + name: "subcom", + shortdescription: "short description", + hidden: false, + app: cl, + } + cl.rootCommand.longestSubcommand = 10 + cl.rootCommand.subCommands = append(cl.rootCommand.subCommands, mockCommand) + cl.PrintHelp() + + mockCommand = &Command{ + name: "subcom", + shortdescription: "short description", + hidden: false, + app: cl, + } + cl.rootCommand.longestSubcommand = 10 + cl.defaultCommand = mockCommand + cl.rootCommand.subCommands = append(cl.rootCommand.subCommands, mockCommand) + cl.PrintHelp() + + cl.rootCommand.flagCount = 3 + cl.PrintHelp() + }) + + t.Run("Run isDefaultCommand()", func(t *testing.T) { + c.isDefaultCommand() + + }) + + t.Run("Run isHidden()", func(t *testing.T) { + c.isHidden() + + }) + + t.Run("Run Hidden()", func(t *testing.T) { + c.Hidden() + }) + + t.Run("Run NewSubCommand()", func(t *testing.T) { + c.NewSubCommand("name", "description") + + }) + + t.Run("Run AddCommand()", func(t *testing.T) { + c.AddCommand(c) + }) + + t.Run("Run StringFlag()", func(t *testing.T) { + var variable = "variable" + c.StringFlag("name", "description", &variable) + + }) + + t.Run("Run IntFlag()", func(t *testing.T) { + var variable int + c.IntFlag("test", "description", &variable) + + }) + + t.Run("Run LongDescription()", func(t *testing.T) { + c.LongDescription("name") + + }) +}