Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Colorful help (#340) and some help refactoring #3541

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Headers.
// cmds.StringOption(apiAddrKwd, "Address for the daemon rpc API (overrides config)"),
// cmds.StringOption(swarmAddrKwd, "Address for the swarm socket (overrides config)"),
},
Subcommands: map[string]*cmds.Command{},
Subcommands: []*cmds.CmdInfo{},
Run: daemonFunc,
}

Expand Down
35 changes: 25 additions & 10 deletions cmd/ipfs/ipfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,42 @@ var commandsClientCmd = commands.CommandsCmd(Root)

// Commands in localCommands should always be run locally (even if daemon is running).
// They can override subcommands in commands.Root by defining a subcommand with the same name.
var localCommands = map[string]*cmds.Command{
"daemon": daemonCmd,
"init": initCmd,
"commands": commandsClientCmd,
// The key in this map is the position in the help text. As with the whole subcommand, the
// explicit positioning here takes precedence over the implicit one in commands.Root.Subcommands.
var localCommands = map[uint]*cmds.CmdInfo{
10: {"daemon", daemonCmd, "ADVANCED COMMANDS"},
0: {"init", initCmd, "BASIC COMMANDS"},
32: {"commands", commandsClientCmd, "TOOL COMMANDS"},
}
var localMap = make(map[*cmds.Command]bool)

func localCommandExists(name string) bool {
for _, v := range localCommands {
if v.Name == name {
return true
}
}
return false
}

func init() {
// setting here instead of in literal to prevent initialization loop
// (some commands make references to Root)
Root.Subcommands = localCommands

// copy all subcommands from commands.Root into this root (if they aren't already present)
for k, v := range commands.Root.Subcommands {
if _, found := Root.Subcommands[k]; !found {
Root.Subcommands[k] = v
// copy all subcommands which don't exist in localCommands from commands.Root into this root
for _, v := range commands.Root.Subcommands {
if !localCommandExists(v.Name) {
Root.Subcommands = append(Root.Subcommands, v)
}
}

// add local commands to this root in the right position
for k, v := range localCommands {
Root.Subcommands = append(Root.Subcommands[:k], append([]*cmds.CmdInfo{v}, Root.Subcommands[k:]...)...)
}

for _, v := range localCommands {
localMap[v] = true
localMap[v.Cmd] = true
}
}

Expand Down
7 changes: 3 additions & 4 deletions cmd/ipfs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func main() {
helpFunc = cmdsCli.LongHelp
}

helpFunc("ipfs", Root, invoc.path, w)
helpFunc("ipfs", Root, invoc.path, invoc.req, w)
}

// this is a message to tell the user how to get the help text
Expand Down Expand Up @@ -366,9 +366,8 @@ func commandDetails(path []string, root *cmds.Command) (*cmdDetails, error) {
// find the last command in path that has a cmdDetailsMap entry
cmd := root
for _, cmp := range path {
var found bool
cmd, found = cmd.Subcommands[cmp]
if !found {
cmd = cmd.Subcommand(cmp)
if cmd == nil {
return nil, fmt.Errorf("subcommand %s should be in root", cmp)
}

Expand Down
12 changes: 6 additions & 6 deletions commands/cli/cmd_suggestion.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
}

// Start with a simple strings.Contains check
for name, _ := range root.Subcommands {
if strings.Contains(arg, name) {
suggestions = append(suggestions, name)
for _, cInfo := range root.Subcommands {
if strings.Contains(arg, cInfo.Name) {
suggestions = append(suggestions, cInfo.Name)
}
}

Expand All @@ -61,10 +61,10 @@ func suggestUnknownCmd(args []string, root *cmds.Command) []string {
return suggestions
}

for name, _ := range root.Subcommands {
lev := levenshtein.DistanceForStrings([]rune(arg), []rune(name), options)
for _, cInfo := range root.Subcommands {
lev := levenshtein.DistanceForStrings([]rune(arg), []rune(cInfo.Name), options)
if lev <= MIN_LEVENSHTEIN {
sortableSuggestions = append(sortableSuggestions, &suggestion{name, lev})
sortableSuggestions = append(sortableSuggestions, &suggestion{cInfo.Name, lev})
}
}
sort.Sort(sortableSuggestions)
Expand Down
155 changes: 97 additions & 58 deletions commands/cli/helptext.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,26 @@ const (
whitespace = "\r\n\t "

indentStr = " "

formatReset = "\033[0m"
formatBold = "\033[1m"
formatUnderline = "\033[4m"
formatCyan = "\033[36m"
)

type helpFields struct {
Indent string
Usage string
Path string
ArgUsage string
Tagline string
Arguments string
Options string
Synopsis string
Subcommands string
Description string
MoreHelp bool
Indent string
Usage string
Path string
ArgUsage string
Tagline string
Arguments string
Options string
Synopsis string
Subcommands string
Description string
AdditionalHelp string
MoreHelp bool
}

// TrimNewlines removes extra newlines from fields. This makes aligning
Expand Down Expand Up @@ -96,6 +102,8 @@ const longHelpFormat = `USAGE
{{.Subcommands}}

{{.Indent}}Use '{{.Path}} <subcmd> --help' for more information about each command.
{{end}}{{if .AdditionalHelp}}
{{.AdditionalHelp}}
{{end}}
`
const shortHelpFormat = `USAGE
Expand All @@ -109,6 +117,8 @@ SUBCOMMANDS
{{.Subcommands}}
{{end}}{{if .MoreHelp}}
Use '{{.Path}} --help' for more information about this command.
{{end}}{{if .AdditionalHelp}}
{{.AdditionalHelp}}
{{end}}
`

Expand All @@ -123,7 +133,7 @@ func init() {
}

// LongHelp writes a formatted CLI helptext string to a Writer for the given command
func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
func LongHelp(rootName string, root *cmds.Command, path []string, req cmds.Request, out io.Writer) error {
cmd, err := root.Get(path)
if err != nil {
return err
Expand All @@ -135,32 +145,37 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
}

fields := helpFields{
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Arguments: cmd.Helptext.Arguments,
Options: cmd.Helptext.Options,
Synopsis: cmd.Helptext.Synopsis,
Subcommands: cmd.Helptext.Subcommands,
Description: cmd.Helptext.ShortDescription,
Usage: cmd.Helptext.Usage,
MoreHelp: (cmd != root),
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Arguments: cmd.Helptext.Arguments,
Options: cmd.Helptext.Options,
Synopsis: cmd.Helptext.Synopsis,
Subcommands: cmd.Helptext.Subcommands,
Description: cmd.Helptext.ShortDescription,
Usage: cmd.Helptext.Usage,
AdditionalHelp: cmd.Helptext.AdditionalHelp,
MoreHelp: (cmd != root),
}

if len(cmd.Helptext.LongDescription) > 0 {
fields.Description = cmd.Helptext.LongDescription
}

// autogen fields that are empty
useColor := false
if req != nil {
useColor, _, _ = req.Option("color").Bool()
}
if len(fields.Arguments) == 0 {
fields.Arguments = strings.Join(argumentText(cmd), "\n")
}
if len(fields.Options) == 0 {
fields.Options = strings.Join(optionText(cmd), "\n")
fields.Options = strings.Join(optionText(useColor, cmd), "\n")
}
if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path, useColor), "\n")
}
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
Expand All @@ -176,7 +191,7 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
}

// ShortHelp writes a formatted CLI helptext string to a Writer for the given command
func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
func ShortHelp(rootName string, root *cmds.Command, path []string, req cmds.Request, out io.Writer) error {
cmd, err := root.Get(path)
if err != nil {
return err
Expand All @@ -193,20 +208,26 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
}

fields := helpFields{
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Synopsis: cmd.Helptext.Synopsis,
Description: cmd.Helptext.ShortDescription,
Subcommands: cmd.Helptext.Subcommands,
Usage: cmd.Helptext.Usage,
MoreHelp: (cmd != root),
Indent: indentStr,
Path: pathStr,
ArgUsage: usageText(cmd),
Tagline: cmd.Helptext.Tagline,
Synopsis: cmd.Helptext.Synopsis,
Description: cmd.Helptext.ShortDescription,
Subcommands: cmd.Helptext.Subcommands,
Usage: cmd.Helptext.Usage,
AdditionalHelp: cmd.Helptext.AdditionalHelp,
MoreHelp: true,
}

// autogen fields that are empty
if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
if req != nil {
useColor, _, _ := req.Option("color").Bool()
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path, useColor), "\n")
} else {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path, false), "\n")
}
}
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
Expand Down Expand Up @@ -291,7 +312,7 @@ func optionFlag(flag string) string {
}
}

func optionText(cmd ...*cmds.Command) []string {
func optionText(useColor bool, cmd ...*cmds.Command) []string {
// get a slice of the options we want to list out
options := make([]cmds.Option, 0)
for _, c := range cmd {
Expand Down Expand Up @@ -340,43 +361,61 @@ func optionText(cmd ...*cmds.Command) []string {

// add option descriptions to output
for i, opt := range options {
lines[i] += " - " + opt.Description()
if useColor {
lines[i] += " - " + formatCyan + opt.Description() + formatReset
} else {
lines[i] += " - " + opt.Description()
}

}

return lines
}

func subcommandText(cmd *cmds.Command, rootName string, path []string) []string {
func subcommandText(cmd *cmds.Command, rootName string, path []string, useColor bool) []string {
prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
if len(path) > 0 {
prefix += " "
}

// Sorting fixes changing order bug #2981.
sortedNames := make([]string, 0)
for name := range cmd.Subcommands {
sortedNames = append(sortedNames, name)
}
sort.Strings(sortedNames)

subcmds := make([]*cmds.Command, len(cmd.Subcommands))
lines := make([]string, len(cmd.Subcommands))

for i, name := range sortedNames {
sub := cmd.Subcommands[name]
usage := usageText(sub)
var lastCommandGroup = ""
var lines []string
for _, cInfo := range cmd.Subcommands {
if lastCommandGroup != cInfo.Group {
lastCommandGroup = cInfo.Group
lines = append(lines, "")
lines = append(lines, lastCommandGroup)
}
usage := usageText(cInfo.Cmd)
if len(usage) > 0 {
usage = " " + usage
}
lines[i] = prefix + name + usage
subcmds[i] = sub
}
if useColor {
lines = append(lines, prefix+formatBold+cInfo.Name+formatReset+usage)
} else {
lines = append(lines, prefix+cInfo.Name+usage)
}

lines = align(lines)
for i, sub := range subcmds {
lines[i] += " - " + sub.Helptext.Tagline
}
lines = align(lines)
lastCommandGroup = ""
groupsBefore := 0
for i, cInfo := range cmd.Subcommands {
if lastCommandGroup != cInfo.Group {
lastCommandGroup = cInfo.Group
groupsBefore++
}
if groupsBefore > 0 {
// groupsBefore * 2 because there are 2 lines per group
lines[i+groupsBefore*2] = indentStr + lines[i+groupsBefore*2]
}
if useColor {
lines[i+groupsBefore*2] += " - " + formatCyan + cInfo.Cmd.Helptext.Tagline + formatReset
} else {
lines[i+groupsBefore*2] += " - " + cInfo.Cmd.Helptext.Tagline
}

}
return lines
}

Expand Down
Loading