diff --git a/internals/cli/cli.go b/internals/cli/cli.go index cfdd4cb3..27edb0a0 100644 --- a/internals/cli/cli.go +++ b/internals/cli/cli.go @@ -21,6 +21,7 @@ import ( "os" "os/user" "path/filepath" + "regexp" "strings" "unicode" "unicode/utf8" @@ -48,70 +49,52 @@ var ( // the pebble client. const defaultPebbleDir = "/var/lib/pebble/default" -type options struct { - Version func() `long:"version"` -} +// ErrExtraArgs is returned if extra arguments to a command are found +var ErrExtraArgs = fmt.Errorf("too many arguments for command") -type argDesc struct { - name string - desc string -} +// CmdInfo holds information needed by the CLI to execute commands and +// populate entries in the help manual. +type CmdInfo struct { + // Name of the command + Name string -var optionsData options + // Summary is a single-line help string that will be displayed + // in the full Pebble help manual (i.e. help --all) + Summary string -// ErrExtraArgs is returned if extra arguments to a command are found -var ErrExtraArgs = fmt.Errorf("too many arguments for command") + // Description contains exhaustive documentation about the command, + // that will be reflected in the specific help manual for the + // command, and in the Pebble man page. + Description string -// cmdInfo holds information needed to call parser.AddCommand(...). -type cmdInfo struct { - name, shortHelp, longHelp string - builder func() flags.Commander - hidden bool - optDescs map[string]string - argDescs []argDesc - alias string - extra func(*flags.Command) -} + // Builder is a function that creates a new instance of the command + // struct containing an Execute(args []string) implementation. + Builder func() flags.Commander -// commands holds information about all non-debug commands. -var commands []*cmdInfo - -// debugCommands holds information about all debug commands. -var debugCommands []*cmdInfo - -// addCommand replaces parser.addCommand() in a way that is compatible with -// re-constructing a pristine parser. -func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { - info := &cmdInfo{ - name: name, - shortHelp: shortHelp, - longHelp: longHelp, - builder: builder, - optDescs: optDescs, - argDescs: argDescs, - } - commands = append(commands, info) - return info + // ArgsHelp (optional) contains help about the command-line arguments + // (including options) supported by the command. + // + // map[string]string{ + // "--long-option": "my very long option", + // "-v": "verbose output", + // "": "named positional argument" + // } + ArgsHelp map[string]string + + // Whether to pass all arguments after the first non-option as remaining + // command line arguments. This is equivalent to strict POSIX processing. + PassAfterNonOption bool + + // When set, the command will be a subcommand of the `debug` command. + Debug bool } -// addDebugCommand replaces parser.addCommand() in a way that is -// compatible with re-constructing a pristine parser. It is meant for -// adding debug commands. -func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { - info := &cmdInfo{ - name: name, - shortHelp: shortHelp, - longHelp: longHelp, - builder: builder, - optDescs: optDescs, - argDescs: argDescs, - } - debugCommands = append(debugCommands, info) - return info -} +// commands holds information about all the regular Pebble commands. +var commands []*CmdInfo -type parserSetter interface { - setParser(*flags.Parser) +// AddCommand adds a command to the top-level parser. +func AddCommand(info *CmdInfo) { + commands = append(commands, info) } func lintDesc(cmdName, optName, desc, origDesc string) { @@ -168,30 +151,48 @@ func (ch *clientMixin) setClient(cli *client.Client) { ch.client = cli } +type parserSetter interface { + setParser(*flags.Parser) +} + +type defaultOptions struct { + Version func() `long:"version" hidden:"yes" description:"Print the version and exit"` +} + // Parser creates and populates a fresh parser. // Since commands have local state a fresh parser is required to isolate tests // from each other. func Parser(cli *client.Client) *flags.Parser { - optionsData.Version = func() { - printVersions(cli) - panic(&exitStatus{0}) + // Implement --version by default on every command + defaultOpts := defaultOptions{ + Version: func() { + printVersions(cli) + panic(&exitStatus{0}) + }, } - flagopts := flags.Options(flags.PassDoubleDash) - parser := flags.NewParser(&optionsData, flagopts) + + flagOpts := flags.Options(flags.PassDoubleDash) + parser := flags.NewParser(&defaultOpts, flagOpts) parser.ShortDescription = "Tool to interact with pebble" parser.LongDescription = longPebbleDescription - // hide the unhelpful "[OPTIONS]" from help output - parser.Usage = "" - if version := parser.FindOptionByLongName("version"); version != nil { - version.Description = "Print the version and exit" - version.Hidden = true - } - // add --help like what go-flags would do for us, but hidden + + // Add --help like what go-flags would do for us, but hidden addHelp(parser) - // Add all regular commands + // Regular expressions for positional and flag arguments + positionalRegexp := regexp.MustCompile(`^<\w+(-\w+)>$`) + flagRegexp := regexp.MustCompile(`^-\w|--\w+(-\w+)*$`) + + // Create debug command + debugCmd, err := parser.AddCommand("debug", cmdDebugSummary, cmdDebugDescription, &cmdDebug{}) + debugCmd.Hidden = true + if err != nil { + logger.Panicf("internal error: cannot add command %q: %v", "debug", err) + } + + // Add all commands for _, c := range commands { - obj := c.builder() + obj := c.Builder() if x, ok := obj.(clientSetter); ok { x.setClient(cli) } @@ -199,103 +200,55 @@ func Parser(cli *client.Client) *flags.Parser { x.setParser(parser) } - cmd, err := parser.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj) - if err != nil { - logger.Panicf("cannot add command %q: %v", c.name, err) - } - cmd.Hidden = c.hidden - if c.alias != "" { - cmd.Aliases = append(cmd.Aliases, c.alias) + var target *flags.Command + if c.Debug { + target = debugCmd + } else { + target = parser.Command } - - opts := cmd.Options() - if c.optDescs != nil && len(opts) != len(c.optDescs) { - logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs)) + cmd, err := target.AddCommand(c.Name, c.Summary, strings.TrimSpace(c.Description), obj) + if err != nil { + logger.Panicf("internal error: cannot add command %q: %v", c.Name, err) } - for _, opt := range opts { - name := opt.LongName - if name == "" { - name = string(opt.ShortName) - } - desc, ok := c.optDescs[name] - if !(c.optDescs == nil || ok) { - logger.Panicf("%s missing description for %s", c.name, name) - } - lintDesc(c.name, name, desc, opt.Description) - if desc != "" { - opt.Description = desc + cmd.PassAfterNonOption = c.PassAfterNonOption + + // Extract help for flags and positional arguments from ArgsHelp + flagHelp := map[string]string{} + positionalHelp := map[string]string{} + for specifier, help := range c.ArgsHelp { + if flagRegexp.MatchString(specifier) { + flagHelp[specifier] = help + } else if positionalRegexp.MatchString(specifier) { + positionalHelp[specifier] = help + } else { + logger.Panicf("internal error: invalid help specifier from %q: %q", c.Name, specifier) } } - args := cmd.Args() - if c.argDescs != nil && len(args) != len(c.argDescs) { - logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs)) - } - for i, arg := range args { - name, desc := arg.Name, "" - if c.argDescs != nil { - name = c.argDescs[i].name - desc = c.argDescs[i].desc - } - lintArg(c.name, name, desc, arg.Description) - name = fixupArg(name) - arg.Name = name - arg.Description = desc - } - if c.extra != nil { - c.extra(cmd) - } - } - // Add the debug command - debugCommand, err := parser.AddCommand("debug", shortDebugHelp, longDebugHelp, &cmdDebug{}) - debugCommand.Hidden = true - if err != nil { - logger.Panicf("cannot add command %q: %v", "debug", err) - } - // Add all the sub-commands of the debug command - for _, c := range debugCommands { - obj := c.builder() - if x, ok := obj.(clientSetter); ok { - x.setClient(cli) - } - cmd, err := debugCommand.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj) - if err != nil { - logger.Panicf("cannot add debug command %q: %v", c.name, err) - } - cmd.Hidden = c.hidden + // Make sure all argument descriptions are set opts := cmd.Options() - if c.optDescs != nil && len(opts) != len(c.optDescs) { - logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs)) + if len(opts) != len(flagHelp) { + logger.Panicf("internal error: wrong number of flag descriptions for %q: expected %d, got %d", c.Name, len(opts), len(flagHelp)) } for _, opt := range opts { - name := opt.LongName - if name == "" { - name = string(opt.ShortName) - } - desc, ok := c.optDescs[name] - if !(c.optDescs == nil || ok) { - logger.Panicf("%s missing description for %s", c.name, name) - } - lintDesc(c.name, name, desc, opt.Description) - if desc != "" { - opt.Description = desc + if description, ok := flagHelp["--"+opt.LongName]; ok { + lintDesc(c.Name, opt.LongName, description, opt.Description) + opt.Description = description + } else if description, ok := flagHelp["-"+string(opt.ShortName)]; ok { + lintDesc(c.Name, string(opt.ShortName), description, opt.Description) + opt.Description = description + } else if !opt.Hidden { + logger.Panicf("internal error: %q missing description for %q", c.Name, opt) } } args := cmd.Args() - if c.argDescs != nil && len(args) != len(c.argDescs) { - logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs)) - } - for i, arg := range args { - name, desc := arg.Name, "" - if c.argDescs != nil { - name = c.argDescs[i].name - desc = c.argDescs[i].desc + for _, arg := range args { + if description, ok := positionalHelp[arg.Name]; ok { + lintArg(c.Name, arg.Name, description, arg.Description) + arg.Name = fixupArg(arg.Name) + arg.Description = description } - lintArg(c.name, name, desc, arg.Description) - name = fixupArg(name) - arg.Name = name - arg.Description = desc } } return parser @@ -361,7 +314,7 @@ func Run() error { sug = "pebble help " + x.Name } } - return fmt.Errorf("unknown command %q, see '%s'.", sub, sug) + return fmt.Errorf("unknown command %q, see '%s'", sub, sug) } } diff --git a/internals/cli/cmd_add.go b/internals/cli/cmd_add.go index 521f1c4c..3a0d3f6e 100644 --- a/internals/cli/cmd_add.go +++ b/internals/cli/cmd_add.go @@ -23,6 +23,14 @@ import ( "github.com/canonical/pebble/client" ) +const cmdAddSummary = "Dynamically add a layer to the plan's layers" +const cmdAddDescription = ` +The add command reads the plan's layer YAML from the path specified and +appends a layer with the given label to the plan's layers. If --combine +is specified, combine the layer with an existing layer that has the given +label (or append if the label is not found). +` + type cmdAdd struct { clientMixin Combine bool `long:"combine"` @@ -32,18 +40,18 @@ type cmdAdd struct { } `positional-args:"yes"` } -var addDescs = map[string]string{ - "combine": `Combine the new layer with an existing layer that has the given label (default is to append)`, +func init() { + AddCommand(&CmdInfo{ + Name: "add", + Summary: cmdAddSummary, + Description: cmdAddDescription, + ArgsHelp: map[string]string{ + "--combine": "Combine the new layer with an existing layer that has the given label (default is to append)", + }, + Builder: func() flags.Commander { return &cmdAdd{} }, + }) } -var shortAddHelp = "Dynamically add a layer to the plan's layers" -var longAddHelp = ` -The add command reads the plan's layer YAML from the path specified and -appends a layer with the given label to the plan's layers. If --combine -is specified, combine the layer with an existing layer that has the given -label (or append if the label is not found). -` - func (cmd *cmdAdd) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -65,7 +73,3 @@ func (cmd *cmdAdd) Execute(args []string) error { cmd.Positional.Label, cmd.Positional.LayerPath) return nil } - -func init() { - addCommand("add", shortAddHelp, longAddHelp, func() flags.Commander { return &cmdAdd{} }, addDescs, nil) -} diff --git a/internals/cli/cmd_autostart.go b/internals/cli/cmd_autostart.go index 229691f4..251c64fc 100644 --- a/internals/cli/cmd_autostart.go +++ b/internals/cli/cmd_autostart.go @@ -20,8 +20,8 @@ import ( "github.com/canonical/pebble/client" ) -var shortAutoStartHelp = "Start services set to start by default" -var longAutoStartHelp = ` +const cmdAutoStartSummary = "Start services set to start by default" +const cmdAutoStartDescription = ` The autostart command starts the services that were configured to start by default. ` @@ -31,7 +31,13 @@ type cmdAutoStart struct { } func init() { - addCommand("autostart", shortAutoStartHelp, longAutoStartHelp, func() flags.Commander { return &cmdAutoStart{} }, waitDescs, nil) + AddCommand(&CmdInfo{ + Name: "autostart", + Summary: cmdAutoStartSummary, + Description: cmdAutoStartDescription, + ArgsHelp: waitArgsHelp, + Builder: func() flags.Commander { return &cmdAutoStart{} }, + }) } func (cmd cmdAutoStart) Execute(args []string) error { diff --git a/internals/cli/cmd_changes.go b/internals/cli/cmd_changes.go index b3888548..0e5fa935 100644 --- a/internals/cli/cmd_changes.go +++ b/internals/cli/cmd_changes.go @@ -24,15 +24,10 @@ import ( "github.com/canonical/pebble/client" ) -var shortChangesHelp = "List system changes" -var shortTasksHelp = "List a change's tasks" -var longChangesHelp = ` +const cmdChangesSummary = "List system changes" +const cmdChangesDescription = ` The changes command displays a summary of system changes performed recently. ` -var longTasksHelp = ` -The tasks command displays a summary of tasks associated with an individual -change that happened recently. -` type cmdChanges struct { clientMixin @@ -42,18 +37,32 @@ type cmdChanges struct { } `positional-args:"yes"` } +const cmdTasksSummary = "List a change's tasks" +const cmdTasksDescription = ` +The tasks command displays a summary of tasks associated with an individual +change that happened recently. +` + type cmdTasks struct { timeMixin changeIDMixin } func init() { - addCommand("changes", shortChangesHelp, longChangesHelp, - func() flags.Commander { return &cmdChanges{} }, timeDescs, nil) - addCommand("tasks", shortTasksHelp, longTasksHelp, - func() flags.Commander { return &cmdTasks{} }, - merge(changeIDMixinOptDesc, timeDescs), - changeIDMixinArgDesc) + AddCommand(&CmdInfo{ + Name: "changes", + Summary: cmdChangesSummary, + Description: cmdChangesDescription, + ArgsHelp: timeArgsHelp, + Builder: func() flags.Commander { return &cmdChanges{} }, + }) + AddCommand(&CmdInfo{ + Name: "tasks", + Summary: cmdTasksSummary, + Description: cmdTasksDescription, + ArgsHelp: merge(changeIDMixinArgsHelp, timeArgsHelp), + Builder: func() flags.Commander { return &cmdTasks{} }, + }) } type changesByTime []*client.Change diff --git a/internals/cli/cmd_checks.go b/internals/cli/cmd_checks.go index 4ee1af84..7da46f18 100644 --- a/internals/cli/cmd_checks.go +++ b/internals/cli/cmd_checks.go @@ -22,6 +22,13 @@ import ( "github.com/canonical/pebble/client" ) +const cmdChecksSummary = "Query the status of configured health checks" +const cmdChecksDescription = ` +The checks command lists status information about the configured health +checks, optionally filtered by level and check names provided as positional +arguments. +` + type cmdChecks struct { clientMixin Level string `long:"level"` @@ -30,17 +37,18 @@ type cmdChecks struct { } `positional-args:"yes"` } -var checksDescs = map[string]string{ - "level": `Check level to filter for ("alive" or "ready")`, +func init() { + AddCommand(&CmdInfo{ + Name: "checks", + Summary: cmdChecksSummary, + Description: cmdChecksDescription, + ArgsHelp: map[string]string{ + "--level": `Check level to filter for ("alive" or "ready")`, + }, + Builder: func() flags.Commander { return &cmdChecks{} }, + }) } -var shortChecksHelp = "Query the status of configured health checks" -var longChecksHelp = ` -The checks command lists status information about the configured health -checks, optionally filtered by level and check names provided as positional -arguments. -` - func (cmd *cmdChecks) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -77,7 +85,3 @@ func (cmd *cmdChecks) Execute(args []string) error { } return nil } - -func init() { - addCommand("checks", shortChecksHelp, longChecksHelp, func() flags.Commander { return &cmdChecks{} }, checksDescs, nil) -} diff --git a/internals/cli/cmd_debug.go b/internals/cli/cmd_debug.go index 201a0a1e..ea999393 100644 --- a/internals/cli/cmd_debug.go +++ b/internals/cli/cmd_debug.go @@ -14,12 +14,12 @@ package cli -type cmdDebug struct{} - -var shortDebugHelp = "Run debug commands" -var longDebugHelp = ` +var cmdDebugSummary = "Run debug commands" +var cmdDebugDescription = ` The debug command contains a selection of additional sub-commands. Debug commands can be removed without notice and may not work on non-development systems. ` + +type cmdDebug struct{} diff --git a/internals/cli/cmd_enter.go b/internals/cli/cmd_enter.go index 981d4034..6e4e6ccb 100644 --- a/internals/cli/cmd_enter.go +++ b/internals/cli/cmd_enter.go @@ -1,3 +1,17 @@ +// Copyright (c) 2023 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package cli import ( @@ -8,8 +22,8 @@ import ( "github.com/canonical/pebble/internals/logger" ) -const shortEnterHelp = "Run subcommand under a container environment" -const longEnterHelp = ` +const cmdEnterSummary = "Run subcommand under a container environment" +const cmdEnterDescription = ` The enter command facilitates the use of Pebble as an entrypoint for containers. When used without a subcommand it mimics the behavior of the run command alone, while if used with a subcommand it runs that subcommand in the most @@ -31,16 +45,6 @@ These subcommands are currently supported: (3) Services continue running after the subcommand succeeds. ` -type enterFlags int - -const ( - enterSilenceLogging enterFlags = 1 << iota - enterNoServiceManager - enterKeepServiceManager - enterRequireServiceAutostart - enterProhibitServiceAutostart -) - type cmdEnter struct { clientMixin sharedRunEnterOpts @@ -52,18 +56,28 @@ type cmdEnter struct { } func init() { - optsHelp := map[string]string{ - "run": "Start default services before executing subcommand", - } - for k, v := range sharedRunEnterOptsHelp { - optsHelp[k] = v - } - cmdInfo := addCommand("enter", shortEnterHelp, longEnterHelp, func() flags.Commander { return &cmdEnter{} }, optsHelp, nil) - cmdInfo.extra = func(cmd *flags.Command) { - cmd.PassAfterNonOption = true - } + AddCommand(&CmdInfo{ + Name: "enter", + Summary: cmdEnterSummary, + Description: cmdEnterDescription, + Builder: func() flags.Commander { return &cmdEnter{} }, + ArgsHelp: merge(sharedRunEnterArgsHelp, map[string]string{ + "--run": "Start default services before executing subcommand", + }), + PassAfterNonOption: true, + }) } +type enterFlags int + +const ( + enterSilenceLogging enterFlags = 1 << iota + enterNoServiceManager + enterKeepServiceManager + enterRequireServiceAutostart + enterProhibitServiceAutostart +) + func commandEnterFlags(commander flags.Commander) (enterFlags enterFlags, supported bool) { supported = true switch commander.(type) { diff --git a/internals/cli/cmd_enter_test.go b/internals/cli/cmd_enter_test.go index efcb76c5..80e43690 100644 --- a/internals/cli/cmd_enter_test.go +++ b/internals/cli/cmd_enter_test.go @@ -1,5 +1,16 @@ -// SPDX-FileCopyrightText: 2023 Canonical Ltd -// SPDX-License-Identifier: GPL-3.0-only +// Copyright (c) 2023 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . package cli_test @@ -99,7 +110,7 @@ func (s *PebbleSuite) TestEnterUnknownCommand(c *C) { defer restore() exitCode := cli.PebbleMain() - c.Check(s.Stderr(), Equals, "error: unknown command \"foo\", see 'pebble help'.\n") + c.Check(s.Stderr(), Equals, "error: unknown command \"foo\", see 'pebble help'\n") c.Check(s.Stdout(), Equals, "") c.Check(exitCode, Equals, 1) } diff --git a/internals/cli/cmd_exec.go b/internals/cli/cmd_exec.go index 1dae5300..d0d2fe43 100644 --- a/internals/cli/cmd_exec.go +++ b/internals/cli/cmd_exec.go @@ -30,6 +30,18 @@ import ( "github.com/canonical/pebble/internals/ptyutil" ) +const cmdExecSummary = "Execute a remote command and wait for it to finish" +const cmdExecDescription = ` +The exec command runs a remote command and waits for it to finish. The local +stdin is sent as the input to the remote process, while the remote stdout and +stderr are output locally. + +To avoid confusion, exec options may be separated from the command and its +arguments using "--", for example: + +pebble exec --timeout 10s -- echo -n foo bar +` + type cmdExec struct { clientMixin WorkingDir string `short:"w"` @@ -49,33 +61,30 @@ type cmdExec struct { } `positional-args:"yes"` } -var execDescs = map[string]string{ - "w": "Working directory to run command in", - "env": "Environment variable to set (in 'FOO=bar' format)", - "uid": "User ID to run command as", - "user": "Username to run command as (user's UID must match uid if both present)", - "gid": "Group ID to run command as", - "group": "Group name to run command as (group's GID must match gid if both present)", - "timeout": "Timeout after which to terminate command", - "context": "Inherit the context of the named service (overridden by -w, --env, --uid/user, --gid/group)", - "t": "Allocate remote pseudo-terminal and connect stdout to it (default if stdout is a TTY)", - "T": "Disable remote pseudo-terminal allocation", - "i": "Interactive mode: connect stdin to the pseudo-terminal (default if stdin and stdout are TTYs)", - "I": "Disable interactive mode and use a pipe for stdin", +func init() { + AddCommand(&CmdInfo{ + Name: "exec", + Summary: cmdExecSummary, + Description: cmdExecDescription, + ArgsHelp: map[string]string{ + "-w": "Working directory to run command in", + "--env": "Environment variable to set (in 'FOO=bar' format)", + "--uid": "User ID to run command as", + "--user": "Username to run command as (user's UID must match uid if both present)", + "--gid": "Group ID to run command as", + "--group": "Group name to run command as (group's GID must match gid if both present)", + "--timeout": "Timeout after which to terminate command", + "--context": "Inherit the context of the named service (overridden by -w, --env, --uid/user, --gid/group)", + "-t": "Allocate remote pseudo-terminal and connect stdout to it (default if stdout is a TTY)", + "-T": "Disable remote pseudo-terminal allocation", + "-i": "Interactive mode: connect stdin to the pseudo-terminal (default if stdin and stdout are TTYs)", + "-I": "Disable interactive mode and use a pipe for stdin", + }, + PassAfterNonOption: true, + Builder: func() flags.Commander { return &cmdExec{} }, + }) } -var shortExecHelp = "Execute a remote command and wait for it to finish" -var longExecHelp = ` -The exec command runs a remote command and waits for it to finish. The local -stdin is sent as the input to the remote process, while the remote stdout and -stderr are output locally. - -To avoid confusion, exec options may be separated from the command and its -arguments using "--", for example: - -pebble exec --timeout 10s -- echo -n foo bar -` - func (cmd *cmdExec) Execute(args []string) error { if cmd.Terminal && cmd.NoTerminal { return errors.New("cannot use -t and -T at the same time") @@ -265,10 +274,3 @@ func execControlHandler(process *client.ExecProcess, terminal bool, stop <-chan } } } - -func init() { - info := addCommand("exec", shortExecHelp, longExecHelp, func() flags.Commander { return &cmdExec{} }, execDescs, nil) - info.extra = func(cmd *flags.Command) { - cmd.PassAfterNonOption = true - } -} diff --git a/internals/cli/cmd_help.go b/internals/cli/cmd_help.go index 02b787f3..fbab7e10 100644 --- a/internals/cli/cmd_help.go +++ b/internals/cli/cmd_help.go @@ -25,11 +25,33 @@ import ( "github.com/canonical/go-flags" ) -var shortHelpHelp = "Show help about a command" -var longHelpHelp = ` +const cmdHelpSummary = "Show help about a command" +const cmdHelpDescription = ` The help command displays information about commands. ` +type cmdHelp struct { + All bool `long:"all"` + Manpage bool `long:"man" hidden:"true"` + Positional struct { + Subs []string `positional-arg-name:""` + } `positional-args:"yes"` + parser *flags.Parser +} + +func init() { + AddCommand(&CmdInfo{ + Name: "help", + Summary: cmdHelpSummary, + Description: cmdHelpDescription, + ArgsHelp: map[string]string{ + "--all": "Show a short summary of all commands", + "--man": "Generate the manpage", + }, + Builder: func() flags.Commander { return &cmdHelp{} }, + }) +} + // addHelp adds --help like what go-flags would do for us, but hidden func addHelp(parser *flags.Parser) error { var help struct { @@ -69,23 +91,6 @@ func addHelp(parser *flags.Parser) error { return nil } -type cmdHelp struct { - All bool `long:"all"` - Manpage bool `long:"man" hidden:"true"` - Positional struct { - Subs []string `positional-arg-name:""` - } `positional-args:"yes"` - parser *flags.Parser -} - -func init() { - addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} }, - map[string]string{ - "all": "Show a short summary of all commands", - "man": "Generate the manpage", - }, nil) -} - func (cmd *cmdHelp) setParser(parser *flags.Parser) { cmd.parser = parser } @@ -158,14 +163,14 @@ func (cmd cmdHelp) Execute(args []string) error { return &flags.Error{Type: flags.ErrCommandRequired} } -type helpCategory struct { +type HelpCategory struct { Label string Description string Commands []string } -// helpCategories helps us by grouping commands -var helpCategories = []helpCategory{{ +// HelpCategories helps us by grouping commands +var HelpCategories = []HelpCategory{{ Label: "Run", Description: "run pebble", Commands: []string{"run", "help", "version"}, @@ -229,12 +234,12 @@ func printShortHelp() { printHelpHeader() fmt.Fprintln(Stdout) maxLen := 0 - for _, categ := range helpCategories { + for _, categ := range HelpCategories { if l := utf8.RuneCountInString(categ.Label); l > maxLen { maxLen = l } } - for _, categ := range helpCategories { + for _, categ := range HelpCategories { fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", ")) } printHelpFooter() @@ -244,7 +249,7 @@ func printShortHelp() { func printLongHelp(parser *flags.Parser) { printHelpHeader() maxLen := 0 - for _, categ := range helpCategories { + for _, categ := range HelpCategories { for _, command := range categ.Commands { if l := len(command); l > maxLen { maxLen = l @@ -259,7 +264,7 @@ func printLongHelp(parser *flags.Parser) { cmdLookup[cmd.Name] = cmd } - for _, categ := range helpCategories { + for _, categ := range HelpCategories { fmt.Fprintln(Stdout) fmt.Fprintf(Stdout, " %s (%s):\n", categ.Label, categ.Description) for _, name := range categ.Commands { diff --git a/internals/cli/cmd_help_test.go b/internals/cli/cmd_help_test.go index 077ca256..cdab5a2b 100644 --- a/internals/cli/cmd_help_test.go +++ b/internals/cli/cmd_help_test.go @@ -109,3 +109,20 @@ func (s *PebbleSuite) TestCommandWithHelpOption(c *C) { c.Check(s.Stdout(), Matches, "(?s)Usage.*pebble help.*The help command.*help command options.*") c.Check(s.Stderr(), Equals, "") } + +func (s *PebbleSuite) TestAddHelpCategory(c *C) { + restore := fakeArgs("pebble") + defer restore() + + cli.HelpCategories = append(cli.HelpCategories, cli.HelpCategory{ + Label: "Test category", + Description: "Test description", + Commands: []string{"run", "logs"}, + }) + + err := cli.RunMain() + c.Assert(err, Equals, nil) + + c.Check(s.Stdout(), Matches, "(?s).*Test category: run, logs\n.*") + c.Check(s.Stderr(), Equals, "") +} diff --git a/internals/cli/cmd_logs.go b/internals/cli/cmd_logs.go index cffc9aa5..a44ba7e9 100644 --- a/internals/cli/cmd_logs.go +++ b/internals/cli/cmd_logs.go @@ -27,9 +27,11 @@ import ( "github.com/canonical/pebble/client" ) -const ( - logTimeFormat = "2006-01-02T15:04:05.000Z07:00" -) +const cmdLogsSummary = "Fetch service logs" +const cmdLogsDescription = ` +The logs command fetches buffered logs from the given services (or all services +if none are specified) and displays them in chronological order. +` type cmdLogs struct { clientMixin @@ -41,17 +43,23 @@ type cmdLogs struct { } `positional-args:"yes"` } -var logsDescs = map[string]string{ - "follow": "Follow (tail) logs for given services until Ctrl-C is\npressed. If no services are specified, show logs from\nall services running when the command starts.", - "format": "Output format: \"text\" (default) or \"json\" (JSON lines).", - "n": "Number of logs to show (before following); defaults to 30.\nIf 'all', show all buffered logs.", +func init() { + AddCommand(&CmdInfo{ + Name: "logs", + Summary: cmdLogsSummary, + Description: cmdLogsDescription, + ArgsHelp: map[string]string{ + "--follow": "Follow (tail) logs for given services until Ctrl-C is\npressed. If no services are specified, show logs from\nall services running when the command starts.", + "--format": "Output format: \"text\" (default) or \"json\" (JSON lines).", + "-n": "Number of logs to show (before following); defaults to 30.\nIf 'all', show all buffered logs.", + }, + Builder: func() flags.Commander { return &cmdLogs{} }, + }) } -var shortLogsHelp = "Fetch service logs" -var longLogsHelp = ` -The logs command fetches buffered logs from the given services (or all services -if none are specified) and displays them in chronological order. -` +const ( + logTimeFormat = "2006-01-02T15:04:05.000Z07:00" +) func (cmd *cmdLogs) Execute(args []string) error { var n int @@ -119,7 +127,3 @@ func notifyContext(parent context.Context, signals ...os.Signal) context.Context }() return ctx } - -func init() { - addCommand("logs", shortLogsHelp, longLogsHelp, func() flags.Commander { return &cmdLogs{} }, logsDescs, nil) -} diff --git a/internals/cli/cmd_ls.go b/internals/cli/cmd_ls.go index 163620c2..8cd31f9e 100644 --- a/internals/cli/cmd_ls.go +++ b/internals/cli/cmd_ls.go @@ -26,6 +26,12 @@ import ( "github.com/canonical/pebble/client" ) +const cmdLsSummary = "List path contents" +const cmdLsDescription = ` +The ls command lists entries in the filesystem at the specified path. A glob pattern +may be specified for the last path element. +` + type cmdLs struct { clientMixin timeMixin @@ -38,17 +44,19 @@ type cmdLs struct { } `positional-args:"yes" required:"yes"` } -var lsDescs = map[string]string{ - "d": `List matching entries themselves, not directory contents`, - "l": `Use a long listing format`, +func init() { + AddCommand(&CmdInfo{ + Name: "ls", + Summary: cmdLsSummary, + Description: cmdLsDescription, + ArgsHelp: merge(timeArgsHelp, map[string]string{ + "-d": "List matching entries themselves, not directory contents", + "-l": "Use a long listing format", + }), + Builder: func() flags.Commander { return &cmdLs{} }, + }) } -var shortLsHelp = "List path contents" -var longLsHelp = ` -The ls command lists entries in the filesystem at the specified path. A glob pattern -may be specified for the last path element. -` - func (cmd *cmdLs) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -101,7 +109,3 @@ func parseGlob(path string) (parsedPath, parsedPattern string, err error) { return path, "", nil } - -func init() { - addCommand("ls", shortLsHelp, longLsHelp, func() flags.Commander { return &cmdLs{} }, merge(lsDescs, timeDescs), nil) -} diff --git a/internals/cli/cmd_mkdir.go b/internals/cli/cmd_mkdir.go index 995c71dd..4e95edaf 100644 --- a/internals/cli/cmd_mkdir.go +++ b/internals/cli/cmd_mkdir.go @@ -24,6 +24,11 @@ import ( "github.com/canonical/pebble/client" ) +const cmdMkdirSummary = "Create a directory" +const cmdMkdirDescription = ` +The mkdir command creates the specified directory. +` + type cmdMkdir struct { clientMixin @@ -39,20 +44,23 @@ type cmdMkdir struct { } `positional-args:"yes" required:"yes"` } -var mkdirDescs = map[string]string{ - "p": "Create parent directories as needed", - "m": "Set permissions (e.g. 0644)", - "uid": "Use specified user ID", - "user": "Use specified username", - "gid": "Use specified group ID", - "group": "Use specified group name", +func init() { + AddCommand(&CmdInfo{ + Name: "mkdir", + Summary: cmdMkdirSummary, + Description: cmdMkdirDescription, + Builder: func() flags.Commander { return &cmdMkdir{} }, + ArgsHelp: map[string]string{ + "-p": "Create parent directories as needed", + "-m": "Set permissions (e.g. 0644)", + "--uid": "Use specified user ID", + "--user": "Use specified username", + "--gid": "Use specified group ID", + "--group": "Use specified group name", + }, + }) } -var shortMkdirHelp = "Create a directory" -var longMkdirHelp = ` -The mkdir command creates the specified directory. -` - func (cmd *cmdMkdir) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -77,7 +85,3 @@ func (cmd *cmdMkdir) Execute(args []string) error { return cmd.client.MakeDir(&opts) } - -func init() { - addCommand("mkdir", shortMkdirHelp, longMkdirHelp, func() flags.Commander { return &cmdMkdir{} }, mkdirDescs, nil) -} diff --git a/internals/cli/cmd_plan.go b/internals/cli/cmd_plan.go index f899339d..0e82796c 100644 --- a/internals/cli/cmd_plan.go +++ b/internals/cli/cmd_plan.go @@ -20,15 +20,24 @@ import ( "github.com/canonical/pebble/client" ) +var cmdPlanSummary = "Show the plan with layers combined" +var cmdPlanDescription = ` +The plan command prints out the effective configuration of pebble in YAML +format. Layers are combined according to the override rules defined in them. +` + type cmdPlan struct { clientMixin } -var shortPlanHelp = "Show the plan with layers combined" -var longPlanHelp = ` -The plan command prints out the effective configuration of pebble in YAML -format. Layers are combined according to the override rules defined in them. -` +func init() { + AddCommand(&CmdInfo{ + Name: "plan", + Summary: cmdPlanSummary, + Description: cmdPlanDescription, + Builder: func() flags.Commander { return &cmdPlan{} }, + }) +} func (cmd *cmdPlan) Execute(args []string) error { if len(args) > 0 { @@ -41,7 +50,3 @@ func (cmd *cmdPlan) Execute(args []string) error { Stdout.Write(planYAML) return nil } - -func init() { - addCommand("plan", shortPlanHelp, longPlanHelp, func() flags.Commander { return &cmdPlan{} }, nil, nil) -} diff --git a/internals/cli/cmd_replan.go b/internals/cli/cmd_replan.go index 3bcc7350..b4b92ad9 100644 --- a/internals/cli/cmd_replan.go +++ b/internals/cli/cmd_replan.go @@ -20,8 +20,8 @@ import ( "github.com/canonical/pebble/client" ) -var shortReplanHelp = "Ensure running services match the current plan" -var longReplanHelp = ` +const cmdReplanSummary = "Ensure running services match the current plan" +const cmdReplanDescription = ` The replan command starts, stops, or restarts services that have changed, so that running services exactly match the desired configuration in the current plan. @@ -32,7 +32,13 @@ type cmdReplan struct { } func init() { - addCommand("replan", shortReplanHelp, longReplanHelp, func() flags.Commander { return &cmdReplan{} }, waitDescs, nil) + AddCommand(&CmdInfo{ + Name: "replan", + Summary: cmdReplanSummary, + Description: cmdReplanDescription, + ArgsHelp: waitArgsHelp, + Builder: func() flags.Commander { return &cmdReplan{} }, + }) } func (cmd cmdReplan) Execute(args []string) error { diff --git a/internals/cli/cmd_restart.go b/internals/cli/cmd_restart.go index b99cd4ad..69c996da 100644 --- a/internals/cli/cmd_restart.go +++ b/internals/cli/cmd_restart.go @@ -20,8 +20,8 @@ import ( "github.com/canonical/pebble/client" ) -var shortRestartHelp = "Restart a service" -var longRestartHelp = ` +const cmdRestartSummary = "Restart a service" +const cmdRestartDescription = ` The restart command restarts the named service(s) in the correct order. ` @@ -33,7 +33,13 @@ type cmdRestart struct { } func init() { - addCommand("restart", shortRestartHelp, longRestartHelp, func() flags.Commander { return &cmdRestart{} }, waitDescs, nil) + AddCommand(&CmdInfo{ + Name: "restart", + Summary: cmdRestartSummary, + Description: cmdRestartDescription, + ArgsHelp: waitArgsHelp, + Builder: func() flags.Commander { return &cmdRestart{} }, + }) } func (cmd cmdRestart) Execute(args []string) error { diff --git a/internals/cli/cmd_rm.go b/internals/cli/cmd_rm.go index 930e5835..a971658a 100644 --- a/internals/cli/cmd_rm.go +++ b/internals/cli/cmd_rm.go @@ -20,25 +20,31 @@ import ( "github.com/canonical/pebble/client" ) +const cmdRmSummary = "Remove a file or directory" +const cmdRmDescription = ` +The rm command removes a file or directory. +` + type cmdRm struct { clientMixin - - Recursive bool `short:"r"` - + Recursive bool `short:"r"` Positional struct { Path string `positional-arg-name:""` } `positional-args:"yes" required:"yes"` } -var rmDescs = map[string]string{ - "r": "Remove all files and directories recursively in the specified path", +func init() { + AddCommand(&CmdInfo{ + Name: "rm", + Summary: cmdRmSummary, + Description: cmdRmDescription, + ArgsHelp: map[string]string{ + "-r": "Remove all files and directories recursively in the specified path", + }, + Builder: func() flags.Commander { return &cmdRm{} }, + }) } -var shortRmHelp = "Remove a file or directory." -var longRmHelp = ` -The rm command removes a file or directory. -` - func (cmd *cmdRm) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -49,7 +55,3 @@ func (cmd *cmdRm) Execute(args []string) error { Recursive: cmd.Recursive, }) } - -func init() { - addCommand("rm", shortRmHelp, longRmHelp, func() flags.Commander { return &cmdRm{} }, rmDescs, nil) -} diff --git a/internals/cli/cmd_run.go b/internals/cli/cmd_run.go index 2c1f2083..c0519c22 100644 --- a/internals/cli/cmd_run.go +++ b/internals/cli/cmd_run.go @@ -31,8 +31,8 @@ import ( "github.com/canonical/pebble/internals/systemd" ) -var shortRunHelp = "Run the pebble environment" -var longRunHelp = ` +const cmdRunSummary = "Run the pebble environment" +const cmdRunDescription = ` The run command starts pebble and runs the configured environment. Additional arguments may be provided to the service command with the --args option, which @@ -51,12 +51,12 @@ type sharedRunEnterOpts struct { Args [][]string `long:"args" terminator:";"` } -var sharedRunEnterOptsHelp = map[string]string{ - "create-dirs": "Create pebble directory on startup if it doesn't exist", - "hold": "Do not start default services automatically", - "http": `Start HTTP API listening on this address (e.g., ":4000")`, - "verbose": "Log all output from services to stdout", - "args": `Provide additional arguments to a service`, +var sharedRunEnterArgsHelp = map[string]string{ + "--create-dirs": "Create pebble directory on startup if it doesn't exist", + "--hold": "Do not start default services automatically", + "--http": `Start HTTP API listening on this address (e.g., ":4000")`, + "--verbose": "Log all output from services to stdout", + "--args": `Provide additional arguments to a service`, } type cmdRun struct { @@ -65,8 +65,13 @@ type cmdRun struct { } func init() { - addCommand("run", shortRunHelp, longRunHelp, func() flags.Commander { return &cmdRun{} }, - sharedRunEnterOptsHelp, nil) + AddCommand(&CmdInfo{ + Name: "run", + Summary: cmdRunSummary, + Description: cmdRunDescription, + ArgsHelp: sharedRunEnterArgsHelp, + Builder: func() flags.Commander { return &cmdRun{} }, + }) } func (rcmd *cmdRun) Execute(args []string) error { diff --git a/internals/cli/cmd_services.go b/internals/cli/cmd_services.go index 348956b9..20c0cb4a 100644 --- a/internals/cli/cmd_services.go +++ b/internals/cli/cmd_services.go @@ -22,6 +22,12 @@ import ( "github.com/canonical/pebble/client" ) +const cmdServicesSummary = "Query the status of configured services" +const cmdServicesDescription = ` +The services command lists status information about the services specified, or +about all services if none are specified. +` + type cmdServices struct { clientMixin timeMixin @@ -30,11 +36,15 @@ type cmdServices struct { } `positional-args:"yes"` } -var shortServicesHelp = "Query the status of configured services" -var longServicesHelp = ` -The services command lists status information about the services specified, or -about all services if none are specified. -` +func init() { + AddCommand(&CmdInfo{ + Name: "services", + Summary: cmdServicesSummary, + Description: cmdServicesDescription, + ArgsHelp: timeArgsHelp, + Builder: func() flags.Commander { return &cmdServices{} }, + }) +} func (cmd *cmdServices) Execute(args []string) error { if len(args) > 0 { @@ -71,9 +81,3 @@ func (cmd *cmdServices) Execute(args []string) error { } return nil } - -func init() { - addCommand("services", shortServicesHelp, longServicesHelp, - func() flags.Commander { return &cmdServices{} }, - timeDescs, nil) -} diff --git a/internals/cli/cmd_signal.go b/internals/cli/cmd_signal.go index 19264728..5b58bc43 100644 --- a/internals/cli/cmd_signal.go +++ b/internals/cli/cmd_signal.go @@ -23,6 +23,14 @@ import ( "github.com/canonical/pebble/client" ) +const cmdSignalSummary = "Send a signal to one or more running services" +const cmdSignalDescription = ` +The signal command sends a signal to one or more running services. The signal +name must be uppercase, for example: + +pebble signal HUP mysql nginx +` + type cmdSignal struct { clientMixin Positional struct { @@ -31,13 +39,14 @@ type cmdSignal struct { } `positional-args:"yes" required:"yes"` } -var shortSignalHelp = "Send a signal to one or more running services" -var longSignalHelp = ` -The signal command sends a signal to one or more running services. The signal -name must be uppercase, for example: - -pebble signal HUP mysql nginx -` +func init() { + AddCommand(&CmdInfo{ + Name: "signal", + Summary: cmdSignalSummary, + Description: cmdSignalDescription, + Builder: func() flags.Commander { return &cmdSignal{} }, + }) +} func (cmd *cmdSignal) Execute(args []string) error { if strings.ToUpper(cmd.Positional.Signal) != cmd.Positional.Signal { @@ -56,7 +65,3 @@ func (cmd *cmdSignal) Execute(args []string) error { } return nil } - -func init() { - addCommand("signal", shortSignalHelp, longSignalHelp, func() flags.Commander { return &cmdSignal{} }, nil, nil) -} diff --git a/internals/cli/cmd_start.go b/internals/cli/cmd_start.go index 9c719f97..9167ff2c 100644 --- a/internals/cli/cmd_start.go +++ b/internals/cli/cmd_start.go @@ -20,8 +20,8 @@ import ( "github.com/canonical/pebble/client" ) -var shortStartHelp = "Start a service and its dependencies" -var longStartHelp = ` +const cmdStartSummary = "Start a service and its dependencies" +const cmdStartDescription = ` The start command starts the service with the provided name and any other services it depends on, in the correct order. ` @@ -34,7 +34,13 @@ type cmdStart struct { } func init() { - addCommand("start", shortStartHelp, longStartHelp, func() flags.Commander { return &cmdStart{} }, waitDescs, nil) + AddCommand(&CmdInfo{ + Name: "start", + Summary: cmdStartSummary, + Description: cmdStartDescription, + ArgsHelp: waitArgsHelp, + Builder: func() flags.Commander { return &cmdStart{} }, + }) } func (cmd cmdStart) Execute(args []string) error { diff --git a/internals/cli/cmd_stop.go b/internals/cli/cmd_stop.go index b5d3411d..671dbff2 100644 --- a/internals/cli/cmd_stop.go +++ b/internals/cli/cmd_stop.go @@ -20,8 +20,8 @@ import ( "github.com/canonical/pebble/client" ) -var shortStopHelp = "Stop a service and its dependents" -var longStopHelp = ` +const cmdStopSummary = "Stop a service and its dependents" +const cmdStopDescription = ` The stop command stops the service with the provided name and any other service that depends on it, in the correct order. ` @@ -34,7 +34,13 @@ type cmdStop struct { } func init() { - addCommand("stop", shortStopHelp, longStopHelp, func() flags.Commander { return &cmdStop{} }, waitDescs, nil) + AddCommand(&CmdInfo{ + Name: "stop", + Summary: cmdStopSummary, + Description: cmdStopDescription, + Builder: func() flags.Commander { return &cmdStop{} }, + ArgsHelp: waitArgsHelp, + }) } func (cmd cmdStop) Execute(args []string) error { diff --git a/internals/cli/cmd_version.go b/internals/cli/cmd_version.go index 9e07f0e5..f8a60042 100644 --- a/internals/cli/cmd_version.go +++ b/internals/cli/cmd_version.go @@ -20,11 +20,11 @@ import ( "github.com/canonical/go-flags" "github.com/canonical/pebble/client" - cmdpkg "github.com/canonical/pebble/cmd" + version "github.com/canonical/pebble/cmd" ) -var shortVersionHelp = "Show version details" -var longVersionHelp = ` +const cmdVersionSummary = "Show version details" +const cmdVersionDescription = ` The version command displays the versions of the running client and server. ` @@ -33,12 +33,16 @@ type cmdVersion struct { ClientOnly bool `long:"client"` } -var versionDescs = map[string]string{ - "client": `Only display the client version`, -} - func init() { - addCommand("version", shortVersionHelp, longVersionHelp, func() flags.Commander { return &cmdVersion{} }, versionDescs, nil) + AddCommand(&CmdInfo{ + Name: "version", + Summary: cmdVersionSummary, + Description: cmdVersionDescription, + ArgsHelp: map[string]string{ + "--client": "Only display the client version", + }, + Builder: func() flags.Commander { return &cmdVersion{} }, + }) } func (cmd cmdVersion) Execute(args []string) error { @@ -47,7 +51,7 @@ func (cmd cmdVersion) Execute(args []string) error { } if cmd.ClientOnly { - fmt.Fprintln(Stdout, cmdpkg.Version) + fmt.Fprintln(Stdout, version.Version) return nil } @@ -61,7 +65,7 @@ func printVersions(cli *client.Client) error { serverVersion = sysInfo.Version } w := tabWriter() - fmt.Fprintf(w, "client\t%s\n", cmdpkg.Version) + fmt.Fprintf(w, "client\t%s\n", version.Version) fmt.Fprintf(w, "server\t%s\n", serverVersion) w.Flush() return nil diff --git a/internals/cli/cmd_warnings.go b/internals/cli/cmd_warnings.go index 192f97a0..24db9ec1 100644 --- a/internals/cli/cmd_warnings.go +++ b/internals/cli/cmd_warnings.go @@ -31,18 +31,8 @@ import ( "github.com/canonical/pebble/internals/osutil" ) -type cmdWarnings struct { - clientMixin - timeMixin - unicodeMixin - All bool `long:"all"` - Verbose bool `long:"verbose"` -} - -type cmdOkay struct{ clientMixin } - -var shortWarningsHelp = "List warnings" -var longWarningsHelp = ` +const cmdWarningsSummary = "List warnings" +const cmdWarningsDescription = ` The warnings command lists the warnings that have been reported to the system. Once warnings have been listed with 'pebble warnings', 'pebble okay' may be used to @@ -52,20 +42,41 @@ again unless it happens again, _and_ a cooldown time has passed. Warnings expire automatically, and once expired they are forgotten. ` -var shortOkayHelp = "Acknowledge warnings" -var longOkayHelp = ` +type cmdWarnings struct { + clientMixin + timeMixin + unicodeMixin + All bool `long:"all"` + Verbose bool `long:"verbose"` +} + +const cmdOkaySummary = "Acknowledge warnings" +const cmdOkayDescription = ` The okay command acknowledges the warnings listed with 'pebble warnings'. Once acknowledged, a warning won't appear again unless it reoccurs and sufficient time has passed. ` +type cmdOkay struct{ clientMixin } + func init() { - addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, merge(timeDescs, unicodeDescs, map[string]string{ - "all": "Show all warnings", - "verbose": "Show more information", - }), nil) - addCommand("okay", shortOkayHelp, longOkayHelp, func() flags.Commander { return &cmdOkay{} }, nil, nil) + AddCommand(&CmdInfo{ + Name: "warnings", + Summary: cmdWarningsSummary, + Description: cmdWarningsDescription, + ArgsHelp: merge(timeArgsHelp, unicodeArgsHelp, map[string]string{ + "--all": "Show all warnings", + "--verbose": "Show more information", + }), + Builder: func() flags.Commander { return &cmdWarnings{} }, + }) + AddCommand(&CmdInfo{ + Name: "okay", + Summary: cmdOkaySummary, + Description: cmdOkayDescription, + Builder: func() flags.Commander { return &cmdOkay{} }, + }) } func (cmd *cmdWarnings) Execute(args []string) error { diff --git a/internals/cli/format.go b/internals/cli/format.go index e0c2050d..ba1146fa 100644 --- a/internals/cli/format.go +++ b/internals/cli/format.go @@ -51,17 +51,6 @@ func (ux unicodeMixin) getEscapes() *escapes { return esc } -type colorMixin struct { - Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"` - unicodeMixin -} - -func (mx colorMixin) getEscapes() *escapes { - esc := colorTable(mx.Color) - mx.addUnicodeChars(&esc) - return &esc -} - func canUnicode(mode string) bool { switch mode { case "always": @@ -111,13 +100,8 @@ func colorTable(mode string) escapes { return color } -var colorDescs = map[string]string{ - "color": "Use a little bit of color to highlight some things.", - "unicode": unicodeDescs["unicode"], -} - -var unicodeDescs = map[string]string{ - "unicode": "Use a little bit of Unicode to improve legibility.", +var unicodeArgsHelp = map[string]string{ + "--unicode": "Use a little bit of Unicode to improve legibility.", } func merge(maps ...map[string]string) map[string]string { @@ -252,12 +236,6 @@ func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error { return wrapGeneric(out, text, indent, indent, termWidth) } -// wrapFlow wraps the text using yaml's flow style, allowing indent -// characters for the first line. -func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error { - return wrapGeneric(out, text, indent, " ", termWidth) -} - // wrapGeneric wraps the given text to the given width, prefixing the // first line with indent and the remaining lines with indent2 func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error { diff --git a/internals/cli/last.go b/internals/cli/last.go index 0114edb9..deafc9a3 100644 --- a/internals/cli/last.go +++ b/internals/cli/last.go @@ -25,33 +25,29 @@ type changeIDMixin struct { clientMixin LastChangeType string `long:"last"` Positional struct { - ID string `positional-arg-name:""` + ChangeID string `positional-arg-name:""` } `positional-args:"yes"` } -var changeIDMixinOptDesc = map[string]string{ - "last": "Select last change of given type (install, refresh, remove, try, auto-refresh, etc.). A question mark at the end of the type means to do nothing (instead of returning an error) if no change of the given type is found. Note the question mark could need protecting from the shell.", +var changeIDMixinArgsHelp = map[string]string{ + "": "Change ID", + "--last": "Select last change of given type (install, refresh, remove, try, auto-refresh, etc.). A question mark at the end of the type means to do nothing (instead of returning an error) if no change of the given type is found. Note the question mark could need protecting from the shell.", } -var changeIDMixinArgDesc = []argDesc{{ - name: "", - desc: "Change ID", -}} - // should not be user-visible, but keep it clear and polite because mistakes happen var noChangeFoundOK = errors.New("no change found but that's ok") func (l *changeIDMixin) GetChangeID() (string, error) { - if l.Positional.ID == "" && l.LastChangeType == "" { + if l.Positional.ChangeID == "" && l.LastChangeType == "" { return "", fmt.Errorf("please provide change ID or type with --last=") } - if l.Positional.ID != "" { + if l.Positional.ChangeID != "" { if l.LastChangeType != "" { return "", fmt.Errorf("cannot use change ID and type together") } - return string(l.Positional.ID), nil + return string(l.Positional.ChangeID), nil } cli := l.client diff --git a/internals/cli/times.go b/internals/cli/times.go index 612b7bbe..ddade772 100644 --- a/internals/cli/times.go +++ b/internals/cli/times.go @@ -15,11 +15,8 @@ package cli import ( - "strings" "time" - "github.com/canonical/x-go/strutil/quantity" - "github.com/canonical/pebble/internals/timeutil" ) @@ -29,8 +26,8 @@ type timeMixin struct { AbsTime bool `long:"abs-time"` } -var timeDescs = map[string]string{ - "abs-time": "Display absolute times (in RFC 3339 format). Otherwise, display relative times up to 60 days, then YYYY-MM-DD.", +var timeArgsHelp = map[string]string{ + "--abs-time": "Display absolute times (in RFC 3339 format). Otherwise, display relative times up to 60 days, then YYYY-MM-DD.", } func (mx timeMixin) fmtTime(t time.Time) string { @@ -39,18 +36,3 @@ func (mx timeMixin) fmtTime(t time.Time) string { } return timeutilHuman(t) } - -type durationMixin struct { - AbsTime bool `long:"abs-time"` -} - -var durationDescs = map[string]string{ - "abs-time": "Display absolute times (in RFC 3339 format). Otherwise, display short relative times.", -} - -func (mx durationMixin) fmtDuration(t time.Time) string { - if mx.AbsTime { - return t.Format(time.RFC3339) - } - return strings.TrimSpace(quantity.FormatDuration(time.Since(t).Seconds())) -} diff --git a/internals/cli/wait.go b/internals/cli/wait.go index ef2ba0d4..750bfce3 100644 --- a/internals/cli/wait.go +++ b/internals/cli/wait.go @@ -36,8 +36,8 @@ type waitMixin struct { skipAbort bool } -var waitDescs = map[string]string{ - "no-wait": "Do not wait for the operation to finish but just print the change id.", +var waitArgsHelp = map[string]string{ + "--no-wait": "Do not wait for the operation to finish but just print the change id.", } var noWait = errors.New("no wait for op")