~samwhited/cli

f671aa35b583043349782d58320aba07eab6232b — Sam Whited 4 years ago fcf5af9 v0.0.4
Add Help command and help articles

Also improve the documentation somewhat and add more examples.
6 files changed, 171 insertions(+), 8 deletions(-)

M cmd.go
M cmd_test.go
A example_article_test.go
A example_help_test.go
M example_test.go
A help.go
M cmd.go => cmd.go +28 -2
@@ 39,6 39,7 @@ type Command struct {

	// 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.
	Run func(c *Command, args ...string) error
}



@@ 46,7 47,9 @@ 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) {
	fmt.Fprintf(w, "Usage: %s\n\n", c.Usage)
	if c.Run != nil {
		fmt.Fprintf(w, "Usage: %s\n\n", c.Usage)
	}
	if c.Flags != nil {
		fmt.Fprint(w, "Options:\n\n")
		c.Flags.SetOutput(w)


@@ 108,6 111,10 @@ func (cs *CommandSet) Run(args ...string) error {
			continue
		}

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


@@ 126,12 133,31 @@ func (cs *CommandSet) Help(w io.Writer) {
		cs.Flags.SetOutput(w)
		cs.Flags.PrintDefaults()
	}
	fmt.Fprint(w, "\nCommands:\n\n")
	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 {
			continue
		}
		name := command.Name()
		if short := command.ShortDesc(); short != "" {
			fmt.Fprintf(w, "\t%s\t%s\n", name, short)
			continue
		}
		fmt.Fprintf(w, "\t%s\n", name)
	}
	found := false
	for _, command := range commands {
		if command.Run != nil {
			continue
		}
		if !found {
			fmt.Fprint(w, "\nArticles:\n\n")
		}
		found = true
		name := command.Name()
		if short := command.ShortDesc(); short != "" {
			fmt.Fprintf(w, "\t%s\t%s\n", name, short)

M cmd_test.go => cmd_test.go +3 -3
@@ 22,7 22,7 @@ type testCase struct {
	desc string
}

var testCases = [...]testCase{
var commandTestCases = [...]testCase{
	0: {},
	1: {cmd: cli.Command{Usage: "name", Description: "desc"}, name: "name", desc: "desc"},
	2: {cmd: cli.Command{Usage: "name [options]", Description: "desc\nlong description"}, name: "name", desc: "desc"},


@@ 30,7 30,7 @@ var testCases = [...]testCase{

func TestCommand(t *testing.T) {
	b := new(bytes.Buffer)
	for i, tc := range testCases {
	for i, tc := range commandTestCases {
		t.Run(fmt.Sprintf("Name/%d", i), func(t *testing.T) {
			if name := tc.cmd.Name(); name != tc.name {
				t.Errorf("Invalid name: want=`%s`, got=`%s`", tc.name, name)


@@ 47,7 47,7 @@ func TestCommand(t *testing.T) {

			b.Reset()
			tc.cmd.Help(b)
			if !bytes.Contains(b.Bytes(), []byte(tc.cmd.Usage)) {
			if tc.cmd.Run != nil && !bytes.Contains(b.Bytes(), []byte(tc.cmd.Usage)) {
				t.Errorf("Expected cmd.Help() output to contain cmd.Usage")
			}
			if !bytes.Contains(b.Bytes(), []byte(tc.cmd.Description)) {

A example_article_test.go => example_article_test.go +65 -0
@@ 0,0 1,65 @@
// Copyright 2017 The Mellium Authors.
// 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"

	"mellium.im/cli"
)

// Returns a help article about the config file format.
func articleHelp() *cli.Command {
	return &cli.Command{
		Usage: `article`,
		Description: `Help article about help articles.

Help articles are "commands" that do not provide any functionality. They
only exist so that their description can be shown using the help command
(or your own help system):

    $ ./yourcmd help articlename`,
	}
}

func Example_articles() {
	cmds := &cli.CommandSet{
		Name: "git",
		Commands: []*cli.Command{
			commitCmd(""),
			articleHelp(),
		},
	}
	cmds.Commands = append(cmds.Commands, cli.Help(cmds))
	fmt.Println("$ git help")
	cmds.Run("help")

	fmt.Println("$ git help article")
	cmds.Run("help", "article")

	// Output:
	// $ git help
	// Usage of git:
	//
	// git [options] command
	//
	// Commands:
	//
	//	commit	Records changes to the repository.
	//	help	Print articles and detailed information about subcommands.
	//
	// Articles:
	//
	//	article	Help article about help articles.
	// $ git help article
	//
	// Help article about help articles.
	//
	// Help articles are "commands" that do not provide any functionality. They
	// only exist so that their description can be shown using the help command
	// (or your own help system):
	//
	//     $ ./yourcmd help articlename
}

A example_help_test.go => example_help_test.go +30 -0
@@ 0,0 1,30 @@
// Copyright 2017 The Mellium Authors.
// 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 (
	"mellium.im/cli"
)

func ExampleHelp() {
	cmds := &cli.CommandSet{
		Name: "git",
	}
	cmds.Commands = []*cli.Command{
		commitCmd(""),
		cli.Help(cmds),
	}
	cmds.Run("help")

	// Output:
	// Usage of git:
	//
	// git [options] command
	//
	// Commands:
	//
	//	commit	Records changes to the repository.
	//	help	Print articles and detailed information about subcommands.
}

M example_test.go => example_test.go +3 -3
@@ 18,7 18,7 @@ func commitCmd(cfg string) *cli.Command {
	interactive := commitFlags.Bool("interactive", false, "Run commit in interactive mode.")

	return &cli.Command{
		Usage: `commit [-h] …`,
		Usage: `commit [-h] [-interactive] …`,
		Description: `Records changes to the repository.

Stores the current contents of the index in a new commit…`,


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

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

	cmds := &cli.CommandSet{


@@ 55,7 55,7 @@ func Example() {
	// Output:
	// Using config file: mygit.config
	// Interactive mode enabled.
	// Usage: commit [-h] …
	// Usage: commit [-h] [-interactive] …
	//
	// Options:
	//

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

package cli

import (
	"fmt"
	"os"
)

// Help returns a Command that prints help information about its command set to
// stdout, or about a specific command if one is provided as an argument.
//
// For example, in a program called "git" running:
//
//     git help commit
//
// would print information about the "commit" subcommand.
func Help(cs *CommandSet) *Command {
	return &Command{
		Usage:       "help [command]",
		Description: `Print articles and detailed information about subcommands.`,
		Run: func(c *Command, args ...string) error {
			// If there aren't any arguments, print the main command help.
			if len(args) == 0 {
				cs.Help(os.Stdout)
				return nil
			}

			// Print the help for the provided subcommand or help topic.
			for _, cmd := range cs.Commands {
				if cmd.Name() != args[0] {
					continue
				}
				cmd.Help(os.Stdout)
				return nil
			}
			return fmt.Errorf("No such help topic %s\n", args[0])
		},
	}
}