github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap/cmd_help.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io"
    26  	"regexp"
    27  	"strings"
    28  	"unicode/utf8"
    29  
    30  	"github.com/jessevdk/go-flags"
    31  
    32  	"github.com/snapcore/snapd/i18n"
    33  )
    34  
    35  var shortHelpHelp = i18n.G("Show help about a command")
    36  var longHelpHelp = i18n.G(`
    37  The help command displays information about snap commands.
    38  `)
    39  
    40  // addHelp adds --help like what go-flags would do for us, but hidden
    41  func addHelp(parser *flags.Parser) error {
    42  	var help struct {
    43  		ShowHelp func() error `short:"h" long:"help"`
    44  	}
    45  	help.ShowHelp = func() error {
    46  		// this function is called via --help (or -h). In that
    47  		// case, parser.Command.Active should be the command
    48  		// on which help is being requested (like "snap foo
    49  		// --help", active is foo), or nil in the toplevel.
    50  		if parser.Command.Active == nil {
    51  			// this means *either* a bare 'snap --help',
    52  			// *or* 'snap --help command'
    53  			//
    54  			// If we return nil in the first case go-flags
    55  			// will throw up an ErrCommandRequired on its
    56  			// own, but in the second case it'll go on to
    57  			// run the command, which is very unexpected.
    58  			//
    59  			// So we force the ErrCommandRequired here.
    60  
    61  			// toplevel --help gets handled via ErrCommandRequired
    62  			return &flags.Error{Type: flags.ErrCommandRequired}
    63  		}
    64  		// not toplevel, so ask for regular help
    65  		return &flags.Error{Type: flags.ErrHelp}
    66  	}
    67  	hlpgrp, err := parser.AddGroup("Help Options", "", &help)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	hlpgrp.Hidden = true
    72  	hlp := parser.FindOptionByLongName("help")
    73  	hlp.Description = i18n.G("Show this help message")
    74  	hlp.Hidden = true
    75  
    76  	return nil
    77  }
    78  
    79  type cmdHelp struct {
    80  	All        bool `long:"all"`
    81  	Manpage    bool `long:"man" hidden:"true"`
    82  	Positional struct {
    83  		// TODO: find a way to make Command tab-complete
    84  		Subs []string `positional-arg-name:"<command>"`
    85  	} `positional-args:"yes"`
    86  	parser *flags.Parser
    87  }
    88  
    89  func init() {
    90  	addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} },
    91  		map[string]string{
    92  			// TRANSLATORS: This should not start with a lowercase letter.
    93  			"all": i18n.G("Show a short summary of all commands"),
    94  			// TRANSLATORS: This should not start with a lowercase letter.
    95  			"man": i18n.G("Generate the manpage"),
    96  		}, nil)
    97  }
    98  
    99  func (cmd *cmdHelp) setParser(parser *flags.Parser) {
   100  	cmd.parser = parser
   101  }
   102  
   103  // manfixer is a hackish way to fix drawbacks in the generated manpage:
   104  // - no way to get it into section 8
   105  // - duplicated TP lines that break older groff (e.g. 14.04), lp:1814767
   106  type manfixer struct {
   107  	bytes.Buffer
   108  	done bool
   109  }
   110  
   111  func (w *manfixer) Write(buf []byte) (int, error) {
   112  	if !w.done {
   113  		w.done = true
   114  		if bytes.HasPrefix(buf, []byte(".TH snap 1 ")) {
   115  			// io.Writer.Write must not modify the buffer, even temporarily
   116  			n, _ := w.Buffer.Write(buf[:9])
   117  			w.Buffer.Write([]byte{'8'})
   118  			m, err := w.Buffer.Write(buf[10:])
   119  			return n + m + 1, err
   120  		}
   121  	}
   122  	return w.Buffer.Write(buf)
   123  }
   124  
   125  var tpRegexp = regexp.MustCompile(`(?m)(?:^\.TP\n)+`)
   126  
   127  func (w *manfixer) flush() {
   128  	str := tpRegexp.ReplaceAllLiteralString(w.Buffer.String(), ".TP\n")
   129  	io.Copy(Stdout, strings.NewReader(str))
   130  }
   131  
   132  func (cmd cmdHelp) Execute(args []string) error {
   133  	if len(args) > 0 {
   134  		return ErrExtraArgs
   135  	}
   136  	if cmd.Manpage {
   137  		// you shouldn't try to to combine --man with --all nor a
   138  		// subcommand, but --man is hidden so no real need to check.
   139  		out := &manfixer{}
   140  		cmd.parser.WriteManPage(out)
   141  		out.flush()
   142  		return nil
   143  	}
   144  	if cmd.All {
   145  		if len(cmd.Positional.Subs) > 0 {
   146  			return fmt.Errorf(i18n.G("help accepts a command, or '--all', but not both."))
   147  		}
   148  		printLongHelp(cmd.parser)
   149  		return nil
   150  	}
   151  
   152  	var subcmd = cmd.parser.Command
   153  	for _, subname := range cmd.Positional.Subs {
   154  		subcmd = subcmd.Find(subname)
   155  		if subcmd == nil {
   156  			sug := "snap help"
   157  			if x := cmd.parser.Command.Active; x != nil && x.Name != "help" {
   158  				sug = "snap help " + x.Name
   159  			}
   160  			// TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help <cmd>'
   161  			return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), subname, sug)
   162  		}
   163  		// this makes "snap help foo" work the same as "snap foo --help"
   164  		cmd.parser.Command.Active = subcmd
   165  	}
   166  	if subcmd != cmd.parser.Command {
   167  		return &flags.Error{Type: flags.ErrHelp}
   168  	}
   169  	return &flags.Error{Type: flags.ErrCommandRequired}
   170  }
   171  
   172  type helpCategory struct {
   173  	Label string
   174  	// Other is set if the category Commands should be listed
   175  	// together under "... Other" in the `snap help` list.
   176  	Other       bool
   177  	Description string
   178  	// Commands list commands belonging to the category that should
   179  	// be listed under both `snap help` and "snap help --all`.
   180  	Commands []string
   181  	// AllOnlyCommands list commands belonging to the category that should
   182  	// be listed only under "snap help --all`.
   183  	AllOnlyCommands []string
   184  }
   185  
   186  // helpCategories helps us by grouping commands
   187  var helpCategories = []helpCategory{
   188  	{
   189  		Label:       i18n.G("Basics"),
   190  		Description: i18n.G("basic snap management"),
   191  		Commands:    []string{"find", "info", "install", "remove", "list"},
   192  	}, {
   193  		Label:       i18n.G("...more"),
   194  		Description: i18n.G("slightly more advanced snap management"),
   195  		Commands:    []string{"refresh", "revert", "switch", "disable", "enable", "create-cohort"},
   196  	}, {
   197  		Label:       i18n.G("History"),
   198  		Description: i18n.G("manage system change transactions"),
   199  		Commands:    []string{"changes", "tasks", "abort", "watch"},
   200  	}, {
   201  		Label:       i18n.G("Daemons"),
   202  		Description: i18n.G("manage services"),
   203  		Commands:    []string{"services", "start", "stop", "restart", "logs"},
   204  	}, {
   205  		Label:       i18n.G("Permissions"),
   206  		Description: i18n.G("manage permissions"),
   207  		Commands:    []string{"connections", "interface", "connect", "disconnect"},
   208  	}, {
   209  		Label:       i18n.G("Configuration"),
   210  		Description: i18n.G("system administration and configuration"),
   211  		Commands:    []string{"get", "set", "unset", "wait"},
   212  	}, {
   213  		Label:       i18n.G("App Aliases"),
   214  		Description: i18n.G("manage aliases"),
   215  		Commands:    []string{"alias", "aliases", "unalias", "prefer"},
   216  	}, {
   217  		Label:       i18n.G("Account"),
   218  		Description: i18n.G("authentication to snapd and the snap store"),
   219  		Commands:    []string{"login", "logout", "whoami"},
   220  	}, {
   221  		Label:       i18n.G("Snapshots"),
   222  		Description: i18n.G("archives of snap data"),
   223  		Commands:    []string{"saved", "save", "check-snapshot", "restore", "forget"},
   224  	}, {
   225  		Label:       i18n.G("Device"),
   226  		Description: i18n.G("manage device"),
   227  		Commands:    []string{"model", "reboot", "recovery"},
   228  	}, {
   229  		Label:       i18n.G("Warnings"),
   230  		Other:       true,
   231  		Description: i18n.G("manage warnings"),
   232  		Commands:    []string{"warnings", "okay"},
   233  	}, {
   234  		Label:       i18n.G("Assertions"),
   235  		Other:       true,
   236  		Description: i18n.G("manage assertions"),
   237  		Commands:    []string{"known", "ack"},
   238  	}, {
   239  		Label:           i18n.G("Introspection"),
   240  		Other:           true,
   241  		Description:     i18n.G("introspection and debugging of snapd"),
   242  		Commands:        []string{"version"},
   243  		AllOnlyCommands: []string{"debug"},
   244  	},
   245  	{
   246  		Label:           i18n.G("Development"),
   247  		Description:     i18n.G("developer-oriented features"),
   248  		Commands:        []string{"download", "pack", "run", "try"},
   249  		AllOnlyCommands: []string{"prepare-image"},
   250  	},
   251  }
   252  
   253  var (
   254  	longSnapDescription = strings.TrimSpace(i18n.G(`
   255  The snap command lets you install, configure, refresh and remove snaps.
   256  Snaps are packages that work across many different Linux distributions,
   257  enabling secure delivery and operation of the latest apps and utilities.
   258  `))
   259  	snapUsage               = i18n.G("Usage: snap <command> [<options>...]")
   260  	snapHelpCategoriesIntro = i18n.G("Commonly used commands can be classified as follows:")
   261  	snapHelpAllIntro        = i18n.G("Commands can be classified as follows:")
   262  	snapHelpAllFooter       = i18n.G("For more information about a command, run 'snap help <command>'.")
   263  	snapHelpFooter          = i18n.G("For a short summary of all commands, run 'snap help --all'.")
   264  )
   265  
   266  func printHelpHeader(cmdsIntro string) {
   267  	fmt.Fprintln(Stdout, longSnapDescription)
   268  	fmt.Fprintln(Stdout)
   269  	fmt.Fprintln(Stdout, snapUsage)
   270  	fmt.Fprintln(Stdout)
   271  	fmt.Fprintln(Stdout, cmdsIntro)
   272  }
   273  
   274  func printHelpAllFooter() {
   275  	fmt.Fprintln(Stdout)
   276  	fmt.Fprintln(Stdout, snapHelpAllFooter)
   277  }
   278  
   279  func printHelpFooter() {
   280  	printHelpAllFooter()
   281  	fmt.Fprintln(Stdout, snapHelpFooter)
   282  }
   283  
   284  // this is called when the Execute returns a flags.Error with ErrCommandRequired
   285  func printShortHelp() {
   286  	printHelpHeader(snapHelpCategoriesIntro)
   287  	maxLen := utf8.RuneCountInString("... Other")
   288  	var otherCommands []string
   289  	var develCateg *helpCategory
   290  	for _, categ := range helpCategories {
   291  		if categ.Other {
   292  			otherCommands = append(otherCommands, categ.Commands...)
   293  			continue
   294  		}
   295  		if categ.Label == "Development" {
   296  			develCateg = &categ
   297  		}
   298  		if l := utf8.RuneCountInString(categ.Label); l > maxLen {
   299  			maxLen = l
   300  		}
   301  	}
   302  
   303  	fmt.Fprintln(Stdout)
   304  	for _, categ := range helpCategories {
   305  		// Other and Development will come last
   306  		if categ.Other || categ.Label == "Development" || len(categ.Commands) == 0 {
   307  			continue
   308  		}
   309  		fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", "))
   310  	}
   311  	// ... Other
   312  	if len(otherCommands) > 0 {
   313  		fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, "... Other", strings.Join(otherCommands, ", "))
   314  	}
   315  	// Development last
   316  	if develCateg != nil && len(develCateg.Commands) > 0 {
   317  		fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, "Development", strings.Join(develCateg.Commands, ", "))
   318  	}
   319  	printHelpFooter()
   320  }
   321  
   322  // this is "snap help --all"
   323  func printLongHelp(parser *flags.Parser) {
   324  	printHelpHeader(snapHelpAllIntro)
   325  	maxLen := 0
   326  	for _, categ := range helpCategories {
   327  		for _, command := range categ.Commands {
   328  			if l := len(command); l > maxLen {
   329  				maxLen = l
   330  			}
   331  		}
   332  		for _, command := range categ.AllOnlyCommands {
   333  			if l := len(command); l > maxLen {
   334  				maxLen = l
   335  			}
   336  		}
   337  	}
   338  
   339  	// flags doesn't have a LookupCommand?
   340  	commands := parser.Commands()
   341  	cmdLookup := make(map[string]*flags.Command, len(commands))
   342  	for _, cmd := range commands {
   343  		cmdLookup[cmd.Name] = cmd
   344  	}
   345  
   346  	listCmds := func(cmds []string) {
   347  		for _, name := range cmds {
   348  			cmd := cmdLookup[name]
   349  			if cmd == nil {
   350  				fmt.Fprintf(Stderr, "??? Cannot find command %q mentioned in help categories, please report!\n", name)
   351  			} else {
   352  				fmt.Fprintf(Stdout, "    %*s  %s\n", -maxLen, name, cmd.ShortDescription)
   353  			}
   354  		}
   355  	}
   356  
   357  	for _, categ := range helpCategories {
   358  		fmt.Fprintln(Stdout)
   359  		fmt.Fprintf(Stdout, "  %s (%s):\n", categ.Label, categ.Description)
   360  		listCmds(categ.Commands)
   361  		listCmds(categ.AllOnlyCommands)
   362  	}
   363  	printHelpAllFooter()
   364  }