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 }