diff --git a/go.mod b/go.mod index 1da8789b..a2237cdb 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/crazy-max/undock go 1.26.0 require ( - github.com/alecthomas/kong v1.15.0 + github.com/alecthomas/kong v1.16.0 github.com/cenkalti/backoff/v5 v5.0.3 github.com/containerd/platforms v0.2.1 github.com/mholt/archives v0.1.5 diff --git a/go.sum b/go.sum index 21f600d0..54bce87e 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpH github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI= -github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= +github.com/alecthomas/kong v1.16.0 h1:g92/kUxBcdcTPOM79yE63viJgtcp5dNyrB3/O2cjYT4= +github.com/alecthomas/kong v1.16.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= diff --git a/vendor/github.com/alecthomas/kong/README.md b/vendor/github.com/alecthomas/kong/README.md index a9763327..1d791d66 100644 --- a/vendor/github.com/alecthomas/kong/README.md +++ b/vendor/github.com/alecthomas/kong/README.md @@ -252,7 +252,7 @@ A more robust approach is to break each command out into their own structs: Once a command node is selected by Kong it will search from that node back to the root. Each encountered command node with a `Run(...) error` will be called in reverse order. This allows -sub-trees to be re-used fairly conveniently. +sub-trees to be reused fairly conveniently. In addition to values bound with the `kong.Bind(...)` option, any values passed through to `kong.Context.Run(...)` are also bindable to the target's @@ -306,18 +306,19 @@ func main() { ``` -## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply() +## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply(), AfterRun() If a node in the CLI, or any of its embedded fields, implements a `BeforeReset(...) error`, `BeforeResolve -(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those will be called as Kong -resets, resolves, validates, and assigns values to the node. +(...) error`, `BeforeApply(...) error`, `AfterApply(...) error`, and/or `AfterRun(...) error` method, those +will be called as Kong resets, resolves, validates, and assigns values to the node. | Hook | Description | | --------------- | ----------------------------------------------------------------------------------------------------------- | | `BeforeReset` | Invoked before values are reset to their defaults (as defined by the grammar) or to zero values | | `BeforeResolve` | Invoked before resolvers are applied to a node | | `BeforeApply` | Invoked before the traced command line arguments are applied to the grammar | -| `AfterApply` | Invoked after command line arguments are applied to the grammar **and validated**` | +| `AfterApply` | Invoked after command line arguments are applied to the grammar **and validated** | +| `AfterRun` | Invoked after `Run()` returns. Will not be called if `os.Exit()` is manually called. | The `--help` flag is implemented with a `BeforeReset` hook. @@ -372,7 +373,7 @@ func (l *LsCmd) Run(cli *CLI) error { return nil } -func (r *RmCmD) Run(author AuthorName) error{ +func (r *RmCmd) Run(author AuthorName) error{ // use binded author here return nil } @@ -599,8 +600,7 @@ Both can coexist with standard Tag parsing. | `passthrough:""`[^1] | If present on a positional argument, it stops flag parsing when encountered, as if `--` was processed before. Useful for external command wrappers, like `exec`. On a command it requires that the command contains only one argument of type `[]string` which is then filled with everything following the command, unparsed. | | `-` | Ignore the field. Useful for adding non-CLI fields to a configuration struct. e.g `` `kong:"-"` `` | -[^1]: `` can be `partial` or `all` (the default). `all` will pass through all arguments including flags, including -flags. `partial` will validate flags until the first positional argument is encountered, then pass through all remaining +[^1]: `` can be `partial` or `all` (the default). `all` will pass through all arguments including flags. `partial` will validate flags until the first positional argument is encountered, then pass through all remaining positional arguments. ## Plugins diff --git a/vendor/github.com/alecthomas/kong/build.go b/vendor/github.com/alecthomas/kong/build.go index 00338852..22c44a50 100644 --- a/vendor/github.com/alecthomas/kong/build.go +++ b/vendor/github.com/alecthomas/kong/build.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "strings" + "unicode/utf8" ) // Plugins are dynamically embedded command-line structures. @@ -344,6 +345,9 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv seenFlags["--"+value.Name] = true for _, alias := range tag.Aliases { aliasFlag := "--" + alias + if utf8.RuneCountInString(alias) == 1 { + aliasFlag = "-" + alias + } if seenFlags[aliasFlag] { return failField(v, ft, "duplicate flag %s", aliasFlag) } diff --git a/vendor/github.com/alecthomas/kong/callbacks.go b/vendor/github.com/alecthomas/kong/callbacks.go index 6096a260..db256466 100644 --- a/vendor/github.com/alecthomas/kong/callbacks.go +++ b/vendor/github.com/alecthomas/kong/callbacks.go @@ -3,6 +3,7 @@ package kong import ( "fmt" "reflect" + "runtime" "strings" ) @@ -127,48 +128,78 @@ func getMethod(value reflect.Value, name string) reflect.Value { return method } +func getExplicitMethod(value reflect.Value, name string) reflect.Value { + if isExplicitMethod(value.Type(), name) { + return value.MethodByName(name) + } + if value.CanAddr() && isExplicitMethod(value.Addr().Type(), name) { + return value.Addr().MethodByName(name) + } + return reflect.Value{} +} + +func isExplicitMethod(t reflect.Type, name string) bool { + method, ok := t.MethodByName(name) + if !ok { + return false + } + // Promoted embedded methods are compiler-generated wrappers. The embedded + // value itself is visited separately, so skip those wrappers here. + fn := runtime.FuncForPC(method.Func.Pointer()) + if fn == nil { + return true + } + file, _ := fn.FileLine(method.Func.Pointer()) + return file != "" +} + // getMethods gets all methods with the given name from the given value // and any embedded fields. // // Returns a slice of bound methods that can be called directly. func getMethods(value reflect.Value, name string) (methods []reflect.Value) { - if value.Kind() == reflect.Ptr { + walkEmbedded(value, func(v reflect.Value) { + if method := getExplicitMethod(v, name); method.IsValid() { + methods = append(methods, method) + } + }) + return +} + +// walkEmbedded calls visit on v and recursively on every exported field +// of v that is either a standard Go anonymous field or tagged `embed:""`. +// Pointer values are dereferenced before traversal; nil/invalid pointers +// are skipped. [Plugins] are descended into element-by-element, matching how +// [flattenedFields] treats them at build time. +func walkEmbedded(value reflect.Value, visit func(reflect.Value)) { + if value.Kind() == reflect.Pointer { value = value.Elem() } if !value.IsValid() { return } - - if method := getMethod(value, name); method.IsValid() { - methods = append(methods, method) + visit(value) + if value.Type() == reflect.TypeOf(Plugins{}) { + for i := 0; i < value.Len(); i++ { + walkEmbedded(value.Index(i).Elem(), visit) + } + return } - if value.Kind() != reflect.Struct { return } - // If the current value is a struct, also consider embedded fields. - // Two kinds of embedded fields are considered if they're exported: - // - // - standard Go embedded fields - // - fields tagged with `embed:""` t := value.Type() for i := 0; i < value.NumField(); i++ { - fieldValue := value.Field(i) field := t.Field(i) - if !field.IsExported() { continue } - - // Consider a field embedded if it's actually embedded - // or if it's tagged with `embed:""`. _, isEmbedded := field.Tag.Lookup("embed") - isEmbedded = isEmbedded || field.Anonymous - if isEmbedded { - methods = append(methods, getMethods(fieldValue, name)...) + if !isEmbedded && !field.Anonymous { + continue } + walkEmbedded(value.Field(i), visit) } - return } func callFunction(f reflect.Value, bindings bindings) error { diff --git a/vendor/github.com/alecthomas/kong/context.go b/vendor/github.com/alecthomas/kong/context.go index 784e8cc8..7ce7d7e1 100644 --- a/vendor/github.com/alecthomas/kong/context.go +++ b/vendor/github.com/alecthomas/kong/context.go @@ -8,6 +8,7 @@ import ( "sort" "strconv" "strings" + "unicode/utf8" ) // Path records the nodes and parsed values from the current command-line. @@ -187,28 +188,21 @@ func (c *Context) Empty() bool { // Validate the current context. func (c *Context) Validate() error { //nolint: gocyclo - err := Visit(c.Model, func(node Visitable, next Next) error { - switch node := node.(type) { - case *Value: - ok := atLeastOneEnvSet(node.Tag.Envs) - if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) { - if err := checkEnum(node, node.Target); err != nil { - return err - } - } - - case *Flag: - ok := atLeastOneEnvSet(node.Tag.Envs) - if node.Enum != "" && (!node.Required || node.HasDefault || (len(node.Tag.Envs) != 0 && ok)) { - if err := checkEnum(node.Value, node.Target); err != nil { + // Only check nodes on the selected command path: an envar shared with + // another command may hold a value that is invalid there. + for _, path := range c.Path { + node := path.Node() + if node == nil { + continue + } + for _, value := range node.Values() { + ok := atLeastOneEnvSet(value.Tag.Envs) + if value.Enum != "" && (!value.Required || value.HasDefault || (len(value.Tag.Envs) != 0 && ok)) { + if err := checkEnum(value, value.Target); err != nil { return err } } } - return next(nil) - }) - if err != nil { - return err } for _, el := range c.Path { var ( @@ -232,7 +226,7 @@ func (c *Context) Validate() error { //nolint: gocyclo value = node.Target desc = node.Path() } - if validate := isValidatable(value); validate != nil { + for _, validate := range getValidators(value) { if err := validate.Validate(c); err != nil { if desc != "" { return fmt.Errorf("%s: %w", desc, err) @@ -349,14 +343,37 @@ func (c *Context) FlagValue(flag *Flag) any { // Reset recursively resets values to defaults (as specified in the grammar) or the zero value. func (c *Context) Reset() error { + selected := c.selectedValues() return Visit(c.Model.Node, func(node Visitable, next Next) error { - if value, ok := node.(*Value); ok { - return next(value.Reset()) + value, ok := node.(*Value) + if !ok { + return next(nil) } - return next(nil) + err := value.Reset() + if err != nil && !selected[value] { + // An envar shared with a node outside the selected command path + // may not parse there; that must not fail this parse. + value.Target.Set(reflect.Zero(value.Target.Type())) + err = nil + } + return next(err) }) } +// selectedValues returns the set of values attached to nodes on the traced +// command path. +func (c *Context) selectedValues() map[*Value]bool { + selected := map[*Value]bool{} + for _, path := range c.Path { + if node := path.Node(); node != nil { + for _, value := range node.Values() { + selected[value] = true + } + } + } + return selected +} + func (c *Context) endParsing() { args := []string{} for { @@ -746,9 +763,12 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) { candidates = append(candidates, short) } for _, alias := range flag.Aliases { - alias = "--" + alias - matched = matched || (alias == match) - candidates = append(candidates, alias) + aliasFlag := "--" + alias + if utf8.RuneCountInString(alias) == 1 { + aliasFlag = "-" + alias + } + matched = matched || (aliasFlag == match) + candidates = append(candidates, aliasFlag) } neg := negatableFlagName(flag.Name, flag.Tag.Negatable) @@ -972,6 +992,10 @@ func checkMissingChildren(node *Node) error { missing = append(missing, strconv.Quote(strings.Join(missingArgs, " "))) } + // A node with a Run() method may run on its own, so it does not require one + // of its subcommands to be selected. + runnable := node.Target.IsValid() && getMethod(node.Target, "Run").IsValid() + for _, child := range node.Children { if child.Hidden { continue @@ -981,7 +1005,7 @@ func checkMissingChildren(node *Node) error { continue } missing = append(missing, strconv.Quote(child.Summary())) - } else { + } else if !runnable { missing = append(missing, strconv.Quote(child.Name)) } } @@ -1178,6 +1202,17 @@ func isValidatable(v reflect.Value) extendedValidatable { return nil } +// getValidators returns validators implemented by v and by any embedded fields, +// matching how hooks are discovered (see getMethods). +func getValidators(v reflect.Value) (validators []extendedValidatable) { + walkEmbedded(v, func(v reflect.Value) { + if validate := isValidatable(v); validate != nil { + validators = append(validators, validate) + } + }) + return +} + func atLeastOneEnvSet(envs []string) bool { for _, env := range envs { if _, ok := os.LookupEnv(env); ok { diff --git a/vendor/github.com/alecthomas/kong/help.go b/vendor/github.com/alecthomas/kong/help.go index 6fc5a41d..1715caae 100644 --- a/vendor/github.com/alecthomas/kong/help.go +++ b/vendor/github.com/alecthomas/kong/help.go @@ -61,6 +61,10 @@ type HelpOptions struct { // ValueFormatter is used to format the help text of flags and positional arguments. ValueFormatter HelpValueFormatter + + // NoAppDescFormat skips all formatting of app description text (wrapping, paragraph + // reflow, newline merging, etc). + NoAppDescFormat bool } // Apply options to Kong as a configuration option. @@ -162,10 +166,16 @@ func printCommand(w *helpWriter, app *Application, cmd *Command) { } } -func printNodeDetail(w *helpWriter, node *Node, hide bool) { +func printNodeDetail(w *helpWriter, node *Node, hide bool) { //nolint:gocyclo if node.Help != "" { w.Print("") - w.Wrap(node.Help) + if w.NoAppDescFormat && node.Parent == nil { + for _, line := range strings.Split(node.Help, "\n") { + w.Print(line) + } + } else { + w.Wrap(node.Help) + } } if w.Summary { return diff --git a/vendor/github.com/alecthomas/kong/kong.go b/vendor/github.com/alecthomas/kong/kong.go index a0bfaf88..93b08304 100644 --- a/vendor/github.com/alecthomas/kong/kong.go +++ b/vendor/github.com/alecthomas/kong/kong.go @@ -266,18 +266,17 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { "default": value.Default, "enum": value.Enum, } - if value.Flag != nil { - for i, env := range value.Flag.Envs { - if value.Flag.Envs[i], err = interpolate(env, vars, updatedVars); err != nil { - return fmt.Errorf("env value for %s: %s", value.Summary(), err) - } - } - value.Tag.Envs = value.Flag.Envs - updatedVars["env"] = "" - if len(value.Flag.Envs) != 0 { - updatedVars["env"] = value.Flag.Envs[0] + for i, env := range value.Tag.Envs { + if value.Tag.Envs[i], err = interpolate(env, vars, updatedVars); err != nil { + return fmt.Errorf("env value for %s: %s", value.Summary(), err) } - + } + updatedVars["env"] = "" + if len(value.Tag.Envs) != 0 { + updatedVars["env"] = value.Tag.Envs[0] + } + if value.Flag != nil { + value.Flag.Envs = value.Tag.Envs value.Flag.PlaceHolder, err = interpolate(value.Flag.PlaceHolder, vars, updatedVars) if err != nil { return fmt.Errorf("placeholder value for %s: %s", value.Summary(), err) @@ -397,7 +396,7 @@ func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value { ) } -// Call hook on any unset flags with default values. +// Call hook on any unset flags with default values or values supplied via env. func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error { if node == nil { return nil @@ -409,7 +408,15 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er } binds := k.bindings.clone().add(ctx).add(node.Vars().CloneWith(k.vars)) for _, flag := range node.Flags { - if !flag.HasDefault || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() { + // Flags handled here are the ones that won't show up in ctx.Path: + // they got their value from the default tag or an env var, both of + // which Reset() applies straight to the target without touching the + // parse path. Anything actually parsed off argv or set by a resolver + // shows up in ctx.values and is covered by the main hook loop. + if ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() { + continue + } + if !flag.HasDefault && !atLeastOneEnvSet(flag.Tag.Envs) { continue } for _, method := range getMethods(flag.Target, name) { diff --git a/vendor/github.com/alecthomas/kong/mapper.go b/vendor/github.com/alecthomas/kong/mapper.go index 273bdf4b..075fb384 100644 --- a/vendor/github.com/alecthomas/kong/mapper.go +++ b/vendor/github.com/alecthomas/kong/mapper.go @@ -446,10 +446,10 @@ func floatDecoder(bits int) MapperFunc { target.SetFloat(v) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - target.Set(reflect.ValueOf(v)) + target.SetFloat(reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0))).Float()) default: - return fmt.Errorf("expected an int but got %q (%T)", t, t.Value) + return fmt.Errorf("expected a float but got %q (%T)", t, t.Value) } return nil } diff --git a/vendor/github.com/alecthomas/kong/model.go b/vendor/github.com/alecthomas/kong/model.go index 33a6f333..c72208c1 100644 --- a/vendor/github.com/alecthomas/kong/model.go +++ b/vendor/github.com/alecthomas/kong/model.go @@ -130,6 +130,20 @@ func (n *Node) Leaves(hide bool) (out []*Node) { return } +// Values returns the values directly attached to this node: its argument +// value, flag values and positionals. +func (n *Node) Values() []*Value { + values := []*Value{} + if n.Argument != nil { + values = append(values, n.Argument) + } + for _, flag := range n.Flags { + values = append(values, flag.Value) + } + values = append(values, n.Positional...) + return values +} + // Depth of the command from the application root. func (n *Node) Depth() int { depth := 0 diff --git a/vendor/github.com/alecthomas/kong/resolver.go b/vendor/github.com/alecthomas/kong/resolver.go index 3e37ca73..6aac0977 100644 --- a/vendor/github.com/alecthomas/kong/resolver.go +++ b/vendor/github.com/alecthomas/kong/resolver.go @@ -29,7 +29,7 @@ func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: // JSON returns a Resolver that retrieves values from a JSON source. // -// Flag names are used as JSON keys indirectly, by tring snake_case and camelCase variants. +// Flag names are used as JSON keys indirectly, by trying snake_case and camelCase variants. func JSON(r io.Reader) (Resolver, error) { values := map[string]any{} err := json.NewDecoder(r).Decode(&values) diff --git a/vendor/github.com/alecthomas/kong/tag.go b/vendor/github.com/alecthomas/kong/tag.go index 3cfc129e..c517af1d 100644 --- a/vendor/github.com/alecthomas/kong/tag.go +++ b/vendor/github.com/alecthomas/kong/tag.go @@ -166,13 +166,25 @@ func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) return d, nil } -func getTagInfo(tag reflect.StructTag) (string, tagChars) { - s, ok := tag.Lookup("kong") - if ok { - return s, kongChars +func parseStructTagItems(tag reflect.StructTag) (map[string][]string, error) { + items, err := parseTagItems(string(tag), bareChars) + if err != nil { + return nil, err } + delete(items, "kong") - return string(tag), bareChars + kongTag, ok := tag.Lookup("kong") + if !ok { + return items, nil + } + kongItems, err := parseTagItems(kongTag, kongChars) + if err != nil { + return nil, err + } + for key, value := range kongItems { + items[key] = append(value, items[key]...) + } + return items, nil } func newEmptyTag() *Tag { @@ -208,14 +220,14 @@ func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) { // First use a [Signature] if present signatureTag, ok := maybeGetSignature(ft.Type) if ok { - signatureItems, err := parseTagItems(getTagInfo(signatureTag)) + signatureItems, err := parseStructTagItems(signatureTag) if err != nil { return nil, err } items = signatureItems } // Next overlay the field's tags. - fieldItems, err := parseTagItems(getTagInfo(ft.Tag)) + fieldItems, err := parseStructTagItems(ft.Tag) if err != nil { return nil, err } diff --git a/vendor/modules.txt b/vendor/modules.txt index e014980e..172ee111 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -27,7 +27,7 @@ github.com/VividCortex/ewma # github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d ## explicit github.com/acarl005/stripansi -# github.com/alecthomas/kong v1.15.0 +# github.com/alecthomas/kong v1.16.0 ## explicit; go 1.20 github.com/alecthomas/kong # github.com/andybalholm/brotli v1.2.0