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 }