github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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  		AllOnlyCommands: []string{"export-snapshot", "import-snapshot"},
   225  	}, {
   226  		Label:       i18n.G("Device"),
   227  		Description: i18n.G("manage device"),
   228  		Commands:    []string{"model", "reboot", "recovery"},
   229  	}, {
   230  		Label:       i18n.G("Warnings"),
   231  		Other:       true,
   232  		Description: i18n.G("manage warnings"),
   233  		Commands:    []string{"warnings", "okay"},
   234  	}, {
   235  		Label:       i18n.G("Assertions"),
   236  		Other:       true,
   237  		Description: i18n.G("manage assertions"),
   238  		Commands:    []string{"known", "ack"},
   239  	}, {
   240  		Label:           i18n.G("Introspection"),
   241  		Other:           true,
   242  		Description:     i18n.G("introspection and debugging of snapd"),
   243  		Commands:        []string{"version"},
   244  		AllOnlyCommands: []string{"debug"},
   245  	},
   246  	{
   247  		Label:           i18n.G("Development"),
   248  		Description:     i18n.G("developer-oriented features"),
   249  		Commands:        []string{"download", "pack", "run", "try"},
   250  		AllOnlyCommands: []string{"prepare-image"},
   251  	},
   252  }
   253  
   254  var (
   255  	longSnapDescription = strings.TrimSpace(i18n.G(`
   256  The snap command lets you install, configure, refresh and remove snaps.
   257  Snaps are packages that work across many different Linux distributions,
   258  enabling secure delivery and operation of the latest apps and utilities.
   259  `))
   260  	snapUsage               = i18n.G("Usage: snap <command> [<options>...]")
   261  	snapHelpCategoriesIntro = i18n.G("Commonly used commands can be classified as follows:")
   262  	snapHelpAllIntro        = i18n.G("Commands can be classified as follows:")
   263  	snapHelpAllFooter       = i18n.G("For more information about a command, run 'snap help <command>'.")
   264  	snapHelpFooter          = i18n.G("For a short summary of all commands, run 'snap help --all'.")
   265  )
   266  
   267  func printHelpHeader(cmdsIntro string) {
   268  	fmt.Fprintln(Stdout, longSnapDescription)
   269  	fmt.Fprintln(Stdout)
   270  	fmt.Fprintln(Stdout, snapUsage)
   271  	fmt.Fprintln(Stdout)
   272  	fmt.Fprintln(Stdout, cmdsIntro)
   273  }
   274  
   275  func printHelpAllFooter() {
   276  	fmt.Fprintln(Stdout)
   277  	fmt.Fprintln(Stdout, snapHelpAllFooter)
   278  }
   279  
   280  func printHelpFooter() {
   281  	printHelpAllFooter()
   282  	fmt.Fprintln(Stdout, snapHelpFooter)
   283  }
   284  
   285  // this is called when the Execute returns a flags.Error with ErrCommandRequired
   286  func printShortHelp() {
   287  	printHelpHeader(snapHelpCategoriesIntro)
   288  	maxLen := utf8.RuneCountInString("... Other")
   289  	var otherCommands []string
   290  	var develCateg *helpCategory
   291  	for _, categ := range helpCategories {
   292  		if categ.Other {
   293  			otherCommands = append(otherCommands, categ.Commands...)
   294  			continue
   295  		}
   296  		if categ.Label == "Development" {
   297  			develCateg = &categ
   298  		}
   299  		if l := utf8.RuneCountInString(categ.Label); l > maxLen {
   300  			maxLen = l
   301  		}
   302  	}
   303  
   304  	fmt.Fprintln(Stdout)
   305  	for _, categ := range helpCategories {
   306  		// Other and Development will come last
   307  		if categ.Other || categ.Label == "Development" || len(categ.Commands) == 0 {
   308  			continue
   309  		}
   310  		fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", "))
   311  	}
   312  	// ... Other
   313  	if len(otherCommands) > 0 {
   314  		fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, "... Other", strings.Join(otherCommands, ", "))
   315  	}
   316  	// Development last
   317  	if develCateg != nil && len(develCateg.Commands) > 0 {
   318  		fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, "Development", strings.Join(develCateg.Commands, ", "))
   319  	}
   320  	printHelpFooter()
   321  }
   322  
   323  // this is "snap help --all"
   324  func printLongHelp(parser *flags.Parser) {
   325  	printHelpHeader(snapHelpAllIntro)
   326  	maxLen := 0
   327  	for _, categ := range helpCategories {
   328  		for _, command := range categ.Commands {
   329  			if l := len(command); l > maxLen {
   330  				maxLen = l
   331  			}
   332  		}
   333  		for _, command := range categ.AllOnlyCommands {
   334  			if l := len(command); l > maxLen {
   335  				maxLen = l
   336  			}
   337  		}
   338  	}
   339  
   340  	// flags doesn't have a LookupCommand?
   341  	commands := parser.Commands()
   342  	cmdLookup := make(map[string]*flags.Command, len(commands))
   343  	for _, cmd := range commands {
   344  		cmdLookup[cmd.Name] = cmd
   345  	}
   346  
   347  	listCmds := func(cmds []string) {
   348  		for _, name := range cmds {
   349  			cmd := cmdLookup[name]
   350  			if cmd == nil {
   351  				fmt.Fprintf(Stderr, "??? Cannot find command %q mentioned in help categories, please report!\n", name)
   352  			} else {
   353  				fmt.Fprintf(Stdout, "    %*s  %s\n", -maxLen, name, cmd.ShortDescription)
   354  			}
   355  		}
   356  	}
   357  
   358  	for _, categ := range helpCategories {
   359  		fmt.Fprintln(Stdout)
   360  		fmt.Fprintf(Stdout, "  %s (%s):\n", categ.Label, categ.Description)
   361  		listCmds(categ.Commands)
   362  		listCmds(categ.AllOnlyCommands)
   363  	}
   364  	printHelpAllFooter()
   365  }