github.com/koron/hk@v0.0.0-20150303213137-b8aeaa3ab34c/help.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"html/template"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  	"text/tabwriter"
    13  )
    14  
    15  var helpEnviron = &Command{
    16  	Usage:    "environ",
    17  	Category: "hk",
    18  	Short:    "environment variables used by hk",
    19  	Long: `
    20  Several environment variables affect hk's behavior.
    21  
    22  HEROKU_API_URL
    23  
    24    The base URL hk will use to make api requests in the format:
    25    https://[username][:password]@host[:port]/
    26  
    27    If username and password are present in the URL, they will
    28    override .netrc.
    29  
    30    Its default value is https://api.heroku.com/
    31  
    32  HEROKU_SSL_VERIFY
    33  
    34    When set to disable, hk will insecurely skip SSL verification.
    35  
    36  HKHEADER
    37  
    38    A NL-separated list of fields to set in each API request header.
    39    These override any fields set by hk if they have the same name.
    40  
    41  HKPATH
    42  
    43    A list of directories to search for plugins. This variable takes
    44    the same form as the system PATH var. If unset, the value is
    45    taken to be "/usr/local/lib/hk/plugin" on Unix.
    46  
    47    See 'hk help plugins' for information about the plugin interface.
    48  
    49  HKDEBUG
    50  
    51    When this is set, hk prints the wire representation of each API
    52    request to stderr just before sending the request, and prints the
    53    response. This will most likely include your secret API key in
    54    the Authorization header field, so be careful with the output.
    55  `,
    56  }
    57  
    58  var cmdVersion = &Command{
    59  	Run:      runVersion,
    60  	Usage:    "version",
    61  	Category: "hk",
    62  	Short:    "show hk version",
    63  	Long:     `Version shows the hk client version string.`,
    64  }
    65  
    66  func runVersion(cmd *Command, args []string) {
    67  	fmt.Println(Version)
    68  }
    69  
    70  var cmdHelp = &Command{
    71  	Usage:    "help [<topic>]",
    72  	Category: "hk",
    73  	Long:     `Help shows usage for a command or other topic.`,
    74  }
    75  
    76  var helpMore = &Command{
    77  	Usage:    "more",
    78  	Category: "hk",
    79  	Short:    "additional commands, less frequently used",
    80  	Long:     "(not displayed; see special case in runHelp)",
    81  }
    82  
    83  var helpCommands = &Command{
    84  	Usage:    "commands",
    85  	Category: "hk",
    86  	Short:    "list all commands with usage",
    87  	Long:     "(not displayed; see special case in runHelp)",
    88  }
    89  
    90  var helpStyleGuide = &Command{
    91  	Usage:    "styleguide",
    92  	Category: "hk",
    93  	Short:    "generate an html styleguide for all commands with usage",
    94  	Long:     "(not displayed; see special case in runHelp)",
    95  }
    96  
    97  func init() {
    98  	cmdHelp.Run = runHelp // break init loop
    99  }
   100  
   101  func runHelp(cmd *Command, args []string) {
   102  	if len(args) == 0 {
   103  		printUsageTo(os.Stdout)
   104  		return // not os.Exit(2); success
   105  	}
   106  	if len(args) != 1 {
   107  		printFatal("too many arguments")
   108  	}
   109  	switch args[0] {
   110  	case helpMore.Name():
   111  		printExtra()
   112  		return
   113  	case helpCommands.Name():
   114  		printAllUsage()
   115  		return
   116  	case helpStyleGuide.Name():
   117  		printStyleGuide()
   118  		return
   119  	}
   120  
   121  	for _, cmd := range commands {
   122  		if cmd.Name() == args[0] {
   123  			cmd.PrintLongUsage()
   124  			return
   125  		}
   126  	}
   127  
   128  	if lookupPlugin(args[0]) != "" {
   129  		_, _, long := pluginInfo(string(args[0]))
   130  		log.Println(long)
   131  		return
   132  	}
   133  
   134  	log.Printf("Unknown help topic: %q. Run 'hk help'.\n", args[0])
   135  	os.Exit(2)
   136  }
   137  
   138  func maxStrLen(strs []string) (strlen int) {
   139  	for i := range strs {
   140  		if len(strs[i]) > strlen {
   141  			strlen = len(strs[i])
   142  		}
   143  	}
   144  	return
   145  }
   146  
   147  var usageTemplate = template.Must(template.New("usage").Parse(`
   148  Usage: hk <command> [-a <app or remote>] [options] [arguments]
   149  
   150  
   151  Commands:
   152  {{range .Commands}}{{if .Runnable}}{{if .List}}
   153      {{.Name | printf (print "%-" $.MaxRunListName "s")}}  {{.Short}}{{end}}{{end}}{{end}}
   154  {{range .Plugins}}
   155      {{.Name | printf (print "%-" $.MaxRunListName "s")}}  {{.Short}} (plugin){{end}}
   156  
   157  Run 'hk help [command]' for details.
   158  
   159  
   160  Additional help topics:
   161  {{range .Commands}}{{if not .Runnable}}
   162      {{.Name | printf "%-8s"}}  {{.Short}}{{end}}{{end}}
   163  
   164  {{if .Dev}}This dev build of hk cannot auto-update itself.
   165  {{end}}`[1:]))
   166  
   167  var extraTemplate = template.Must(template.New("usage").Parse(`
   168  Additional commands:
   169  {{range .Commands}}{{if .Runnable}}{{if .ListAsExtra}}
   170      {{.Name | printf (print "%-" $.MaxRunExtraName "s")}}  {{.ShortExtra}}{{end}}{{end}}{{end}}
   171  
   172  Run 'hk help [command]' for details.
   173  
   174  `[1:]))
   175  
   176  func printUsageTo(w io.Writer) {
   177  	var plugins []plugin
   178  	for _, path := range strings.Split(hkPath, ":") {
   179  		d, err := os.Open(path)
   180  		if err != nil {
   181  			if os.IsNotExist(err) {
   182  				continue
   183  			}
   184  			printFatal(err.Error())
   185  		}
   186  		fi, err := d.Readdir(-1)
   187  		if err != nil {
   188  			printFatal(err.Error())
   189  		}
   190  		for _, f := range fi {
   191  			if !f.IsDir() && f.Mode()&0111 != 0 {
   192  				plugins = append(plugins, plugin(f.Name()))
   193  			}
   194  		}
   195  	}
   196  
   197  	var runListNames []string
   198  	for i := range commands {
   199  		if commands[i].Runnable() && commands[i].List() {
   200  			runListNames = append(runListNames, commands[i].Name())
   201  		}
   202  	}
   203  	for i := range plugins {
   204  		runListNames = append(runListNames, plugins[i].Name())
   205  	}
   206  
   207  	usageTemplate.Execute(w, struct {
   208  		Commands       []*Command
   209  		Plugins        []plugin
   210  		Dev            bool
   211  		MaxRunListName int
   212  	}{
   213  		commands,
   214  		plugins,
   215  		Version == "dev",
   216  		maxStrLen(runListNames),
   217  	})
   218  }
   219  
   220  func printExtra() {
   221  	var runExtraNames []string
   222  	for i := range commands {
   223  		if commands[i].Runnable() && commands[i].ListAsExtra() {
   224  			runExtraNames = append(runExtraNames, commands[i].Name())
   225  		}
   226  	}
   227  
   228  	extraTemplate.Execute(os.Stdout, struct {
   229  		Commands        []*Command
   230  		MaxRunExtraName int
   231  	}{
   232  		commands,
   233  		maxStrLen(runExtraNames),
   234  	})
   235  }
   236  
   237  func printAllUsage() {
   238  	w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0)
   239  	defer w.Flush()
   240  	cl := commandList(commands)
   241  	sort.Sort(cl)
   242  	for i := range cl {
   243  		if cl[i].Runnable() {
   244  			listRec(w, "hk "+cl[i].FullUsage(), "# "+cl[i].Short)
   245  		}
   246  	}
   247  }
   248  
   249  func printStyleGuide() {
   250  	cmap := make(map[string]commandList)
   251  	// group by category
   252  	for i := range commands {
   253  		if _, exists := cmap[commands[i].Category]; !exists {
   254  			cmap[commands[i].Category] = commandList{commands[i]}
   255  		} else {
   256  			cmap[commands[i].Category] = append(cmap[commands[i].Category], commands[i])
   257  		}
   258  	}
   259  	// sort each category
   260  	for _, cl := range cmap {
   261  		sort.Sort(cl)
   262  	}
   263  	err := styleGuideTemplate.Execute(os.Stdout, struct {
   264  		CommandMap commandMap
   265  	}{
   266  		cmap,
   267  	})
   268  	if err != nil {
   269  		printFatal(err.Error())
   270  	}
   271  }
   272  
   273  func (c *Command) UsageJSON() commandJSON {
   274  	return commandJSON{Root: c.Name(), Arguments: strings.TrimLeft(c.FullUsage(), c.Name()+" "), Comment: c.Short}
   275  }
   276  
   277  type commandJSON struct {
   278  	Root      string `json:"root"`
   279  	Arguments string `json:"arguments"`
   280  	Comment   string `json:"comment"`
   281  }
   282  
   283  type commandList []*Command
   284  
   285  func (cl commandList) Len() int           { return len(cl) }
   286  func (cl commandList) Swap(i, j int)      { cl[i], cl[j] = cl[j], cl[i] }
   287  func (cl commandList) Less(i, j int) bool { return cl[i].Name() < cl[j].Name() }
   288  
   289  func (cl commandList) UsageJSON() []commandJSON {
   290  	a := make([]commandJSON, len(cl))
   291  	for i := range cl {
   292  		a[i] = cl[i].UsageJSON()
   293  	}
   294  	return a
   295  }
   296  
   297  type commandMap map[string]commandList
   298  
   299  func (cm commandMap) UsageJSON(prefix string) template.JS {
   300  	all := make([]map[string]interface{}, 0)
   301  	categories := make([]string, len(cm))
   302  	iall := 0
   303  	for k := range cm {
   304  		categories[iall] = k
   305  		iall += 1
   306  	}
   307  	sort.Strings(categories)
   308  	for _, k := range categories {
   309  		m := map[string]interface{}{"title": k, "commands": cm[k].UsageJSON()}
   310  		all = append(all, m)
   311  	}
   312  	buf, err := json.MarshalIndent(all, prefix, "  ")
   313  	if err != nil {
   314  		return template.JS(fmt.Sprintf("{\"error\": %q}", err.Error()))
   315  	}
   316  	resp := strings.Replace(string(buf), "\\u003c", "<", -1)
   317  	resp = strings.Replace(resp, "\\u003e", ">", -1)
   318  	return template.JS(resp)
   319  }
   320  
   321  var styleGuideTemplate = template.Must(template.New("styleguide").Delims("{{{", "}}}").Parse(`
   322  <!DOCTYPE html>
   323  <html>
   324    <head>
   325      <title>hk style guide</title>
   326  
   327      <style>
   328        body {
   329          background: #282A36;
   330          color: white;
   331          font-family: Helvetica;
   332        }
   333  
   334        #viewing-options {
   335          padding: 0;
   336        }
   337  
   338        #viewing-options li {
   339          display: inline-block;
   340          margin-right: 20px;
   341        }
   342  
   343        td {
   344          font-family: monospace;
   345          padding-right: 10px;
   346        }
   347  
   348        td:first-child {
   349          width: 540px;
   350        }
   351  
   352        h2 {
   353          color: #5A5D6E;
   354        }
   355  
   356        .prompt,
   357        .comment {
   358          color: #6272A4;
   359        }
   360  
   361        .command {
   362          color: white;
   363        }
   364  
   365        .root {
   366          color: #FF79C6;
   367          font-weight: bold;
   368        }
   369  
   370        .arguments {
   371          color: #66D9D0;
   372        }
   373      </style>
   374    </head>
   375  
   376    <body>
   377      <script id="command-structure" type="text/x-handlebars-template">
   378        {{#groups}}
   379        <h2>{{title}}</h2>
   380  
   381        <table>
   382          {{#commands}}
   383          <tr>
   384            <td>
   385              <span class='prompt'>$</span>
   386              <span class='command'>hk</span>
   387              <span class='root'>{{root}}</span>
   388              <span class='arguments'>{{arguments}}</span>
   389            </td>
   390            <td class='comment'># {{comment}}</td>
   391          </tr>
   392          {{/commands}}
   393        </table>
   394        {{/groups}}
   395      </script>
   396  
   397      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
   398      <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.1.2/handlebars.min.js"></script>
   399  
   400      <script>
   401        var source = $('#command-structure').html();
   402        var template = Handlebars.compile(source);
   403  
   404        var data = {{{.CommandMap.UsageJSON "      "}}}
   405  
   406        $('body').append(template({groups: data}));
   407      </script>
   408    </body>
   409  </html>
   410  `[1:]))