Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): implement personality for application name-agnostic help manuals #238

Merged
merged 16 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,17 @@ func MockVersion(version string) (restore func()) {
Version = version
return func() { Version = old }
}

// PersonalityInfo contains information about the application identity.
// These values will be used across the CLI.
type PersonalityInfo struct {
anpep marked this conversation as resolved.
Show resolved Hide resolved
// ProgramName represents the name of the application binary (i.e. pebble)
ProgramName string
// DisplayName represents the user-facing name of the application (i.e. Pebble)
DisplayName string
}

var Personality PersonalityInfo = PersonalityInfo{
ProgramName: "pebble",
DisplayName: "Pebble",
}
13 changes: 7 additions & 6 deletions internals/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"golang.org/x/crypto/ssh/terminal"

"github.com/canonical/pebble/client"
"github.com/canonical/pebble/cmd"
anpep marked this conversation as resolved.
Show resolved Hide resolved
"github.com/canonical/pebble/internals/logger"
)

Expand Down Expand Up @@ -179,8 +180,8 @@ func Parser(cli *client.Client) *flags.Parser {
}
flagopts := flags.Options(flags.PassDoubleDash)
parser := flags.NewParser(&optionsData, flagopts)
parser.ShortDescription = "Tool to interact with pebble"
parser.LongDescription = longPebbleDescription
parser.ShortDescription = fmt.Sprintf("Tool to interact with %s", cmd.Personality.DisplayName)
anpep marked this conversation as resolved.
Show resolved Hide resolved
parser.LongDescription = longPebbleDescription()
anpep marked this conversation as resolved.
Show resolved Hide resolved
// hide the unhelpful "[OPTIONS]" from help output
parser.Usage = ""
if version := parser.FindOptionByLongName("version"); version != nil {
Expand Down Expand Up @@ -333,7 +334,7 @@ func Run() error {
}
}()

logger.SetLogger(logger.New(os.Stderr, "[pebble] "))
logger.SetLogger(logger.New(os.Stderr, fmt.Sprintf("[%s] ", cmd.Personality.ProgramName)))

_, clientConfig.Socket = getEnvPaths()

Expand All @@ -355,11 +356,11 @@ func Run() error {
return nil
case flags.ErrUnknownCommand:
sub := os.Args[1]
sug := "pebble help"
sug := cmd.Personality.ProgramName + " help"
if len(xtra) > 0 {
sub = xtra[0]
if x := parser.Command.Active; x != nil && x.Name != "help" {
sug = "pebble help " + x.Name
sug = cmd.Personality.ProgramName + " help " + x.Name
}
}
return fmt.Errorf("unknown command %q, see '%s'.", sub, sug)
Expand Down Expand Up @@ -403,7 +404,7 @@ func errorToMessage(e error) (normalMessage string, err error) {
}
case client.ErrorKindSystemRestart:
isError = false
msg = "pebble is about to reboot the system"
msg = fmt.Sprintf("%s is about to reboot the system", cmd.Personality.ProgramName)
case client.ErrorKindNoDefaultServices:
msg = "no default services"
default:
Expand Down
31 changes: 16 additions & 15 deletions internals/cli/cmd_enter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (

"github.com/canonical/go-flags"

"github.com/canonical/pebble/cmd"
"github.com/canonical/pebble/internals/logger"
)

const shortEnterHelp = "Run subcommand under a container environment"
const longEnterHelp = `
The enter command facilitates the use of Pebble as an entrypoint for containers.
The enter command facilitates the use of the daemon as an entrypoint for containers.
anpep marked this conversation as resolved.
Show resolved Hide resolved
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
appropriate environment taking into account its purpose.
Expand Down Expand Up @@ -83,37 +84,37 @@ func commandEnterFlags(commander flags.Commander) (enterFlags enterFlags, suppor
return
}

func (cmd *cmdEnter) Execute(args []string) error {
func (rcmd *cmdEnter) Execute(args []string) error {
anpep marked this conversation as resolved.
Show resolved Hide resolved
if len(args) > 0 {
return ErrExtraArgs
}

runCmd := cmdRun{
sharedRunEnterOpts: cmd.sharedRunEnterOpts,
sharedRunEnterOpts: rcmd.sharedRunEnterOpts,
}
runCmd.setClient(cmd.client)
runCmd.setClient(rcmd.client)

if len(cmd.Positional.Cmd) == 0 {
if len(rcmd.Positional.Cmd) == 0 {
runCmd.run(nil)
return nil
}

runCmd.Hold = !cmd.Run
runCmd.Hold = !rcmd.Run

var (
commander flags.Commander
extraArgs []string
)

parser := Parser(cmd.client)
parser := Parser(rcmd.client)
parser.CommandHandler = func(c flags.Commander, a []string) error {
commander = c
extraArgs = a
return nil
}

if _, err := parser.ParseArgs(cmd.Positional.Cmd); err != nil {
cmd.parser.Command.Active = parser.Command.Active
if _, err := parser.ParseArgs(rcmd.Positional.Cmd); err != nil {
rcmd.parser.Command.Active = parser.Command.Active
return err
}

Expand All @@ -127,23 +128,23 @@ func (cmd *cmdEnter) Execute(args []string) error {
return fmt.Errorf("enter: subcommand %q is not supported", parser.Active.Name)
}

if enterFlags&enterRequireServiceAutostart != 0 && !cmd.Run {
if enterFlags&enterRequireServiceAutostart != 0 && !rcmd.Run {
return fmt.Errorf("enter: must use --run before %q subcommand", parser.Active.Name)
}

if enterFlags&(enterProhibitServiceAutostart|enterNoServiceManager) != 0 && cmd.Run {
if enterFlags&(enterProhibitServiceAutostart|enterNoServiceManager) != 0 && rcmd.Run {
return fmt.Errorf("enter: cannot provide --run before %q subcommand", parser.Active.Name)
}

if enterFlags&enterNoServiceManager != 0 {
if err := commander.Execute(extraArgs); err != nil {
cmd.parser.Command.Active = parser.Command.Active
rcmd.parser.Command.Active = parser.Command.Active
return err
}
return nil
}

if enterFlags&enterSilenceLogging != 0 && !cmd.Verbose {
if enterFlags&enterSilenceLogging != 0 && !rcmd.Verbose {
logger.SetLogger(logger.NullLogger)
}

Expand All @@ -160,15 +161,15 @@ func (cmd *cmdEnter) Execute(args []string) error {
case runStop = <-runReadyCh:
case runPanic := <-runResultCh:
if runPanic == nil {
panic("internal error: pebble daemon stopped early")
panic(fmt.Sprintf("internal error: %s daemon stopped early", cmd.Personality.ProgramName))
}
panic(runPanic)
}

err := commander.Execute(extraArgs)

if err != nil {
cmd.parser.Command.Active = parser.Command.Active
rcmd.parser.Command.Active = parser.Command.Active
}

if err != nil || enterFlags&enterKeepServiceManager == 0 {
Expand Down
2 changes: 1 addition & 1 deletion internals/cli/cmd_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ 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
exec --timeout 10s -- echo -n foo bar
benhoyt marked this conversation as resolved.
Show resolved Hide resolved
`

func (cmd *cmdExec) Execute(args []string) error {
Expand Down
71 changes: 35 additions & 36 deletions internals/cli/cmd_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"unicode/utf8"

"github.com/canonical/go-flags"
"github.com/canonical/pebble/cmd"
)

var shortHelpHelp = "Show help about a command"
Expand Down Expand Up @@ -101,11 +102,11 @@ type manfixer struct {
func (w *manfixer) Write(buf []byte) (int, error) {
if !w.done {
w.done = true
if bytes.HasPrefix(buf, []byte(".TH pebble 1 ")) {
if bytes.HasPrefix(buf, []byte(fmt.Sprintf(".TH %s 1 ", cmd.Personality.ProgramName))) {
anpep marked this conversation as resolved.
Show resolved Hide resolved
// io.Writer.Write must not modify the buffer, even temporarily
anpep marked this conversation as resolved.
Show resolved Hide resolved
n, _ := w.Buffer.Write(buf[:9])
n, _ := w.Buffer.Write(buf[:5+len(cmd.Personality.ProgramName)])
w.Buffer.Write([]byte{'8'})
m, err := w.Buffer.Write(buf[10:])
m, err := w.Buffer.Write(buf[6+len(cmd.Personality.ProgramName):])
anpep marked this conversation as resolved.
Show resolved Hide resolved
anpep marked this conversation as resolved.
Show resolved Hide resolved
return n + m + 1, err
}
}
Expand All @@ -119,40 +120,40 @@ func (w *manfixer) flush() {
io.Copy(Stdout, strings.NewReader(str))
}

func (cmd cmdHelp) Execute(args []string) error {
func (rcmd cmdHelp) Execute(args []string) error {
anpep marked this conversation as resolved.
Show resolved Hide resolved
if len(args) > 0 {
return ErrExtraArgs
}
if cmd.Manpage {
if rcmd.Manpage {
// you shouldn't try to to combine --man with --all nor a
// subcommand, but --man is hidden so no real need to check.
out := &manfixer{}
cmd.parser.WriteManPage(out)
rcmd.parser.WriteManPage(out)
out.flush()
return nil
}
if cmd.All {
if len(cmd.Positional.Subs) > 0 {
if rcmd.All {
if len(rcmd.Positional.Subs) > 0 {
return fmt.Errorf("help accepts a command, or '--all', but not both.")
}
printLongHelp(cmd.parser)
printLongHelp(rcmd.parser)
return nil
}

var subcmd = cmd.parser.Command
for _, subname := range cmd.Positional.Subs {
var subcmd = rcmd.parser.Command
for _, subname := range rcmd.Positional.Subs {
subcmd = subcmd.Find(subname)
if subcmd == nil {
sug := "pebble help"
if x := cmd.parser.Command.Active; x != nil && x.Name != "help" {
sug = "pebble help " + x.Name
sug := cmd.Personality.ProgramName + " help"
if x := rcmd.parser.Command.Active; x != nil && x.Name != "help" {
sug = cmd.Personality.ProgramName + " help " + x.Name
}
return fmt.Errorf("unknown command %q, see '%s'.", subname, sug)
}
// this makes "pebble help foo" work the same as "pebble foo --help"
cmd.parser.Command.Active = subcmd
rcmd.parser.Command.Active = subcmd
}
if subcmd != cmd.parser.Command {
if subcmd != rcmd.parser.Command {
return &flags.Error{Type: flags.ErrHelp}
}
return &flags.Error{Type: flags.ErrCommandRequired}
Expand All @@ -167,7 +168,7 @@ type helpCategory struct {
// helpCategories helps us by grouping commands
var helpCategories = []helpCategory{{
Label: "Run",
Description: "run pebble",
Description: "run the daemon",
anpep marked this conversation as resolved.
Show resolved Hide resolved
Commands: []string{"run", "help", "version"},
}, {
Label: "Plan",
Expand All @@ -191,37 +192,35 @@ var helpCategories = []helpCategory{{
Commands: []string{"warnings", "okay"},
}}

var (
longPebbleDescription = strings.TrimSpace(`
Pebble lets you control services and perform management actions on
the system that is running them.
`)
pebbleUsage = "Usage: pebble <command> [<options>...]"
pebbleHelpCategoriesIntro = "Commands can be classified as follows:"
pebbleHelpAllFooter = "Set the PEBBLE environment variable to override the configuration directory \n" +
"(which defaults to " + defaultPebbleDir + "). Set PEBBLE_SOCKET to override \n" +
"the unix socket used for the API (defaults to $PEBBLE/.pebble.socket).\n" +
"\n" +
"For more information about a command, run 'pebble help <command>'."
pebbleHelpFooter = "For a short summary of all commands, run 'pebble help --all'."
)
func longPebbleDescription() string {
anpep marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Sprintf(strings.TrimSpace(`
%s lets you control services and perform management actions on the
system that is running them
`), cmd.Personality.DisplayName)
}

func printHelpHeader() {
fmt.Fprintln(Stdout, longPebbleDescription)
fmt.Fprintln(Stdout, longPebbleDescription())
anpep marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintln(Stdout)
fmt.Fprintln(Stdout, pebbleUsage)
fmt.Fprintf(Stdout, "Usage: %s <command> [<options>...]\n", cmd.Personality.ProgramName)
anpep marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintln(Stdout)
fmt.Fprintln(Stdout, pebbleHelpCategoriesIntro)
fmt.Fprintln(Stdout, "Commands can be classified as follows:")
anpep marked this conversation as resolved.
Show resolved Hide resolved
}

func printHelpAllFooter() {
fmt.Fprintln(Stdout)
anpep marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintln(Stdout, pebbleHelpAllFooter)
fmt.Fprintf(Stdout, strings.TrimSpace(`
Set the PEBBLE environment variable to override the configuration directory
(which defaults to %s). Set PEBBLE_SOCKET to override
the unix socket used for the API (defaults to $PEBBLE/.pebble.socket).
anpep marked this conversation as resolved.
Show resolved Hide resolved

For more information about a command, run '%s help <command>'.
`)+"\n", defaultPebbleDir, cmd.Personality.ProgramName)
}

func printHelpFooter() {
printHelpAllFooter()
fmt.Fprintln(Stdout, pebbleHelpFooter)
fmt.Fprintf(Stdout, "For a short summary of all commands, run '%s help --all'.\n", cmd.Personality.ProgramName)
anpep marked this conversation as resolved.
Show resolved Hide resolved
}

// this is called when the Execute returns a flags.Error with ErrCommandRequired
Expand Down
2 changes: 1 addition & 1 deletion internals/cli/cmd_help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (s *PebbleSuite) TestHelpMan(c *C) {

err := cli.RunMain()
c.Assert(err, Equals, nil)
c.Check(s.Stdout(), Matches, `(?s)\.TH.*\.SH NAME.*pebble \\- Tool to interact with pebble.*`)
c.Check(s.Stdout(), Matches, `(?s)\.TH.*\.SH NAME.*pebble \\- Tool to interact with Pebble.*`)
c.Check(s.Stderr(), Equals, "")
}

Expand Down
5 changes: 3 additions & 2 deletions internals/cli/cmd_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type cmdPlan struct {

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.
The plan command prints out the effective configuration of the service manager
in YAML format. Layers are combined according to the override rules defined in
them.
`

func (cmd *cmdPlan) Execute(args []string) error {
Expand Down
12 changes: 6 additions & 6 deletions internals/cli/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ import (
"github.com/canonical/pebble/internals/systemd"
)

var shortRunHelp = "Run the pebble environment"
var shortRunHelp = "Run the daemon"
var longRunHelp = `
The run command starts pebble and runs the configured environment.
The run command starts the daemon and runs the configured environment.

Additional arguments may be provided to the service command with the --args option, which
must be terminated with ";" unless there are no further Pebble options. These arguments
must be terminated with ";" unless there are no further program options. These arguments
are appended to the end of the service command, and replace any default arguments defined
in the service plan. For example:

$ pebble run --args myservice --port 8080 \; --hold
run --args myservice --port 8080 \; --hold
anpep marked this conversation as resolved.
Show resolved Hide resolved
`

type sharedRunEnterOpts struct {
Expand All @@ -52,7 +52,7 @@ type sharedRunEnterOpts struct {
}

var sharedRunEnterOptsHelp = map[string]string{
"create-dirs": "Create pebble directory on startup if it doesn't exist",
"create-dirs": "Create state 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",
Expand Down Expand Up @@ -90,7 +90,7 @@ func (rcmd *cmdRun) run(ready chan<- func()) {
// This exit code must be in system'd SuccessExitStatus.
panic(&exitStatus{42})
}
fmt.Fprintf(os.Stderr, "cannot run pebble: %v\n", err)
fmt.Fprintf(os.Stderr, "cannot run %s: %v\n", cmd.Personality.ProgramName, err)
panic(&exitStatus{1})
}
}
Expand Down
2 changes: 1 addition & 1 deletion internals/cli/cmd_signal.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ 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
signal HUP mysql nginx
`

func (cmd *cmdSignal) Execute(args []string) error {
Expand Down
Loading