~samwhited/cli

8c8ba2c6e0b25fd47fad0700a94dc36972c09791 — Sam Whited 2 years ago a4f0924 v0.0.6
support subcommands and remove CommandSet
7 files changed, 188 insertions(+), 77 deletions(-)

M cmd.go
M cmd_test.go
M example_article_test.go
M example_help_test.go
A example_subcommands_test.go
M example_test.go
M help.go
M cmd.go => cmd.go +47 -40
@@ 19,7 19,6 @@ import (
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
)



@@ 37,6 36,9 @@ type Command struct {
	// subcommand.
	Flags *flag.FlagSet

	// Commands is a set of subcommands.
	Commands []*Command

	// The action to take when this command is executed. The args will be the
	// remaining command line args after all flags have been parsed.
	// Run is normally called by a CommandSet and shouldn't be called directly.


@@ 47,16 49,23 @@ type Command struct {
// provided io.Writer.
// If c.Flags is a valid flag set, calling Help sets the output of c.Flags.
func (c *Command) Help(w io.Writer) {
	if c.Run != nil {
	if c == nil {
		return
	}
	// If there is a usage line and it's more than just the name, print it.
	if c.Usage != "" && c.Name() != c.Usage {
		fmt.Fprintf(w, "Usage: %s\n\n", c.Usage)
	}
	if c.Flags != nil {
		fmt.Fprint(w, "Options:\n\n")
		c.Flags.SetOutput(w)
		c.Flags.PrintDefaults()
		fmt.Fprintln(w, "")
	}
	if c.Description != "" {
		fmt.Fprintln(w, c.Description)
	}
	fmt.Fprintln(w, "")
	fmt.Fprintln(w, c.Description)
	printCmds(w, c.Commands...)
}

// Name returns the first word of c.Usage which will be the name of the command.


@@ 89,54 98,52 @@ func (c *Command) ShortDesc() string {
	return c.Description[:idx]
}

// CommandSet is a set of application subcommands and application level flags.
type CommandSet struct {
	Name     string
	Flags    *flag.FlagSet
	Commands []*Command
}

// Run attempts to run the command in the CommandSet that matches the first
// argument passed in.
// If no arguments are passed in, run prints help information to stdout.
// If the first argument does not match a command in the CommandSet, run prints
// the same help information to stderr.
func (cs *CommandSet) Run(args ...string) error {
	if len(args) == 0 || cs == nil {
		cs.Help(os.Stderr)
// Exec attempts to run the command that matches the first argument passed in
// (or the current command if no command name is provided and a Run function has
// been specified).
// It parses unparsed flags for each subcommand it encounters.
// If no command matches help information is written to stderr.
// If a command matches, there are remaining arguments after flag parsing
// completes, and no Run function is provided, help information is written to
// stdout.
func (c *Command) Exec(stdout, stderr io.Writer, args ...string) error {
	if c == nil {
		return nil
	}
	for _, cmd := range cs.Commands {
	if c.Flags != nil {
		if !c.Flags.Parsed() {
			err := c.Flags.Parse(args)
			if err != nil {
				return err
			}
		}
		args = c.Flags.Args()
	}
	if len(args) == 0 {
		if c.Run != nil {
			return c.Run(c)
		}
		c.Help(stdout)
		return nil
	}
	for _, cmd := range c.Commands {
		if cmd.Name() != args[0] {
			continue
		}

		if cmd.Run == nil {
			cmd.Help(os.Stdout)
			return nil
		}
		return cmd.Run(cmd, args[1:]...)
		return cmd.Exec(stdout, stderr, args[1:]...)
	}
	if c.Run != nil {
		return c.Run(c, args...)
	}
	cs.Help(os.Stderr)
	c.Help(stderr)
	return nil
}

// Help prints a usage line for the command set and a list of commands to the
// provided writer.
func (cs *CommandSet) Help(w io.Writer) {
	if cs == nil {
func printCmds(w io.Writer, commands ...*Command) {
	if len(commands) == 0 {
		return
	}
	fmt.Fprintf(w, "Usage of %s:\n\n", cs.Name)
	fmt.Fprintf(w, "%s [options] command\n\n", cs.Name)
	if cs.Flags != nil {
		cs.Flags.SetOutput(w)
		cs.Flags.PrintDefaults()
	}
	printCmds(w, cs.Commands...)
}

func printCmds(w io.Writer, commands ...*Command) {
	fmt.Fprint(w, "Commands:\n\n")
	for _, command := range commands {
		if command.Run == nil {

M cmd_test.go => cmd_test.go +4 -7
@@ 67,13 67,13 @@ func TestCommand(t *testing.T) {
}

var csTestCase = [...]struct {
	cs  *cli.CommandSet
	cs  *cli.Command
	run string
	err error
}{
	0: {},
	1: {
		cs: &cli.CommandSet{
		cs: &cli.Command{
			Commands: []*cli.Command{
				{Usage: "one [opts]"},
				{Usage: "two [opts]"},


@@ 88,17 88,14 @@ var csTestCase = [...]struct {
func TestCommandSet(t *testing.T) {
	for i, tc := range csTestCase {
		t.Run(fmt.Sprintf("Run/%d", i), func(t *testing.T) {
			stderr := os.Stderr
			r, w, _ := os.Pipe()
			os.Stderr = w
			go io.Copy(ioutil.Discard, r)
			if err := tc.cs.Run(); err != nil {
			if err := tc.cs.Exec(ioutil.Discard, w); err != nil {
				t.Errorf("Expected nil error when running with zero args, got=%v", err)
			}
			if err := tc.cs.Run(tc.run + " " + "arg1 " + "arg2"); err != tc.err {
			if err := tc.cs.Exec(ioutil.Discard, w, tc.run+" "+"arg1 "+"arg2"); err != tc.err {
				t.Errorf("Wrong err when running with args, want='%v', got='%v'", tc.err, err)
			}
			os.Stderr = stderr
		})
		if tc.cs != nil {
			t.Run(fmt.Sprintf("Help/%d", i), func(t *testing.T) {

M example_article_test.go => example_article_test.go +8 -9
@@ 6,6 6,7 @@ package cli_test

import (
	"fmt"
	"os"

	"mellium.im/cli"
)


@@ 25,25 26,23 @@ only exist so that their description can be shown using the help command
}

func Example_articles() {
	cmds := &cli.CommandSet{
		Name: "git",
	cmds := &cli.Command{
		Usage: "git <command>",
		Commands: []*cli.Command{
			commitCmd(""),
			commitCmd(nil),
			articleHelp(),
		},
	}
	cmds.Commands = append(cmds.Commands, cli.Help(cmds))
	fmt.Println("$ git help")
	cmds.Run("help")
	cmds.Exec(os.Stdout, os.Stdout, "help")

	fmt.Println("$ git help article")
	cmds.Run("help", "article")
	fmt.Print("$ git help article\n\n")
	cmds.Exec(os.Stdout, os.Stdout, "help", "article")

	// Output:
	// $ git help
	// Usage of git:
	//
	// git [options] command
	// Usage: git <command>
	//
	// Commands:
	//

M example_help_test.go => example_help_test.go +7 -7
@@ 5,23 5,23 @@
package cli_test

import (
	"os"

	"mellium.im/cli"
)

func ExampleHelp() {
	cmds := &cli.CommandSet{
		Name: "git",
	cmds := &cli.Command{
		Usage: "git [options] command",
	}
	cmds.Commands = []*cli.Command{
		commitCmd(""),
		commitCmd(nil),
		cli.Help(cmds),
	}
	cmds.Run("help")
	cmds.Exec(os.Stdout, os.Stdout, "help")

	// Output:
	// Usage of git:
	//
	// git [options] command
	// Usage: git [options] command
	//
	// Commands:
	//

A example_subcommands_test.go => example_subcommands_test.go +99 -0
@@ 0,0 1,99 @@
// Copyright 2017 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package cli_test

import (
	"fmt"
	"os"

	"mellium.im/cli"
)

func Example_subcommands() {
	cmds := &cli.Command{
		Usage: "go <command>",
		Run: func(c *cli.Command, args ...string) error {
			fmt.Println("Ran go")
			return nil
		},
		Commands: []*cli.Command{
			&cli.Command{
				Usage: `mod <command> [arguments]`,
				Description: `Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands…`,
				Run: func(c *cli.Command, args ...string) error {
					fmt.Println("Ran go mod")
					return nil
				},
				Commands: []*cli.Command{
					&cli.Command{
						Usage: `tidy [-v]`,
						Description: `Add missing and remove unused modules.

Tidy makes sure go.mod matches the source code in the module…`,
						Run: func(c *cli.Command, args ...string) error {
							fmt.Println("Ran go mod tidy")
							return nil
						},
					},
				},
			},
		},
	}
	cmds.Commands = append(cmds.Commands, cli.Help(cmds))
	fmt.Println("$ go help")
	cmds.Exec(os.Stdout, os.Stdout, "help")

	fmt.Print("$ go help mod\n\n")
	cmds.Exec(os.Stdout, os.Stdout, "help", "mod")

	fmt.Print("$ go help mod tidy\n\n")
	cmds.Exec(os.Stdout, os.Stdout, "help", "mod", "tidy")

	fmt.Print("$ go\n\n")
	cmds.Exec(os.Stdout, os.Stdout)

	fmt.Print("$ go mod\n\n")
	cmds.Exec(os.Stdout, os.Stdout, "mod")

	fmt.Print("$ go mod tidy\n\n")
	cmds.Exec(os.Stdout, os.Stdout, "mod", "tidy")

	// Output:
	// $ go help
	// Usage: go <command>
	//
	// Commands:
	//
	//	mod	Go mod provides access to operations on modules.
	//	help	Print articles and detailed information about subcommands.
	// $ go help mod
	//
	// Usage: mod <command> [arguments]
	//
	// Go mod provides access to operations on modules.
	//
	// Note that support for modules is built into all the go commands…
	// Commands:
	//
	//	tidy	Add missing and remove unused modules.
	// $ go help mod tidy
	//
	// Usage: tidy [-v]
	//
	// Add missing and remove unused modules.
	//
	// Tidy makes sure go.mod matches the source code in the module…
	// $ go
	//
	// Ran go
	// $ go mod
	//
	// Ran go mod
	// $ go mod tidy
	//
	// Ran go mod tidy
}

M example_test.go => example_test.go +13 -10
@@ 12,10 12,14 @@ import (
	"mellium.im/cli"
)

func commitCmd(cfg string) *cli.Command {
func commitCmd(cfg *string) *cli.Command {
	commitFlags := flag.NewFlagSet("commit", flag.ExitOnError)
	help := commitFlags.Bool("h", false, "Print this commands help output…")
	interactive := commitFlags.Bool("interactive", false, "Run commit in interactive mode.")
	if cfg == nil {
		empty := ""
		cfg = &empty
	}

	return &cli.Command{
		Usage: `commit [-h] [-interactive] …`,


@@ 24,8 28,7 @@ func commitCmd(cfg string) *cli.Command {
Stores the current contents of the index in a new commit…`,
		Flags: commitFlags,
		Run: func(c *cli.Command, args ...string) error {
			commitFlags.Parse(args)
			fmt.Printf("Using config file: %s\n", cfg)
			fmt.Printf("Using config file: %s\n", *cfg)
			if *interactive {
				fmt.Println("Interactive mode enabled.")
			}


@@ 41,16 44,16 @@ func Example() {
	globalFlags := flag.NewFlagSet("git", flag.ExitOnError)
	cfg := globalFlags.String("config", "gitconfig", "A custom config file to load")

	// In a real main function, this would probably be os.Args[1:]
	globalFlags.Parse([]string{"-config", "mygit.config", "commit", "-interactive", "-h"})

	cmds := &cli.CommandSet{
		Name: "git",
	cmds := &cli.Command{
		Usage: "git",
		Flags: globalFlags,
		Commands: []*cli.Command{
			commitCmd(*cfg),
			commitCmd(cfg),
		},
	}
	cmds.Run(globalFlags.Args()...)

	// In a real main function, this would probably be os.Args[1:]
	cmds.Exec(os.Stdout, os.Stdout, "-config", "mygit.config", "commit", "-interactive", "-h")

	// Output:
	// Using config file: mygit.config

M help.go => help.go +10 -4
@@ 17,7 17,7 @@ import (
//     git help commit
//
// would print information about the "commit" subcommand.
func Help(cs *CommandSet) *Command {
func Help(cs *Command) *Command {
	return &Command{
		Usage:       "help [command]",
		Description: `Print articles and detailed information about subcommands.`,


@@ 33,10 33,16 @@ func Help(cs *CommandSet) *Command {
				if cmd.Name() != args[0] {
					continue
				}
				cmd.Help(os.Stdout)
				return nil
				// If this is the article, run its help command.
				if len(args) == 1 {
					cmd.Help(os.Stdout)
					return nil
				}

				// Recurse into subcommands:
				return Help(cmd).Run(cmd, args[1:]...)
			}
			return fmt.Errorf("no such help topic %s", args[0])
			return fmt.Errorf("unknown help topic")
		},
	}
}