Files
cli/cli-plugins/hooks/template.go
T
Nicolas Beck de8f4032ad cli-plugins: Add generic hook messages
Allow plugins to return informational messages without the next-steps heading
or indentation. Invoke each hook once and render generic messages before the
shared next-steps block.

Centralize grouping and formatting in PrintMessages, with a callback for
template evaluation and error handling. This avoids the need for an
intermediate EvaluatedMessage type and simplifies hook collection to return
the original responses.

Skip messages containing only whitespace while preserving empty lines
within non-empty messages. Keep PrintNextSteps as a deprecated wrapper
to preserve existing callers and output.

Update the printer tests and remove the obsolete appendMessages helper
and its tests.

Co-authored-by: Nicolas Beck <nicolas.beck@docker.com>
Signed-off-by: Nicolas Beck <nicolas.beck@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-10-12 16:31:43 +02:00

106 lines
2.9 KiB
Go

// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package hooks
import (
"bytes"
"errors"
"fmt"
"slices"
"strings"
"text/template"
"github.com/spf13/cobra"
)
const maxMessages = 10
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
out := hookTemplate
if strings.Contains(hookTemplate, "{{") {
// Message may be a template.
msgContext := commandInfo{cmd: cmd}
tmpl, err := template.New("").Funcs(template.FuncMap{
"command": msgContext.command,
"flagValue": msgContext.flagValue,
"argValue": msgContext.argValue,
// kept for backward-compatibility with old templates.
"flag": func(_ any, flagName string) (string, error) { return msgContext.flagValue(flagName) },
"arg": func(_ any, i int) (string, error) { return msgContext.argValue(i) },
}).Parse(hookTemplate)
if err != nil {
return nil, err
}
var b bytes.Buffer
err = tmpl.Execute(&b, msgContext)
if err != nil {
return nil, err
}
out = b.String()
}
if n := strings.Count(out, "\n"); n > maxMessages {
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
}
lines := strings.SplitN(out, "\n", maxMessages)
// Skip messages containing only whitespace, but preserve empty lines
// in messages that have content (e.g., for paragraph spacing).
if !slices.ContainsFunc(lines, func(line string) bool {
return strings.TrimSpace(line) != ""
}) {
return nil, nil
}
return lines, nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
// commandInfo provides info about the command for which the hook was invoked.
// It is used for templated hook-messages.
type commandInfo struct {
cmd *cobra.Command
}
// Name returns the name of the (sub)command for which the hook was invoked.
//
// It's used for backward-compatibility with old templates.
func (c commandInfo) Name() string {
return c.command()
}
// command returns the name of the (sub)command for which the hook was invoked.
func (c commandInfo) command() string {
if c.cmd == nil {
return ""
}
return c.cmd.Name()
}
// flagValue returns the value that was set for the given flag when the hook was invoked.
func (c commandInfo) flagValue(flagName string) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: flagValue: cmd is nil", ErrHookTemplateParse)
}
f := c.cmd.Flag(flagName)
if f == nil {
return "", fmt.Errorf("%w: flagValue: no flags found", ErrHookTemplateParse)
}
return f.Value.String(), nil
}
// argValue returns the value of the nth argument.
func (c commandInfo) argValue(n int) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: arg: cmd is nil", ErrHookTemplateParse)
}
flags := c.cmd.Flags()
v := flags.Arg(n)
if v == "" && n >= flags.NArg() {
return "", fmt.Errorf("%w: arg: %dth argument not set", ErrHookTemplateParse, n)
}
return v, nil
}