build(deps): bump github.com/urfave/cli/v3 from 3.9.1 to 3.10.0

Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.9.1 to 3.10.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.9.1...v3.10.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-06-15 04:52:27 +00:00
committed by GitHub
parent 122fb7a642
commit 8c112e2f63
14 changed files with 306 additions and 75 deletions

2
go.mod
View File

@@ -20,7 +20,7 @@ require (
github.com/opencontainers/selinux v1.15.1
github.com/seccomp/libseccomp-golang v0.11.1
github.com/sirupsen/logrus v1.9.4
github.com/urfave/cli/v3 v3.9.1
github.com/urfave/cli/v3 v3.10.0
github.com/vishvananda/netlink v1.3.1
github.com/vishvananda/netns v0.0.5
golang.org/x/net v0.56.0

4
go.sum
View File

@@ -60,8 +60,8 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.9.1 h1:OLU13atWZ0M+a4xmyBuBNOLZsSRYXyPeMeNjOvgYP54=
github.com/urfave/cli/v3 v3.9.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=
github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=

View File

@@ -161,21 +161,12 @@ type Command struct {
globaHelpFlagAdded bool
// whether global version flag was added
globaVersionFlagAdded bool
// generated root version flag
versionFlag Flag
// whether this is a completion command
isCompletionCommand bool
}
// FullName returns the full name of the command.
// For commands with parents this ensures that the parent commands
// are part of the command path.
func (cmd *Command) FullName() string {
namePath := []string{}
if cmd.parent != nil {
namePath = append(namePath, cmd.parent.FullName())
}
return strings.Join(append(namePath, cmd.Name), " ")
// whether this is the built-in help command
builtInHelp bool
}
func (cmd *Command) Command(name string) *Command {
@@ -430,7 +421,7 @@ func (cmd *Command) checkAllRequiredFlags() requiredFlagsErr {
// The help and completion commands are allowed to run without
// enforcement of required flags, since they do not invoke user
// actions that depend on those flag values.
if cmd.Name == helpName || cmd.isCompletionCommand {
if cmd.builtInHelp || cmd.isCompletionCommand {
return nil
}
for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent {
@@ -575,6 +566,39 @@ func (cmd *Command) Lineage() []*Command {
return lineage
}
// FullName returns the full name of the command.
// Includes parent commands separated by space.
func (cmd *Command) FullName() string {
return strings.Join(cmd.Path(), " ")
}
// Path returns the path of command names from the root to cmd, inclusive.
// Each element is a Command.Name. Path traverses upward via parent pointers
// similar to Lineage. FullName() is equivalent to strings.Join(cmd.Path(), " ").
func (cmd *Command) Path() []string {
if cmd.parent != nil {
return append(cmd.parent.Path(), cmd.Name)
}
return []string{cmd.Name}
}
// Walk visits cmd and every descendant. If fn returns a non-nil error, the
// walk terminates and the error is returned to the caller.
func (cmd *Command) Walk(fn func(*Command) error) error {
if fn == nil {
return nil
}
if err := fn(cmd); err != nil {
return err
}
for _, sub := range cmd.Commands {
if err := sub.Walk(fn); err != nil {
return err
}
}
return nil
}
// Count returns the num of occurrences of this flag
func (cmd *Command) Count(name string) int {
if cf, ok := cmd.lookupFlag(name).(Countable); ok {

View File

@@ -9,6 +9,8 @@ import (
"unicode"
)
type helpShownKey struct{}
func (cmd *Command) parseArgsFromStdin() ([]string, error) {
type state int
const (
@@ -161,7 +163,12 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
tracef("using post-parse arguments %[1]q (cmd=%[2]q)", args, cmd.Name)
if checkCompletions(ctx, cmd) {
if shouldRunCompletion(cmd) {
var beforeErr error
if ctx, beforeErr = runBefore(ctx, commandChain(cmd)); beforeErr != nil {
return ctx, beforeErr
}
runCompletion(ctx, cmd)
return ctx, nil
}
@@ -170,6 +177,15 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
deferErr = err
cmd.isInError = true
if cmd.checkHelp() {
ctx = context.WithValue(ctx, helpShownKey{}, true)
if cmd.parent == nil {
_ = ShowRootCommandHelp(cmd)
} else {
_ = ShowSubcommandHelp(cmd)
}
return ctx, nil
}
if cmd.OnUsageError != nil {
err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil)
err = cmd.handleExitCoder(ctx, err)
@@ -188,10 +204,8 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
tracef("SILENTLY IGNORING ERROR running ShowRootCommandHelp %[1]v (cmd=%[2]q)", err, cmd.Name)
}
} else {
tracef("running ShowCommandHelp with %[1]q", cmd.Name)
if err := ShowCommandHelp(ctx, cmd, cmd.Name); err != nil {
tracef("SILENTLY IGNORING ERROR running ShowCommandHelp with %[1]q %[2]v", cmd.Name, err)
}
tracef("running ShowSubcommandHelp for %[1]q", cmd.Name)
_ = ShowSubcommandHelp(cmd)
}
}
@@ -199,6 +213,7 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
}
if cmd.checkHelp() {
ctx = context.WithValue(ctx, helpShownKey{}, true)
return ctx, helpCommandAction(ctx, cmd)
} else {
tracef("no help is wanted (cmd=%[1]q)", cmd.Name)
@@ -223,6 +238,9 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
if cmd.After != nil && !cmd.Root().shellCompletion {
defer func() {
if ctx.Value(helpShownKey{}) != nil {
return
}
if err := cmd.After(ctx, cmd); err != nil {
err = cmd.handleExitCoder(ctx, err)
@@ -243,7 +261,14 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
if cmd.OnUsageError != nil {
err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil)
} else {
_ = ShowSubcommandHelp(cmd)
fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error())
if cmd.parent == nil {
_ = ShowRootCommandHelp(cmd)
} else {
if err := ShowCommandHelp(ctx, cmd.parent, cmd.Name); err != nil {
_ = ShowSubcommandHelp(cmd)
}
}
}
return ctx, err
}
@@ -300,23 +325,12 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
// perform the command action.
//
// First, resolve the chain of nested commands up to the parent.
var cmdChain []*Command
for p := cmd; p != nil; p = p.parent {
cmdChain = append(cmdChain, p)
}
slices.Reverse(cmdChain)
cmdChain := commandChain(cmd)
// Run Before actions in order.
for _, cmd := range cmdChain {
if cmd.Before == nil {
continue
}
if bctx, err := cmd.Before(ctx, cmd); err != nil {
deferErr = cmd.handleExitCoder(ctx, err)
return ctx, deferErr
} else if bctx != nil {
ctx = bctx
}
if ctx, err = runBefore(ctx, cmdChain); err != nil {
deferErr = err
return ctx, deferErr
}
// Run flag actions in order.
@@ -334,7 +348,14 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
if cmd.OnUsageError != nil {
err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil)
} else {
_ = ShowSubcommandHelp(cmd)
fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error())
if cmd.parent == nil {
_ = ShowRootCommandHelp(cmd)
} else {
if err := ShowCommandHelp(ctx, cmd.parent, cmd.Name); err != nil {
_ = ShowSubcommandHelp(cmd)
}
}
}
return ctx, err
}
@@ -366,3 +387,26 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
tracef("returning deferErr (cmd=%[1]q) %[2]q", cmd.Name, deferErr)
return ctx, deferErr
}
func commandChain(cmd *Command) []*Command {
var cmdChain []*Command
for p := cmd; p != nil; p = p.parent {
cmdChain = append(cmdChain, p)
}
slices.Reverse(cmdChain)
return cmdChain
}
func runBefore(ctx context.Context, cmdChain []*Command) (context.Context, error) {
for _, cmd := range cmdChain {
if cmd.Before == nil {
continue
}
if bctx, err := cmd.Before(ctx, cmd); err != nil {
return ctx, cmd.handleExitCoder(ctx, err)
} else if bctx != nil {
ctx = bctx
}
}
return ctx, nil
}

View File

@@ -104,8 +104,11 @@ func (cmd *Command) setupDefaults(osArgs []string) {
localVersionFlag = VersionFlag
}
cmd.appendFlag(localVersionFlag)
cmd.globaVersionFlagAdded = true
if !flagNamesInUse(cmd.allFlags(), localVersionFlag.Names()) {
cmd.appendFlag(localVersionFlag)
cmd.versionFlag = localVersionFlag
cmd.globaVersionFlagAdded = true
}
}
}
@@ -162,11 +165,13 @@ func (cmd *Command) setupDefaults(osArgs []string) {
func (cmd *Command) setupCommandGraph() {
tracef("setting up command graph (cmd=%[1]q)", cmd.Name)
for _, subCmd := range cmd.Commands {
subCmd.parent = cmd
subCmd.setupSubcommand()
subCmd.setupCommandGraph()
}
_ = cmd.Walk(func(sub *Command) error {
for _, subCmd := range sub.Commands {
subCmd.parent = sub
subCmd.setupSubcommand()
}
return nil
})
}
func (cmd *Command) setupSubcommand() {
@@ -193,6 +198,20 @@ func (cmd *Command) setupSubcommand() {
cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags())
}
func flagNamesInUse(flags []Flag, names []string) bool {
for _, name := range names {
for _, fl := range flags {
for _, flagName := range fl.Names() {
if flagName == name {
return true
}
}
}
}
return false
}
func (cmd *Command) hideHelp() bool {
tracef("hide help (cmd=%[1]q)", cmd.Name)
for c := cmd; c != nil; c = c.parent {

View File

@@ -151,6 +151,23 @@ type DocGenerationMultiValueFlag interface {
IsMultiValueFlag() bool
}
// SchemaTyper is an optional interface for flags that can report their
// JSON Schema type for programmatic introspection.
type SchemaTyper interface {
// SchemaType returns the JSON Schema type name for the value this
// flag accepts: "boolean", "integer", "number", "string", "array",
// "object". Returns "" if the flag does not map cleanly.
SchemaType() string
}
// SchemaItemsTyper is an optional interface for multi-value flags that
// can report the JSON Schema type of their elements.
type SchemaItemsTyper interface {
// SchemaItemsType returns the JSON Schema type of elements for
// array-type flags. Returns "" for single-value or object flags.
SchemaItemsType() string
}
// Countable is an interface to enable detection of flag values which support
// repetitive flags
type Countable interface {

View File

@@ -263,3 +263,11 @@ func (bif *BoolWithInverseFlag) IsDefaultVisible() bool {
func (bif *BoolWithInverseFlag) TypeName() string {
return "bool"
}
func (bif *BoolWithInverseFlag) SchemaType() string {
return "boolean"
}
func (bif *BoolWithInverseFlag) SchemaItemsType() string {
return ""
}

View File

@@ -1,6 +1,9 @@
package cli
import "flag"
import (
"flag"
"time"
)
type extFlag struct {
f *flag.Flag
@@ -61,3 +64,26 @@ func (e *extFlag) GetDefaultText() string {
func (e *extFlag) GetEnvVars() []string {
return nil
}
func (e *extFlag) SchemaType() string {
switch e.Get().(type) {
case bool:
return "boolean"
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return "integer"
case float32, float64:
return "number"
case string:
return "string"
case time.Duration:
return "duration"
case time.Time:
return "date-time"
default:
return ""
}
}
func (e *extFlag) SchemaItemsType() string {
return ""
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"reflect"
"strings"
"time"
)
// Value represents a value as used by cli.
@@ -285,6 +286,51 @@ func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error {
return nil
}
// SchemaType returns the JSON Schema type for the flag's value type.
func (f *FlagBase[T, C, V]) SchemaType() string {
var zero T
switch any(zero).(type) {
case bool:
return "boolean"
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return "integer"
case float32, float64:
return "number"
case string:
return "string"
case time.Duration:
return "duration"
case time.Time:
return "date-time"
case []string, []int, []int8, []int16, []int32, []int64,
[]uint, []uint8, []uint16, []uint32, []uint64,
[]float32, []float64:
return "array"
case map[string]string:
return "object"
default:
return ""
}
}
// SchemaItemsType returns the JSON Schema element type for slice flags.
func (f *FlagBase[T, C, V]) SchemaItemsType() string {
var zero T
t := reflect.TypeOf(zero)
if t.Kind() == reflect.Slice {
switch t.Elem().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "integer"
case reflect.Float32, reflect.Float64:
return "number"
case reflect.String:
return "string"
}
}
return ""
}
// IsMultiValueFlag returns true if the value type T can take multiple
// values from cmd line. This is true for slice and map type flags
func (f *FlagBase[T, C, VC]) IsMultiValueFlag() bool {

View File

@@ -170,7 +170,9 @@ COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategori
OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}}
GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}}
`
SubcommandHelpTemplate is the text template for the subcommand help topic.
cli.go uses text/template to render templates. You can render custom help
@@ -405,6 +407,10 @@ func (bif *BoolWithInverseFlag) PreParse() error
func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error
func (bif *BoolWithInverseFlag) SchemaItemsType() string
func (bif *BoolWithInverseFlag) SchemaType() string
func (bif *BoolWithInverseFlag) Set(name, val string) error
func (bif *BoolWithInverseFlag) SetCategory(c string)
@@ -606,8 +612,8 @@ func (cmd *Command) FloatSlice(name string) []float64
found
func (cmd *Command) FullName() string
FullName returns the full name of the command. For commands with parents
this ensures that the parent commands are part of the command path.
FullName returns the full name of the command. Includes parent commands
separated by space.
func (cmd *Command) Generic(name string) Value
Generic looks up the value of a local GenericFlag, returns nil if not found
@@ -689,6 +695,12 @@ func (cmd *Command) Names() []string
func (cmd *Command) NumFlags() int
NumFlags returns the number of flags set
func (cmd *Command) Path() []string
Path returns the path of command names from the root to cmd, inclusive.
Each element is a Command.Name. Path traverses upward via parent pointers
similar to Lineage. FullName() is equivalent to strings.Join(cmd.Path(),
" ").
func (cmd *Command) Root() *Command
Root returns the Command at the root of the graph
@@ -801,6 +813,10 @@ func (cmd *Command) VisiblePersistentFlags() []Flag
VisiblePersistentFlags returns a slice of LocalFlag with Persistent=true and
Hidden=false.
func (cmd *Command) Walk(fn func(*Command) error) error
Walk visits cmd and every descendant. If fn returns a non-nil error,
the walk terminates and the error is returned to the caller.
type CommandCategories interface {
// AddCommand adds a command to a category, creating a new category if necessary.
AddCommand(category string, command *Command)
@@ -1032,6 +1048,12 @@ func (f *FlagBase[T, C, V]) PreParse() error
func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error
RunAction executes flag action if set
func (f *FlagBase[T, C, V]) SchemaItemsType() string
SchemaItemsType returns the JSON Schema element type for slice flags.
func (f *FlagBase[T, C, V]) SchemaType() string
SchemaType returns the JSON Schema type for the flag's value type.
func (f *FlagBase[T, C, V]) Set(_ string, val string) error
Set applies given value from string
@@ -1296,6 +1318,23 @@ type RequiredFlag interface {
it allows flags required flags to be backwards compatible with the Flag
interface
type SchemaItemsTyper interface {
// SchemaItemsType returns the JSON Schema type of elements for
// array-type flags. Returns "" for single-value or object flags.
SchemaItemsType() string
}
SchemaItemsTyper is an optional interface for multi-value flags that can
report the JSON Schema type of their elements.
type SchemaTyper interface {
// SchemaType returns the JSON Schema type name for the value this
// flag accepts: "boolean", "integer", "number", "string", "array",
// "object". Returns "" if the flag does not map cleanly.
SchemaType() string
}
SchemaTyper is an optional interface for flags that can report their JSON
Schema type for programmatic introspection.
type Serializer interface {
Serialize() string
}

View File

@@ -65,11 +65,12 @@ var ArgsUsageCommandHelp = "[command]"
func buildHelpCommand(withAction bool) *Command {
cmd := &Command{
Name: helpName,
Aliases: []string{helpAlias},
Usage: UsageCommandHelp,
ArgsUsage: ArgsUsageCommandHelp,
HideHelp: true,
Name: helpName,
Aliases: []string{helpAlias},
Usage: UsageCommandHelp,
ArgsUsage: ArgsUsageCommandHelp,
HideHelp: true,
builtInHelp: true,
}
if withAction {
@@ -85,14 +86,22 @@ func helpCommandAction(ctx context.Context, cmd *Command) error {
tracef("doing help for cmd %[1]q with args %[2]q", cmd, args)
// This action can be triggered by a "default" action of a command
// or via cmd.Run when cmd == helpCmd. So we have following possibilities
// helpCommandAction is triggered in several ways:
//
// 1 $ app
// 2 $ app help
// 3 $ app foo
// 4 $ app help foo
// 5 $ app foo help
// * the command has no user-defined Action (default action fallback)
// * the --help / -h flag was parsed (via cmd.checkHelp())
// * the "help" subcommand (or "h" alias) was dispatched
//
// Possible invocations:
//
// $ app # default action; show root help
// $ app --help / -h # flag; show root help (ignores subsequent args)
// $ app help / h # subcommand; show root help
// $ app help / h foo # subcommand; show help for subcommand "foo"
// $ app --help / -h foo # flag; show help for subcommand "foo"
// $ app foo --help / -h # flag on subcommand; show help for "foo"
// $ app foo help / h # subcommand on subcommand; show help for "foo"
// $ app foo (no action) # default action on subcommand; show help for "foo"
// Case 4. when executing a help command set the context to parent
// to allow resolution of subsequent args. This will transform
@@ -100,7 +109,7 @@ func helpCommandAction(ctx context.Context, cmd *Command) error {
// to
// $ app foo
// which will then be handled as case 3
if cmd.parent != nil && (cmd.HasName(helpName) || cmd.HasName(helpAlias)) {
if cmd.parent != nil && cmd.builtInHelp {
tracef("setting cmd to cmd.parent")
cmd = cmd.parent
}
@@ -459,13 +468,7 @@ func DefaultPrintHelp(out io.Writer, templ string, data any) {
}
func checkVersion(cmd *Command) bool {
found := false
for _, name := range VersionFlag.Names() {
if cmd.Bool(name) {
found = true
}
}
return found
return cmd.versionFlag != nil && cmd.versionFlag.IsSet()
}
func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) {
@@ -492,7 +495,7 @@ func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) {
return true, arguments[:pos]
}
func checkCompletions(ctx context.Context, cmd *Command) bool {
func shouldRunCompletion(cmd *Command) bool {
tracef("checking completions on command %[1]q", cmd.Name)
if !cmd.Root().shellCompletion {
@@ -509,13 +512,14 @@ func checkCompletions(ctx context.Context, cmd *Command) bool {
}
tracef("no subcommand found for completion %[1]q", cmd.Name)
return true
}
func runCompletion(ctx context.Context, cmd *Command) {
if cmd.ShellComplete != nil {
tracef("running shell completion func for command %[1]q", cmd.Name)
cmd.ShellComplete(ctx, cmd)
}
return true
}
func subtract(a, b int) int {

View File

@@ -20,6 +20,8 @@ nav:
- v3 Manual:
- Getting Started: v3/getting-started.md
- Migrating From Older Releases: v3/migrating-from-older-releases.md
- Path and Walk: v3/path-and-walk.md
- Binary Size: v3/binary-size.md
- Examples:
- Greet: v3/examples/greet.md
- Flags:

View File

@@ -107,7 +107,9 @@ COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategori
OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}}
GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}}
`
var FishCompletionTemplate = `# {{ .Command.Name }} fish shell completion

2
vendor/modules.txt vendored
View File

@@ -94,7 +94,7 @@ github.com/seccomp/libseccomp-golang
## explicit; go 1.17
github.com/sirupsen/logrus
github.com/sirupsen/logrus/hooks/test
# github.com/urfave/cli/v3 v3.9.1
# github.com/urfave/cli/v3 v3.10.0
## explicit; go 1.22
github.com/urfave/cli/v3
# github.com/vishvananda/netlink v1.3.1