github.com/keltia/go-ipfs@v0.3.8-0.20150909044612-210793031c63/commands/cli/helptext.go (about)

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"sort"
     7  	"strings"
     8  	"text/template"
     9  
    10  	cmds "github.com/ipfs/go-ipfs/commands"
    11  )
    12  
    13  const (
    14  	requiredArg = "<%v>"
    15  	optionalArg = "[<%v>]"
    16  	variadicArg = "%v..."
    17  	shortFlag   = "-%v"
    18  	longFlag    = "--%v"
    19  	optionType  = "(%v)"
    20  
    21  	whitespace = "\r\n\t "
    22  
    23  	indentStr = "    "
    24  )
    25  
    26  type helpFields struct {
    27  	Indent      string
    28  	Usage       string
    29  	Path        string
    30  	ArgUsage    string
    31  	Tagline     string
    32  	Arguments   string
    33  	Options     string
    34  	Synopsis    string
    35  	Subcommands string
    36  	Description string
    37  	MoreHelp    bool
    38  }
    39  
    40  // TrimNewlines removes extra newlines from fields. This makes aligning
    41  // commands easier. Below, the leading + tralining newlines are removed:
    42  //	Synopsis: `
    43  //	    ipfs config <key>          - Get value of <key>
    44  //	    ipfs config <key> <value>  - Set value of <key> to <value>
    45  //	    ipfs config --show         - Show config file
    46  //	    ipfs config --edit         - Edit config file in $EDITOR
    47  //	`
    48  func (f *helpFields) TrimNewlines() {
    49  	f.Path = strings.Trim(f.Path, "\n")
    50  	f.ArgUsage = strings.Trim(f.ArgUsage, "\n")
    51  	f.Tagline = strings.Trim(f.Tagline, "\n")
    52  	f.Arguments = strings.Trim(f.Arguments, "\n")
    53  	f.Options = strings.Trim(f.Options, "\n")
    54  	f.Synopsis = strings.Trim(f.Synopsis, "\n")
    55  	f.Subcommands = strings.Trim(f.Subcommands, "\n")
    56  	f.Description = strings.Trim(f.Description, "\n")
    57  }
    58  
    59  // Indent adds whitespace the lines of fields.
    60  func (f *helpFields) IndentAll() {
    61  	indent := func(s string) string {
    62  		if s == "" {
    63  			return s
    64  		}
    65  		return indentString(s, indentStr)
    66  	}
    67  
    68  	f.Arguments = indent(f.Arguments)
    69  	f.Options = indent(f.Options)
    70  	f.Synopsis = indent(f.Synopsis)
    71  	f.Subcommands = indent(f.Subcommands)
    72  	f.Description = indent(f.Description)
    73  }
    74  
    75  const usageFormat = "{{if .Usage}}{{.Usage}}{{else}}{{.Path}}{{if .ArgUsage}} {{.ArgUsage}}{{end}} - {{.Tagline}}{{end}}"
    76  
    77  const longHelpFormat = `
    78  {{.Indent}}{{template "usage" .}}
    79  
    80  {{if .Arguments}}ARGUMENTS:
    81  
    82  {{.Arguments}}
    83  
    84  {{end}}{{if .Options}}OPTIONS:
    85  
    86  {{.Options}}
    87  
    88  {{end}}{{if .Subcommands}}SUBCOMMANDS:
    89  
    90  {{.Subcommands}}
    91  
    92  {{.Indent}}Use '{{.Path}} <subcmd> --help' for more information about each command.
    93  
    94  {{end}}{{if .Description}}DESCRIPTION:
    95  
    96  {{.Description}}
    97  
    98  {{end}}
    99  `
   100  const shortHelpFormat = `USAGE:
   101  
   102  {{.Indent}}{{template "usage" .}}
   103  {{if .Synopsis}}
   104  {{.Synopsis}}
   105  {{end}}{{if .Description}}
   106  {{.Description}}
   107  {{end}}
   108  {{if .MoreHelp}}Use '{{.Path}} --help' for more information about this command.
   109  {{end}}
   110  `
   111  
   112  var usageTemplate *template.Template
   113  var longHelpTemplate *template.Template
   114  var shortHelpTemplate *template.Template
   115  
   116  func init() {
   117  	usageTemplate = template.Must(template.New("usage").Parse(usageFormat))
   118  	longHelpTemplate = template.Must(usageTemplate.New("longHelp").Parse(longHelpFormat))
   119  	shortHelpTemplate = template.Must(usageTemplate.New("shortHelp").Parse(shortHelpFormat))
   120  }
   121  
   122  // LongHelp returns a formatted CLI helptext string, generated for the given command
   123  func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
   124  	cmd, err := root.Get(path)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	pathStr := rootName
   130  	if len(path) > 0 {
   131  		pathStr += " " + strings.Join(path, " ")
   132  	}
   133  
   134  	fields := helpFields{
   135  		Indent:      indentStr,
   136  		Path:        pathStr,
   137  		ArgUsage:    usageText(cmd),
   138  		Tagline:     cmd.Helptext.Tagline,
   139  		Arguments:   cmd.Helptext.Arguments,
   140  		Options:     cmd.Helptext.Options,
   141  		Synopsis:    cmd.Helptext.Synopsis,
   142  		Subcommands: cmd.Helptext.Subcommands,
   143  		Description: cmd.Helptext.ShortDescription,
   144  		Usage:       cmd.Helptext.Usage,
   145  		MoreHelp:    (cmd != root),
   146  	}
   147  
   148  	if len(cmd.Helptext.LongDescription) > 0 {
   149  		fields.Description = cmd.Helptext.LongDescription
   150  	}
   151  
   152  	// autogen fields that are empty
   153  	if len(fields.Arguments) == 0 {
   154  		fields.Arguments = strings.Join(argumentText(cmd), "\n")
   155  	}
   156  	if len(fields.Options) == 0 {
   157  		fields.Options = strings.Join(optionText(cmd), "\n")
   158  	}
   159  	if len(fields.Subcommands) == 0 {
   160  		fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
   161  	}
   162  
   163  	// trim the extra newlines (see TrimNewlines doc)
   164  	fields.TrimNewlines()
   165  
   166  	// indent all fields that have been set
   167  	fields.IndentAll()
   168  
   169  	return longHelpTemplate.Execute(out, fields)
   170  }
   171  
   172  // ShortHelp returns a formatted CLI helptext string, generated for the given command
   173  func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
   174  	cmd, err := root.Get(path)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	// default cmd to root if there is no path
   180  	if path == nil && cmd == nil {
   181  		cmd = root
   182  	}
   183  
   184  	pathStr := rootName
   185  	if len(path) > 0 {
   186  		pathStr += " " + strings.Join(path, " ")
   187  	}
   188  
   189  	fields := helpFields{
   190  		Indent:      indentStr,
   191  		Path:        pathStr,
   192  		ArgUsage:    usageText(cmd),
   193  		Tagline:     cmd.Helptext.Tagline,
   194  		Synopsis:    cmd.Helptext.Synopsis,
   195  		Description: cmd.Helptext.ShortDescription,
   196  		Usage:       cmd.Helptext.Usage,
   197  		MoreHelp:    (cmd != root),
   198  	}
   199  
   200  	// trim the extra newlines (see TrimNewlines doc)
   201  	fields.TrimNewlines()
   202  
   203  	// indent all fields that have been set
   204  	fields.IndentAll()
   205  
   206  	return shortHelpTemplate.Execute(out, fields)
   207  }
   208  
   209  func argumentText(cmd *cmds.Command) []string {
   210  	lines := make([]string, len(cmd.Arguments))
   211  
   212  	for i, arg := range cmd.Arguments {
   213  		lines[i] = argUsageText(arg)
   214  	}
   215  	lines = align(lines)
   216  	for i, arg := range cmd.Arguments {
   217  		lines[i] += " - " + arg.Description
   218  	}
   219  
   220  	return lines
   221  }
   222  
   223  func optionFlag(flag string) string {
   224  	if len(flag) == 1 {
   225  		return fmt.Sprintf(shortFlag, flag)
   226  	} else {
   227  		return fmt.Sprintf(longFlag, flag)
   228  	}
   229  }
   230  
   231  func optionText(cmd ...*cmds.Command) []string {
   232  	// get a slice of the options we want to list out
   233  	options := make([]cmds.Option, 0)
   234  	for _, c := range cmd {
   235  		for _, opt := range c.Options {
   236  			options = append(options, opt)
   237  		}
   238  	}
   239  
   240  	// add option names to output (with each name aligned)
   241  	lines := make([]string, 0)
   242  	j := 0
   243  	for {
   244  		done := true
   245  		i := 0
   246  		for _, opt := range options {
   247  			if len(lines) < i+1 {
   248  				lines = append(lines, "")
   249  			}
   250  
   251  			names := sortByLength(opt.Names())
   252  			if len(names) >= j+1 {
   253  				lines[i] += optionFlag(names[j])
   254  			}
   255  			if len(names) > j+1 {
   256  				lines[i] += ", "
   257  				done = false
   258  			}
   259  
   260  			i++
   261  		}
   262  
   263  		if done {
   264  			break
   265  		}
   266  
   267  		lines = align(lines)
   268  		j++
   269  	}
   270  	lines = align(lines)
   271  
   272  	// add option types to output
   273  	for i, opt := range options {
   274  		lines[i] += " " + fmt.Sprintf("%v", opt.Type())
   275  	}
   276  	lines = align(lines)
   277  
   278  	// add option descriptions to output
   279  	for i, opt := range options {
   280  		lines[i] += " - " + opt.Description()
   281  	}
   282  
   283  	return lines
   284  }
   285  
   286  func subcommandText(cmd *cmds.Command, rootName string, path []string) []string {
   287  	prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
   288  	if len(path) > 0 {
   289  		prefix += " "
   290  	}
   291  	subcmds := make([]*cmds.Command, len(cmd.Subcommands))
   292  	lines := make([]string, len(cmd.Subcommands))
   293  
   294  	i := 0
   295  	for name, sub := range cmd.Subcommands {
   296  		usage := usageText(sub)
   297  		if len(usage) > 0 {
   298  			usage = " " + usage
   299  		}
   300  		lines[i] = prefix + name + usage
   301  		subcmds[i] = sub
   302  		i++
   303  	}
   304  
   305  	lines = align(lines)
   306  	for i, sub := range subcmds {
   307  		lines[i] += " - " + sub.Helptext.Tagline
   308  	}
   309  
   310  	return lines
   311  }
   312  
   313  func usageText(cmd *cmds.Command) string {
   314  	s := ""
   315  	for i, arg := range cmd.Arguments {
   316  		if i != 0 {
   317  			s += " "
   318  		}
   319  		s += argUsageText(arg)
   320  	}
   321  
   322  	return s
   323  }
   324  
   325  func argUsageText(arg cmds.Argument) string {
   326  	s := arg.Name
   327  
   328  	if arg.Required {
   329  		s = fmt.Sprintf(requiredArg, s)
   330  	} else {
   331  		s = fmt.Sprintf(optionalArg, s)
   332  	}
   333  
   334  	if arg.Variadic {
   335  		s = fmt.Sprintf(variadicArg, s)
   336  	}
   337  
   338  	return s
   339  }
   340  
   341  func align(lines []string) []string {
   342  	longest := 0
   343  	for _, line := range lines {
   344  		length := len(line)
   345  		if length > longest {
   346  			longest = length
   347  		}
   348  	}
   349  
   350  	for i, line := range lines {
   351  		length := len(line)
   352  		if length > 0 {
   353  			lines[i] += strings.Repeat(" ", longest-length)
   354  		}
   355  	}
   356  
   357  	return lines
   358  }
   359  
   360  func indent(lines []string, prefix string) []string {
   361  	for i, line := range lines {
   362  		lines[i] = prefix + indentString(line, prefix)
   363  	}
   364  	return lines
   365  }
   366  
   367  func indentString(line string, prefix string) string {
   368  	return prefix + strings.Replace(line, "\n", "\n"+prefix, -1)
   369  }
   370  
   371  type lengthSlice []string
   372  
   373  func (ls lengthSlice) Len() int {
   374  	return len(ls)
   375  }
   376  func (ls lengthSlice) Swap(a, b int) {
   377  	ls[a], ls[b] = ls[b], ls[a]
   378  }
   379  func (ls lengthSlice) Less(a, b int) bool {
   380  	return len(ls[a]) < len(ls[b])
   381  }
   382  
   383  func sortByLength(slice []string) []string {
   384  	output := make(lengthSlice, len(slice))
   385  	for i, val := range slice {
   386  		output[i] = val
   387  	}
   388  	sort.Sort(output)
   389  	return []string(output)
   390  }