chore(deps): upgrade urfave/cli from v1 to v3

Migrate from urfave/cli v1 (maintenance mode) to v3 to benefit from
active development, improved features, and long-term support.

Signed-off-by: lifubang <lifubang@acmcoder.com>
This commit is contained in:
lifubang
2026-03-18 03:00:21 +00:00
parent 84762a5c1a
commit 57fad01d52
124 changed files with 8429 additions and 13736 deletions
-2
View File
@@ -1,2 +0,0 @@
[flake8]
max-line-length = 120
-51
View File
@@ -1,51 +0,0 @@
cli
===
[![Run Tests](https://github.com/urfave/cli/actions/workflows/cli.yml/badge.svg?branch=v1-maint)](https://github.com/urfave/cli/actions/workflows/cli.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/urfave/cli/.svg)](https://pkg.go.dev/github.com/urfave/cli/)
[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli)
[![codecov](https://codecov.io/gh/urfave/cli/branch/v1-maint/graph/badge.svg)](https://codecov.io/gh/urfave/cli)
cli is a simple, fast, and fun package for building command line apps in Go. The
goal is to enable developers to write fast and distributable command line
applications in an expressive way.
## Usage Documentation
Usage documentation for `v1` is available [at the docs
site](https://cli.urfave.org/v1/getting-started/) or in-tree at
[./docs/v1/manual.md](./docs/v1/manual.md)
## Installation
Make sure you have a working Go environment. Go version 1.18+ is supported.
### Supported platforms
cli is tested against multiple versions of Go on Linux, and against the latest released
version of Go on OS X and Windows. For full details, see
[./.github/workflows/cli.yml](./.github/workflows/cli.yml).
### Build tags
You can use the following build tags:
#### `urfave_cli_no_docs`
When set, this removes `ToMarkdown` and `ToMan` methods, so your application
won't be able to call those. This reduces the resulting binary size by about
300-400 KB (measured using Go 1.18.1 on Linux/amd64), due to less dependencies.
### Using `v1` releases
```
$ go get github.com/urfave/cli
```
```go
...
import (
"github.com/urfave/cli"
)
...
```
-531
View File
@@ -1,531 +0,0 @@
package cli
import (
"flag"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"time"
)
var (
changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md"
appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
// unused variable. commented for now. will remove in future if agreed upon by everyone
//runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL)
contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you."
errInvalidActionType = NewExitError("ERROR invalid Action type. "+
fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2)
)
// App is the main structure of a cli application. It is recommended that
// an app be created with the cli.NewApp() function
type App struct {
// The name of the program. Defaults to path.Base(os.Args[0])
Name string
// Full name of command for help, defaults to Name
HelpName string
// Description of the program.
Usage string
// Text to override the USAGE section of help
UsageText string
// Description of the program argument format.
ArgsUsage string
// Version of the program
Version string
// Description of the program
Description string
// List of commands to execute
Commands []Command
// List of flags to parse
Flags []Flag
// Boolean to enable bash completion commands
EnableBashCompletion bool
// Boolean to hide built-in help command
HideHelp bool
// Boolean to hide built-in version flag and the VERSION section of help
HideVersion bool
// Populate on app startup, only gettable through method Categories()
categories CommandCategories
// An action to execute when the bash-completion flag is set
BashComplete BashCompleteFunc
// An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run
Before BeforeFunc
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After AfterFunc
// The action to execute when no subcommands are specified
// Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}`
// *Note*: support for the deprecated `Action` signature will be removed in a future version
Action interface{}
// Execute this function if the proper command cannot be found
CommandNotFound CommandNotFoundFunc
// Execute this function if an usage error occurs
OnUsageError OnUsageErrorFunc
// Compilation date
Compiled time.Time
// List of all authors who contributed
Authors []Author
// Copyright of the binary if any
Copyright string
// Name of Author (Note: Use App.Authors, this is deprecated)
Author string
// Email of Author (Note: Use App.Authors, this is deprecated)
Email string
// Writer writer to write output to
Writer io.Writer
// ErrWriter writes error output
ErrWriter io.Writer
// Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to
// function as a default, so this is optional.
ExitErrHandler ExitErrHandlerFunc
// Other custom info
Metadata map[string]interface{}
// Carries a function which returns app specific info.
ExtraInfo func() map[string]string
// CustomAppHelpTemplate the text template for app help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomAppHelpTemplate string
// Boolean to enable short-option handling so user can combine several
// single-character bool arguements into one
// i.e. foobar -o -v -> foobar -ov
UseShortOptionHandling bool
didSetup bool
}
// Tries to find out when this binary was compiled.
// Returns the current time if it fails to find it.
func compileTime() time.Time {
info, err := os.Stat(os.Args[0])
if err != nil {
return time.Now()
}
return info.ModTime()
}
// NewApp creates a new cli Application with some reasonable defaults for Name,
// Usage, Version and Action.
func NewApp() *App {
return &App{
Name: filepath.Base(os.Args[0]),
HelpName: filepath.Base(os.Args[0]),
Usage: "A new cli application",
UsageText: "",
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Compiled: compileTime(),
Writer: os.Stdout,
}
}
// Setup runs initialization code to ensure all data structures are ready for
// `Run` or inspection prior to `Run`. It is internally called by `Run`, but
// will return early if setup has already happened.
func (a *App) Setup() {
if a.didSetup {
return
}
a.didSetup = true
if a.Author != "" || a.Email != "" {
a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email})
}
var newCmds []Command
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
if a.Version == "" {
a.HideVersion = true
}
if !a.HideVersion {
a.appendFlag(VersionFlag)
}
a.categories = CommandCategories{}
for _, command := range a.Commands {
a.categories = a.categories.AddCommand(command.Category, command)
}
sort.Sort(a.categories)
if a.Metadata == nil {
a.Metadata = make(map[string]interface{})
}
if a.Writer == nil {
a.Writer = os.Stdout
}
}
func (a *App) newFlagSet() (*flag.FlagSet, error) {
return flagSet(a.Name, a.Flags)
}
func (a *App) useShortOptionHandling() bool {
return a.UseShortOptionHandling
}
// Run is the entry point to the cli app. Parses the arguments slice and routes
// to the proper flag/args combination
func (a *App) Run(arguments []string) (err error) {
a.Setup()
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
shellComplete, arguments := checkShellCompleteFlag(a, arguments)
set, err := a.newFlagSet()
if err != nil {
return err
}
err = parseIter(set, a, arguments[1:], shellComplete)
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, nil)
if nerr != nil {
_, _ = fmt.Fprintln(a.Writer, nerr)
_ = ShowAppHelp(context)
return nerr
}
context.shellComplete = shellComplete
if checkCompletions(context) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err := a.OnUsageError(context, err, false)
a.handleExitCoder(context, err)
return err
}
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
_ = ShowAppHelp(context)
return err
}
if !a.HideHelp && checkHelp(context) {
_ = ShowAppHelp(context)
return nil
}
if !a.HideVersion && checkVersion(context) {
ShowVersion(context)
return nil
}
cerr := checkRequiredFlags(a.Flags, context)
if cerr != nil {
_ = ShowAppHelp(context)
return cerr
}
if a.After != nil && !context.shellComplete {
defer func() {
if afterErr := a.After(context); afterErr != nil {
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if a.Before != nil && !context.shellComplete {
beforeErr := a.Before(context)
if beforeErr != nil {
a.handleExitCoder(context, beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
if a.Action == nil {
a.Action = helpCommand.Action
}
// Run default Action
err = HandleAction(a.Action, context)
a.handleExitCoder(context, err)
return err
}
// RunAndExitOnError calls .Run() and exits non-zero if an error was returned
//
// Deprecated: instead you should return an error that fulfills cli.ExitCoder
// to cli.App.Run. This will cause the application to exit with the given eror
// code in the cli.ExitCoder
func (a *App) RunAndExitOnError() {
if err := a.Run(os.Args); err != nil {
_, _ = fmt.Fprintln(a.errWriter(), err)
OsExiter(1)
}
}
// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to
// generate command-specific flags
func (a *App) RunAsSubcommand(ctx *Context) (err error) {
// append help to commands
if len(a.Commands) > 0 {
if a.Command(helpCommand.Name) == nil && !a.HideHelp {
a.Commands = append(a.Commands, helpCommand)
if (HelpFlag != BoolFlag{}) {
a.appendFlag(HelpFlag)
}
}
}
newCmds := []Command{}
for _, c := range a.Commands {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}
newCmds = append(newCmds, c)
}
a.Commands = newCmds
set, err := a.newFlagSet()
if err != nil {
return err
}
err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete)
nerr := normalizeFlags(a.Flags, set)
context := NewContext(a, set, ctx)
if nerr != nil {
_, _ = fmt.Fprintln(a.Writer, nerr)
_, _ = fmt.Fprintln(a.Writer)
if len(a.Commands) > 0 {
_ = ShowSubcommandHelp(context)
} else {
_ = ShowCommandHelp(ctx, context.Args().First())
}
return nerr
}
if checkCompletions(context) {
return nil
}
if err != nil {
if a.OnUsageError != nil {
err = a.OnUsageError(context, err, true)
a.handleExitCoder(context, err)
return err
}
_, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error())
_ = ShowSubcommandHelp(context)
return err
}
if len(a.Commands) > 0 {
if checkSubcommandHelp(context) {
return nil
}
} else {
if checkCommandHelp(ctx, context.Args().First()) {
return nil
}
}
cerr := checkRequiredFlags(a.Flags, context)
if cerr != nil {
_ = ShowSubcommandHelp(context)
return cerr
}
if a.After != nil && !context.shellComplete {
defer func() {
afterErr := a.After(context)
if afterErr != nil {
a.handleExitCoder(context, err)
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if a.Before != nil && !context.shellComplete {
beforeErr := a.Before(context)
if beforeErr != nil {
a.handleExitCoder(context, beforeErr)
err = beforeErr
return err
}
}
args := context.Args()
if args.Present() {
name := args.First()
c := a.Command(name)
if c != nil {
return c.Run(context)
}
}
// Run default Action
err = HandleAction(a.Action, context)
a.handleExitCoder(context, err)
return err
}
// Command returns the named command on App. Returns nil if the command does not exist
func (a *App) Command(name string) *Command {
for _, c := range a.Commands {
if c.HasName(name) {
return &c
}
}
return nil
}
// Categories returns a slice containing all the categories with the commands they contain
func (a *App) Categories() CommandCategories {
return a.categories
}
// VisibleCategories returns a slice of categories and commands that are
// Hidden=false
func (a *App) VisibleCategories() []*CommandCategory {
ret := []*CommandCategory{}
for _, category := range a.categories {
if visible := func() *CommandCategory {
for _, command := range category.Commands {
if !command.Hidden {
return category
}
}
return nil
}(); visible != nil {
ret = append(ret, visible)
}
}
return ret
}
// VisibleCommands returns a slice of the Commands with Hidden=false
func (a *App) VisibleCommands() []Command {
var ret []Command
for _, command := range a.Commands {
if !command.Hidden {
ret = append(ret, command)
}
}
return ret
}
// VisibleFlags returns a slice of the Flags with Hidden=false
func (a *App) VisibleFlags() []Flag {
return visibleFlags(a.Flags)
}
func (a *App) hasFlag(flag Flag) bool {
for _, f := range a.Flags {
if flag == f {
return true
}
}
return false
}
func (a *App) errWriter() io.Writer {
// When the app ErrWriter is nil use the package level one.
if a.ErrWriter == nil {
return ErrWriter
}
return a.ErrWriter
}
func (a *App) appendFlag(flag Flag) {
if !a.hasFlag(flag) {
a.Flags = append(a.Flags, flag)
}
}
func (a *App) handleExitCoder(context *Context, err error) {
if a.ExitErrHandler != nil {
a.ExitErrHandler(context, err)
} else {
HandleExitCoder(err)
}
}
// Author represents someone who has contributed to a cli project.
type Author struct {
Name string // The Authors name
Email string // The Authors email
}
// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
func (a Author) String() string {
e := ""
if a.Email != "" {
e = " <" + a.Email + ">"
}
return fmt.Sprintf("%v%v", a.Name, e)
}
// HandleAction attempts to figure out which Action signature was used. If
// it's an ActionFunc or a func with the legacy signature for Action, the func
// is run!
func HandleAction(action interface{}, context *Context) (err error) {
switch a := action.(type) {
case ActionFunc:
return a(context)
case func(*Context) error:
return a(context)
case func(*Context): // deprecated function signature
a(context)
return nil
}
return errInvalidActionType
}
-44
View File
@@ -1,44 +0,0 @@
package cli
// CommandCategories is a slice of *CommandCategory.
type CommandCategories []*CommandCategory
// CommandCategory is a category containing commands.
type CommandCategory struct {
Name string
Commands Commands
}
func (c CommandCategories) Less(i, j int) bool {
return lexicographicLess(c[i].Name, c[j].Name)
}
func (c CommandCategories) Len() int {
return len(c)
}
func (c CommandCategories) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// AddCommand adds a command to a category.
func (c CommandCategories) AddCommand(category string, command Command) CommandCategories {
for _, commandCategory := range c {
if commandCategory.Name == category {
commandCategory.Commands = append(commandCategory.Commands, command)
return c
}
}
return append(c, &CommandCategory{Name: category, Commands: []Command{command}})
}
// VisibleCommands returns a slice of the Commands with Hidden=false
func (c *CommandCategory) VisibleCommands() []Command {
ret := []Command{}
for _, command := range c.Commands {
if !command.Hidden {
ret = append(ret, command)
}
}
return ret
}
-22
View File
@@ -1,22 +0,0 @@
// Package cli provides a minimal framework for creating and organizing command line
// Go applications. cli is designed to be easy to understand and write, the most simple
// cli application can be written as follows:
// func main() {
// cli.NewApp().Run(os.Args)
// }
//
// Of course this application does not do much, so let's make this an actual application:
// func main() {
// app := cli.NewApp()
// app.Name = "greet"
// app.Usage = "say a greeting"
// app.Action = func(c *cli.Context) error {
// println("Greetings")
// return nil
// }
//
// app.Run(os.Args)
// }
package cli
//go:generate go run flag-gen/main.go flag-gen/assets_vfsdata.go
-386
View File
@@ -1,386 +0,0 @@
package cli
import (
"flag"
"fmt"
"sort"
"strings"
)
// Command is a subcommand for a cli.App.
type Command struct {
// The name of the command
Name string
// short name of the command. Typically one character (deprecated, use `Aliases`)
ShortName string
// A list of aliases for the command
Aliases []string
// A short description of the usage of this command
Usage string
// Custom text to show on USAGE section of help
UsageText string
// A longer explanation of how the command works
Description string
// A short description of the arguments of this command
ArgsUsage string
// The category the command is part of
Category string
// The function to call when checking for bash command completions
BashComplete BashCompleteFunc
// An action to execute before any sub-subcommands are run, but after the context is ready
// If a non-nil error is returned, no sub-subcommands are run
Before BeforeFunc
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After AfterFunc
// The function to call when this command is invoked
Action interface{}
// TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind
// of deprecation period has passed, maybe?
// Execute this function if a usage error occurs.
OnUsageError OnUsageErrorFunc
// List of child commands
Subcommands Commands
// List of flags to parse
Flags []Flag
// Treat all flags as normal arguments if true
SkipFlagParsing bool
// Skip argument reordering which attempts to move flags before arguments,
// but only works if all flags appear after all arguments. This behavior was
// removed n version 2 since it only works under specific conditions so we
// backport here by exposing it as an option for compatibility.
SkipArgReorder bool
// Boolean to hide built-in help command
HideHelp bool
// Boolean to hide this command from help or completion
Hidden bool
// Boolean to enable short-option handling so user can combine several
// single-character bool arguments into one
// i.e. foobar -o -v -> foobar -ov
UseShortOptionHandling bool
// Full name of command for help, defaults to full command name, including parent commands.
HelpName string
commandNamePath []string
// CustomHelpTemplate the text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomHelpTemplate string
}
type CommandsByName []Command
func (c CommandsByName) Len() int {
return len(c)
}
func (c CommandsByName) Less(i, j int) bool {
return lexicographicLess(c[i].Name, c[j].Name)
}
func (c CommandsByName) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// FullName returns the full name of the command.
// For subcommands this ensures that parent commands are part of the command path
func (c Command) FullName() string {
if c.commandNamePath == nil {
return c.Name
}
return strings.Join(c.commandNamePath, " ")
}
// Commands is a slice of Command
type Commands []Command
// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags
func (c Command) Run(ctx *Context) (err error) {
if !c.SkipFlagParsing {
if len(c.Subcommands) > 0 {
return c.startApp(ctx)
}
}
if !c.HideHelp && (HelpFlag != BoolFlag{}) {
// append help to flags
c.Flags = append(
c.Flags,
HelpFlag,
)
}
if ctx.App.UseShortOptionHandling {
c.UseShortOptionHandling = true
}
set, err := c.parseFlags(ctx.Args().Tail(), ctx.shellComplete)
context := NewContext(ctx.App, set, ctx)
context.Command = c
if checkCommandCompletions(context, c.Name) {
return nil
}
if err != nil {
if c.OnUsageError != nil {
err := c.OnUsageError(context, err, false)
context.App.handleExitCoder(context, err)
return err
}
_, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error())
_, _ = fmt.Fprintln(context.App.Writer)
_ = ShowCommandHelp(context, c.Name)
return err
}
if checkCommandHelp(context, c.Name) {
return nil
}
cerr := checkRequiredFlags(c.Flags, context)
if cerr != nil {
_ = ShowCommandHelp(context, c.Name)
return cerr
}
if c.After != nil {
defer func() {
afterErr := c.After(context)
if afterErr != nil {
context.App.handleExitCoder(context, err)
if err != nil {
err = NewMultiError(err, afterErr)
} else {
err = afterErr
}
}
}()
}
if c.Before != nil {
err = c.Before(context)
if err != nil {
context.App.handleExitCoder(context, err)
return err
}
}
if c.Action == nil {
c.Action = helpSubcommand.Action
}
err = HandleAction(c.Action, context)
if err != nil {
context.App.handleExitCoder(context, err)
}
return err
}
func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) {
if c.SkipFlagParsing {
set, err := c.newFlagSet()
if err != nil {
return nil, err
}
return set, set.Parse(append([]string{"--"}, args...))
}
if !c.SkipArgReorder {
args = reorderArgs(c.Flags, args)
}
set, err := c.newFlagSet()
if err != nil {
return nil, err
}
err = parseIter(set, c, args, shellComplete)
if err != nil {
return nil, err
}
err = normalizeFlags(c.Flags, set)
if err != nil {
return nil, err
}
return set, nil
}
func (c *Command) newFlagSet() (*flag.FlagSet, error) {
return flagSet(c.Name, c.Flags)
}
func (c *Command) useShortOptionHandling() bool {
return c.UseShortOptionHandling
}
// reorderArgs moves all flags (via reorderedArgs) before the rest of
// the arguments (remainingArgs) as this is what flag expects.
func reorderArgs(commandFlags []Flag, args []string) []string {
var remainingArgs, reorderedArgs []string
nextIndexMayContainValue := false
for i, arg := range args {
// if we're expecting an option-value, check if this arg is a value, in
// which case it should be re-ordered next to its associated flag
if nextIndexMayContainValue && !argIsFlag(commandFlags, arg) {
nextIndexMayContainValue = false
reorderedArgs = append(reorderedArgs, arg)
} else if arg == "--" {
// don't reorder any args after the -- delimiter As described in the POSIX spec:
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
// > Guideline 10:
// > The first -- argument that is not an option-argument should be accepted
// > as a delimiter indicating the end of options. Any following arguments
// > should be treated as operands, even if they begin with the '-' character.
// make sure the "--" delimiter itself is at the start
remainingArgs = append([]string{"--"}, remainingArgs...)
remainingArgs = append(remainingArgs, args[i+1:]...)
break
// checks if this is an arg that should be re-ordered
} else if argIsFlag(commandFlags, arg) {
// we have determined that this is a flag that we should re-order
reorderedArgs = append(reorderedArgs, arg)
// if this arg does not contain a "=", then the next index may contain the value for this flag
nextIndexMayContainValue = !strings.Contains(arg, "=")
// simply append any remaining args
} else {
remainingArgs = append(remainingArgs, arg)
}
}
return append(reorderedArgs, remainingArgs...)
}
// argIsFlag checks if an arg is one of our command flags
func argIsFlag(commandFlags []Flag, arg string) bool {
if arg == "-" || arg == "--" {
// `-` is never a flag
// `--` is an option-value when following a flag, and a delimiter indicating the end of options in other cases.
return false
}
// flags always start with a -
if !strings.HasPrefix(arg, "-") {
return false
}
// this line turns `--flag` into `flag`
if strings.HasPrefix(arg, "--") {
arg = strings.Replace(arg, "-", "", 2)
}
// this line turns `-flag` into `flag`
if strings.HasPrefix(arg, "-") {
arg = strings.Replace(arg, "-", "", 1)
}
// this line turns `flag=value` into `flag`
arg = strings.Split(arg, "=")[0]
// look through all the flags, to see if the `arg` is one of our flags
for _, flag := range commandFlags {
for _, key := range strings.Split(flag.GetName(), ",") {
key := strings.TrimSpace(key)
if key == arg {
return true
}
}
}
// return false if this arg was not one of our flags
return false
}
// Names returns the names including short names and aliases.
func (c Command) Names() []string {
names := []string{c.Name}
if c.ShortName != "" {
names = append(names, c.ShortName)
}
return append(names, c.Aliases...)
}
// HasName returns true if Command.Name or Command.ShortName matches given name
func (c Command) HasName(name string) bool {
for _, n := range c.Names() {
if n == name {
return true
}
}
return false
}
func (c Command) startApp(ctx *Context) error {
app := NewApp()
app.Metadata = ctx.App.Metadata
app.ExitErrHandler = ctx.App.ExitErrHandler
// set the name and usage
app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name)
if c.HelpName == "" {
app.HelpName = c.HelpName
} else {
app.HelpName = app.Name
}
app.Usage = c.Usage
app.Description = c.Description
app.ArgsUsage = c.ArgsUsage
// set CommandNotFound
app.CommandNotFound = ctx.App.CommandNotFound
app.CustomAppHelpTemplate = c.CustomHelpTemplate
// set the flags and commands
app.Commands = c.Subcommands
app.Flags = c.Flags
app.HideHelp = c.HideHelp
app.Version = ctx.App.Version
app.HideVersion = ctx.App.HideVersion
app.Compiled = ctx.App.Compiled
app.Author = ctx.App.Author
app.Email = ctx.App.Email
app.Writer = ctx.App.Writer
app.ErrWriter = ctx.App.ErrWriter
app.UseShortOptionHandling = ctx.App.UseShortOptionHandling
app.categories = CommandCategories{}
for _, command := range c.Subcommands {
app.categories = app.categories.AddCommand(command.Category, command)
}
sort.Sort(app.categories)
// bash completion
app.EnableBashCompletion = ctx.App.EnableBashCompletion
if c.BashComplete != nil {
app.BashComplete = c.BashComplete
}
// set the actions
app.Before = c.Before
app.After = c.After
if c.Action != nil {
app.Action = c.Action
} else {
app.Action = helpSubcommand.Action
}
app.OnUsageError = c.OnUsageError
for index, cc := range app.Commands {
app.Commands[index].commandNamePath = []string{c.Name, cc.Name}
}
return app.RunAsSubcommand(ctx)
}
// VisibleFlags returns a slice of the Flags with Hidden=false
func (c Command) VisibleFlags() []Flag {
return visibleFlags(c.Flags)
}
-348
View File
@@ -1,348 +0,0 @@
package cli
import (
"errors"
"flag"
"fmt"
"os"
"reflect"
"strings"
"syscall"
)
// Context is a type that is passed through to
// each Handler action in a cli application. Context
// can be used to retrieve context-specific Args and
// parsed command-line options.
type Context struct {
App *App
Command Command
shellComplete bool
flagSet *flag.FlagSet
setFlags map[string]bool
parentContext *Context
}
// NewContext creates a new context. For use in when invoking an App or Command action.
func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
c := &Context{App: app, flagSet: set, parentContext: parentCtx}
if parentCtx != nil {
c.shellComplete = parentCtx.shellComplete
}
return c
}
// NumFlags returns the number of flags set
func (c *Context) NumFlags() int {
return c.flagSet.NFlag()
}
// Set sets a context flag to a value.
func (c *Context) Set(name, value string) error {
c.setFlags = nil
return c.flagSet.Set(name, value)
}
// GlobalSet sets a context flag to a value on the global flagset
func (c *Context) GlobalSet(name, value string) error {
globalContext(c).setFlags = nil
return globalContext(c).flagSet.Set(name, value)
}
// IsSet determines if the flag was actually set
func (c *Context) IsSet(name string) bool {
if c.setFlags == nil {
c.setFlags = make(map[string]bool)
c.flagSet.Visit(func(f *flag.Flag) {
c.setFlags[f.Name] = true
})
c.flagSet.VisitAll(func(f *flag.Flag) {
if _, ok := c.setFlags[f.Name]; ok {
return
}
c.setFlags[f.Name] = false
})
// XXX hack to support IsSet for flags with EnvVar
//
// There isn't an easy way to do this with the current implementation since
// whether a flag was set via an environment variable is very difficult to
// determine here. Instead, we intend to introduce a backwards incompatible
// change in version 2 to add `IsSet` to the Flag interface to push the
// responsibility closer to where the information required to determine
// whether a flag is set by non-standard means such as environment
// variables is available.
//
// See https://github.com/urfave/cli/issues/294 for additional discussion
flags := c.Command.Flags
if c.Command.Name == "" { // cannot == Command{} since it contains slice types
if c.App != nil {
flags = c.App.Flags
}
}
for _, f := range flags {
eachName(f.GetName(), func(name string) {
if isSet, ok := c.setFlags[name]; isSet || !ok {
// Check if a flag is set
if isSet {
// If the flag is set, also set its other aliases
eachName(f.GetName(), func(name string) {
c.setFlags[name] = true
})
}
return
}
val := reflect.ValueOf(f)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
filePathValue := val.FieldByName("FilePath")
if filePathValue.IsValid() {
eachName(filePathValue.String(), func(filePath string) {
if _, err := os.Stat(filePath); err == nil {
c.setFlags[name] = true
return
}
})
}
envVarValue := val.FieldByName("EnvVar")
if envVarValue.IsValid() {
eachName(envVarValue.String(), func(envVar string) {
envVar = strings.TrimSpace(envVar)
if _, ok := syscall.Getenv(envVar); ok {
c.setFlags[name] = true
return
}
})
}
})
}
}
return c.setFlags[name]
}
// GlobalIsSet determines if the global flag was actually set
func (c *Context) GlobalIsSet(name string) bool {
ctx := c
if ctx.parentContext != nil {
ctx = ctx.parentContext
}
for ; ctx != nil; ctx = ctx.parentContext {
if ctx.IsSet(name) {
return true
}
}
return false
}
// FlagNames returns a slice of flag names used in this context.
func (c *Context) FlagNames() (names []string) {
for _, f := range c.Command.Flags {
name := strings.Split(f.GetName(), ",")[0]
if name == "help" {
continue
}
names = append(names, name)
}
return
}
// GlobalFlagNames returns a slice of global flag names used by the app.
func (c *Context) GlobalFlagNames() (names []string) {
for _, f := range c.App.Flags {
name := strings.Split(f.GetName(), ",")[0]
if name == "help" || name == "version" {
continue
}
names = append(names, name)
}
return
}
// Parent returns the parent context, if any
func (c *Context) Parent() *Context {
return c.parentContext
}
// value returns the value of the flag coressponding to `name`
func (c *Context) value(name string) interface{} {
return c.flagSet.Lookup(name).Value.(flag.Getter).Get()
}
// Args contains apps console arguments
type Args []string
// Args returns the command line arguments associated with the context.
func (c *Context) Args() Args {
args := Args(c.flagSet.Args())
return args
}
// NArg returns the number of the command line arguments.
func (c *Context) NArg() int {
return len(c.Args())
}
// Get returns the nth argument, or else a blank string
func (a Args) Get(n int) string {
if len(a) > n {
return a[n]
}
return ""
}
// First returns the first argument, or else a blank string
func (a Args) First() string {
return a.Get(0)
}
// Tail returns the rest of the arguments (not the first one)
// or else an empty string slice
func (a Args) Tail() []string {
if len(a) >= 2 {
return []string(a)[1:]
}
return []string{}
}
// Present checks if there are any arguments present
func (a Args) Present() bool {
return len(a) != 0
}
// Swap swaps arguments at the given indexes
func (a Args) Swap(from, to int) error {
if from >= len(a) || to >= len(a) {
return errors.New("index out of range")
}
a[from], a[to] = a[to], a[from]
return nil
}
func globalContext(ctx *Context) *Context {
if ctx == nil {
return nil
}
for {
if ctx.parentContext == nil {
return ctx
}
ctx = ctx.parentContext
}
}
func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet {
if ctx.parentContext != nil {
ctx = ctx.parentContext
}
for ; ctx != nil; ctx = ctx.parentContext {
if f := ctx.flagSet.Lookup(name); f != nil {
return ctx.flagSet
}
}
return nil
}
func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case *StringSlice:
default:
_ = set.Set(name, ff.Value.String())
}
}
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
for _, f := range flags {
parts := strings.Split(f.GetName(), ",")
if len(parts) == 1 {
continue
}
var ff *flag.Flag
for _, name := range parts {
name = strings.Trim(name, " ")
if visited[name] {
if ff != nil {
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
}
ff = set.Lookup(name)
}
}
if ff == nil {
continue
}
for _, name := range parts {
name = strings.Trim(name, " ")
if !visited[name] {
copyFlag(name, ff, set)
}
}
}
return nil
}
type requiredFlagsErr interface {
error
getMissingFlags() []string
}
type errRequiredFlags struct {
missingFlags []string
}
func (e *errRequiredFlags) Error() string {
numberOfMissingFlags := len(e.missingFlags)
if numberOfMissingFlags == 1 {
return fmt.Sprintf("Required flag %q not set", e.missingFlags[0])
}
joinedMissingFlags := strings.Join(e.missingFlags, ", ")
return fmt.Sprintf("Required flags %q not set", joinedMissingFlags)
}
func (e *errRequiredFlags) getMissingFlags() []string {
return e.missingFlags
}
func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr {
var missingFlags []string
for _, f := range flags {
if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
var flagPresent bool
var flagName string
for _, key := range strings.Split(f.GetName(), ",") {
key = strings.TrimSpace(key)
if len(key) > 1 {
flagName = key
}
if context.IsSet(key) {
flagPresent = true
}
}
if !flagPresent && flagName != "" {
missingFlags = append(missingFlags, flagName)
}
}
}
if len(missingFlags) != 0 {
return &errRequiredFlags{missingFlags: missingFlags}
}
return nil
}
-151
View File
@@ -1,151 +0,0 @@
//go:build !urfave_cli_no_docs
// +build !urfave_cli_no_docs
package cli
import (
"bytes"
"fmt"
"io"
"sort"
"strings"
"text/template"
"github.com/cpuguy83/go-md2man/v2/md2man"
)
// ToMarkdown creates a markdown string for the `*App`
// The function errors if either parsing or writing of the string fails.
func (a *App) ToMarkdown() (string, error) {
var w bytes.Buffer
if err := a.writeDocTemplate(&w); err != nil {
return "", err
}
return w.String(), nil
}
// ToMan creates a man page string for the `*App`
// The function errors if either parsing or writing of the string fails.
func (a *App) ToMan() (string, error) {
var w bytes.Buffer
if err := a.writeDocTemplate(&w); err != nil {
return "", err
}
man := md2man.Render(w.Bytes())
return string(man), nil
}
type cliTemplate struct {
App *App
Commands []string
GlobalArgs []string
SynopsisArgs []string
}
func (a *App) writeDocTemplate(w io.Writer) error {
const name = "cli"
t, err := template.New(name).Parse(MarkdownDocTemplate)
if err != nil {
return err
}
return t.ExecuteTemplate(w, name, &cliTemplate{
App: a,
Commands: prepareCommands(a.Commands, 0),
GlobalArgs: prepareArgsWithValues(a.Flags),
SynopsisArgs: prepareArgsSynopsis(a.Flags),
})
}
func prepareCommands(commands []Command, level int) []string {
coms := []string{}
for i := range commands {
command := &commands[i]
if command.Hidden {
continue
}
usage := ""
if command.Usage != "" {
usage = command.Usage
}
prepared := fmt.Sprintf("%s %s\n\n%s\n",
strings.Repeat("#", level+2),
strings.Join(command.Names(), ", "),
usage,
)
flags := prepareArgsWithValues(command.Flags)
if len(flags) > 0 {
prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n"))
}
coms = append(coms, prepared)
// recursevly iterate subcommands
if len(command.Subcommands) > 0 {
coms = append(
coms,
prepareCommands(command.Subcommands, level+1)...,
)
}
}
return coms
}
func prepareArgsWithValues(flags []Flag) []string {
return prepareFlags(flags, ", ", "**", "**", `""`, true)
}
func prepareArgsSynopsis(flags []Flag) []string {
return prepareFlags(flags, "|", "[", "]", "[value]", false)
}
func prepareFlags(
flags []Flag,
sep, opener, closer, value string,
addDetails bool,
) []string {
args := []string{}
for _, f := range flags {
flag, ok := f.(DocGenerationFlag)
if !ok {
continue
}
modifiedArg := opener
for _, s := range strings.Split(flag.GetName(), ",") {
trimmed := strings.TrimSpace(s)
if len(modifiedArg) > len(opener) {
modifiedArg += sep
}
if len(trimmed) > 1 {
modifiedArg += fmt.Sprintf("--%s", trimmed)
} else {
modifiedArg += fmt.Sprintf("-%s", trimmed)
}
}
modifiedArg += closer
if flag.TakesValue() {
modifiedArg += fmt.Sprintf("=%s", value)
}
if addDetails {
modifiedArg += flagDetails(flag)
}
args = append(args, modifiedArg+"\n")
}
sort.Strings(args)
return args
}
// flagDetails returns a string containing the flags metadata
func flagDetails(flag DocGenerationFlag) string {
description := flag.GetUsage()
value := flag.GetValue()
if value != "" {
description += " (default: " + value + ")"
}
return ": " + description
}
-115
View File
@@ -1,115 +0,0 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
)
// OsExiter is the function used when the app exits. If not set defaults to os.Exit.
var OsExiter = os.Exit
// ErrWriter is used to write errors to the user. This can be anything
// implementing the io.Writer interface and defaults to os.Stderr.
var ErrWriter io.Writer = os.Stderr
// MultiError is an error that wraps multiple errors.
type MultiError struct {
Errors []error
}
// NewMultiError creates a new MultiError. Pass in one or more errors.
func NewMultiError(err ...error) MultiError {
return MultiError{Errors: err}
}
// Error implements the error interface.
func (m MultiError) Error() string {
errs := make([]string, len(m.Errors))
for i, err := range m.Errors {
errs[i] = err.Error()
}
return strings.Join(errs, "\n")
}
type ErrorFormatter interface {
Format(s fmt.State, verb rune)
}
// ExitCoder is the interface checked by `App` and `Command` for a custom exit
// code
type ExitCoder interface {
error
ExitCode() int
}
// ExitError fulfills both the builtin `error` interface and `ExitCoder`
type ExitError struct {
exitCode int
message interface{}
}
// NewExitError makes a new *ExitError
func NewExitError(message interface{}, exitCode int) *ExitError {
return &ExitError{
exitCode: exitCode,
message: message,
}
}
// Error returns the string message, fulfilling the interface required by
// `error`
func (ee *ExitError) Error() string {
return fmt.Sprintf("%v", ee.message)
}
// ExitCode returns the exit code, fulfilling the interface required by
// `ExitCoder`
func (ee *ExitError) ExitCode() int {
return ee.exitCode
}
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the
// given exit code. If the given error is a MultiError, then this func is
// called on all members of the Errors slice and calls OsExiter with the last exit code.
func HandleExitCoder(err error) {
if err == nil {
return
}
if exitErr, ok := err.(ExitCoder); ok {
if err.Error() != "" {
if _, ok := exitErr.(ErrorFormatter); ok {
fmt.Fprintf(ErrWriter, "%+v\n", err)
} else {
fmt.Fprintln(ErrWriter, err)
}
}
OsExiter(exitErr.ExitCode())
return
}
if multiErr, ok := err.(MultiError); ok {
code := handleMultiError(multiErr)
OsExiter(code)
return
}
}
func handleMultiError(multiErr MultiError) int {
code := 1
for _, merr := range multiErr.Errors {
if multiErr2, ok := merr.(MultiError); ok {
code = handleMultiError(multiErr2)
} else {
fmt.Fprintln(ErrWriter, merr)
if exitErr, ok := merr.(ExitCoder); ok {
code = exitErr.ExitCode()
}
}
}
return code
}
-194
View File
@@ -1,194 +0,0 @@
package cli
import (
"bytes"
"fmt"
"io"
"strings"
"text/template"
)
// ToFishCompletion creates a fish completion string for the `*App`
// The function errors if either parsing or writing of the string fails.
func (a *App) ToFishCompletion() (string, error) {
var w bytes.Buffer
if err := a.writeFishCompletionTemplate(&w); err != nil {
return "", err
}
return w.String(), nil
}
type fishCompletionTemplate struct {
App *App
Completions []string
AllCommands []string
}
func (a *App) writeFishCompletionTemplate(w io.Writer) error {
const name = "cli"
t, err := template.New(name).Parse(FishCompletionTemplate)
if err != nil {
return err
}
allCommands := []string{}
// Add global flags
completions := a.prepareFishFlags(a.VisibleFlags(), allCommands)
// Add help flag
if !a.HideHelp {
completions = append(
completions,
a.prepareFishFlags([]Flag{HelpFlag}, allCommands)...,
)
}
// Add version flag
if !a.HideVersion {
completions = append(
completions,
a.prepareFishFlags([]Flag{VersionFlag}, allCommands)...,
)
}
// Add commands and their flags
completions = append(
completions,
a.prepareFishCommands(a.VisibleCommands(), &allCommands, []string{})...,
)
return t.ExecuteTemplate(w, name, &fishCompletionTemplate{
App: a,
Completions: completions,
AllCommands: allCommands,
})
}
func (a *App) prepareFishCommands(commands []Command, allCommands *[]string, previousCommands []string) []string {
completions := []string{}
for i := range commands {
command := &commands[i]
if command.Hidden {
continue
}
var completion strings.Builder
completion.WriteString(fmt.Sprintf(
"complete -r -c %s -n '%s' -a '%s'",
a.Name,
a.fishSubcommandHelper(previousCommands),
strings.Join(command.Names(), " "),
))
if command.Usage != "" {
completion.WriteString(fmt.Sprintf(" -d '%s'",
escapeSingleQuotes(command.Usage)))
}
if !command.HideHelp {
completions = append(
completions,
a.prepareFishFlags([]Flag{HelpFlag}, command.Names())...,
)
}
*allCommands = append(*allCommands, command.Names()...)
completions = append(completions, completion.String())
completions = append(
completions,
a.prepareFishFlags(command.Flags, command.Names())...,
)
// recursevly iterate subcommands
if len(command.Subcommands) > 0 {
completions = append(
completions,
a.prepareFishCommands(
command.Subcommands, allCommands, command.Names(),
)...,
)
}
}
return completions
}
func (a *App) prepareFishFlags(flags []Flag, previousCommands []string) []string {
completions := []string{}
for _, f := range flags {
flag, ok := f.(DocGenerationFlag)
if !ok {
continue
}
completion := &strings.Builder{}
completion.WriteString(fmt.Sprintf(
"complete -c %s -n '%s'",
a.Name,
a.fishSubcommandHelper(previousCommands),
))
fishAddFileFlag(f, completion)
for idx, opt := range strings.Split(flag.GetName(), ",") {
if idx == 0 {
completion.WriteString(fmt.Sprintf(
" -l %s", strings.TrimSpace(opt),
))
} else {
completion.WriteString(fmt.Sprintf(
" -s %s", strings.TrimSpace(opt),
))
}
}
if flag.TakesValue() {
completion.WriteString(" -r")
}
if flag.GetUsage() != "" {
completion.WriteString(fmt.Sprintf(" -d '%s'",
escapeSingleQuotes(flag.GetUsage())))
}
completions = append(completions, completion.String())
}
return completions
}
func fishAddFileFlag(flag Flag, completion *strings.Builder) {
switch f := flag.(type) {
case GenericFlag:
if f.TakesFile {
return
}
case StringFlag:
if f.TakesFile {
return
}
case StringSliceFlag:
if f.TakesFile {
return
}
}
completion.WriteString(" -f")
}
func (a *App) fishSubcommandHelper(allCommands []string) string {
fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", a.Name)
if len(allCommands) > 0 {
fishHelper = fmt.Sprintf(
"__fish_seen_subcommand_from %s",
strings.Join(allCommands, " "),
)
}
return fishHelper
}
func escapeSingleQuotes(input string) string {
return strings.Replace(input, `'`, `\'`, -1)
}
-348
View File
@@ -1,348 +0,0 @@
package cli
import (
"flag"
"fmt"
"io/ioutil"
"reflect"
"runtime"
"strconv"
"strings"
"syscall"
)
const defaultPlaceholder = "value"
// BashCompletionFlag enables bash-completion for all commands and subcommands
var BashCompletionFlag Flag = BoolFlag{
Name: "generate-bash-completion",
Hidden: true,
}
// VersionFlag prints the version for the application
var VersionFlag Flag = BoolFlag{
Name: "version, v",
Usage: "print the version",
}
// HelpFlag prints the help for all commands and subcommands
// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand
// unless HideHelp is set to true)
var HelpFlag Flag = BoolFlag{
Name: "help, h",
Usage: "show help",
}
// FlagStringer converts a flag definition to a string. This is used by help
// to display a flag.
var FlagStringer FlagStringFunc = stringifyFlag
// FlagNamePrefixer converts a full flag name and its placeholder into the help
// message flag prefix. This is used by the default FlagStringer.
var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames
// FlagEnvHinter annotates flag help message with the environment variable
// details. This is used by the default FlagStringer.
var FlagEnvHinter FlagEnvHintFunc = withEnvHint
// FlagFileHinter annotates flag help message with the environment variable
// details. This is used by the default FlagStringer.
var FlagFileHinter FlagFileHintFunc = withFileHint
// FlagsByName is a slice of Flag.
type FlagsByName []Flag
func (f FlagsByName) Len() int {
return len(f)
}
func (f FlagsByName) Less(i, j int) bool {
return lexicographicLess(f[i].GetName(), f[j].GetName())
}
func (f FlagsByName) Swap(i, j int) {
f[i], f[j] = f[j], f[i]
}
// Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recommended that
// this interface be implemented.
type Flag interface {
fmt.Stringer
// Apply Flag settings to the given flag set
Apply(*flag.FlagSet)
GetName() string
}
// RequiredFlag is an interface that allows us to mark flags as required
// it allows flags required flags to be backwards compatible with the Flag interface
type RequiredFlag interface {
Flag
IsRequired() bool
}
// DocGenerationFlag is an interface that allows documentation generation for the flag
type DocGenerationFlag interface {
Flag
// TakesValue returns true if the flag takes a value, otherwise false
TakesValue() bool
// GetUsage returns the usage string for the flag
GetUsage() string
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
GetValue() string
}
// errorableFlag is an interface that allows us to return errors during apply
// it allows flags defined in this library to return errors in a fashion backwards compatible
// TODO remove in v2 and modify the existing Flag interface to return errors
type errorableFlag interface {
Flag
ApplyWithError(*flag.FlagSet) error
}
func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
set := flag.NewFlagSet(name, flag.ContinueOnError)
for _, f := range flags {
//TODO remove in v2 when errorableFlag is removed
if ef, ok := f.(errorableFlag); ok {
if err := ef.ApplyWithError(set); err != nil {
return nil, err
}
} else {
f.Apply(set)
}
}
set.SetOutput(ioutil.Discard)
return set, nil
}
func eachName(longName string, fn func(string)) {
parts := strings.Split(longName, ",")
for _, name := range parts {
name = strings.Trim(name, " ")
fn(name)
}
}
func visibleFlags(fl []Flag) []Flag {
var visible []Flag
for _, f := range fl {
field := flagValue(f).FieldByName("Hidden")
if !field.IsValid() || !field.Bool() {
visible = append(visible, f)
}
}
return visible
}
func prefixFor(name string) (prefix string) {
if len(name) == 1 {
prefix = "-"
} else {
prefix = "--"
}
return
}
// Returns the placeholder, if any, and the unquoted usage string.
func unquoteUsage(usage string) (string, string) {
for i := 0; i < len(usage); i++ {
if usage[i] == '`' {
for j := i + 1; j < len(usage); j++ {
if usage[j] == '`' {
name := usage[i+1 : j]
usage = usage[:i] + name + usage[j+1:]
return name, usage
}
}
break
}
}
return "", usage
}
func prefixedNames(fullName, placeholder string) string {
var prefixed string
parts := strings.Split(fullName, ",")
for i, name := range parts {
name = strings.Trim(name, " ")
prefixed += prefixFor(name) + name
if placeholder != "" {
prefixed += " " + placeholder
}
if i < len(parts)-1 {
prefixed += ", "
}
}
return prefixed
}
func withEnvHint(envVar, str string) string {
envText := ""
if envVar != "" {
prefix := "$"
suffix := ""
sep := ", $"
if runtime.GOOS == "windows" {
prefix = "%"
suffix = "%"
sep = "%, %"
}
envText = " [" + prefix + strings.Join(strings.Split(envVar, ","), sep) + suffix + "]"
}
return str + envText
}
func withFileHint(filePath, str string) string {
fileText := ""
if filePath != "" {
fileText = fmt.Sprintf(" [%s]", filePath)
}
return str + fileText
}
func flagValue(f Flag) reflect.Value {
fv := reflect.ValueOf(f)
for fv.Kind() == reflect.Ptr {
fv = reflect.Indirect(fv)
}
return fv
}
func stringifyFlag(f Flag) string {
fv := flagValue(f)
switch f.(type) {
case IntSliceFlag:
return FlagFileHinter(
fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
stringifyIntSliceFlag(f.(IntSliceFlag)),
),
)
case Int64SliceFlag:
return FlagFileHinter(
fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
stringifyInt64SliceFlag(f.(Int64SliceFlag)),
),
)
case StringSliceFlag:
return FlagFileHinter(
fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
stringifyStringSliceFlag(f.(StringSliceFlag)),
),
)
}
placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String())
needsPlaceholder := false
defaultValueString := ""
if val := fv.FieldByName("Value"); val.IsValid() {
needsPlaceholder = true
defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface())
if val.Kind() == reflect.String && val.String() != "" {
defaultValueString = fmt.Sprintf(" (default: %q)", val.String())
}
}
if defaultValueString == " (default: )" {
defaultValueString = ""
}
if needsPlaceholder && placeholder == "" {
placeholder = defaultPlaceholder
}
usageWithDefault := strings.TrimSpace(usage + defaultValueString)
return FlagFileHinter(
fv.FieldByName("FilePath").String(),
FlagEnvHinter(
fv.FieldByName("EnvVar").String(),
FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder)+"\t"+usageWithDefault,
),
)
}
func stringifyIntSliceFlag(f IntSliceFlag) string {
var defaultVals []string
if f.Value != nil && len(f.Value.Value()) > 0 {
for _, i := range f.Value.Value() {
defaultVals = append(defaultVals, strconv.Itoa(i))
}
}
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifyInt64SliceFlag(f Int64SliceFlag) string {
var defaultVals []string
if f.Value != nil && len(f.Value.Value()) > 0 {
for _, i := range f.Value.Value() {
defaultVals = append(defaultVals, strconv.FormatInt(i, 10))
}
}
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifyStringSliceFlag(f StringSliceFlag) string {
var defaultVals []string
if f.Value != nil && len(f.Value.Value()) > 0 {
for _, s := range f.Value.Value() {
if len(s) > 0 {
defaultVals = append(defaultVals, strconv.Quote(s))
}
}
}
return stringifySliceFlag(f.Usage, f.Name, defaultVals)
}
func stringifySliceFlag(usage, name string, defaultVals []string) string {
placeholder, usage := unquoteUsage(usage)
if placeholder == "" {
placeholder = defaultPlaceholder
}
defaultVal := ""
if len(defaultVals) > 0 {
defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", "))
}
usageWithDefault := strings.TrimSpace(usage + defaultVal)
return FlagNamePrefixer(name, placeholder) + "\t" + usageWithDefault
}
func flagFromFileEnv(filePath, envName string) (val string, ok bool) {
for _, envVar := range strings.Split(envName, ",") {
envVar = strings.TrimSpace(envVar)
if envVal, ok := syscall.Getenv(envVar); ok {
return envVal, true
}
}
for _, fileVar := range strings.Split(filePath, ",") {
if fileVar != "" {
if data, err := ioutil.ReadFile(fileVar); err == nil {
return string(data), true
}
}
}
return "", false
}
-109
View File
@@ -1,109 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
)
// BoolFlag is a flag with type bool
type BoolFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Destination *bool
}
// String returns a readable representation of this value
// (for usage defaults)
func (f BoolFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f BoolFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f BoolFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f BoolFlag) TakesValue() bool {
return false
}
// GetUsage returns the usage string for the flag
func (f BoolFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f BoolFlag) GetValue() string {
return ""
}
// Bool looks up the value of a local BoolFlag, returns
// false if not found
func (c *Context) Bool(name string) bool {
return lookupBool(name, c.flagSet)
}
// GlobalBool looks up the value of a global BoolFlag, returns
// false if not found
func (c *Context) GlobalBool(name string) bool {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupBool(name, fs)
}
return false
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f BoolFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error {
val := false
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
if envVal == "" {
val = false
} else {
envValBool, err := strconv.ParseBool(envVal)
if err != nil {
return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err)
}
val = envValBool
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.BoolVar(f.Destination, name, val, f.Usage)
return
}
set.Bool(name, val, f.Usage)
})
return nil
}
func lookupBool(name string, set *flag.FlagSet) bool {
f := set.Lookup(name)
if f != nil {
parsed, err := strconv.ParseBool(f.Value.String())
if err != nil {
return false
}
return parsed
}
return false
}
-110
View File
@@ -1,110 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
)
// BoolTFlag is a flag with type bool that is true by default
type BoolTFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Destination *bool
}
// String returns a readable representation of this value
// (for usage defaults)
func (f BoolTFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f BoolTFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f BoolTFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f BoolTFlag) TakesValue() bool {
return false
}
// GetUsage returns the usage string for the flag
func (f BoolTFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f BoolTFlag) GetValue() string {
return ""
}
// BoolT looks up the value of a local BoolTFlag, returns
// false if not found
func (c *Context) BoolT(name string) bool {
return lookupBoolT(name, c.flagSet)
}
// GlobalBoolT looks up the value of a global BoolTFlag, returns
// false if not found
func (c *Context) GlobalBoolT(name string) bool {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupBoolT(name, fs)
}
return false
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f BoolTFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
val := true
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
if envVal == "" {
val = false
} else {
envValBool, err := strconv.ParseBool(envVal)
if err != nil {
return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err)
}
val = envValBool
}
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.BoolVar(f.Destination, name, val, f.Usage)
return
}
set.Bool(name, val, f.Usage)
})
return nil
}
func lookupBoolT(name string, set *flag.FlagSet) bool {
f := set.Lookup(name)
if f != nil {
parsed, err := strconv.ParseBool(f.Value.String())
if err != nil {
return false
}
return parsed
}
return false
}
-106
View File
@@ -1,106 +0,0 @@
package cli
import (
"flag"
"fmt"
"time"
)
// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration)
type DurationFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value time.Duration
Destination *time.Duration
}
// String returns a readable representation of this value
// (for usage defaults)
func (f DurationFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f DurationFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f DurationFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f DurationFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f DurationFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f DurationFlag) GetValue() string {
return f.Value.String()
}
// Duration looks up the value of a local DurationFlag, returns
// 0 if not found
func (c *Context) Duration(name string) time.Duration {
return lookupDuration(name, c.flagSet)
}
// GlobalDuration looks up the value of a global DurationFlag, returns
// 0 if not found
func (c *Context) GlobalDuration(name string) time.Duration {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupDuration(name, fs)
}
return 0
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f DurationFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
envValDuration, err := time.ParseDuration(envVal)
if err != nil {
return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err)
}
f.Value = envValDuration
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.DurationVar(f.Destination, name, f.Value, f.Usage)
return
}
set.Duration(name, f.Value, f.Usage)
})
return nil
}
func lookupDuration(name string, set *flag.FlagSet) time.Duration {
f := set.Lookup(name)
if f != nil {
parsed, err := time.ParseDuration(f.Value.String())
if err != nil {
return 0
}
return parsed
}
return 0
}
-106
View File
@@ -1,106 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
)
// Float64Flag is a flag with type float64
type Float64Flag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value float64
Destination *float64
}
// String returns a readable representation of this value
// (for usage defaults)
func (f Float64Flag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f Float64Flag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f Float64Flag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f Float64Flag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f Float64Flag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f Float64Flag) GetValue() string {
return fmt.Sprintf("%f", f.Value)
}
// Float64 looks up the value of a local Float64Flag, returns
// 0 if not found
func (c *Context) Float64(name string) float64 {
return lookupFloat64(name, c.flagSet)
}
// GlobalFloat64 looks up the value of a global Float64Flag, returns
// 0 if not found
func (c *Context) GlobalFloat64(name string) float64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupFloat64(name, fs)
}
return 0
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f Float64Flag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
envValFloat, err := strconv.ParseFloat(envVal, 10)
if err != nil {
return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err)
}
f.Value = envValFloat
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Float64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Float64(name, f.Value, f.Usage)
})
return nil
}
func lookupFloat64(name string, set *flag.FlagSet) float64 {
f := set.Lookup(name)
if f != nil {
parsed, err := strconv.ParseFloat(f.Value.String(), 64)
if err != nil {
return 0
}
return parsed
}
return 0
}
-110
View File
@@ -1,110 +0,0 @@
package cli
import (
"flag"
"fmt"
)
// Generic is a generic parseable type identified by a specific flag
type Generic interface {
Set(value string) error
String() string
}
// GenericFlag is a flag with type Generic
type GenericFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
TakesFile bool
Value Generic
}
// String returns a readable representation of this value
// (for usage defaults)
func (f GenericFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f GenericFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f GenericFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f GenericFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f GenericFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f GenericFlag) GetValue() string {
if f.Value != nil {
return f.Value.String()
}
return ""
}
// Apply takes the flagset and calls Set on the generic flag with the value
// provided by the user for parsing by the flag
// Ignores parsing errors
func (f GenericFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError takes the flagset and calls Set on the generic flag with the value
// provided by the user for parsing by the flag
func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error {
val := f.Value
if fileEnvVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
if err := val.Set(fileEnvVal); err != nil {
return fmt.Errorf("could not parse %s as value for flag %s: %s", fileEnvVal, f.Name, err)
}
}
eachName(f.Name, func(name string) {
set.Var(f.Value, name, f.Usage)
})
return nil
}
// Generic looks up the value of a local GenericFlag, returns
// nil if not found
func (c *Context) Generic(name string) interface{} {
return lookupGeneric(name, c.flagSet)
}
// GlobalGeneric looks up the value of a global GenericFlag, returns
// nil if not found
func (c *Context) GlobalGeneric(name string) interface{} {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupGeneric(name, fs)
}
return nil
}
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
f := set.Lookup(name)
if f != nil {
parsed, err := f.Value, error(nil)
if err != nil {
return nil
}
return parsed
}
return nil
}
-105
View File
@@ -1,105 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
)
// IntFlag is a flag with type int
type IntFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value int
Destination *int
}
// String returns a readable representation of this value
// (for usage defaults)
func (f IntFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f IntFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f IntFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f IntFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f IntFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f IntFlag) GetValue() string {
return fmt.Sprintf("%d", f.Value)
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f IntFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f IntFlag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err != nil {
return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
}
f.Value = int(envValInt)
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.IntVar(f.Destination, name, f.Value, f.Usage)
return
}
set.Int(name, f.Value, f.Usage)
})
return nil
}
// Int looks up the value of a local IntFlag, returns
// 0 if not found
func (c *Context) Int(name string) int {
return lookupInt(name, c.flagSet)
}
// GlobalInt looks up the value of a global IntFlag, returns
// 0 if not found
func (c *Context) GlobalInt(name string) int {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt(name, fs)
}
return 0
}
func lookupInt(name string, set *flag.FlagSet) int {
f := set.Lookup(name)
if f != nil {
parsed, err := strconv.ParseInt(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return int(parsed)
}
return 0
}
-106
View File
@@ -1,106 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
)
// Int64Flag is a flag with type int64
type Int64Flag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value int64
Destination *int64
}
// String returns a readable representation of this value
// (for usage defaults)
func (f Int64Flag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f Int64Flag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f Int64Flag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f Int64Flag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f Int64Flag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f Int64Flag) GetValue() string {
return fmt.Sprintf("%d", f.Value)
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f Int64Flag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
envValInt, err := strconv.ParseInt(envVal, 0, 64)
if err != nil {
return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err)
}
f.Value = envValInt
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Int64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Int64(name, f.Value, f.Usage)
})
return nil
}
// Int64 looks up the value of a local Int64Flag, returns
// 0 if not found
func (c *Context) Int64(name string) int64 {
return lookupInt64(name, c.flagSet)
}
// GlobalInt64 looks up the value of a global Int64Flag, returns
// 0 if not found
func (c *Context) GlobalInt64(name string) int64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt64(name, fs)
}
return 0
}
func lookupInt64(name string, set *flag.FlagSet) int64 {
f := set.Lookup(name)
if f != nil {
parsed, err := strconv.ParseInt(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return parsed
}
return 0
}
-199
View File
@@ -1,199 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
"strings"
)
// Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter
type Int64Slice []int64
// Set parses the value into an integer and appends it to the list of values
func (f *Int64Slice) Set(value string) error {
tmp, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
*f = append(*f, tmp)
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (f *Int64Slice) String() string {
slice := make([]string, len(*f))
for i, v := range *f {
slice[i] = strconv.FormatInt(v, 10)
}
return strings.Join(slice, ",")
}
// Value returns the slice of ints set by this flag
func (f *Int64Slice) Value() []int64 {
return *f
}
// Get returns the slice of ints set by this flag
func (f *Int64Slice) Get() interface{} {
return *f
}
// Int64SliceFlag is a flag with type *Int64Slice
type Int64SliceFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value *Int64Slice
}
// String returns a readable representation of this value
// (for usage defaults)
func (f Int64SliceFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f Int64SliceFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f Int64SliceFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f Int64SliceFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f Int64SliceFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f Int64SliceFlag) GetValue() string {
if f.Value != nil {
return f.Value.String()
}
return ""
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f Int64SliceFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
newVal := &Int64Slice{}
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
if err := newVal.Set(s); err != nil {
return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err)
}
}
if f.Value == nil {
f.Value = newVal
} else {
*f.Value = *newVal
}
}
eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &Int64Slice{}
}
set.Var(f.Value, name, f.Usage)
})
return nil
}
// Int64Slice looks up the value of a local Int64SliceFlag, returns
// nil if not found
func (c *Context) Int64Slice(name string) []int64 {
return lookupInt64Slice(name, c.flagSet)
}
// GlobalInt64Slice looks up the value of a global Int64SliceFlag, returns
// nil if not found
func (c *Context) GlobalInt64Slice(name string) []int64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupInt64Slice(name, fs)
}
return nil
}
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
f := set.Lookup(name)
if f != nil {
value, ok := f.Value.(*Int64Slice)
if !ok {
return nil
}
// extract the slice from asserted value
parsed := value.Value()
// extract default value from the flag
var defaultVal []int64
for _, v := range strings.Split(f.DefValue, ",") {
if v != "" {
int64Value, err := strconv.ParseInt(v, 10, 64)
if err != nil {
panic(err)
}
defaultVal = append(defaultVal, int64Value)
}
}
// if the current value is not equal to the default value
// remove the default values from the flag
if !isInt64SliceEqual(parsed, defaultVal) {
for _, v := range defaultVal {
parsed = removeFromInt64Slice(parsed, v)
}
}
return parsed
}
return nil
}
func removeFromInt64Slice(slice []int64, val int64) []int64 {
for i, v := range slice {
if v == val {
ret := append([]int64{}, slice[:i]...)
ret = append(ret, slice[i+1:]...)
return ret
}
}
return slice
}
func isInt64SliceEqual(newValue, defaultValue []int64) bool {
// If one is nil, the other must also be nil.
if (newValue == nil) != (defaultValue == nil) {
return false
}
if len(newValue) != len(defaultValue) {
return false
}
for i, v := range newValue {
if v != defaultValue[i] {
return false
}
}
return true
}
-198
View File
@@ -1,198 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
"strings"
)
// IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter
type IntSlice []int
// Set parses the value into an integer and appends it to the list of values
func (f *IntSlice) Set(value string) error {
tmp, err := strconv.Atoi(value)
if err != nil {
return err
}
*f = append(*f, tmp)
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (f *IntSlice) String() string {
slice := make([]string, len(*f))
for i, v := range *f {
slice[i] = strconv.Itoa(v)
}
return strings.Join(slice, ",")
}
// Value returns the slice of ints set by this flag
func (f *IntSlice) Value() []int {
return *f
}
// Get returns the slice of ints set by this flag
func (f *IntSlice) Get() interface{} {
return *f
}
// IntSliceFlag is a flag with type *IntSlice
type IntSliceFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value *IntSlice
}
// String returns a readable representation of this value
// (for usage defaults)
func (f IntSliceFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f IntSliceFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f IntSliceFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f IntSliceFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f IntSliceFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f IntSliceFlag) GetValue() string {
if f.Value != nil {
return f.Value.String()
}
return ""
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
newVal := &IntSlice{}
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
if err := newVal.Set(s); err != nil {
return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err)
}
}
if f.Value == nil {
f.Value = newVal
} else {
*f.Value = *newVal
}
}
eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &IntSlice{}
}
set.Var(f.Value, name, f.Usage)
})
return nil
}
// IntSlice looks up the value of a local IntSliceFlag, returns
// nil if not found
func (c *Context) IntSlice(name string) []int {
return lookupIntSlice(name, c.flagSet)
}
// GlobalIntSlice looks up the value of a global IntSliceFlag, returns
// nil if not found
func (c *Context) GlobalIntSlice(name string) []int {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupIntSlice(name, fs)
}
return nil
}
func lookupIntSlice(name string, set *flag.FlagSet) []int {
f := set.Lookup(name)
if f != nil {
value, ok := f.Value.(*IntSlice)
if !ok {
return nil
}
// extract the slice from asserted value
slice := value.Value()
// extract default value from the flag
var defaultVal []int
for _, v := range strings.Split(f.DefValue, ",") {
if v != "" {
intValue, err := strconv.Atoi(v)
if err != nil {
panic(err)
}
defaultVal = append(defaultVal, intValue)
}
}
// if the current value is not equal to the default value
// remove the default values from the flag
if !isIntSliceEqual(slice, defaultVal) {
for _, v := range defaultVal {
slice = removeFromIntSlice(slice, v)
}
}
return slice
}
return nil
}
func removeFromIntSlice(slice []int, val int) []int {
for i, v := range slice {
if v == val {
ret := append([]int{}, slice[:i]...)
ret = append(ret, slice[i+1:]...)
return ret
}
}
return slice
}
func isIntSliceEqual(newValue, defaultValue []int) bool {
// If one is nil, the other must also be nil.
if (newValue == nil) != (defaultValue == nil) {
return false
}
if len(newValue) != len(defaultValue) {
return false
}
for i, v := range newValue {
if v != defaultValue[i] {
return false
}
}
return true
}
-98
View File
@@ -1,98 +0,0 @@
package cli
import "flag"
// StringFlag is a flag with type string
type StringFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
TakesFile bool
Value string
Destination *string
}
// String returns a readable representation of this value
// (for usage defaults)
func (f StringFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f StringFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f StringFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f StringFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f StringFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f StringFlag) GetValue() string {
return f.Value
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f StringFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f StringFlag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
f.Value = envVal
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.StringVar(f.Destination, name, f.Value, f.Usage)
return
}
set.String(name, f.Value, f.Usage)
})
return nil
}
// String looks up the value of a local StringFlag, returns
// "" if not found
func (c *Context) String(name string) string {
return lookupString(name, c.flagSet)
}
// GlobalString looks up the value of a global StringFlag, returns
// "" if not found
func (c *Context) GlobalString(name string) string {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupString(name, fs)
}
return ""
}
func lookupString(name string, set *flag.FlagSet) string {
f := set.Lookup(name)
if f != nil {
parsed, err := f.Value.String(), error(nil)
if err != nil {
return ""
}
return parsed
}
return ""
}
-184
View File
@@ -1,184 +0,0 @@
package cli
import (
"flag"
"fmt"
"strings"
)
// StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter
type StringSlice []string
// Set appends the string value to the list of values
func (f *StringSlice) Set(value string) error {
*f = append(*f, value)
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (f *StringSlice) String() string {
return strings.Join(*f, ",")
}
// Value returns the slice of strings set by this flag
func (f *StringSlice) Value() []string {
return *f
}
// Get returns the slice of strings set by this flag
func (f *StringSlice) Get() interface{} {
return *f
}
// StringSliceFlag is a flag with type *StringSlice
type StringSliceFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
TakesFile bool
Value *StringSlice
}
// String returns a readable representation of this value
// (for usage defaults)
func (f StringSliceFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f StringSliceFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f StringSliceFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f StringSliceFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f StringSliceFlag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f StringSliceFlag) GetValue() string {
if f.Value != nil {
return f.Value.String()
}
return ""
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
newVal := &StringSlice{}
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
if err := newVal.Set(s); err != nil {
return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err)
}
}
if f.Value == nil {
f.Value = newVal
} else {
*f.Value = *newVal
}
}
eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &StringSlice{}
}
set.Var(f.Value, name, f.Usage)
})
return nil
}
// StringSlice looks up the value of a local StringSliceFlag, returns
// nil if not found
func (c *Context) StringSlice(name string) []string {
return lookupStringSlice(name, c.flagSet)
}
// GlobalStringSlice looks up the value of a global StringSliceFlag, returns
// nil if not found
func (c *Context) GlobalStringSlice(name string) []string {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupStringSlice(name, fs)
}
return nil
}
func lookupStringSlice(name string, set *flag.FlagSet) []string {
f := set.Lookup(name)
if f != nil {
value, ok := f.Value.(*StringSlice)
if !ok {
return nil
}
// extract the slice from asserted value
slice := value.Value()
// extract default value from the flag
var defaultVal []string
for _, v := range strings.Split(f.DefValue, ",") {
defaultVal = append(defaultVal, v)
}
// if the current value is not equal to the default value
// remove the default values from the flag
if !isStringSliceEqual(slice, defaultVal) {
for _, v := range defaultVal {
slice = removeFromStringSlice(slice, v)
}
}
return slice
}
return nil
}
func removeFromStringSlice(slice []string, val string) []string {
for i, v := range slice {
if v == val {
ret := append([]string{}, slice[:i]...)
ret = append(ret, slice[i+1:]...)
return ret
}
}
return slice
}
func isStringSliceEqual(newValue, defaultValue []string) bool {
// If one is nil, the other must also be nil.
if (newValue == nil) != (defaultValue == nil) {
return false
}
if len(newValue) != len(defaultValue) {
return false
}
for i, v := range newValue {
if v != defaultValue[i] {
return false
}
}
return true
}
-106
View File
@@ -1,106 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
)
// UintFlag is a flag with type uint
type UintFlag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value uint
Destination *uint
}
// String returns a readable representation of this value
// (for usage defaults)
func (f UintFlag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f UintFlag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f UintFlag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f UintFlag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f UintFlag) GetUsage() string {
return f.Usage
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f UintFlag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f UintFlag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err != nil {
return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err)
}
f.Value = uint(envValInt)
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.UintVar(f.Destination, name, f.Value, f.Usage)
return
}
set.Uint(name, f.Value, f.Usage)
})
return nil
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f UintFlag) GetValue() string {
return fmt.Sprintf("%d", f.Value)
}
// Uint looks up the value of a local UintFlag, returns
// 0 if not found
func (c *Context) Uint(name string) uint {
return lookupUint(name, c.flagSet)
}
// GlobalUint looks up the value of a global UintFlag, returns
// 0 if not found
func (c *Context) GlobalUint(name string) uint {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupUint(name, fs)
}
return 0
}
func lookupUint(name string, set *flag.FlagSet) uint {
f := set.Lookup(name)
if f != nil {
parsed, err := strconv.ParseUint(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return uint(parsed)
}
return 0
}
-106
View File
@@ -1,106 +0,0 @@
package cli
import (
"flag"
"fmt"
"strconv"
)
// Uint64Flag is a flag with type uint64
type Uint64Flag struct {
Name string
Usage string
EnvVar string
FilePath string
Required bool
Hidden bool
Value uint64
Destination *uint64
}
// String returns a readable representation of this value
// (for usage defaults)
func (f Uint64Flag) String() string {
return FlagStringer(f)
}
// GetName returns the name of the flag
func (f Uint64Flag) GetName() string {
return f.Name
}
// IsRequired returns whether or not the flag is required
func (f Uint64Flag) IsRequired() bool {
return f.Required
}
// TakesValue returns true of the flag takes a value, otherwise false
func (f Uint64Flag) TakesValue() bool {
return true
}
// GetUsage returns the usage string for the flag
func (f Uint64Flag) GetUsage() string {
return f.Usage
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f Uint64Flag) GetValue() string {
return fmt.Sprintf("%d", f.Value)
}
// Apply populates the flag given the flag set and environment
// Ignores errors
func (f Uint64Flag) Apply(set *flag.FlagSet) {
_ = f.ApplyWithError(set)
}
// ApplyWithError populates the flag given the flag set and environment
func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok {
envValInt, err := strconv.ParseUint(envVal, 0, 64)
if err != nil {
return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err)
}
f.Value = envValInt
}
eachName(f.Name, func(name string) {
if f.Destination != nil {
set.Uint64Var(f.Destination, name, f.Value, f.Usage)
return
}
set.Uint64(name, f.Value, f.Usage)
})
return nil
}
// Uint64 looks up the value of a local Uint64Flag, returns
// 0 if not found
func (c *Context) Uint64(name string) uint64 {
return lookupUint64(name, c.flagSet)
}
// GlobalUint64 looks up the value of a global Uint64Flag, returns
// 0 if not found
func (c *Context) GlobalUint64(name string) uint64 {
if fs := lookupGlobalFlagSet(name, c); fs != nil {
return lookupUint64(name, fs)
}
return 0
}
func lookupUint64(name string, set *flag.FlagSet) uint64 {
f := set.Lookup(name)
if f != nil {
parsed, err := strconv.ParseUint(f.Value.String(), 0, 64)
if err != nil {
return 0
}
return parsed
}
return 0
}
-44
View File
@@ -1,44 +0,0 @@
package cli
// BashCompleteFunc is an action to execute when the bash-completion flag is set
type BashCompleteFunc func(*Context)
// BeforeFunc is an action to execute before any subcommands are run, but after
// the context is ready if a non-nil error is returned, no subcommands are run
type BeforeFunc func(*Context) error
// AfterFunc is an action to execute after any subcommands are run, but after the
// subcommand has finished it is run even if Action() panics
type AfterFunc func(*Context) error
// ActionFunc is the action to execute when no subcommands are specified
type ActionFunc func(*Context) error
// CommandNotFoundFunc is executed if the proper command cannot be found
type CommandNotFoundFunc func(*Context, string)
// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying
// customized usage error messages. This function is able to replace the
// original error messages. If this function is not set, the "Incorrect usage"
// is displayed and the execution is interrupted.
type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error
// ExitErrHandlerFunc is executed if provided in order to handle ExitError values
// returned by Actions and Before/After functions.
type ExitErrHandlerFunc func(context *Context, err error)
// FlagStringFunc is used by the help generation to display a flag, which is
// expected to be a single line.
type FlagStringFunc func(Flag) string
// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix
// text for a flag's full name.
type FlagNamePrefixFunc func(fullName, placeholder string) string
// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help
// with the environment variable details.
type FlagEnvHintFunc func(envVar, str string) string
// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help
// with the file path details.
type FlagFileHintFunc func(filePath, str string) string
-363
View File
@@ -1,363 +0,0 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"text/template"
"unicode/utf8"
)
var helpCommand = Command{
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(c *Context) error {
args := c.Args()
if args.Present() {
return ShowCommandHelp(c, args.First())
}
_ = ShowAppHelp(c)
return nil
},
}
var helpSubcommand = Command{
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(c *Context) error {
args := c.Args()
if args.Present() {
return ShowCommandHelp(c, args.First())
}
return ShowSubcommandHelp(c)
},
}
// Prints help for the App or Command
type helpPrinter func(w io.Writer, templ string, data interface{})
// Prints help for the App or Command with custom template function.
type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{})
// HelpPrinter is a function that writes the help output. If not set explicitly,
// this calls HelpPrinterCustom using only the default template functions.
//
// If custom logic for printing help is required, this function can be
// overridden. If the ExtraInfo field is defined on an App, this function
// should not be modified, as HelpPrinterCustom will be used directly in order
// to capture the extra information.
var HelpPrinter helpPrinter = printHelp
// HelpPrinterCustom is a function that writes the help output. It is used as
// the default implementation of HelpPrinter, and may be called directly if
// the ExtraInfo field is set on an App.
var HelpPrinterCustom helpPrinterCustom = printHelpCustom
// VersionPrinter prints the version for the App
var VersionPrinter = printVersion
// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code.
func ShowAppHelpAndExit(c *Context, exitCode int) {
_ = ShowAppHelp(c)
os.Exit(exitCode)
}
// ShowAppHelp is an action that displays the help.
func ShowAppHelp(c *Context) error {
template := c.App.CustomAppHelpTemplate
if template == "" {
template = AppHelpTemplate
}
if c.App.ExtraInfo == nil {
HelpPrinter(c.App.Writer, template, c.App)
return nil
}
customAppData := func() map[string]interface{} {
return map[string]interface{}{
"ExtraInfo": c.App.ExtraInfo,
}
}
HelpPrinterCustom(c.App.Writer, template, c.App, customAppData())
return nil
}
// DefaultAppComplete prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) {
DefaultCompleteWithFlags(nil)(c)
}
func printCommandSuggestions(commands []Command, writer io.Writer) {
for _, command := range commands {
if command.Hidden {
continue
}
if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" {
for _, name := range command.Names() {
_, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage)
}
} else {
for _, name := range command.Names() {
_, _ = fmt.Fprintf(writer, "%s\n", name)
}
}
}
}
func cliArgContains(flagName string) bool {
for _, name := range strings.Split(flagName, ",") {
name = strings.TrimSpace(name)
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2
}
flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
for _, a := range os.Args {
if a == flag {
return true
}
}
}
return false
}
func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
cur := strings.TrimPrefix(lastArg, "-")
cur = strings.TrimPrefix(cur, "-")
for _, flag := range flags {
if bflag, ok := flag.(BoolFlag); ok && bflag.Hidden {
continue
}
for _, name := range strings.Split(flag.GetName(), ",") {
name = strings.TrimSpace(name)
// this will get total count utf8 letters in flag name
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2 // resuse this count to generate single - or -- in flag completion
}
// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
// skip flag completion for short flags example -v or -x
if strings.HasPrefix(lastArg, "--") && count == 1 {
continue
}
// match if last argument matches this flag and it is not repeated
if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(flag.GetName()) {
flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
_, _ = fmt.Fprintln(writer, flagCompletion)
}
}
}
}
func DefaultCompleteWithFlags(cmd *Command) func(c *Context) {
return func(c *Context) {
if len(os.Args) > 2 {
lastArg := os.Args[len(os.Args)-2]
if strings.HasPrefix(lastArg, "-") {
printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer)
if cmd != nil {
printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer)
}
return
}
}
if cmd != nil {
printCommandSuggestions(cmd.Subcommands, c.App.Writer)
} else {
printCommandSuggestions(c.App.Commands, c.App.Writer)
}
}
}
// ShowCommandHelpAndExit - exits with code after showing help
func ShowCommandHelpAndExit(c *Context, command string, code int) {
_ = ShowCommandHelp(c, command)
os.Exit(code)
}
// ShowCommandHelp prints help for the given command
func ShowCommandHelp(ctx *Context, command string) error {
// show the subcommand help for a command with subcommands
if command == "" {
HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App)
return nil
}
for _, c := range ctx.App.Commands {
if c.HasName(command) {
templ := c.CustomHelpTemplate
if templ == "" {
templ = CommandHelpTemplate
}
HelpPrinter(ctx.App.Writer, templ, c)
return nil
}
}
if ctx.App.CommandNotFound == nil {
return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3)
}
ctx.App.CommandNotFound(ctx, command)
return nil
}
// ShowSubcommandHelp prints help for the given subcommand
func ShowSubcommandHelp(c *Context) error {
return ShowCommandHelp(c, c.Command.Name)
}
// ShowVersion prints the version number of the App
func ShowVersion(c *Context) {
VersionPrinter(c)
}
func printVersion(c *Context) {
_, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version)
}
// ShowCompletions prints the lists of commands within a given context
func ShowCompletions(c *Context) {
a := c.App
if a != nil && a.BashComplete != nil {
a.BashComplete(c)
}
}
// ShowCommandCompletions prints the custom completions for a given command
func ShowCommandCompletions(ctx *Context, command string) {
c := ctx.App.Command(command)
if c != nil {
if c.BashComplete != nil {
c.BashComplete(ctx)
} else {
DefaultCompleteWithFlags(c)(ctx)
}
}
}
// printHelpCustom is the default implementation of HelpPrinterCustom.
//
// The customFuncs map will be combined with a default template.FuncMap to
// allow using arbitrary functions in template rendering.
func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
funcMap := template.FuncMap{
"join": strings.Join,
}
for key, value := range customFuncs {
funcMap[key] = value
}
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
err := t.Execute(w, data)
if err != nil {
// If the writer is closed, t.Execute will fail, and there's nothing
// we can do to recover.
if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
_, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
}
return
}
_ = w.Flush()
}
func printHelp(out io.Writer, templ string, data interface{}) {
HelpPrinterCustom(out, templ, data, nil)
}
func checkVersion(c *Context) bool {
found := false
if VersionFlag.GetName() != "" {
eachName(VersionFlag.GetName(), func(name string) {
if c.GlobalBool(name) || c.Bool(name) {
found = true
}
})
}
return found
}
func checkHelp(c *Context) bool {
found := false
if HelpFlag.GetName() != "" {
eachName(HelpFlag.GetName(), func(name string) {
if c.GlobalBool(name) || c.Bool(name) {
found = true
}
})
}
return found
}
func checkCommandHelp(c *Context, name string) bool {
if c.Bool("h") || c.Bool("help") {
_ = ShowCommandHelp(c, name)
return true
}
return false
}
func checkSubcommandHelp(c *Context) bool {
if c.Bool("h") || c.Bool("help") {
_ = ShowSubcommandHelp(c)
return true
}
return false
}
func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) {
if !a.EnableBashCompletion {
return false, arguments
}
pos := len(arguments) - 1
lastArg := arguments[pos]
if lastArg != "--"+BashCompletionFlag.GetName() {
return false, arguments
}
return true, arguments[:pos]
}
func checkCompletions(c *Context) bool {
if !c.shellComplete {
return false
}
if args := c.Args(); args.Present() {
name := args.First()
if cmd := c.App.Command(name); cmd != nil {
// let the command handle the completion
return false
}
}
ShowCompletions(c)
return true
}
func checkCommandCompletions(c *Context, name string) bool {
if !c.shellComplete {
return false
}
ShowCommandCompletions(c, name)
return true
}
-94
View File
@@ -1,94 +0,0 @@
package cli
import (
"flag"
"strings"
)
type iterativeParser interface {
newFlagSet() (*flag.FlagSet, error)
useShortOptionHandling() bool
}
// To enable short-option handling (e.g., "-it" vs "-i -t") we have to
// iteratively catch parsing errors. This way we achieve LR parsing without
// transforming any arguments. Otherwise, there is no way we can discriminate
// combined short options from common arguments that should be left untouched.
// Pass `shellComplete` to continue parsing options on failure during shell
// completion when, the user-supplied options may be incomplete.
func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComplete bool) error {
for {
err := set.Parse(args)
if !ip.useShortOptionHandling() || err == nil {
if shellComplete {
return nil
}
return err
}
errStr := err.Error()
trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: -")
if errStr == trimmed {
return err
}
// regenerate the initial args with the split short opts
argsWereSplit := false
for i, arg := range args {
// skip args that are not part of the error message
if name := strings.TrimLeft(arg, "-"); name != trimmed {
continue
}
// if we can't split, the error was accurate
shortOpts := splitShortOptions(set, arg)
if len(shortOpts) == 1 {
return err
}
// swap current argument with the split version
args = append(args[:i], append(shortOpts, args[i+1:]...)...)
argsWereSplit = true
break
}
// This should be an impossible to reach code path, but in case the arg
// splitting failed to happen, this will prevent infinite loops
if !argsWereSplit {
return err
}
// Since custom parsing failed, replace the flag set before retrying
newSet, err := ip.newFlagSet()
if err != nil {
return err
}
*set = *newSet
}
}
func splitShortOptions(set *flag.FlagSet, arg string) []string {
shortFlagsExist := func(s string) bool {
for _, c := range s[1:] {
if f := set.Lookup(string(c)); f == nil {
return false
}
}
return true
}
if !isSplittable(arg) || !shortFlagsExist(arg) {
return []string{arg}
}
separated := make([]string, 0, len(arg)-1)
for _, flagChar := range arg[1:] {
separated = append(separated, "-"+string(flagChar))
}
return separated
}
func isSplittable(flagArg string) bool {
return strings.HasPrefix(flagArg, "-") && !strings.HasPrefix(flagArg, "--") && len(flagArg) > 2
}
-121
View File
@@ -1,121 +0,0 @@
package cli
// AppHelpTemplate is the text template for the Default help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var AppHelpTemplate = `NAME:
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
VERSION:
{{.Version}}{{end}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if len .Authors}}
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
{{range $index, $author := .Authors}}{{if $index}}
{{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}}{{end}}{{end}}{{if .Copyright}}
COPYRIGHT:
{{.Copyright}}{{end}}
`
// CommandHelpTemplate is the text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
// SubcommandHelpTemplate is the text template for the subcommand help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var SubcommandHelpTemplate = `NAME:
{{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
var MarkdownDocTemplate = `% {{ .App.Name }}(8) {{ .App.Description }}
% {{ .App.Author }}
# NAME
{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }}
# SYNOPSIS
{{ .App.Name }}
{{ if .SynopsisArgs }}
` + "```" + `
{{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + `
{{ end }}{{ if .App.UsageText }}
# DESCRIPTION
{{ .App.UsageText }}
{{ end }}
**Usage**:
` + "```" + `
{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
` + "```" + `
{{ if .GlobalArgs }}
# GLOBAL OPTIONS
{{ range $v := .GlobalArgs }}
{{ $v }}{{ end }}
{{ end }}{{ if .Commands }}
# COMMANDS
{{ range $v := .Commands }}
{{ $v }}{{ end }}{{ end }}`
var FishCompletionTemplate = `# {{ .App.Name }} fish shell completion
function __fish_{{ .App.Name }}_no_subcommand --description 'Test if there has been any subcommand yet'
for i in (commandline -opc)
if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }}
return 1
end
end
return 0
end
{{ range $v := .Completions }}{{ $v }}
{{ end }}`
@@ -1,10 +1,11 @@
*.coverprofile
coverage.txt
node_modules/
vendor
*.exe
*.orig
.*envrc
.envrc
.idea
/.local/
/internal/
/site/
package.json
package-lock.json
coverage.txt
examples/*/built-example
vendor
+13
View File
@@ -0,0 +1,13 @@
version: "2"
formatters:
enable:
- gofumpt
linters:
enable:
- makezero
- misspell
exclusions:
presets:
- std-error-handling
@@ -55,11 +55,12 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be
reviewed and investigated and will result in a response that is deemed necessary
and appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
reported by contacting urfave-governance@googlegroups.com, a members-only group
that is world-postable. All complaints will be reviewed and investigated and
will result in a response that is deemed necessary and appropriate to the
circumstances. The project team is obligated to maintain confidentiality with
regard to the reporter of an incident. Further details of specific enforcement
policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Jeremy Saenz & Contributors
Copyright (c) 2023 urfave/cli maintainers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+26
View File
@@ -0,0 +1,26 @@
# NOTE: this Makefile is meant to provide a simplified entry point for humans to
# run all of the critical steps to verify one's changes are harmonious in
# nature. Keeping target bodies to one line each and abstaining from make magic
# are very important so that maintainers and contributors can focus their
# attention on files that are primarily Go.
GO_RUN_BUILD := go run scripts/build.go
.PHONY: all
all: generate vet test check-binary-size gfmrun
# NOTE: this is a special catch-all rule to run any of the commands
# defined in scripts/build.go with optional arguments passed
# via GFLAGS (global flags) and FLAGS (command-specific flags), e.g.:
#
# $ make test GFLAGS='--packages cli'
%:
$(GO_RUN_BUILD) $(GFLAGS) $* $(FLAGS)
.PHONY: docs
docs:
mkdocs build
.PHONY: serve-docs
serve-docs:
mkdocs serve
+56
View File
@@ -0,0 +1,56 @@
# Welcome to urfave/cli
[![Go Reference][goreference_badge]][goreference_link]
[![Go Report Card][goreportcard_badge]][goreportcard_link]
[![codecov][codecov_badge]][codecov_link]
[![Tests status][test_badge]][test_link]
urfave/cli is a **declarative**, simple, fast, and fun package for building
command line tools in Go featuring:
- commands and subcommands with alias and prefix match support
- flexible and permissive help system
- dynamic shell completion for `bash`, `zsh`, `fish`, and `powershell`
- no dependencies except Go standard library
- input flags for simple types, slices of simple types, time, duration, and
others
- compound short flag support (`-a` `-b` `-c` can be shortened to `-abc`)
- documentation generation in `man` and Markdown (supported via the
[`urfave/cli-docs`][urfave/cli-docs] module)
- input lookup from:
- environment variables
- plain text files
- structured file formats (supported via the
[`urfave/cli-altsrc`][urfave/cli-altsrc] module)
## Documentation
See the hosted documentation website at <https://cli.urfave.org>. Contents of
this website are built from the [`./docs`](./docs) directory.
## Support
Check the [Q&A discussions]. If you don't find answer to your question, [create
a new discussion].
If you found a bug or have a feature request, [create a new issue].
Please keep in mind that this project is run by unpaid volunteers.
### License
See [`LICENSE`](./LICENSE).
[test_badge]: https://github.com/urfave/cli/actions/workflows/test.yml/badge.svg
[test_link]: https://github.com/urfave/cli/actions/workflows/test.yml
[goreference_badge]: https://pkg.go.dev/badge/github.com/urfave/cli/v3.svg
[goreference_link]: https://pkg.go.dev/github.com/urfave/cli/v3
[goreportcard_badge]: https://goreportcard.com/badge/github.com/urfave/cli/v3
[goreportcard_link]: https://goreportcard.com/report/github.com/urfave/cli/v3
[codecov_badge]: https://codecov.io/gh/urfave/cli/branch/main/graph/badge.svg?token=t9YGWLh05g
[codecov_link]: https://codecov.io/gh/urfave/cli
[Q&A discussions]: https://github.com/urfave/cli/discussions/categories/q-a
[create a new discussion]: https://github.com/urfave/cli/discussions/new?category=q-a
[urfave/cli-docs]: https://github.com/urfave/cli-docs
[urfave/cli-altsrc]: https://github.com/urfave/cli-altsrc
[create a new issue]: https://github.com/urfave/cli/issues/new/choose
+400
View File
@@ -0,0 +1,400 @@
package cli
import (
"fmt"
"time"
)
type Args interface {
// Get returns the nth argument, or else a blank string
Get(n int) string
// First returns the first argument, or else a blank string
First() string
// Tail returns the rest of the arguments (not the first one)
// or else an empty string slice
Tail() []string
// Len returns the length of the wrapped slice
Len() int
// Present checks if there are any arguments present
Present() bool
// Slice returns a copy of the internal slice
Slice() []string
}
type stringSliceArgs struct {
v []string
}
func (a *stringSliceArgs) Get(n int) string {
if len(a.v) > n {
return a.v[n]
}
return ""
}
func (a *stringSliceArgs) First() string {
return a.Get(0)
}
func (a *stringSliceArgs) Tail() []string {
if a.Len() >= 2 {
tail := a.v[1:]
ret := make([]string, len(tail))
copy(ret, tail)
return ret
}
return []string{}
}
func (a *stringSliceArgs) Len() int {
return len(a.v)
}
func (a *stringSliceArgs) Present() bool {
return a.Len() != 0
}
func (a *stringSliceArgs) Slice() []string {
ret := make([]string, len(a.v))
copy(ret, a.v)
return ret
}
// Argument captures a positional argument that can
// be parsed
type Argument interface {
// which this argument can be accessed using the given name
HasName(string) bool
// Parse the given args and return unparsed args and/or error
Parse([]string) ([]string, error)
// The usage template for this argument to use in help
Usage() string
// The Value of this Arg
Get() any
}
// AnyArguments to differentiate between no arguments(nil) vs aleast one
var AnyArguments = []Argument{
&StringArgs{
Max: -1,
},
}
type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct {
Name string `json:"name"` // the name of this argument
Value T `json:"value"` // the default value of this argument
Destination *T `json:"-"` // the destination point for this argument
UsageText string `json:"usageText"` // the usage text to show
Config C `json:"config"` // config for this argument similar to Flag Config
value *T
}
func (a *ArgumentBase[T, C, VC]) HasName(s string) bool {
return s == a.Name
}
func (a *ArgumentBase[T, C, VC]) Usage() string {
if a.UsageText != "" {
return a.UsageText
}
usageFormat := "%[1]s"
return fmt.Sprintf(usageFormat, a.Name)
}
func (a *ArgumentBase[T, C, VC]) Parse(s []string) ([]string, error) {
tracef("calling arg%[1] parse with args %[2]", a.Name, s)
var vc VC
var t T
value := vc.Create(a.Value, &t, a.Config)
a.value = &t
tracef("attempting arg%[1] parse", &a.Name)
if len(s) > 0 {
if err := value.Set(s[0]); err != nil {
return s, fmt.Errorf("invalid value %q for argument %s: %v", s[0], a.Name, err)
}
*a.value = value.Get().(T)
tracef("set arg%[1] one value", a.Name, *a.value)
}
if a.Destination != nil {
tracef("setting destination")
*a.Destination = *a.value
}
if len(s) > 0 {
return s[1:], nil
}
return s, nil
}
func (a *ArgumentBase[T, C, VC]) Get() any {
if a.value != nil {
return *a.value
}
return a.Value
}
// ArgumentsBase is a base type for slice arguments
type ArgumentsBase[T any, C any, VC ValueCreator[T, C]] struct {
Name string `json:"name"` // the name of this argument
Value T `json:"value"` // the default value of this argument
Destination *[]T `json:"-"` // the destination point for this argument
UsageText string `json:"usageText"` // the usage text to show
Min int `json:"minTimes"` // the min num of occurrences of this argument
Max int `json:"maxTimes"` // the max num of occurrences of this argument, set to -1 for unlimited
Config C `json:"config"` // config for this argument similar to Flag Config
values []T
}
func (a *ArgumentsBase[T, C, VC]) HasName(s string) bool {
return s == a.Name
}
func (a *ArgumentsBase[T, C, VC]) Usage() string {
if a.UsageText != "" {
return a.UsageText
}
usageFormat := ""
if a.Min == 0 {
if a.Max == 1 {
usageFormat = "[%[1]s]"
} else {
usageFormat = "[%[1]s ...]"
}
} else {
usageFormat = "%[1]s [%[1]s ...]"
}
return fmt.Sprintf(usageFormat, a.Name)
}
func (a *ArgumentsBase[T, C, VC]) Parse(s []string) ([]string, error) {
tracef("calling arg%[1] parse with args %[2]", &a.Name, s)
if a.Max == 0 {
return s, fmt.Errorf("args %s has max 0, not parsing argument", a.Name)
}
if a.Max != -1 && a.Min > a.Max {
return s, fmt.Errorf("args %s has min[%d] > max[%d], not parsing argument", a.Name, a.Min, a.Max)
}
count := 0
var vc VC
var t T
value := vc.Create(a.Value, &t, a.Config)
a.values = []T{}
tracef("attempting arg%[1] parse", &a.Name)
for _, arg := range s {
if err := value.Set(arg); err != nil {
return s, fmt.Errorf("invalid value %q for argument %s: %v", arg, a.Name, err)
}
tracef("set arg%[1] one value", &a.Name, value.Get().(T))
a.values = append(a.values, value.Get().(T))
count++
if count >= a.Max && a.Max > -1 {
break
}
}
if count < a.Min {
return s, fmt.Errorf("sufficient count of arg %s not provided, given %d expected %d", a.Name, count, a.Min)
}
if a.Destination != nil {
tracef("appending destination")
*a.Destination = a.values // append(*a.Destination, a.values...)
}
return s[count:], nil
}
func (a *ArgumentsBase[T, C, VC]) Get() any {
if a.values != nil {
return a.values
}
return []T{}
}
type (
FloatArg = ArgumentBase[float64, NoConfig, floatValue[float64]]
Float32Arg = ArgumentBase[float32, NoConfig, floatValue[float32]]
Float64Arg = ArgumentBase[float64, NoConfig, floatValue[float64]]
IntArg = ArgumentBase[int, IntegerConfig, intValue[int]]
Int8Arg = ArgumentBase[int8, IntegerConfig, intValue[int8]]
Int16Arg = ArgumentBase[int16, IntegerConfig, intValue[int16]]
Int32Arg = ArgumentBase[int32, IntegerConfig, intValue[int32]]
Int64Arg = ArgumentBase[int64, IntegerConfig, intValue[int64]]
StringArg = ArgumentBase[string, StringConfig, stringValue]
StringMapArgs = ArgumentBase[map[string]string, StringConfig, StringMap]
TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]
UintArg = ArgumentBase[uint, IntegerConfig, uintValue[uint]]
Uint8Arg = ArgumentBase[uint8, IntegerConfig, uintValue[uint8]]
Uint16Arg = ArgumentBase[uint16, IntegerConfig, uintValue[uint16]]
Uint32Arg = ArgumentBase[uint32, IntegerConfig, uintValue[uint32]]
Uint64Arg = ArgumentBase[uint64, IntegerConfig, uintValue[uint64]]
FloatArgs = ArgumentsBase[float64, NoConfig, floatValue[float64]]
Float32Args = ArgumentsBase[float32, NoConfig, floatValue[float32]]
Float64Args = ArgumentsBase[float64, NoConfig, floatValue[float64]]
IntArgs = ArgumentsBase[int, IntegerConfig, intValue[int]]
Int8Args = ArgumentsBase[int8, IntegerConfig, intValue[int8]]
Int16Args = ArgumentsBase[int16, IntegerConfig, intValue[int16]]
Int32Args = ArgumentsBase[int32, IntegerConfig, intValue[int32]]
Int64Args = ArgumentsBase[int64, IntegerConfig, intValue[int64]]
StringArgs = ArgumentsBase[string, StringConfig, stringValue]
TimestampArgs = ArgumentsBase[time.Time, TimestampConfig, timestampValue]
UintArgs = ArgumentsBase[uint, IntegerConfig, uintValue[uint]]
Uint8Args = ArgumentsBase[uint8, IntegerConfig, uintValue[uint8]]
Uint16Args = ArgumentsBase[uint16, IntegerConfig, uintValue[uint16]]
Uint32Args = ArgumentsBase[uint32, IntegerConfig, uintValue[uint32]]
Uint64Args = ArgumentsBase[uint64, IntegerConfig, uintValue[uint64]]
)
func (c *Command) getArgValue(name string) any {
tracef("command %s looking for args %s", c.Name, name)
for _, arg := range c.Arguments {
if arg.HasName(name) {
tracef("command %s found args %s", c.Name, name)
return arg.Get()
}
}
tracef("command %s did not find args %s", c.Name, name)
return nil
}
func arg[T any](name string, c *Command) T {
val := c.getArgValue(name)
if a, ok := val.(T); ok {
return a
}
var zero T
return zero
}
func (c *Command) StringArg(name string) string {
return arg[string](name, c)
}
func (c *Command) StringArgs(name string) []string {
return arg[[]string](name, c)
}
func (c *Command) FloatArg(name string) float64 {
return arg[float64](name, c)
}
func (c *Command) FloatArgs(name string) []float64 {
return arg[[]float64](name, c)
}
func (c *Command) Float32Arg(name string) float32 {
return arg[float32](name, c)
}
func (c *Command) Float32Args(name string) []float32 {
return arg[[]float32](name, c)
}
func (c *Command) Float64Arg(name string) float64 {
return arg[float64](name, c)
}
func (c *Command) Float64Args(name string) []float64 {
return arg[[]float64](name, c)
}
func (c *Command) IntArg(name string) int {
return arg[int](name, c)
}
func (c *Command) IntArgs(name string) []int {
return arg[[]int](name, c)
}
func (c *Command) Int8Arg(name string) int8 {
return arg[int8](name, c)
}
func (c *Command) Int8Args(name string) []int8 {
return arg[[]int8](name, c)
}
func (c *Command) Int16Arg(name string) int16 {
return arg[int16](name, c)
}
func (c *Command) Int16Args(name string) []int16 {
return arg[[]int16](name, c)
}
func (c *Command) Int32Arg(name string) int32 {
return arg[int32](name, c)
}
func (c *Command) Int32Args(name string) []int32 {
return arg[[]int32](name, c)
}
func (c *Command) Int64Arg(name string) int64 {
return arg[int64](name, c)
}
func (c *Command) Int64Args(name string) []int64 {
return arg[[]int64](name, c)
}
func (c *Command) UintArg(name string) uint {
return arg[uint](name, c)
}
func (c *Command) Uint8Arg(name string) uint8 {
return arg[uint8](name, c)
}
func (c *Command) Uint16Arg(name string) uint16 {
return arg[uint16](name, c)
}
func (c *Command) Uint32Arg(name string) uint32 {
return arg[uint32](name, c)
}
func (c *Command) Uint64Arg(name string) uint64 {
return arg[uint64](name, c)
}
func (c *Command) UintArgs(name string) []uint {
return arg[[]uint](name, c)
}
func (c *Command) Uint8Args(name string) []uint8 {
return arg[[]uint8](name, c)
}
func (c *Command) Uint16Args(name string) []uint16 {
return arg[[]uint16](name, c)
}
func (c *Command) Uint32Args(name string) []uint32 {
return arg[[]uint32](name, c)
}
func (c *Command) Uint64Args(name string) []uint64 {
return arg[[]uint64](name, c)
}
func (c *Command) TimestampArg(name string) time.Time {
return arg[time.Time](name, c)
}
func (c *Command) TimestampArgs(name string) []time.Time {
return arg[[]time.Time](name, c)
}
+107
View File
@@ -0,0 +1,107 @@
# This is a shell completion script auto-generated by https://github.com/urfave/cli for bash.
# macOS still ships Bash 3, where bash-completion may not provide
# _init_completion. This is a minimal compatible fallback.
__%[1]s_init_completion() {
COMPREPLY=()
if declare -F _comp_initialize >/dev/null 2>&1; then
_comp_initialize "$@"
else
_get_comp_words_by_ref "$@" cur prev words cword
fi
}
__%[1]s_build_completion_request() {
local -a words_before_cursor=("${COMP_WORDS[@]:0:${COMP_CWORD}}")
local current_word="${COMP_WORDS[COMP_CWORD]}"
if [[ "${current_word}" == "-"* ]]; then
printf '%%s %%s --generate-shell-completion' "${words_before_cursor[*]}" "${current_word}"
else
printf '%%s --generate-shell-completion' "${words_before_cursor[*]}"
fi
}
# Keep Bash 3 compatibility: associative arrays require Bash 4+, so
# descriptions are looked up via parallel indexed arrays.
__%[1]s_find_description_for() {
local candidate="$1"
local i
for i in "${!__cli_completion_tokens[@]}"; do
if [[ "${__cli_completion_tokens[i]}" == "${candidate}" ]]; then
printf '%%s' "${__cli_completion_descriptions[i]}"
return 0
fi
done
return 1
}
__%[1]s_bash_autocomplete() {
local words=("${COMP_WORDS[@]}")
if [[ "${words[0]}" != "source" ]]; then
local cur opts
local cword="${COMP_CWORD}"
local request_comp
COMPREPLY=()
cur="${words[$cword]}"
__%[1]s_init_completion -n "=:" || return
request_comp="$(__%[1]s_build_completion_request)"
opts=$(eval "${request_comp}" 2>/dev/null)
# Completion output lines use "token:description" format.
# Keep token/description in parallel arrays for Bash 3 compatibility.
__cli_completion_tokens=()
__cli_completion_descriptions=()
local line
local longest=0
while IFS=$'\n' read -r line; do
local token="${line}"
local description=""
if [[ "${line}" == *:* ]]; then
token="${line%%:*}"
description="${line#*:}"
fi
if [[ -z "${token}" ]]; then
continue
fi
__cli_completion_tokens+=("${token}")
__cli_completion_descriptions+=("${description}")
(( ${#token} > longest )) && longest=${#token}
done <<< "${opts}"
local matches=( $(compgen -W "${__cli_completion_tokens[*]}" -- "${cur}") )
# COMP_TYPE=63 means Bash is listing matches (usually on second TAB).
if [[ "${COMP_TYPE:-}" == "63" && ${#matches[@]} -gt 0 ]]; then
local listed=()
local candidate
for candidate in "${matches[@]}"; do
local desc="$(__%[1]s_find_description_for "${candidate}")"
if [[ -n "${desc}" ]]; then
local padded="$(printf '%%-*s' "${longest}" "${candidate}")"
listed+=("${padded} -- ${desc}")
else
listed+=("${candidate}")
fi
done
COMPREPLY=("${listed[@]}")
else
COMPREPLY=("${matches[@]}")
fi
return 0
fi
}
complete -o bashdefault -o default -o nospace -F __%[1]s_bash_autocomplete %[1]s
+35
View File
@@ -0,0 +1,35 @@
# This is a shell completion script auto-generated by https://github.com/urfave/cli for fish.
function __%[1]s_perform_completion
# Extract all args except the last one
set -l args (commandline -opc)
# Extract the last arg (partial input)
set -l lastArg (commandline -ct)
set -l results ($args[1] $args[2..-1] $lastArg --generate-shell-completion 2> /dev/null)
# Remove trailing empty lines
for line in $results[-1..1]
if test (string trim -- $line) = ""
set results $results[1..-2]
else
break
end
end
for line in $results
if not string match -q -- "%[1]s*" $line
set -l parts (string split -m 1 ":" -- "$line")
if test (count $parts) -eq 2
printf "%%s\t%%s\n" "$parts[1]" "$parts[2]"
else
printf "%%s\n" "$line"
end
end
end
end
# Clear existing completions for %[1]s
complete -c %[1]s -e
# Register completion function
complete -c %[1]s -f -a '(__%[1]s_perform_completion)'
@@ -0,0 +1,16 @@
$fn = $($MyInvocation.MyCommand.Name)
$name = $fn -replace "(.*)\.ps1$", '$1'
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$other = "$wordToComplete --generate-shell-completion"
Invoke-Expression $other | ForEach-Object {
$parts = $_.Split(':', 2)
if ($parts.Count -eq 2) {
$completion = $parts[0].Trim()
$description = $parts[1].Trim()
[System.Management.Automation.CompletionResult]::new($completion, $completion, 'ParameterValue', $description)
} else {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
}
+29
View File
@@ -0,0 +1,29 @@
#compdef %[1]s
compdef _%[1]s %[1]s
# This is a shell completion script auto-generated by https://github.com/urfave/cli for zsh.
_%[1]s() {
local -a opts # Declare a local array
local current
current=${words[-1]} # -1 means "the last element"
if [[ "$current" == "-"* ]]; then
# Current word starts with a hyphen, so complete flags/options
opts=("${(@f)$(${words[@]:0:#words[@]-1} ${current} --generate-shell-completion)}")
else
# Current word does not start with a hyphen, so complete subcommands
opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts
else
_files
fi
}
# Don't run the completion function when being source-ed or eval-ed.
# See https://github.com/urfave/cli/issues/1874 for discussion.
if [ "$funcstack[1]" = "_%[1]s" ]; then
_%[1]s
fi
+195
View File
@@ -0,0 +1,195 @@
package cli
import "sort"
// CommandCategories interface allows for category manipulation
type CommandCategories interface {
// AddCommand adds a command to a category, creating a new category if necessary.
AddCommand(category string, command *Command)
// Categories returns a slice of categories sorted by name
Categories() []CommandCategory
}
type commandCategories []*commandCategory
func newCommandCategories() CommandCategories {
ret := commandCategories([]*commandCategory{})
return &ret
}
func (c *commandCategories) Less(i, j int) bool {
return lexicographicLess((*c)[i].Name(), (*c)[j].Name())
}
func (c *commandCategories) Len() int {
return len(*c)
}
func (c *commandCategories) Swap(i, j int) {
(*c)[i], (*c)[j] = (*c)[j], (*c)[i]
}
func (c *commandCategories) AddCommand(category string, command *Command) {
for _, commandCategory := range []*commandCategory(*c) {
if commandCategory.name == category {
commandCategory.commands = append(commandCategory.commands, command)
return
}
}
newVal := append(*c,
&commandCategory{name: category, commands: []*Command{command}})
*c = newVal
}
func (c *commandCategories) Categories() []CommandCategory {
ret := make([]CommandCategory, len(*c))
for i, cat := range *c {
ret[i] = cat
}
return ret
}
// CommandCategory is a category containing commands.
type CommandCategory interface {
// Name returns the category name string
Name() string
// VisibleCommands returns a slice of the Commands with Hidden=false
VisibleCommands() []*Command
}
type commandCategory struct {
name string
commands []*Command
}
func (c *commandCategory) Name() string {
return c.name
}
func (c *commandCategory) VisibleCommands() []*Command {
if c.commands == nil {
c.commands = []*Command{}
}
var ret []*Command
for _, command := range c.commands {
if !command.Hidden {
ret = append(ret, command)
}
}
return ret
}
// FlagCategories interface allows for category manipulation
type FlagCategories interface {
// AddFlags adds a flag to a category, creating a new category if necessary.
AddFlag(category string, fl Flag)
// VisibleCategories returns a slice of visible flag categories sorted by name
VisibleCategories() []VisibleFlagCategory
}
type defaultFlagCategories struct {
m map[string]*defaultVisibleFlagCategory
}
func newFlagCategories() FlagCategories {
return &defaultFlagCategories{
m: map[string]*defaultVisibleFlagCategory{},
}
}
func newFlagCategoriesFromFlags(fs []Flag) FlagCategories {
fc := newFlagCategories()
var categorized bool
for _, fl := range fs {
if cf, ok := fl.(CategorizableFlag); ok {
visible := false
if vf, ok := fl.(VisibleFlag); ok {
visible = vf.IsVisible()
}
if cat := cf.GetCategory(); cat != "" && visible {
fc.AddFlag(cat, fl)
categorized = true
}
}
}
if categorized {
for _, fl := range fs {
if cf, ok := fl.(CategorizableFlag); ok {
visible := false
if vf, ok := fl.(VisibleFlag); ok {
visible = vf.IsVisible()
}
if cf.GetCategory() == "" && visible {
fc.AddFlag("", fl)
}
}
}
}
return fc
}
func (f *defaultFlagCategories) AddFlag(category string, fl Flag) {
if _, ok := f.m[category]; !ok {
f.m[category] = &defaultVisibleFlagCategory{name: category, m: map[string]Flag{}}
}
f.m[category].m[fl.String()] = fl
}
func (f *defaultFlagCategories) VisibleCategories() []VisibleFlagCategory {
catNames := []string{}
for name := range f.m {
catNames = append(catNames, name)
}
sort.Strings(catNames)
ret := make([]VisibleFlagCategory, len(catNames))
for i, name := range catNames {
ret[i] = f.m[name]
}
return ret
}
// VisibleFlagCategory is a category containing flags.
type VisibleFlagCategory interface {
// Name returns the category name string
Name() string
// Flags returns a slice of VisibleFlag sorted by name
Flags() []Flag
}
type defaultVisibleFlagCategory struct {
name string
m map[string]Flag
}
func (fc *defaultVisibleFlagCategory) Name() string {
return fc.name
}
func (fc *defaultVisibleFlagCategory) Flags() []Flag {
vfNames := []string{}
for flName, fl := range fc.m {
if vf, ok := fl.(VisibleFlag); ok {
if vf.IsVisible() {
vfNames = append(vfNames, flName)
}
}
}
sort.Strings(vfNames)
ret := make([]Flag, len(vfNames))
for i, flName := range vfNames {
ret[i] = fc.m[flName]
}
return ret
}
+60
View File
@@ -0,0 +1,60 @@
// Package cli provides a minimal framework for creating and organizing command line
// Go applications. cli is designed to be easy to understand and write, the most simple
// cli application can be written as follows:
//
// func main() {
// (&cli.Command{}).Run(context.Background(), os.Args)
// }
//
// Of course this application does not do much, so let's make this an actual application:
//
// func main() {
// cmd := &cli.Command{
// Name: "greet",
// Usage: "say a greeting",
// Action: func(c *cli.Context) error {
// fmt.Println("Greetings")
// return nil
// },
// }
//
// cmd.Run(context.Background(), os.Args)
// }
package cli
import (
"fmt"
"os"
"runtime"
"strings"
)
var isTracingOn = os.Getenv("URFAVE_CLI_TRACING") == "on"
func tracef(format string, a ...any) {
if !isTracingOn {
return
}
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
pc, file, line, _ := runtime.Caller(1)
cf := runtime.FuncForPC(pc)
fmt.Fprintf(
os.Stderr,
strings.Join([]string{
"## URFAVE CLI TRACE ",
file,
":",
fmt.Sprintf("%v", line),
" ",
fmt.Sprintf("(%s)", cf.Name()),
" ",
format,
}, ""),
a...,
)
}
+604
View File
@@ -0,0 +1,604 @@
package cli
import (
"context"
"fmt"
"io"
"slices"
"strings"
)
const (
// ignoreFlagPrefix is to ignore test flags when adding flags from other packages
ignoreFlagPrefix = "test."
commandContextKey = contextKey("cli.context")
)
type contextKey string
// Command contains everything needed to run an application that
// accepts a string slice of arguments such as os.Args. A given
// Command may contain Flags and sub-commands in Commands.
type Command struct {
// The name of the command
Name string `json:"name"`
// A list of aliases for the command
Aliases []string `json:"aliases"`
// A short description of the usage of this command
Usage string `json:"usage"`
// Text to override the USAGE section of help
UsageText string `json:"usageText"`
// A short description of the arguments of this command
ArgsUsage string `json:"argsUsage"`
// Version of the command
Version string `json:"version"`
// Longer explanation of how the command works
Description string `json:"description"`
// DefaultCommand is the (optional) name of a command
// to run if no command names are passed as CLI arguments.
DefaultCommand string `json:"defaultCommand"`
// The category the command is part of
Category string `json:"category"`
// List of child commands
Commands []*Command `json:"commands"`
// List of flags to parse
Flags []Flag `json:"flags"`
// Boolean to hide built-in help command and help flag
HideHelp bool `json:"hideHelp"`
// Ignored if HideHelp is true.
HideHelpCommand bool `json:"hideHelpCommand"`
// Boolean to hide built-in version flag and the VERSION section of help
HideVersion bool `json:"hideVersion"`
// Boolean to enable shell completion commands
EnableShellCompletion bool `json:"-"`
// Shell Completion generation command name
ShellCompletionCommandName string `json:"-"`
// The function to call when checking for shell command completions
ShellComplete ShellCompleteFunc `json:"-"`
// The function to configure a shell completion command
ConfigureShellCompletionCommand ConfigureShellCompletionCommand `json:"-"`
// An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run
Before BeforeFunc `json:"-"`
// An action to execute after any subcommands are run, but after the subcommand has finished
// It is run even if Action() panics
After AfterFunc `json:"-"`
// The function to call when this command is invoked
Action ActionFunc `json:"-"`
// Execute this function if the proper command cannot be found
CommandNotFound CommandNotFoundFunc `json:"-"`
// Execute this function if a usage error occurs.
OnUsageError OnUsageErrorFunc `json:"-"`
// Execute this function when an invalid flag is accessed from the context
InvalidFlagAccessHandler InvalidFlagAccessFunc `json:"-"`
// Boolean to hide this command from help or completion
Hidden bool `json:"hidden"`
// List of all authors who contributed (string or fmt.Stringer)
// TODO: ~string | fmt.Stringer when interface unions are available
Authors []any `json:"authors"`
// Copyright of the binary if any
Copyright string `json:"copyright"`
// Reader reader to write input to (useful for tests)
Reader io.Reader `json:"-"`
// Writer writer to write output to
Writer io.Writer `json:"-"`
// ErrWriter writes error output
ErrWriter io.Writer `json:"-"`
// ExitErrHandler processes any error encountered while running a Command before it is
// returned to the caller. If no function is provided, HandleExitCoder is used as the
// default behavior.
ExitErrHandler ExitErrHandlerFunc `json:"-"`
// Other custom info
Metadata map[string]any `json:"metadata"`
// Carries a function which returns app specific info.
ExtraInfo func() map[string]string `json:"-"`
// CustomRootCommandHelpTemplate the text template for app help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomRootCommandHelpTemplate string `json:"-"`
// SliceFlagSeparator is used to customize the separator for SliceFlag, the default is ","
SliceFlagSeparator string `json:"sliceFlagSeparator"`
// DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false
DisableSliceFlagSeparator bool `json:"disableSliceFlagSeparator"`
// MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "="
MapFlagKeyValueSeparator string `json:"mapFlagKeyValueSeparator"`
// Boolean to enable short-option handling so user can combine several
// single-character bool arguments into one
// i.e. foobar -o -v -> foobar -ov
UseShortOptionHandling bool `json:"useShortOptionHandling"`
// Enable suggestions for commands and flags
Suggest bool `json:"suggest"`
// Allows global flags set by libraries which use flag.XXXVar(...) directly
// to be parsed through this library
AllowExtFlags bool `json:"allowExtFlags"`
// Treat all flags as normal arguments if true
SkipFlagParsing bool `json:"skipFlagParsing"`
// CustomHelpTemplate the text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
CustomHelpTemplate string `json:"-"`
// Use longest prefix match for commands
PrefixMatchCommands bool `json:"prefixMatchCommands"`
// Custom suggest command for matching
SuggestCommandFunc SuggestCommandFunc `json:"-"`
// Flag exclusion group
MutuallyExclusiveFlags []MutuallyExclusiveFlags `json:"mutuallyExclusiveFlags"`
// Arguments to parse for this command
Arguments []Argument `json:"arguments"`
// Whether to read arguments from stdin
// applicable to root command only
ReadArgsFromStdin bool `json:"readArgsFromStdin"`
// StopOnNthArg provides v2-like behavior for specific commands by stopping
// flag parsing after N positional arguments are encountered. When set to N,
// all remaining arguments after the Nth positional argument will be treated
// as arguments, not flags.
//
// A value of 0 means all arguments are treated as positional (no flag parsing).
// A nil value means normal v3 flag parsing behavior (flags can appear anywhere).
StopOnNthArg *int `json:"stopOnNthArg"`
// categories contains the categorized commands and is populated on app startup
categories CommandCategories
// flagCategories contains the categorized flags and is populated on app startup
flagCategories FlagCategories
// flags that have been applied in current parse
appliedFlags []Flag
// flags that have been set
setFlags map[Flag]struct{}
// The parent of this command. This value will be nil for the
// command at the root of the graph.
parent *Command
// parsed args
parsedArgs Args
// track state of error handling
isInError bool
// track state of defaults
didSetupDefaults bool
// whether in shell completion mode
shellCompletion bool
// whether global help flag was added
globaHelpFlagAdded bool
// whether global version flag was added
globaVersionFlagAdded bool
// 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), " ")
}
func (cmd *Command) Command(name string) *Command {
for _, subCmd := range cmd.Commands {
if subCmd.HasName(name) {
return subCmd
}
}
return nil
}
func (cmd *Command) checkHelp() bool {
tracef("checking if help is wanted (cmd=%[1]q)", cmd.Name)
return HelpFlag != nil && slices.ContainsFunc(HelpFlag.Names(), cmd.Bool)
}
func (cmd *Command) allFlags() []Flag {
var flags []Flag
flags = append(flags, cmd.Flags...)
for _, grpf := range cmd.MutuallyExclusiveFlags {
for _, f1 := range grpf.Flags {
flags = append(flags, f1...)
}
}
return flags
}
// useShortOptionHandling traverses Lineage() for *any* ancestors
// with UseShortOptionHandling
func (cmd *Command) useShortOptionHandling() bool {
for _, pCmd := range cmd.Lineage() {
if pCmd.UseShortOptionHandling {
return true
}
}
return false
}
func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, error) {
fl, parseErr := flagFromError(err)
if parseErr != nil {
return "", err
}
flags := cmd.Flags
hideHelp := cmd.hideHelp()
if commandName != "" {
subCmd := cmd.Command(commandName)
if subCmd == nil {
return "", err
}
flags = subCmd.Flags
hideHelp = hideHelp || subCmd.HideHelp
}
suggestion := SuggestFlag(flags, fl, hideHelp)
if len(suggestion) == 0 {
return "", err
}
return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil
}
// Names returns the names including short names and aliases.
func (cmd *Command) Names() []string {
return append([]string{cmd.Name}, cmd.Aliases...)
}
// HasName returns true if Command.Name matches given name
func (cmd *Command) HasName(name string) bool {
return slices.Contains(cmd.Names(), name)
}
// VisibleCategories returns a slice of categories and commands that are
// Hidden=false
func (cmd *Command) VisibleCategories() []CommandCategory {
ret := []CommandCategory{}
for _, category := range cmd.categories.Categories() {
if visible := func() CommandCategory {
if len(category.VisibleCommands()) > 0 {
return category
}
return nil
}(); visible != nil {
ret = append(ret, visible)
}
}
return ret
}
// VisibleCommands returns a slice of the Commands with Hidden=false
func (cmd *Command) VisibleCommands() []*Command {
var ret []*Command
for _, command := range cmd.Commands {
if command.Hidden || command.Name == helpName {
continue
}
ret = append(ret, command)
}
return ret
}
// VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain
func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory {
if cmd.flagCategories == nil {
cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags())
}
return cmd.flagCategories.VisibleCategories()
}
// VisibleFlags returns a slice of the Flags with Hidden=false
func (cmd *Command) VisibleFlags() []Flag {
return visibleFlags(cmd.allFlags())
}
func (cmd *Command) appendFlag(fl Flag) {
if !hasFlag(cmd.Flags, fl) {
cmd.Flags = append(cmd.Flags, fl)
}
}
// VisiblePersistentFlags returns a slice of [LocalFlag] with Persistent=true and Hidden=false.
func (cmd *Command) VisiblePersistentFlags() []Flag {
var flags []Flag
for _, fl := range cmd.Root().Flags {
pfl, ok := fl.(LocalFlag)
if !ok || pfl.IsLocal() {
continue
}
flags = append(flags, fl)
}
return visibleFlags(flags)
}
func (cmd *Command) appendCommand(aCmd *Command) {
if !slices.Contains(cmd.Commands, aCmd) {
aCmd.parent = cmd
cmd.Commands = append(cmd.Commands, aCmd)
}
}
func (cmd *Command) handleExitCoder(ctx context.Context, err error) error {
if cmd.parent != nil {
return cmd.parent.handleExitCoder(ctx, err)
}
if cmd.ExitErrHandler != nil {
cmd.ExitErrHandler(ctx, cmd, err)
return err
}
HandleExitCoder(err)
return err
}
func (cmd *Command) argsWithDefaultCommand(oldArgs Args) Args {
rawArgs := append([]string{cmd.DefaultCommand}, oldArgs.Slice()...)
newArgs := &stringSliceArgs{v: rawArgs}
return newArgs
}
// Root returns the Command at the root of the graph
func (cmd *Command) Root() *Command {
if cmd.parent == nil {
return cmd
}
return cmd.parent.Root()
}
func (cmd *Command) set(fName string, f Flag, val string) error {
cmd.setFlags[f] = struct{}{}
cmd.setMultiValueParsingConfig(f)
if err := f.Set(fName, val); err != nil {
return fmt.Errorf("invalid value %q for flag -%s: %v", val, fName, err)
}
return nil
}
func (cmd *Command) lFlag(name string) Flag {
for _, f := range cmd.allFlags() {
if slices.Contains(f.Names(), name) {
tracef("flag found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
return f
}
}
return nil
}
func (cmd *Command) lookupFlag(name string) Flag {
for _, pCmd := range cmd.Lineage() {
if f := pCmd.lFlag(name); f != nil {
return f
}
}
tracef("flag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
cmd.onInvalidFlag(context.TODO(), name)
return nil
}
// this looks up only allowed flags, i.e. local flags for current command
// or persistent flags from ancestors
func (cmd *Command) lookupAppliedFlag(name string) Flag {
for _, f := range cmd.appliedFlags {
if slices.Contains(f.Names(), name) {
tracef("appliedFlag found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
return f
}
}
tracef("lookupAppliedflag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
cmd.onInvalidFlag(context.TODO(), name)
return nil
}
func (cmd *Command) checkRequiredFlag(f Flag) (bool, string) {
if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
flagName := f.Names()[0]
if !f.IsSet() {
return false, flagName
}
}
return true, ""
}
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 {
return nil
}
for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent {
if err := pCmd.checkRequiredFlags(); err != nil {
return err
}
}
return nil
}
func (cmd *Command) checkRequiredFlags() requiredFlagsErr {
tracef("checking for required flags (cmd=%[1]q)", cmd.Name)
missingFlags := []string{}
for _, f := range cmd.appliedFlags {
if ok, name := cmd.checkRequiredFlag(f); !ok {
missingFlags = append(missingFlags, name)
}
}
if len(missingFlags) != 0 {
tracef("found missing required flags %[1]q (cmd=%[2]q)", missingFlags, cmd.Name)
return &errRequiredFlags{missingFlags: missingFlags}
}
tracef("all required flags set (cmd=%[1]q)", cmd.Name)
return nil
}
func (cmd *Command) onInvalidFlag(ctx context.Context, name string) {
for cmd != nil {
if cmd.InvalidFlagAccessHandler != nil {
cmd.InvalidFlagAccessHandler(ctx, cmd, name)
break
}
cmd = cmd.parent
}
}
// NumFlags returns the number of flags set
func (cmd *Command) NumFlags() int {
tracef("numFlags numAppliedFlags %d", len(cmd.appliedFlags))
count := 0
for _, f := range cmd.appliedFlags {
if f.IsSet() {
count++
}
}
return count // cmd.flagSet.NFlag()
}
func (cmd *Command) setMultiValueParsingConfig(f Flag) {
tracef("setMultiValueParsingConfig %T, %+v", f, f)
if cf, ok := f.(multiValueParsingConfigSetter); ok {
cf.setMultiValueParsingConfig(multiValueParsingConfig{
SliceFlagSeparator: cmd.SliceFlagSeparator,
DisableSliceFlagSeparator: cmd.DisableSliceFlagSeparator,
MapFlagKeyValueSeparator: cmd.MapFlagKeyValueSeparator,
})
}
}
// Set sets a context flag to a value.
func (cmd *Command) Set(name, value string) error {
if f := cmd.lookupFlag(name); f != nil {
cmd.setMultiValueParsingConfig(f)
return f.Set(name, value)
}
return fmt.Errorf("no such flag -%s", name)
}
// IsSet determines if the flag was actually set
func (cmd *Command) IsSet(name string) bool {
fl := cmd.lookupFlag(name)
if fl == nil {
tracef("flag with name %[1]q NOT found; assuming not set (cmd=%[2]q)", name, cmd.Name)
return false
}
isSet := fl.IsSet()
if isSet {
tracef("flag with name %[1]q is set (cmd=%[2]q)", name, cmd.Name)
} else {
tracef("flag with name %[1]q is no set (cmd=%[2]q)", name, cmd.Name)
}
return isSet
}
// LocalFlagNames returns a slice of flag names used in this
// command.
func (cmd *Command) LocalFlagNames() []string {
names := []string{}
// Check the flags which have been set via env or file
for _, f := range cmd.allFlags() {
if f.IsSet() {
names = append(names, f.Names()...)
}
}
// Sort out the duplicates since flag could be set via multiple
// paths
m := map[string]struct{}{}
uniqNames := []string{}
for _, name := range names {
if _, ok := m[name]; !ok {
m[name] = struct{}{}
uniqNames = append(uniqNames, name)
}
}
return uniqNames
}
// FlagNames returns a slice of flag names used by the this command
// and all of its parent commands.
func (cmd *Command) FlagNames() []string {
names := cmd.LocalFlagNames()
if cmd.parent != nil {
names = append(cmd.parent.FlagNames(), names...)
}
return names
}
// Lineage returns *this* command and all of its ancestor commands
// in order from child to parent
func (cmd *Command) Lineage() []*Command {
lineage := []*Command{cmd}
if cmd.parent != nil {
lineage = append(lineage, cmd.parent.Lineage()...)
}
return lineage
}
// Count returns the num of occurrences of this flag
func (cmd *Command) Count(name string) int {
if cf, ok := cmd.lookupFlag(name).(Countable); ok {
return cf.Count()
}
return 0
}
// Value returns the value of the flag corresponding to `name`
func (cmd *Command) Value(name string) any {
if fs := cmd.lookupFlag(name); fs != nil {
tracef("value found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
return fs.Get()
}
tracef("value NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
return nil
}
// Args returns the command line arguments associated with the
// command.
func (cmd *Command) Args() Args {
return cmd.parsedArgs
}
// NArg returns the number of the command line arguments.
func (cmd *Command) NArg() int {
return cmd.Args().Len()
}
func (cmd *Command) runFlagActions(ctx context.Context) error {
tracef("runFlagActions")
// run the flag actions in the same order that they are defined
// to maintain consistency.
for _, fl := range cmd.appliedFlags {
if _, inSet := cmd.setFlags[fl]; inSet {
if af, ok := fl.(ActionableFlag); ok {
if err := af.RunAction(ctx, cmd); err != nil {
return err
}
}
}
}
return nil
}
+241
View File
@@ -0,0 +1,241 @@
package cli
import (
"fmt"
"strings"
"unicode"
)
const (
providedButNotDefinedErrMsg = "flag provided but not defined: -"
argumentNotProvidedErrMsg = "flag needs an argument: "
)
// flagFromError tries to parse a provided flag from an error message. If the
// parsing fails, it returns the input error and an empty string
func flagFromError(err error) (string, error) {
errStr := err.Error()
trimmed := strings.TrimPrefix(errStr, providedButNotDefinedErrMsg)
if errStr == trimmed {
return "", err
}
return trimmed, nil
}
func (cmd *Command) parseFlags(args Args) (Args, error) {
tracef("parsing flags from arguments %[1]q (cmd=%[2]q)", args, cmd.Name)
cmd.setFlags = map[Flag]struct{}{}
cmd.appliedFlags = cmd.allFlags()
tracef("walking command lineage for persistent flags (cmd=%[1]q)", cmd.Name)
for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent {
tracef(
"checking ancestor command=%[1]q for persistent flags (cmd=%[2]q)",
pCmd.Name, cmd.Name,
)
for _, fl := range pCmd.allFlags() {
flNames := fl.Names()
pfl, ok := fl.(LocalFlag)
if !ok || pfl.IsLocal() {
tracef("skipping non-persistent flag %[1]q (cmd=%[2]q)", flNames, cmd.Name)
continue
}
tracef(
"checking for applying persistent flag=%[1]q pCmd=%[2]q (cmd=%[3]q)",
flNames, pCmd.Name, cmd.Name,
)
applyPersistentFlag := true
for _, name := range flNames {
if cmd.lFlag(name) != nil {
applyPersistentFlag = false
break
}
}
if !applyPersistentFlag {
tracef("not applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name)
continue
}
tracef("applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name)
tracef("appending to applied flags flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name)
cmd.appliedFlags = append(cmd.appliedFlags, fl)
}
}
tracef("parsing flags iteratively tail=%[1]q (cmd=%[2]q)", args.Tail(), cmd.Name)
defer tracef("done parsing flags (cmd=%[1]q)", cmd.Name)
posArgs := []string{}
for rargs := args.Slice(); len(rargs) > 0; rargs = rargs[1:] {
tracef("rearrange:1 (cmd=%[1]q) %[2]q", cmd.Name, rargs)
firstArg := strings.TrimSpace(rargs[0])
if len(firstArg) == 0 {
posArgs = append(posArgs, rargs[0])
continue
}
// stop parsing once we see a "--"
if firstArg == "--" {
// In shell completion mode, preserve "--" so that completion can detect
// when the user is completing "--" itself vs. completing after "--"
if cmd.Root().shellCompletion {
posArgs = append(posArgs, firstArg)
}
posArgs = append(posArgs, rargs[1:]...)
return &stringSliceArgs{posArgs}, nil
}
// Check if we've reached the Nth argument and should stop flag parsing
if cmd.StopOnNthArg != nil && len(posArgs) == *cmd.StopOnNthArg {
// Append current arg and all remaining args without parsing
posArgs = append(posArgs, rargs[0:]...)
return &stringSliceArgs{posArgs}, nil
}
// handle positional args
if firstArg[0] != '-' {
// positional argument probably
tracef("rearrange-3 (cmd=%[1]q) check %[2]q", cmd.Name, firstArg)
// if there is a command by that name let the command handle the
// rest of the parsing
if cmd.Command(firstArg) != nil {
posArgs = append(posArgs, rargs...)
return &stringSliceArgs{posArgs}, nil
}
posArgs = append(posArgs, firstArg)
continue
}
numMinuses := 1
// this is same as firstArg == "-"
if len(firstArg) == 1 {
posArgs = append(posArgs, firstArg)
break
}
shortOptionHandling := cmd.useShortOptionHandling()
// stop parsing -- as short flags
if firstArg[1] == '-' {
numMinuses++
shortOptionHandling = false
} else if !unicode.IsLetter(rune(firstArg[1])) {
// this is not a flag
tracef("parseFlags not a unicode letter. Stop parsing")
posArgs = append(posArgs, rargs...)
return &stringSliceArgs{posArgs}, nil
}
tracef("parseFlags (shortOptionHandling=%[1]q)", shortOptionHandling)
flagName := firstArg[numMinuses:]
flagVal := ""
valFromEqual := false
tracef("flagName:1 (fName=%[1]q)", flagName)
if index := strings.Index(flagName, "="); index != -1 {
flagVal = flagName[index+1:]
flagName = flagName[:index]
valFromEqual = true
}
tracef("flagName:2 (fName=%[1]q) (fVal=%[2]q)", flagName, flagVal)
f := cmd.lookupAppliedFlag(flagName)
// found a flag matching given flagName
if f != nil {
tracef("Trying flag type (fName=%[1]q) (type=%[2]T)", flagName, f)
if fb, ok := f.(boolFlag); ok && fb.IsBoolFlag() {
if flagVal == "" {
flagVal = "true"
}
tracef("parse Apply bool flag (fName=%[1]q) (fVal=%[2]q)", flagName, flagVal)
if err := cmd.set(flagName, f, flagVal); err != nil {
return &stringSliceArgs{posArgs}, err
}
continue
}
tracef("processing non bool flag (fName=%[1]q)", flagName)
// not a bool flag so need to get the next arg
if flagVal == "" && !valFromEqual {
if len(rargs) == 1 {
// In shell completion mode, preserve the flag so that DefaultCompleteWithFlags can use it
// as lastArg and offer suggestions for it.
if cmd.Root().shellCompletion {
posArgs = append(posArgs, rargs...)
return &stringSliceArgs{posArgs}, nil
}
return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", argumentNotProvidedErrMsg, firstArg)
}
flagVal = rargs[1]
rargs = rargs[1:]
}
tracef("setting non bool flag (fName=%[1]q) (fVal=%[2]q)", flagName, flagVal)
if err := cmd.set(flagName, f, flagVal); err != nil {
return &stringSliceArgs{posArgs}, err
}
continue
}
// no flag lookup found and short handling is disabled
if !shortOptionHandling {
// In shell completion mode, preserve the partial flag so that DefaultCompleteWithFlags can use it
// as lastArg and offer suggestions that match the prefix.
if cmd.Root().shellCompletion {
posArgs = append(posArgs, rargs...)
return &stringSliceArgs{posArgs}, nil
}
return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", providedButNotDefinedErrMsg, flagName)
}
// try to split the flags
for index, c := range flagName {
tracef("processing flag (fName=%[1]q)", string(c))
if sf := cmd.lookupFlag(string(c)); sf == nil {
return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", providedButNotDefinedErrMsg, flagName)
} else if fb, ok := sf.(boolFlag); ok && fb.IsBoolFlag() {
fv := flagVal
if index == (len(flagName)-1) && flagVal == "" {
fv = "true"
}
if fv == "" {
fv = "true"
}
if err := cmd.set(flagName, sf, fv); err != nil {
tracef("processing flag.2 (fName=%[1]q)", string(c))
return &stringSliceArgs{posArgs}, err
}
} else if index == len(flagName)-1 { // last flag can take an arg
if flagVal == "" {
if len(rargs) == 1 {
return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", argumentNotProvidedErrMsg, string(c))
}
flagVal = rargs[1]
rargs = rargs[1:]
}
tracef("parseFlags (flagName %[1]q) (flagVal %[2]q)", flagName, flagVal)
if err := cmd.set(flagName, sf, flagVal); err != nil {
tracef("processing flag.4 (fName=%[1]q)", string(c))
return &stringSliceArgs{posArgs}, err
}
}
}
}
tracef("returning-2 (cmd=%[1]q) args %[2]q", cmd.Name, posArgs)
return &stringSliceArgs{posArgs}, nil
}
+372
View File
@@ -0,0 +1,372 @@
package cli
import (
"bufio"
"context"
"fmt"
"io"
"slices"
"unicode"
)
func (cmd *Command) parseArgsFromStdin() ([]string, error) {
type state int
const (
stateSearchForToken state = -1
stateSearchForString state = 0
)
st := stateSearchForToken
linenum := 1
token := ""
args := []string{}
breader := bufio.NewReader(cmd.Reader)
outer:
for {
ch, _, err := breader.ReadRune()
if err == io.EOF {
switch st {
case stateSearchForToken:
if token != "--" {
args = append(args, token)
}
case stateSearchForString:
// make sure string is not empty
for _, t := range token {
if !unicode.IsSpace(t) {
args = append(args, token)
}
}
}
break outer
}
if err != nil {
return nil, err
}
switch st {
case stateSearchForToken:
if unicode.IsSpace(ch) || ch == '"' {
if ch == '\n' {
linenum++
}
if token != "" {
// end the processing here
if token == "--" {
break outer
}
args = append(args, token)
token = ""
}
if ch == '"' {
st = stateSearchForString
}
continue
}
token += string(ch)
case stateSearchForString:
if ch != '"' {
token += string(ch)
} else {
if token != "" {
args = append(args, token)
token = ""
}
/*else {
//TODO. Should we pass in empty strings ?
}*/
st = stateSearchForToken
}
}
}
tracef("parsed stdin args as %v (cmd=%[2]q)", args, cmd.Name)
return args, nil
}
// Run is the entry point to the command graph. The positional
// arguments are parsed according to the Flag and Command
// definitions and the matching Action functions are run.
func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
_, deferErr = cmd.run(ctx, osArgs)
return deferErr
}
func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context, deferErr error) {
tracef("running with arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name)
cmd.setupDefaults(osArgs)
// Validate StopOnNthArg
if cmd.StopOnNthArg != nil && *cmd.StopOnNthArg < 0 {
return ctx, fmt.Errorf("StopOnNthArg must be non-negative, got %d", *cmd.StopOnNthArg)
}
if v, ok := ctx.Value(commandContextKey).(*Command); ok {
tracef("setting parent (cmd=%[1]q) command from context.Context value (cmd=%[2]q)", v.Name, cmd.Name)
cmd.parent = v
}
if cmd.parent == nil {
if cmd.ReadArgsFromStdin {
if args, err := cmd.parseArgsFromStdin(); err != nil {
return ctx, err
} else {
osArgs = append(osArgs, args...)
}
}
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
tracef("checking osArgs %v (cmd=%[2]q)", osArgs, cmd.Name)
cmd.shellCompletion, osArgs = checkShellCompleteFlag(cmd, osArgs)
tracef("setting cmd.shellCompletion=%[1]v from checkShellCompleteFlag (cmd=%[2]q)", cmd.shellCompletion && cmd.EnableShellCompletion, cmd.Name)
cmd.shellCompletion = cmd.EnableShellCompletion && cmd.shellCompletion
}
tracef("using post-checkShellCompleteFlag arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name)
tracef("setting self as cmd in context (cmd=%[1]q)", cmd.Name)
ctx = context.WithValue(ctx, commandContextKey, cmd)
if cmd.parent == nil {
cmd.setupCommandGraph()
}
var rargs Args = &stringSliceArgs{v: osArgs}
var args Args = &stringSliceArgs{rargs.Tail()}
if cmd.isCompletionCommand {
tracef("completion command detected, skipping pre-parse (cmd=%[1]q)", cmd.Name)
cmd.parsedArgs = args
return ctx, cmd.Action(ctx, cmd)
}
for _, f := range cmd.allFlags() {
if err := f.PreParse(); err != nil {
return ctx, err
}
}
var err error
if cmd.SkipFlagParsing {
tracef("skipping flag parsing (cmd=%[1]q)", cmd.Name)
cmd.parsedArgs = args
} else {
cmd.parsedArgs, err = cmd.parseFlags(args)
}
tracef("using post-parse arguments %[1]q (cmd=%[2]q)", args, cmd.Name)
if checkCompletions(ctx, cmd) {
return ctx, nil
}
if err != nil {
tracef("setting deferErr from %[1]q (cmd=%[2]q)", err, cmd.Name)
deferErr = err
cmd.isInError = true
if cmd.OnUsageError != nil {
err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil)
err = cmd.handleExitCoder(ctx, err)
return ctx, err
}
fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error())
if cmd.Suggest {
if suggestion, err := cmd.suggestFlagFromError(err, ""); err == nil {
fmt.Fprintf(cmd.Root().ErrWriter, "%s", suggestion)
}
}
if !cmd.hideHelp() {
if cmd.parent == nil {
tracef("running ShowRootCommandHelp")
if err := ShowRootCommandHelp(cmd); err != nil {
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)
}
}
}
return ctx, err
}
if cmd.checkHelp() {
return ctx, helpCommandAction(ctx, cmd)
} else {
tracef("no help is wanted (cmd=%[1]q)", cmd.Name)
}
if cmd.parent == nil && !cmd.HideVersion && checkVersion(cmd) {
ShowVersion(cmd)
return ctx, nil
}
for _, flag := range cmd.allFlags() {
cmd.setMultiValueParsingConfig(flag)
isSet := flag.IsSet()
if err := flag.PostParse(); err != nil {
return ctx, err
}
// add env set flags here
if !isSet && flag.IsSet() {
cmd.setFlags[flag] = struct{}{}
}
}
if cmd.After != nil && !cmd.Root().shellCompletion {
defer func() {
if err := cmd.After(ctx, cmd); err != nil {
err = cmd.handleExitCoder(ctx, err)
if deferErr != nil {
deferErr = newMultiError(deferErr, err)
} else {
deferErr = err
}
}
}()
}
// Walk the parent chain to check mutually exclusive flag groups
// defined on ancestor commands, since persistent flags are inherited.
for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent {
for _, grp := range pCmd.MutuallyExclusiveFlags {
if err := grp.check(cmd); err != nil {
if cmd.OnUsageError != nil {
err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil)
} else {
_ = ShowSubcommandHelp(cmd)
}
return ctx, err
}
}
}
var subCmd *Command
if cmd.parsedArgs.Present() {
tracef("checking positional args %[1]q (cmd=%[2]q)", cmd.parsedArgs, cmd.Name)
name := cmd.parsedArgs.First()
tracef("using first positional argument as sub-command name=%[1]q (cmd=%[2]q)", name, cmd.Name)
if cmd.SuggestCommandFunc != nil && name != "--" {
name = cmd.SuggestCommandFunc(cmd.Commands, name)
tracef("suggested command name=%1[q] (cmd=%[2]q)", name, cmd.Name)
}
subCmd = cmd.Command(name)
if subCmd == nil {
hasDefault := cmd.DefaultCommand != ""
isFlagName := slices.Contains(cmd.FlagNames(), name)
if hasDefault {
tracef("using default command=%[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name)
}
if isFlagName || hasDefault {
argsWithDefault := cmd.argsWithDefaultCommand(cmd.parsedArgs)
tracef("using default command args=%[1]q (cmd=%[2]q)", argsWithDefault, cmd.Name)
subCmd = cmd.Command(argsWithDefault.First())
cmd.parsedArgs = argsWithDefault
}
}
} else if cmd.DefaultCommand != "" {
tracef("no positional args present; checking default command %[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name)
if dc := cmd.Command(cmd.DefaultCommand); dc != cmd {
subCmd = dc
}
}
// If a subcommand has been resolved, let it handle the remaining execution.
if subCmd != nil {
tracef("running sub-command %[1]q with arguments %[2]q (cmd=%[3]q)", subCmd.Name, cmd.Args(), cmd.Name)
// It is important that we overwrite the ctx variable in the current
// function so any defer'd functions use the new context returned
// from the sub command.
ctx, err = subCmd.run(ctx, cmd.Args().Slice())
return ctx, err
}
// This code path is the innermost command execution. Here we actually
// 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)
// 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
}
}
// Run flag actions in order.
// These take a context, so this has to happen after Before actions.
for _, cmd := range cmdChain {
tracef("running flag actions (cmd=%[1]q)", cmd.Name)
if err := cmd.runFlagActions(ctx); err != nil {
deferErr = cmd.handleExitCoder(ctx, err)
return ctx, deferErr
}
}
if err := cmd.checkAllRequiredFlags(); err != nil {
cmd.isInError = true
if cmd.OnUsageError != nil {
err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil)
} else {
_ = ShowSubcommandHelp(cmd)
}
return ctx, err
}
// Run the command action.
if len(cmd.Arguments) > 0 {
rargs := cmd.Args().Slice()
tracef("calling argparse with %[1]v", rargs)
for _, arg := range cmd.Arguments {
var err error
rargs, err = arg.Parse(rargs)
if err != nil {
tracef("calling with %[1]v (cmd=%[2]q)", err, cmd.Name)
if cmd.OnUsageError != nil {
err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil)
}
err = cmd.handleExitCoder(ctx, err)
return ctx, err
}
}
cmd.parsedArgs = &stringSliceArgs{v: rargs}
}
if err := cmd.Action(ctx, cmd); err != nil {
tracef("calling handleExitCoder with %[1]v (cmd=%[2]q)", err, cmd.Name)
deferErr = cmd.handleExitCoder(ctx, err)
}
tracef("returning deferErr (cmd=%[1]q) %[2]q", cmd.Name, deferErr)
return ctx, deferErr
}
+223
View File
@@ -0,0 +1,223 @@
package cli
import (
"flag"
"os"
"path/filepath"
"sort"
"strings"
)
func (cmd *Command) setupDefaults(osArgs []string) {
if cmd.didSetupDefaults {
tracef("already did setup (cmd=%[1]q)", cmd.Name)
return
}
cmd.didSetupDefaults = true
isRoot := cmd.parent == nil
tracef("isRoot? %[1]v (cmd=%[2]q)", isRoot, cmd.Name)
if cmd.ShellComplete == nil {
tracef("setting default ShellComplete (cmd=%[1]q)", cmd.Name)
cmd.ShellComplete = DefaultCompleteWithFlags
}
if cmd.Name == "" && isRoot {
name := filepath.Base(osArgs[0])
tracef("setting cmd.Name from first arg basename (cmd=%[1]q)", name)
cmd.Name = name
}
if cmd.Usage == "" && isRoot {
tracef("setting default Usage (cmd=%[1]q)", cmd.Name)
cmd.Usage = "A new cli application"
}
if cmd.Version == "" {
tracef("setting HideVersion=true due to empty Version (cmd=%[1]q)", cmd.Name)
cmd.HideVersion = true
}
if cmd.Action == nil {
tracef("setting default Action as help command action (cmd=%[1]q)", cmd.Name)
cmd.Action = helpCommandAction
}
if cmd.Reader == nil {
tracef("setting default Reader as os.Stdin (cmd=%[1]q)", cmd.Name)
cmd.Reader = os.Stdin
}
if cmd.Writer == nil {
tracef("setting default Writer as os.Stdout (cmd=%[1]q)", cmd.Name)
cmd.Writer = os.Stdout
}
if cmd.ErrWriter == nil {
tracef("setting default ErrWriter as os.Stderr (cmd=%[1]q)", cmd.Name)
cmd.ErrWriter = os.Stderr
}
if cmd.AllowExtFlags {
tracef("visiting all flags given AllowExtFlags=true (cmd=%[1]q)", cmd.Name)
// add global flags added by other packages
flag.VisitAll(func(f *flag.Flag) {
// skip test flags
if !strings.HasPrefix(f.Name, ignoreFlagPrefix) {
cmd.Flags = append(cmd.Flags, &extFlag{f})
}
})
}
for _, subCmd := range cmd.Commands {
tracef("setting sub-command (cmd=%[1]q) parent as self (cmd=%[2]q)", subCmd.Name, cmd.Name)
subCmd.parent = cmd
}
cmd.ensureHelp()
if !cmd.HideVersion && isRoot {
tracef("appending version flag (cmd=%[1]q)", cmd.Name)
if !cmd.globaVersionFlagAdded {
var localVersionFlag Flag
if globalVersionFlag, ok := VersionFlag.(*BoolFlag); ok {
flag := *globalVersionFlag
localVersionFlag = &flag
} else {
localVersionFlag = VersionFlag
}
cmd.appendFlag(localVersionFlag)
cmd.globaVersionFlagAdded = true
}
}
if cmd.PrefixMatchCommands && cmd.SuggestCommandFunc == nil {
tracef("setting default SuggestCommandFunc (cmd=%[1]q)", cmd.Name)
cmd.SuggestCommandFunc = suggestCommand
}
if isRoot && cmd.EnableShellCompletion || cmd.ConfigureShellCompletionCommand != nil {
completionCommand := buildCompletionCommand(cmd.Name)
if cmd.ShellCompletionCommandName != "" {
tracef(
"setting completion command name (%[1]q) from "+
"cmd.ShellCompletionCommandName (cmd=%[2]q)",
cmd.ShellCompletionCommandName, cmd.Name,
)
completionCommand.Name = cmd.ShellCompletionCommandName
}
tracef("appending completionCommand (cmd=%[1]q)", cmd.Name)
cmd.appendCommand(completionCommand)
if cmd.ConfigureShellCompletionCommand != nil {
cmd.ConfigureShellCompletionCommand(completionCommand)
}
}
tracef("setting command categories (cmd=%[1]q)", cmd.Name)
cmd.categories = newCommandCategories()
for _, subCmd := range cmd.Commands {
cmd.categories.AddCommand(subCmd.Category, subCmd)
}
tracef("sorting command categories (cmd=%[1]q)", cmd.Name)
sort.Sort(cmd.categories.(*commandCategories))
tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name)
for _, grp := range cmd.MutuallyExclusiveFlags {
grp.propagateCategory()
}
tracef("setting flag categories (cmd=%[1]q)", cmd.Name)
cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags())
if cmd.Metadata == nil {
tracef("setting default Metadata (cmd=%[1]q)", cmd.Name)
cmd.Metadata = map[string]any{}
}
cmd.setFlags = map[Flag]struct{}{}
}
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()
}
}
func (cmd *Command) setupSubcommand() {
tracef("setting up self as sub-command (cmd=%[1]q)", cmd.Name)
cmd.ensureHelp()
tracef("setting command categories (cmd=%[1]q)", cmd.Name)
cmd.categories = newCommandCategories()
for _, subCmd := range cmd.Commands {
cmd.categories.AddCommand(subCmd.Category, subCmd)
}
tracef("sorting command categories (cmd=%[1]q)", cmd.Name)
sort.Sort(cmd.categories.(*commandCategories))
tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name)
for _, grp := range cmd.MutuallyExclusiveFlags {
grp.propagateCategory()
}
tracef("setting flag categories (cmd=%[1]q)", cmd.Name)
cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags())
}
func (cmd *Command) hideHelp() bool {
tracef("hide help (cmd=%[1]q)", cmd.Name)
for c := cmd; c != nil; c = c.parent {
if c.HideHelp {
return true
}
}
return false
}
func (cmd *Command) ensureHelp() {
tracef("ensuring help (cmd=%[1]q)", cmd.Name)
helpCommand := buildHelpCommand(true)
if !cmd.hideHelp() {
if cmd.Command(helpCommand.Name) == nil {
if !cmd.HideHelpCommand {
tracef("appending helpCommand (cmd=%[1]q)", cmd.Name)
cmd.appendCommand(helpCommand)
}
}
if HelpFlag != nil {
if !cmd.globaHelpFlagAdded {
var localHelpFlag Flag
if globalHelpFlag, ok := HelpFlag.(*BoolFlag); ok {
flag := *globalHelpFlag
localHelpFlag = &flag
} else {
localHelpFlag = HelpFlag
}
tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name)
cmd.appendFlag(localHelpFlag)
cmd.globaHelpFlagAdded = true
} else {
tracef("HelpFlag already added, skip (cmd=%[1]q)", cmd.Name)
}
}
}
}
+102
View File
@@ -0,0 +1,102 @@
package cli
import (
"context"
"embed"
"fmt"
"sort"
"strings"
)
const (
completionCommandName = "completion"
// This flag is supposed to only be used by the completion script itself to generate completions on the fly.
completionFlag = "--generate-shell-completion"
)
type renderCompletion func(cmd *Command, appName string) (string, error)
var (
//go:embed autocomplete
autoCompleteFS embed.FS
shellCompletions = map[string]renderCompletion{
"bash": func(c *Command, appName string) (string, error) {
b, err := autoCompleteFS.ReadFile("autocomplete/bash_autocomplete")
return fmt.Sprintf(string(b), appName), err
},
"zsh": func(c *Command, appName string) (string, error) {
b, err := autoCompleteFS.ReadFile("autocomplete/zsh_autocomplete")
return fmt.Sprintf(string(b), appName), err
},
"fish": func(c *Command, appName string) (string, error) {
b, err := autoCompleteFS.ReadFile("autocomplete/fish_autocomplete")
return fmt.Sprintf(string(b), appName), err
},
"pwsh": func(c *Command, appName string) (string, error) {
b, err := autoCompleteFS.ReadFile("autocomplete/powershell_autocomplete.ps1")
return string(b), err
},
}
)
const completionDescription = `Output shell completion script for bash, zsh, fish, or Powershell.
Source the output to enable completion.
# .bashrc
source <($COMMAND completion bash)
# .zshrc
source <($COMMAND completion zsh)
# fish
$COMMAND completion fish > ~/.config/fish/completions/$COMMAND.fish
# Powershell
Output the script to path/to/autocomplete/$COMMAND.ps1 an run it.
`
func buildCompletionCommand(appName string) *Command {
return &Command{
Name: completionCommandName,
Hidden: true,
Usage: "Output shell completion script for bash, zsh, fish, or Powershell",
Description: strings.ReplaceAll(completionDescription, "$COMMAND", appName),
Action: func(ctx context.Context, cmd *Command) error {
return printShellCompletion(ctx, cmd, appName)
},
isCompletionCommand: true,
}
}
func printShellCompletion(_ context.Context, cmd *Command, appName string) error {
var shells []string
for k := range shellCompletions {
shells = append(shells, k)
}
sort.Strings(shells)
if cmd.Args().Len() == 0 {
return Exit(fmt.Sprintf("no shell provided for completion command. available shells are %+v", shells), 1)
}
s := cmd.Args().First()
renderCompletion, ok := shellCompletions[s]
if !ok {
return Exit(fmt.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1)
}
completionScript, err := renderCompletion(cmd, appName)
if err != nil {
return Exit(err, 1)
}
_, err = cmd.Writer.Write([]byte(completionScript))
if err != nil {
return Exit(err, 1)
}
return nil
}
+128
View File
@@ -0,0 +1,128 @@
package cli
import (
"fmt"
"os"
"runtime"
"strings"
)
func prefixFor(name string) (prefix string) {
if len(name) == 1 {
prefix = "-"
} else {
prefix = "--"
}
return prefix
}
// Returns the placeholder, if any, and the unquoted usage string.
func unquoteUsage(usage string) (string, string) {
for i := 0; i < len(usage); i++ {
if usage[i] == '`' {
for j := i + 1; j < len(usage); j++ {
if usage[j] == '`' {
name := usage[i+1 : j]
usage = usage[:i] + name + usage[j+1:]
return name, usage
}
}
break
}
}
return "", usage
}
func prefixedNames(names []string, placeholder string) string {
var prefixed strings.Builder
for i, name := range names {
if name == "" {
continue
}
prefixed.WriteString(prefixFor(name) + name)
if placeholder != "" {
prefixed.WriteString(" " + placeholder)
}
if i < len(names)-1 {
prefixed.WriteString(", ")
}
}
return prefixed.String()
}
func envFormat(envVars []string, prefix, sep, suffix string) string {
if len(envVars) > 0 {
return fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix)
}
return ""
}
func defaultEnvFormat(envVars []string) string {
return envFormat(envVars, "$", ", $", "")
}
func withEnvHint(envVars []string, str string) string {
envText := ""
if runtime.GOOS != "windows" || os.Getenv("PSHOME") != "" {
envText = defaultEnvFormat(envVars)
} else {
envText = envFormat(envVars, "%", "%, %", "%")
}
return str + envText
}
func withFileHint(filePath, str string) string {
fileText := ""
if filePath != "" {
fileText = fmt.Sprintf(" [%s]", filePath)
}
return str + fileText
}
func formatDefault(format string) string {
return " (default: " + format + ")"
}
func stringifyFlag(f Flag) string {
// enforce DocGeneration interface on flags to avoid reflection
df, ok := f.(DocGenerationFlag)
if !ok {
return ""
}
placeholder, usage := unquoteUsage(df.GetUsage())
needsPlaceholder := df.TakesValue()
// if needsPlaceholder is true, placeholder is empty
if needsPlaceholder && placeholder == "" {
// try to get type from flag
if tname := df.TypeName(); tname != "" {
placeholder = tname
} else {
placeholder = defaultPlaceholder
}
}
defaultValueString := ""
// don't print default text for required flags
if rf, ok := f.(RequiredFlag); !ok || !rf.IsRequired() {
if df.IsDefaultVisible() {
if s := df.GetDefaultText(); s != "" {
defaultValueString = fmt.Sprintf(formatDefault("%s"), s)
} else if df.TakesValue() && df.GetValue() != "" {
defaultValueString = fmt.Sprintf(formatDefault("%s"), df.GetValue())
}
}
}
usageWithDefault := strings.TrimSpace(usage + defaultValueString)
pn := prefixedNames(f.Names(), placeholder)
sliceFlag, ok := f.(DocGenerationMultiValueFlag)
if ok && sliceFlag.IsMultiValueFlag() {
pn = pn + " [ " + pn + " ]"
}
return withEnvHint(df.GetEnvVars(), fmt.Sprintf("%s\t%s", pn, usageWithDefault))
}
+184
View File
@@ -0,0 +1,184 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
)
// OsExiter is the function used when the app exits. If not set defaults to os.Exit.
var OsExiter = os.Exit
// ErrWriter is used to write errors to the user. This can be anything
// implementing the io.Writer interface and defaults to os.Stderr.
var ErrWriter io.Writer = os.Stderr
// MultiError is an error that wraps multiple errors.
type MultiError interface {
error
Errors() []error
}
// newMultiError creates a new MultiError. Pass in one or more errors.
func newMultiError(err ...error) MultiError {
ret := multiError(err)
return &ret
}
type multiError []error
// Error implements the error interface.
func (m *multiError) Error() string {
errs := make([]string, len(*m))
for i, err := range *m {
errs[i] = err.Error()
}
return strings.Join(errs, "\n")
}
// Errors returns a copy of the errors slice
func (m *multiError) Errors() []error {
errs := make([]error, len(*m))
copy(errs, *m)
return errs
}
type requiredFlagsErr interface {
error
}
type errRequiredFlags struct {
missingFlags []string
}
func (e *errRequiredFlags) Error() string {
if len(e.missingFlags) == 1 {
return fmt.Sprintf("Required flag %q not set", e.missingFlags[0])
}
joinedMissingFlags := strings.Join(e.missingFlags, ", ")
return fmt.Sprintf("Required flags %q not set", joinedMissingFlags)
}
type mutuallyExclusiveGroup struct {
flag1Name string
flag2Name string
}
func (e *mutuallyExclusiveGroup) Error() string {
return fmt.Sprintf("option %s cannot be set along with option %s", e.flag1Name, e.flag2Name)
}
type mutuallyExclusiveGroupRequiredFlag struct {
flags *MutuallyExclusiveFlags
}
func (e *mutuallyExclusiveGroupRequiredFlag) Error() string {
var missingFlags []string
for _, grpf := range e.flags.Flags {
var grpString []string
for _, f := range grpf {
grpString = append(grpString, f.Names()...)
}
missingFlags = append(missingFlags, strings.Join(grpString, " "))
}
return fmt.Sprintf("one of these flags needs to be provided: %s", strings.Join(missingFlags, ", "))
}
// ErrorFormatter is the interface that will suitably format the error output
type ErrorFormatter interface {
Format(s fmt.State, verb rune)
}
// ExitCoder is the interface checked by `Command` for a custom exit code.
type ExitCoder interface {
error
ExitCode() int
}
type exitError struct {
exitCode int
err error
}
// Exit wraps a message and exit code into an error, which by default is
// handled with a call to os.Exit during default error handling.
//
// This is the simplest way to trigger a non-zero exit code for a Command without
// having to call os.Exit manually. During testing, this behavior can be avoided
// by overriding the ExitErrHandler function on a Command or the package-global
// OsExiter function.
func Exit(message any, exitCode int) ExitCoder {
var err error
switch e := message.(type) {
case ErrorFormatter:
err = fmt.Errorf("%+v", message)
case error:
err = e
default:
err = fmt.Errorf("%+v", message)
}
return &exitError{
err: err,
exitCode: exitCode,
}
}
func (ee *exitError) Error() string {
return ee.err.Error()
}
func (ee *exitError) ExitCode() int {
return ee.exitCode
}
// HandleExitCoder handles errors implementing ExitCoder by printing their
// message and calling OsExiter with the given exit code.
//
// If the given error instead implements MultiError, each error will be checked
// for the ExitCoder interface, and OsExiter will be called with the last exit
// code found, or exit code 1 if no ExitCoder is found.
//
// This function is the default error-handling behavior for a Command.
func HandleExitCoder(err error) {
if err == nil {
return
}
if exitErr, ok := err.(ExitCoder); ok {
if msg := err.Error(); msg != "" {
if _, ok := exitErr.(ErrorFormatter); ok {
_, _ = fmt.Fprintf(ErrWriter, "%+v\n", err)
} else {
_, _ = fmt.Fprintln(ErrWriter, err)
}
}
OsExiter(exitErr.ExitCode())
return
}
if multiErr, ok := err.(MultiError); ok {
code := handleMultiError(multiErr)
OsExiter(code)
return
}
}
func handleMultiError(multiErr MultiError) int {
code := 1
for _, merr := range multiErr.Errors() {
if multiErr2, ok := merr.(MultiError); ok {
code = handleMultiError(multiErr2)
} else if merr != nil {
fmt.Fprintln(ErrWriter, merr)
if exitErr, ok := merr.(ExitCoder); ok {
code = exitErr.ExitCode()
}
}
}
return code
}
+221
View File
@@ -0,0 +1,221 @@
package cli
import (
"bytes"
"fmt"
"io"
"strings"
"text/template"
)
// ToFishCompletion creates a fish completion string for the `*Command`
// The function errors if either parsing or writing of the string fails.
func (cmd *Command) ToFishCompletion() (string, error) {
var w bytes.Buffer
if err := cmd.writeFishCompletionTemplate(&w); err != nil {
return "", err
}
return w.String(), nil
}
type fishCommandCompletionTemplate struct {
Command *Command
Completions []string
AllCommands []string
}
func (cmd *Command) writeFishCompletionTemplate(w io.Writer) error {
const name = "cli"
t, err := template.New(name).Parse(FishCompletionTemplate)
if err != nil {
return err
}
// Add global flags
completions := prepareFishFlags(cmd.Name, cmd)
if cmd.ShellComplete != nil {
var completion strings.Builder
fmt.Fprintf(&completion,
"complete -c %s -n '%s' -xa '(%s %s 2>/dev/null)'",
cmd.Name,
fishFlagHelper(cmd.Name, cmd),
cmd.Name,
completionFlag,
)
completions = append(completions, completion.String())
}
// Add commands and their flags
completions = append(
completions,
prepareFishCommands(cmd.Name, cmd)...,
)
toplevelCommandNames := []string{}
for _, child := range cmd.Commands {
toplevelCommandNames = append(toplevelCommandNames, child.Names()...)
}
return t.ExecuteTemplate(w, name, &fishCommandCompletionTemplate{
Command: cmd,
Completions: completions,
AllCommands: toplevelCommandNames,
})
}
func prepareFishCommands(binary string, parent *Command) []string {
commands := parent.Commands
completions := []string{}
for _, command := range commands {
if !command.Hidden {
var completion strings.Builder
fmt.Fprintf(&completion,
"complete -x -c %s -n '%s' -a '%s'",
binary,
fishSubcommandHelper(binary, parent, commands),
command.Name,
)
if command.Usage != "" {
fmt.Fprintf(&completion,
" -d '%s'",
escapeSingleQuotes(command.Usage))
}
completions = append(completions, completion.String())
}
if command.ShellComplete != nil {
var completion strings.Builder
var path []string
lineage := command.Lineage()
for i := len(lineage) - 2; i >= 0; i-- {
path = append(path, lineage[i].Name)
}
fmt.Fprintf(&completion,
"complete -c %s -n '%s' -xa '(%s %s %s 2>/dev/null)'",
binary,
fishFlagHelper(binary, command),
binary,
strings.Join(path, " "),
completionFlag,
)
completions = append(completions, completion.String())
}
completions = append(
completions,
prepareFishFlags(binary, command)...,
)
// recursively iterate subcommands
completions = append(
completions,
prepareFishCommands(binary, command)...,
)
}
return completions
}
func prepareFishFlags(binary string, owner *Command) []string {
flags := owner.VisibleFlags()
completions := []string{}
for _, f := range flags {
completion := &strings.Builder{}
fmt.Fprintf(completion,
"complete -c %s -n '%s'",
binary,
fishFlagHelper(binary, owner),
)
fishAddFileFlag(f, completion)
for idx, opt := range f.Names() {
if idx == 0 {
fmt.Fprintf(completion,
" -l %s", strings.TrimSpace(opt),
)
} else {
fmt.Fprintf(completion,
" -s %s", strings.TrimSpace(opt),
)
}
}
if flag, ok := f.(DocGenerationFlag); ok {
if flag.TakesValue() {
completion.WriteString(" -r")
}
if flag.GetUsage() != "" {
fmt.Fprintf(completion,
" -d '%s'",
escapeSingleQuotes(flag.GetUsage()))
}
}
completions = append(completions, completion.String())
}
return completions
}
func fishAddFileFlag(flag Flag, completion *strings.Builder) {
switch f := flag.(type) {
case *StringFlag:
if f.TakesFile {
return
}
case *StringSliceFlag:
if f.TakesFile {
return
}
}
completion.WriteString(" -f")
}
func fishSubcommandHelper(binary string, command *Command, siblings []*Command) string {
fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary)
if len(command.Lineage()) > 1 {
var siblingNames []string
for _, sibling := range siblings {
siblingNames = append(siblingNames, sibling.Names()...)
}
ancestry := commandAncestry(command)
fishHelper = fmt.Sprintf(
"%s; and not __fish_seen_subcommand_from %s",
ancestry,
strings.Join(siblingNames, " "),
)
}
return fishHelper
}
func fishFlagHelper(binary string, command *Command) string {
fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", binary)
if len(command.Lineage()) > 1 {
fishHelper = commandAncestry(command)
}
return fishHelper
}
func commandAncestry(command *Command) string {
var ancestry []string
ancestors := command.Lineage()
for i := len(ancestors) - 2; i >= 0; i-- {
ancestry = append(
ancestry,
fmt.Sprintf(
"__fish_seen_subcommand_from %s",
strings.Join(ancestors[i].Names(), " "),
),
)
}
return strings.Join(ancestry, "; and ")
}
func escapeSingleQuotes(input string) string {
return strings.ReplaceAll(input, `'`, `\'`)
}
+223
View File
@@ -0,0 +1,223 @@
package cli
import (
"context"
"fmt"
"slices"
"strings"
"time"
)
const defaultPlaceholder = "value"
const (
defaultSliceFlagSeparator = ","
defaultMapFlagKeyValueSeparator = "="
disableSliceFlagSeparator = false
)
var slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano())
// GenerateShellCompletionFlag enables shell completion
var GenerateShellCompletionFlag Flag = &BoolFlag{
Name: "generate-shell-completion",
Hidden: true,
}
// VersionFlag prints the version for the application
var VersionFlag Flag = &BoolFlag{
Name: "version",
Aliases: []string{"v"},
Usage: "print the version",
HideDefault: true,
Local: true,
}
// HelpFlag prints the help for all commands and subcommands.
// Set to nil to disable the flag. The subcommand
// will still be added unless HideHelp or HideHelpCommand is set to true.
var HelpFlag Flag = &BoolFlag{
Name: "help",
Aliases: []string{"h"},
Usage: "show help",
HideDefault: true,
Local: true,
}
// FlagStringer converts a flag definition to a string. This is used by help
// to display a flag.
var FlagStringer FlagStringFunc = stringifyFlag
// Serializer is used to circumvent the limitations of flag.FlagSet.Set
type Serializer interface {
Serialize() string
}
// FlagNamePrefixer converts a full flag name and its placeholder into the help
// message flag prefix. This is used by the default FlagStringer.
var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames
// FlagEnvHinter annotates flag help message with the environment variable
// details. This is used by the default FlagStringer.
var FlagEnvHinter FlagEnvHintFunc = withEnvHint
// FlagFileHinter annotates flag help message with the environment variable
// details. This is used by the default FlagStringer.
var FlagFileHinter FlagFileHintFunc = withFileHint
// FlagsByName is a slice of Flag.
type FlagsByName []Flag
func (f FlagsByName) Len() int {
return len(f)
}
func (f FlagsByName) Less(i, j int) bool {
return lexicographicLess(f[i].Names()[0], f[j].Names()[0])
}
func (f FlagsByName) Swap(i, j int) {
f[i], f[j] = f[j], f[i]
}
// ActionableFlag is an interface that wraps Flag interface and RunAction operation.
type ActionableFlag interface {
RunAction(context.Context, *Command) error
}
// Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recommended that
// this interface be implemented.
type Flag interface {
fmt.Stringer
// Retrieve the value of the Flag
Get() any
// Lifecycle methods.
// flag callback prior to parsing
PreParse() error
// flag callback post parsing
PostParse() error
// Apply Flag settings to the given flag set
Set(string, string) error
// All possible names for this flag
Names() []string
// Whether the flag has been set or not
IsSet() bool
}
// RequiredFlag is an interface that allows us to mark flags as required
// it allows flags required flags to be backwards compatible with the Flag interface
type RequiredFlag interface {
// whether the flag is a required flag or not
IsRequired() bool
}
// DocGenerationFlag is an interface that allows documentation generation for the flag
type DocGenerationFlag interface {
// TakesValue returns true if the flag takes a value, otherwise false
TakesValue() bool
// GetUsage returns the usage string for the flag
GetUsage() string
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
GetValue() string
// GetDefaultText returns the default text for this flag
GetDefaultText() string
// GetEnvVars returns the env vars for this flag
GetEnvVars() []string
// IsDefaultVisible returns whether the default value should be shown in
// help text
IsDefaultVisible() bool
// TypeName to detect if a flag is a string, bool, etc.
TypeName() string
}
// DocGenerationMultiValueFlag extends DocGenerationFlag for slice/map based flags.
type DocGenerationMultiValueFlag interface {
DocGenerationFlag
// IsMultiValueFlag returns true for flags that can be given multiple times.
IsMultiValueFlag() bool
}
// Countable is an interface to enable detection of flag values which support
// repetitive flags
type Countable interface {
Count() int
}
// VisibleFlag is an interface that allows to check if a flag is visible
type VisibleFlag interface {
// IsVisible returns true if the flag is not hidden, otherwise false
IsVisible() bool
}
// CategorizableFlag is an interface that allows us to potentially
// use a flag in a categorized representation.
type CategorizableFlag interface {
// Returns the category of the flag
GetCategory() string
// Sets the category of the flag
SetCategory(string)
}
// LocalFlag is an interface to enable detection of flags which are local
// to current command
type LocalFlag interface {
IsLocal() bool
}
func visibleFlags(fl []Flag) []Flag {
var visible []Flag
for _, f := range fl {
if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() {
visible = append(visible, f)
}
}
return visible
}
func FlagNames(name string, aliases []string) []string {
var ret []string
for _, part := range append([]string{name}, aliases...) {
// v1 -> v2 migration warning zone:
// Strip off anything after the first found comma or space, which
// *hopefully* makes it a tiny bit more obvious that unexpected behavior is
// caused by using the v1 form of stringly typed "Name".
if i := strings.IndexAny(part, ", "); i >= 0 {
ret = append(ret, part[:i])
} else {
ret = append(ret, part)
}
}
return ret
}
func hasFlag(flags []Flag, fl Flag) bool {
return slices.Contains(flags, fl)
}
func flagSplitMultiValues(val string, sliceSeparator string, disableSliceSeparator bool) []string {
if disableSliceSeparator {
return []string{val}
}
if len(sliceSeparator) == 0 {
sliceSeparator = defaultSliceFlagSeparator
}
return strings.Split(val, sliceSeparator)
}
+78
View File
@@ -0,0 +1,78 @@
package cli
import (
"errors"
"strconv"
)
type BoolFlag = FlagBase[bool, BoolConfig, boolValue]
// BoolConfig defines the configuration for bool flags
type BoolConfig struct {
Count *int
}
// boolValue needs to implement the boolFlag internal interface in flag
// to be able to capture bool fields and values
//
// type boolFlag interface {
// Value
// IsBoolFlag() bool
// }
type boolValue struct {
destination *bool
count *int
}
func (cmd *Command) Bool(name string) bool {
if v, ok := cmd.Value(name).(bool); ok {
tracef("bool available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("bool NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return false
}
// Below functions are to satisfy the ValueCreator interface
// Create creates the bool value
func (b boolValue) Create(val bool, p *bool, c BoolConfig) Value {
*p = val
if c.Count == nil {
c.Count = new(int)
}
return &boolValue{
destination: p,
count: c.Count,
}
}
// ToString formats the bool value
func (b boolValue) ToString(value bool) string {
b.destination = &value
return b.String()
}
// Below functions are to satisfy the flag.Value interface
func (b *boolValue) Set(s string) error {
v, err := strconv.ParseBool(s)
if err != nil {
err = errors.New("parse error")
return err
}
*b.destination = v
if b.count != nil {
*b.count = *b.count + 1
}
return err
}
func (b *boolValue) Get() any { return *b.destination }
func (b *boolValue) String() string {
return strconv.FormatBool(*b.destination)
}
func (b *boolValue) IsBoolFlag() bool { return true }
+265
View File
@@ -0,0 +1,265 @@
package cli
import (
"context"
"fmt"
"slices"
"strings"
)
var DefaultInverseBoolPrefix = "no-"
type BoolWithInverseFlag struct {
Name string `json:"name"` // name of the flag
Category string `json:"category"` // category of the flag, if any
DefaultText string `json:"defaultText"` // default text of the flag for usage purposes
HideDefault bool `json:"hideDefault"` // whether to hide the default value in output
Usage string `json:"usage"` // usage string for help output
Sources ValueSourceChain `json:"-"` // sources to load flag value from
Required bool `json:"required"` // whether the flag is required or not
Hidden bool `json:"hidden"` // whether to hide the flag in help output
Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well
Value bool `json:"defaultValue"` // default value for this flag if not set by from any source
Destination *bool `json:"-"` // destination pointer for value when set
Aliases []string `json:"aliases"` // Aliases that are allowed for this flag
TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes
Action func(context.Context, *Command, bool) error `json:"-"` // Action callback to be called when flag is set
OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line
Validator func(bool) error `json:"-"` // custom function to validate this flag value
ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not
Config BoolConfig `json:"config"` // Additional/Custom configuration associated with this flag type
InversePrefix string `json:"invPrefix"` // The prefix used to indicate a negative value. Default: `env` becomes `no-env`
// unexported fields for internal use
count int // number of times the flag has been set
hasBeenSet bool // whether the flag has been set from env or file
applied bool // whether the flag has been applied to a flag set already
value Value // value representing this flag's value
pset bool
nset bool
}
func (bif *BoolWithInverseFlag) IsSet() bool {
return bif.hasBeenSet
}
func (bif *BoolWithInverseFlag) Get() any {
return bif.value.Get()
}
func (bif *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error {
if bif.Action != nil {
return bif.Action(ctx, cmd, bif.Get().(bool))
}
return nil
}
func (bif *BoolWithInverseFlag) IsLocal() bool {
return bif.Local
}
func (bif *BoolWithInverseFlag) inversePrefix() string {
if bif.InversePrefix == "" {
bif.InversePrefix = DefaultInverseBoolPrefix
}
return bif.InversePrefix
}
func (bif *BoolWithInverseFlag) PreParse() error {
count := bif.Config.Count
if count == nil {
count = &bif.count
}
dest := bif.Destination
if dest == nil {
dest = new(bool)
}
*dest = bif.Value
bif.value = &boolValue{
destination: dest,
count: count,
}
// Validate the given default or values set from external sources as well
if bif.Validator != nil && bif.ValidateDefaults {
if err := bif.Validator(bif.value.Get().(bool)); err != nil {
return err
}
}
bif.applied = true
return nil
}
func (bif *BoolWithInverseFlag) PostParse() error {
tracef("postparse (flag=%[1]q)", bif.Name)
if !bif.hasBeenSet {
if val, source, found := bif.Sources.LookupWithSource(); found {
if val == "" {
val = "false"
}
if err := bif.Set(bif.Name, val); err != nil {
return fmt.Errorf(
"could not parse %[1]q as %[2]T value from %[3]s for flag %[4]s: %[5]s",
val, bif.Value, source, bif.Name, err,
)
}
bif.hasBeenSet = true
}
}
return nil
}
func (bif *BoolWithInverseFlag) Set(name, val string) error {
if bif.count > 0 && bif.OnlyOnce {
return fmt.Errorf("cant duplicate this flag")
}
bif.hasBeenSet = true
if slices.Contains(append([]string{bif.Name}, bif.Aliases...), name) {
if bif.nset {
return fmt.Errorf("cannot set both flags `--%s` and `--%s`", bif.Name, bif.inversePrefix()+bif.Name)
}
if err := bif.value.Set(val); err != nil {
return err
}
bif.pset = true
} else {
if bif.pset {
return fmt.Errorf("cannot set both flags `--%s` and `--%s`", bif.Name, bif.inversePrefix()+bif.Name)
}
if err := bif.value.Set("false"); err != nil {
return err
}
bif.nset = true
}
if bif.Validator != nil {
return bif.Validator(bif.value.Get().(bool))
}
return nil
}
func (bif *BoolWithInverseFlag) Names() []string {
names := append([]string{bif.Name}, bif.Aliases...)
for _, name := range names {
names = append(names, bif.inversePrefix()+name)
}
return names
}
func (bif *BoolWithInverseFlag) IsRequired() bool {
return bif.Required
}
func (bif *BoolWithInverseFlag) IsVisible() bool {
return !bif.Hidden
}
// String implements the standard Stringer interface.
//
// Example for BoolFlag{Name: "env"}
// --[no-]env (default: false)
//
// Example for BoolFlag{Name: "env", Aliases: []string{"e"}}
// --[no-]env, -e (default: false)
func (bif *BoolWithInverseFlag) String() string {
out := FlagStringer(bif)
i := strings.Index(out, "\t")
prefix := "--"
// single character flags are prefixed with `-` instead of `--`
if len(bif.Name) == 1 {
prefix = "-"
}
// Guard against a FlagStringer that returns a string without a tab (e.g.
// a custom stringer or the default stringer when the flag does not
// implement DocGenerationFlag). In that case treat the entire output as
// the tab-delimited suffix so the slice never goes out of bounds.
if i < 0 {
i = 0
}
var aliasParts []string
for _, alias := range bif.Aliases {
aPrefix := "--"
if len(alias) == 1 {
aPrefix = "-"
}
aliasParts = append(aliasParts, aPrefix+alias)
}
names := fmt.Sprintf("%s[%s]%s", prefix, bif.inversePrefix(), bif.Name)
if len(aliasParts) > 0 {
names = names + ", " + strings.Join(aliasParts, ", ")
}
return fmt.Sprintf("%s%s", names, out[i:])
}
// IsBoolFlag returns whether the flag doesnt need to accept args
func (bif *BoolWithInverseFlag) IsBoolFlag() bool {
return true
}
// Count returns the number of times this flag has been invoked
func (bif *BoolWithInverseFlag) Count() int {
return bif.count
}
// GetDefaultText returns the default text for this flag
func (bif *BoolWithInverseFlag) GetDefaultText() string {
if bif.Required {
return bif.DefaultText
}
return boolValue{}.ToString(bif.Value)
}
// GetCategory returns the category of the flag
func (bif *BoolWithInverseFlag) GetCategory() string {
return bif.Category
}
func (bif *BoolWithInverseFlag) SetCategory(c string) {
bif.Category = c
}
// GetUsage returns the usage string for the flag
func (bif *BoolWithInverseFlag) GetUsage() string {
return bif.Usage
}
// GetEnvVars returns the env vars for this flag
func (bif *BoolWithInverseFlag) GetEnvVars() []string {
return bif.Sources.EnvKeys()
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (bif *BoolWithInverseFlag) GetValue() string {
return ""
}
func (bif *BoolWithInverseFlag) TakesValue() bool {
return false
}
// IsDefaultVisible returns true if the flag is not hidden, otherwise false
func (bif *BoolWithInverseFlag) IsDefaultVisible() bool {
return !bif.HideDefault
}
// TypeName is used for stringify/docs. For bool its a no-op
func (bif *BoolWithInverseFlag) TypeName() string {
return "bool"
}
+50
View File
@@ -0,0 +1,50 @@
package cli
import (
"fmt"
"time"
)
type DurationFlag = FlagBase[time.Duration, NoConfig, durationValue]
// -- time.Duration Value
type durationValue time.Duration
// Below functions are to satisfy the ValueCreator interface
func (d durationValue) Create(val time.Duration, p *time.Duration, c NoConfig) Value {
*p = val
return (*durationValue)(p)
}
func (d durationValue) ToString(val time.Duration) string {
d = durationValue(val)
return d.String()
}
// Below functions are to satisfy the flag.Value interface
func (d *durationValue) Set(s string) error {
v, err := time.ParseDuration(s)
if err != nil {
return err
}
*d = durationValue(v)
return err
}
func (d *durationValue) Get() any { return time.Duration(*d) }
func (d *durationValue) String() string {
return fmt.Sprintf("%v", time.Duration(*d))
}
func (cmd *Command) Duration(name string) time.Duration {
if v, ok := cmd.Value(name).(time.Duration); ok {
tracef("duration available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("duration NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return 0
}
+63
View File
@@ -0,0 +1,63 @@
package cli
import "flag"
type extFlag struct {
f *flag.Flag
}
func (e *extFlag) PreParse() error {
if e.f.DefValue != "" {
return e.Set("", e.f.DefValue)
}
return nil
}
func (e *extFlag) PostParse() error {
return nil
}
func (e *extFlag) Set(_ string, val string) error {
return e.f.Value.Set(val)
}
func (e *extFlag) Get() any {
return e.f.Value.(flag.Getter).Get()
}
func (e *extFlag) Names() []string {
return []string{e.f.Name}
}
func (e *extFlag) IsSet() bool {
return false
}
func (e *extFlag) String() string {
return FlagStringer(e)
}
func (e *extFlag) IsVisible() bool {
return true
}
func (e *extFlag) TakesValue() bool {
return false
}
func (e *extFlag) GetUsage() string {
return e.f.Usage
}
func (e *extFlag) GetValue() string {
return e.f.Value.String()
}
func (e *extFlag) GetDefaultText() string {
return e.f.DefValue
}
func (e *extFlag) GetEnvVars() []string {
return nil
}
+76
View File
@@ -0,0 +1,76 @@
package cli
import (
"strconv"
"unsafe"
)
type (
FloatFlag = FlagBase[float64, NoConfig, floatValue[float64]]
Float32Flag = FlagBase[float32, NoConfig, floatValue[float32]]
Float64Flag = FlagBase[float64, NoConfig, floatValue[float64]]
)
// -- float Value
type floatValue[T float32 | float64] struct {
val *T
}
// Below functions are to satisfy the ValueCreator interface
func (f floatValue[T]) Create(val T, p *T, c NoConfig) Value {
*p = val
return &floatValue[T]{val: p}
}
func (f floatValue[T]) ToString(b T) string {
f.val = &b
return f.String()
}
// Below functions are to satisfy the flag.Value interface
func (f *floatValue[T]) Set(s string) error {
v, err := strconv.ParseFloat(s, int(unsafe.Sizeof(T(0))*8))
if err != nil {
return err
}
*f.val = T(v)
return nil
}
func (f *floatValue[T]) Get() any { return *f.val }
func (f *floatValue[T]) String() string {
return strconv.FormatFloat(float64(*f.val), 'g', -1, int(unsafe.Sizeof(T(0))*8))
}
// Float looks up the value of a local FloatFlag, returns
// 0 if not found
func (cmd *Command) Float(name string) float64 {
return getFloat[float64](cmd, name)
}
// Float32 looks up the value of a local Float32Flag, returns
// 0 if not found
func (cmd *Command) Float32(name string) float32 {
return getFloat[float32](cmd, name)
}
// Float64 looks up the value of a local Float64Flag, returns
// 0 if not found
func (cmd *Command) Float64(name string) float64 {
return getFloat[float64](cmd, name)
}
func getFloat[T float32 | float64](cmd *Command, name string) T {
if v, ok := cmd.Value(name).(T); ok {
tracef("float available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("float NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return 0
}
+34
View File
@@ -0,0 +1,34 @@
package cli
type (
FloatSlice = SliceBase[float64, NoConfig, floatValue[float64]]
Float32Slice = SliceBase[float32, NoConfig, floatValue[float32]]
Float64Slice = SliceBase[float64, NoConfig, floatValue[float64]]
FloatSliceFlag = FlagBase[[]float64, NoConfig, FloatSlice]
Float32SliceFlag = FlagBase[[]float32, NoConfig, Float32Slice]
Float64SliceFlag = FlagBase[[]float64, NoConfig, Float64Slice]
)
var (
NewFloatSlice = NewSliceBase[float64, NoConfig, floatValue[float64]]
NewFloat32Slice = NewSliceBase[float32, NoConfig, floatValue[float32]]
NewFloat64Slice = NewSliceBase[float64, NoConfig, floatValue[float64]]
)
// FloatSlice looks up the value of a local FloatSliceFlag, returns
// nil if not found
func (cmd *Command) FloatSlice(name string) []float64 {
return getNumberSlice[float64](cmd, name)
}
// Float32Slice looks up the value of a local Float32Slice, returns
// nil if not found
func (cmd *Command) Float32Slice(name string) []float32 {
return getNumberSlice[float32](cmd, name)
}
// Float64Slice looks up the value of a local Float64SliceFlag, returns
// nil if not found
func (cmd *Command) Float64Slice(name string) []float64 {
return getNumberSlice[float64](cmd, name)
}
+65
View File
@@ -0,0 +1,65 @@
package cli
type GenericFlag = FlagBase[Value, NoConfig, genericValue]
// -- Value Value
type genericValue struct {
val Value
}
// Below functions are to satisfy the ValueCreator interface
func (f genericValue) Create(val Value, p *Value, c NoConfig) Value {
*p = val
return &genericValue{
val: *p,
}
}
func (f genericValue) ToString(b Value) string {
f.val = b
return f.String()
}
// Below functions are to satisfy the flag.Value interface
func (f *genericValue) Set(s string) error {
if f.val != nil {
return f.val.Set(s)
}
return nil
}
func (f *genericValue) Get() any {
if f.val != nil {
return f.val.Get()
}
return nil
}
func (f *genericValue) String() string {
if f.val != nil {
return f.val.String()
}
return ""
}
func (f *genericValue) IsBoolFlag() bool {
if f.val == nil {
return false
}
bf, ok := f.val.(boolFlag)
return ok && bf.IsBoolFlag()
}
// Generic looks up the value of a local GenericFlag, returns
// nil if not found
func (cmd *Command) Generic(name string) Value {
if v, ok := cmd.Value(name).(Value); ok {
tracef("generic available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("generic NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return nil
}
+313
View File
@@ -0,0 +1,313 @@
package cli
import (
"context"
"flag"
"fmt"
"reflect"
"strings"
)
// Value represents a value as used by cli.
// For now it implements the golang flag.Value interface
type Value interface {
flag.Value
flag.Getter
}
type boolFlag interface {
IsBoolFlag() bool
}
type multiValueParsingConfig struct {
// SliceFlagSeparator is used to customize the separator for SliceFlag, the default is ","
SliceFlagSeparator string
// DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false
DisableSliceFlagSeparator bool
// MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "="
MapFlagKeyValueSeparator string
}
type multiValueParsingConfigSetter interface {
// configuration of parsing
setMultiValueParsingConfig(c multiValueParsingConfig)
}
// ValueCreator is responsible for creating a flag.Value emulation
// as well as custom formatting
//
// T specifies the type
// C specifies the config for the type
type ValueCreator[T any, C any] interface {
Create(T, *T, C) Value
ToString(T) string
}
// NoConfig is for flags which dont need a custom configuration
type NoConfig struct{}
// FlagBase [T,C,VC] is a generic flag base which can be used
// as a boilerplate to implement the most common interfaces
// used by urfave/cli.
//
// T specifies the type
// C specifies the configuration required(if any for that flag type)
// VC specifies the value creator which creates the flag.Value emulation
type FlagBase[T any, C any, VC ValueCreator[T, C]] struct {
Name string `json:"name"` // name of the flag
Category string `json:"category"` // category of the flag, if any
DefaultText string `json:"defaultText"` // default text of the flag for usage purposes
HideDefault bool `json:"hideDefault"` // whether to hide the default value in output
Usage string `json:"usage"` // usage string for help output
Sources ValueSourceChain `json:"-"` // sources to load flag value from
Required bool `json:"required"` // whether the flag is required or not
Hidden bool `json:"hidden"` // whether to hide the flag in help output
Local bool `json:"local"` // whether the flag needs to be applied to subcommands as well
Value T `json:"defaultValue"` // default value for this flag if not set by from any source
Destination *T `json:"-"` // destination pointer for value when set
Aliases []string `json:"aliases"` // Aliases that are allowed for this flag
TakesFile bool `json:"takesFileArg"` // whether this flag takes a file argument, mainly for shell completion purposes
Action func(context.Context, *Command, T) error `json:"-"` // Action callback to be called when flag is set
Config C `json:"config"` // Additional/Custom configuration associated with this flag type
OnlyOnce bool `json:"onlyOnce"` // whether this flag can be duplicated on the command line
Validator func(T) error `json:"-"` // custom function to validate this flag value
ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not
// unexported fields for internal use
count int // number of times the flag has been set
hasBeenSet bool // whether the flag has been set from env or file
applied bool // whether the flag has been applied to a flag set already
creator VC // value creator for this flag type
value Value // value representing this flag's value
}
// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *FlagBase[T, C, V]) GetValue() string {
var v V
return v.ToString(f.Value)
}
// TypeName returns the type of the flag.
func (f *FlagBase[T, C, V]) TypeName() string {
ty := reflect.TypeOf(f.Value)
if ty == nil {
return ""
}
// convert the typename to generic type
convertToGenericType := func(name string) string {
prefixMap := map[string]string{
"float": "float",
"int": "int",
"uint": "uint",
}
for prefix, genericType := range prefixMap {
if strings.HasPrefix(name, prefix) {
return genericType
}
}
return strings.ToLower(name)
}
switch ty.Kind() {
// if it is a Slice, then return the slice's inner type. Will nested slices be used in the future?
case reflect.Slice:
elemType := ty.Elem()
return convertToGenericType(elemType.Name())
// if it is a Map, then return the map's key and value types.
case reflect.Map:
keyType := ty.Key()
valueType := ty.Elem()
return fmt.Sprintf("%s=%s", convertToGenericType(keyType.Name()), convertToGenericType(valueType.Name()))
default:
return convertToGenericType(ty.Name())
}
}
// PostParse populates the flag given the flag set and environment
func (f *FlagBase[T, C, V]) PostParse() error {
tracef("postparse (flag=%[1]q)", f.Name)
if !f.hasBeenSet {
if val, source, found := f.Sources.LookupWithSource(); found {
if val != "" || reflect.TypeOf(f.Value).Kind() == reflect.String {
if err := f.Set(f.Name, val); err != nil {
return fmt.Errorf(
"could not parse %[1]q as %[2]T value from %[3]s for flag %[4]s: %[5]s",
val, f.Value, source, f.Name, err,
)
}
} else if val == "" && reflect.TypeOf(f.Value).Kind() == reflect.Bool {
_ = f.Set(f.Name, "false")
}
f.hasBeenSet = true
}
}
return nil
}
// pass configuration of parsing to value
func (f *FlagBase[T, C, V]) setMultiValueParsingConfig(c multiValueParsingConfig) {
tracef("setMultiValueParsingConfig %T, %+v", f.value, f.value)
if cf, ok := f.value.(multiValueParsingConfigSetter); ok {
cf.setMultiValueParsingConfig(c)
}
}
func (f *FlagBase[T, C, V]) PreParse() error {
newVal := f.Value
if f.Destination == nil {
f.value = f.creator.Create(newVal, new(T), f.Config)
} else {
f.value = f.creator.Create(newVal, f.Destination, f.Config)
}
// Validate the given default or values set from external sources as well
if f.Validator != nil && f.ValidateDefaults {
if err := f.Validator(f.value.Get().(T)); err != nil {
return err
}
}
f.applied = true
return nil
}
// Set applies given value from string
func (f *FlagBase[T, C, V]) Set(_ string, val string) error {
tracef("apply (flag=%[1]q)", f.Name)
// TODO move this phase into a separate flag initialization function
// if flag has been applied previously then it would have already been set
// from env or file. So no need to apply the env set again. However
// lots of units tests prior to persistent flags assumed that the
// flag can be applied to different flag sets multiple times while still
// keeping the env set.
if !f.applied {
if err := f.PreParse(); err != nil {
return err
}
f.applied = true
}
if f.count == 1 && f.OnlyOnce {
return fmt.Errorf("cant duplicate this flag")
}
f.count++
if err := f.value.Set(val); err != nil {
return err
}
f.hasBeenSet = true
if f.Validator != nil {
if err := f.Validator(f.value.Get().(T)); err != nil {
return err
}
}
return nil
}
func (f *FlagBase[T, C, V]) Get() any {
if f.value != nil {
return f.value.Get()
}
return f.Value
}
// IsDefaultVisible returns true if the flag is not hidden, otherwise false
func (f *FlagBase[T, C, V]) IsDefaultVisible() bool {
return !f.HideDefault
}
// String returns a readable representation of this value (for usage defaults)
func (f *FlagBase[T, C, V]) String() string {
return FlagStringer(f)
}
// IsSet returns whether or not the flag has been set through env or file
func (f *FlagBase[T, C, V]) IsSet() bool {
return f.hasBeenSet
}
// Names returns the names of the flag
func (f *FlagBase[T, C, V]) Names() []string {
return FlagNames(f.Name, f.Aliases)
}
// IsRequired returns whether or not the flag is required
func (f *FlagBase[T, C, V]) IsRequired() bool {
return f.Required
}
// IsVisible returns true if the flag is not hidden, otherwise false
func (f *FlagBase[T, C, V]) IsVisible() bool {
return !f.Hidden
}
// GetCategory returns the category of the flag
func (f *FlagBase[T, C, V]) GetCategory() string {
return f.Category
}
func (f *FlagBase[T, C, V]) SetCategory(c string) {
f.Category = c
}
// GetUsage returns the usage string for the flag
func (f *FlagBase[T, C, V]) GetUsage() string {
return f.Usage
}
// GetEnvVars returns the env vars for this flag
func (f *FlagBase[T, C, V]) GetEnvVars() []string {
return f.Sources.EnvKeys()
}
// TakesValue returns true if the flag takes a value, otherwise false
func (f *FlagBase[T, C, V]) TakesValue() bool {
var t T
return reflect.TypeOf(t) == nil || reflect.TypeOf(t).Kind() != reflect.Bool
}
// GetDefaultText returns the default text for this flag
func (f *FlagBase[T, C, V]) GetDefaultText() string {
return f.DefaultText
}
// RunAction executes flag action if set
func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error {
if f.Action != nil {
return f.Action(ctx, cmd, f.value.Get().(T))
}
return nil
}
// 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 {
// TBD how to specify
if reflect.TypeOf(f.Value) == nil {
return false
}
kind := reflect.TypeOf(f.Value).Kind()
return kind == reflect.Slice || kind == reflect.Map
}
// IsLocal returns false if flag needs to be persistent across subcommands
func (f *FlagBase[T, C, VC]) IsLocal() bool {
return f.Local
}
// IsBoolFlag returns whether the flag doesnt need to accept args
func (f *FlagBase[T, C, VC]) IsBoolFlag() bool {
bf, ok := f.value.(boolFlag)
return ok && bf.IsBoolFlag()
}
// Count returns the number of times this flag has been invoked
func (f *FlagBase[T, C, VC]) Count() int {
return f.count
}
+104
View File
@@ -0,0 +1,104 @@
package cli
import (
"strconv"
"unsafe"
)
type (
IntFlag = FlagBase[int, IntegerConfig, intValue[int]]
Int8Flag = FlagBase[int8, IntegerConfig, intValue[int8]]
Int16Flag = FlagBase[int16, IntegerConfig, intValue[int16]]
Int32Flag = FlagBase[int32, IntegerConfig, intValue[int32]]
Int64Flag = FlagBase[int64, IntegerConfig, intValue[int64]]
)
// IntegerConfig is the configuration for all integer type flags
type IntegerConfig struct {
Base int
}
// -- int Value
type intValue[T int | int8 | int16 | int32 | int64] struct {
val *T
base int
}
// Below functions are to satisfy the ValueCreator interface
func (i intValue[T]) Create(val T, p *T, c IntegerConfig) Value {
*p = val
return &intValue[T]{
val: p,
base: c.Base,
}
}
func (i intValue[T]) ToString(b T) string {
i.val = &b
return i.String()
}
// Below functions are to satisfy the flag.Value interface
func (i *intValue[T]) Set(s string) error {
v, err := strconv.ParseInt(s, i.base, int(unsafe.Sizeof(T(0))*8))
if err != nil {
return err
}
*i.val = T(v)
return err
}
func (i *intValue[T]) Get() any { return *i.val }
func (i *intValue[T]) String() string {
base := i.base
if base == 0 {
base = 10
}
return strconv.FormatInt(int64(*i.val), base)
}
// Int looks up the value of a local Int64Flag, returns
// 0 if not found
func (cmd *Command) Int(name string) int {
return getInt[int](cmd, name)
}
// Int8 looks up the value of a local Int8Flag, returns
// 0 if not found
func (cmd *Command) Int8(name string) int8 {
return getInt[int8](cmd, name)
}
// Int16 looks up the value of a local Int16Flag, returns
// 0 if not found
func (cmd *Command) Int16(name string) int16 {
return getInt[int16](cmd, name)
}
// Int32 looks up the value of a local Int32Flag, returns
// 0 if not found
func (cmd *Command) Int32(name string) int32 {
return getInt[int32](cmd, name)
}
// Int64 looks up the value of a local Int64Flag, returns
// 0 if not found
func (cmd *Command) Int64(name string) int64 {
return getInt[int64](cmd, name)
}
func getInt[T int | int8 | int16 | int32 | int64](cmd *Command, name string) T {
if v, ok := cmd.Value(name).(T); ok {
tracef("int available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("int NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return 0
}
+52
View File
@@ -0,0 +1,52 @@
package cli
type (
IntSlice = SliceBase[int, IntegerConfig, intValue[int]]
Int8Slice = SliceBase[int8, IntegerConfig, intValue[int8]]
Int16Slice = SliceBase[int16, IntegerConfig, intValue[int16]]
Int32Slice = SliceBase[int32, IntegerConfig, intValue[int32]]
Int64Slice = SliceBase[int64, IntegerConfig, intValue[int64]]
IntSliceFlag = FlagBase[[]int, IntegerConfig, IntSlice]
Int8SliceFlag = FlagBase[[]int8, IntegerConfig, Int8Slice]
Int16SliceFlag = FlagBase[[]int16, IntegerConfig, Int16Slice]
Int32SliceFlag = FlagBase[[]int32, IntegerConfig, Int32Slice]
Int64SliceFlag = FlagBase[[]int64, IntegerConfig, Int64Slice]
)
var (
NewIntSlice = NewSliceBase[int, IntegerConfig, intValue[int]]
NewInt8Slice = NewSliceBase[int8, IntegerConfig, intValue[int8]]
NewInt16Slice = NewSliceBase[int16, IntegerConfig, intValue[int16]]
NewInt32Slice = NewSliceBase[int32, IntegerConfig, intValue[int32]]
NewInt64Slice = NewSliceBase[int64, IntegerConfig, intValue[int64]]
)
// IntSlice looks up the value of a local IntSliceFlag, returns
// nil if not found
func (cmd *Command) IntSlice(name string) []int {
return getNumberSlice[int](cmd, name)
}
// Int8Slice looks up the value of a local Int8SliceFlag, returns
// nil if not found
func (cmd *Command) Int8Slice(name string) []int8 {
return getNumberSlice[int8](cmd, name)
}
// Int16Slice looks up the value of a local Int16SliceFlag, returns
// nil if not found
func (cmd *Command) Int16Slice(name string) []int16 {
return getNumberSlice[int16](cmd, name)
}
// Int32Slice looks up the value of a local Int32SliceFlag, returns
// nil if not found
func (cmd *Command) Int32Slice(name string) []int32 {
return getNumberSlice[int32](cmd, name)
}
// Int64Slice looks up the value of a local Int64SliceFlag, returns
// nil if not found
func (cmd *Command) Int64Slice(name string) []int64 {
return getNumberSlice[int64](cmd, name)
}
+138
View File
@@ -0,0 +1,138 @@
package cli
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
)
// MapBase wraps map[string]T to satisfy flag.Value
type MapBase[T any, C any, VC ValueCreator[T, C]] struct {
dict *map[string]T
hasBeenSet bool
value Value
multiValueConfig multiValueParsingConfig
}
func (i MapBase[T, C, VC]) Create(val map[string]T, p *map[string]T, c C) Value {
*p = map[string]T{}
for k, v := range val {
(*p)[k] = v
}
var t T
np := new(T)
var vc VC
return &MapBase[T, C, VC]{
dict: p,
value: vc.Create(t, np, c),
}
}
// NewMapBase makes a *MapBase with default values
func NewMapBase[T any, C any, VC ValueCreator[T, C]](defaults map[string]T) *MapBase[T, C, VC] {
return &MapBase[T, C, VC]{
dict: &defaults,
}
}
// configuration of slicing
func (i *MapBase[T, C, VC]) setMultiValueParsingConfig(c multiValueParsingConfig) {
i.multiValueConfig = c
mvc := &i.multiValueConfig
tracef(
"set map parsing config - keyValueSeparator '%s', slice separator '%s', disable separator:%v",
mvc.MapFlagKeyValueSeparator,
mvc.SliceFlagSeparator,
mvc.DisableSliceFlagSeparator,
)
}
// Set parses the value and appends it to the list of values
func (i *MapBase[T, C, VC]) Set(value string) error {
if !i.hasBeenSet {
*i.dict = map[string]T{}
i.hasBeenSet = true
}
if strings.HasPrefix(value, slPfx) {
// Deserializing assumes overwrite
_ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.dict)
i.hasBeenSet = true
return nil
}
mvc := &i.multiValueConfig
keyValueSeparator := mvc.MapFlagKeyValueSeparator
if len(keyValueSeparator) == 0 {
keyValueSeparator = defaultMapFlagKeyValueSeparator
}
tracef(
"splitting map value '%s', keyValueSeparator '%s', slice separator '%s', disable separator:%v",
value,
keyValueSeparator,
mvc.SliceFlagSeparator,
mvc.DisableSliceFlagSeparator,
)
for _, item := range flagSplitMultiValues(value, mvc.SliceFlagSeparator, mvc.DisableSliceFlagSeparator) {
key, value, ok := strings.Cut(item, keyValueSeparator)
if !ok {
return fmt.Errorf("item %q is missing separator %q", item, keyValueSeparator)
}
if err := i.value.Set(value); err != nil {
return err
}
(*i.dict)[key] = i.value.Get().(T)
}
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (i *MapBase[T, C, VC]) String() string {
v := i.Value()
var t T
if reflect.TypeOf(t).Kind() == reflect.String {
return fmt.Sprintf("%v", v)
}
return fmt.Sprintf("%T{%s}", v, i.ToString(v))
}
// Serialize allows MapBase to fulfill Serializer
func (i *MapBase[T, C, VC]) Serialize() string {
jsonBytes, _ := json.Marshal(i.dict)
return fmt.Sprintf("%s%s", slPfx, string(jsonBytes))
}
// Value returns the mapping of values set by this flag
func (i *MapBase[T, C, VC]) Value() map[string]T {
if i.dict == nil {
return map[string]T{}
}
return *i.dict
}
// Get returns the mapping of values set by this flag
func (i *MapBase[T, C, VC]) Get() any {
return *i.dict
}
func (i MapBase[T, C, VC]) ToString(t map[string]T) string {
var defaultVals []string
var vc VC
for _, k := range sortedKeys(t) {
defaultVals = append(defaultVals, k+defaultMapFlagKeyValueSeparator+vc.ToString(t[k]))
}
return strings.Join(defaultVals, ", ")
}
func sortedKeys[T any](dict map[string]T) []string {
keys := make([]string, 0, len(dict))
for k := range dict {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
+54
View File
@@ -0,0 +1,54 @@
package cli
// MutuallyExclusiveFlags defines a mutually exclusive flag group
// Multiple option paths can be provided out of which
// only one can be defined on cmdline
// So for example
// [ --foo | [ --bar something --darth somethingelse ] ]
type MutuallyExclusiveFlags struct {
// Flag list
Flags [][]Flag
// whether this group is required
Required bool
// Category to apply to all flags within group
Category string
}
func (grp MutuallyExclusiveFlags) check(_ *Command) error {
oneSet := false
e := &mutuallyExclusiveGroup{}
for _, grpf := range grp.Flags {
for _, f := range grpf {
if f.IsSet() {
if oneSet {
e.flag2Name = f.Names()[0]
return e
}
e.flag1Name = f.Names()[0]
oneSet = true
break
}
if oneSet {
break
}
}
}
if !oneSet && grp.Required {
return &mutuallyExclusiveGroupRequiredFlag{flags: &grp}
}
return nil
}
func (grp MutuallyExclusiveFlags) propagateCategory() {
for _, grpf := range grp.Flags {
for _, f := range grpf {
if cf, ok := f.(CategorizableFlag); ok {
cf.SetCategory(grp.Category)
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
package cli
type numberType interface {
int | int8 | int16 | int32 | int64 | float32 | float64
}
func getNumberSlice[T numberType](cmd *Command, name string) []T {
if v, ok := cmd.Value(name).([]T); ok {
tracef("%T slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", *new(T), name, v, cmd.Name)
return v
}
tracef("%T slice NOT available for flag name %[1]q (cmd=%[2]q)", *new(T), name, cmd.Name)
return nil
}
+115
View File
@@ -0,0 +1,115 @@
package cli
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
// SliceBase wraps []T to satisfy flag.Value
type SliceBase[T any, C any, VC ValueCreator[T, C]] struct {
slice *[]T
hasBeenSet bool
value Value
sliceSeparator string
disableSliceSeparator bool
}
func (i SliceBase[T, C, VC]) Create(val []T, p *[]T, c C) Value {
*p = []T{}
*p = append(*p, val...)
var t T
np := new(T)
var vc VC
return &SliceBase[T, C, VC]{
slice: p,
value: vc.Create(t, np, c),
}
}
// NewSliceBase makes a *SliceBase with default values
func NewSliceBase[T any, C any, VC ValueCreator[T, C]](defaults ...T) *SliceBase[T, C, VC] {
return &SliceBase[T, C, VC]{
slice: &defaults,
}
}
// configuration of slicing
func (i *SliceBase[T, C, VC]) setMultiValueParsingConfig(c multiValueParsingConfig) {
i.disableSliceSeparator = c.DisableSliceFlagSeparator
i.sliceSeparator = c.SliceFlagSeparator
tracef("set slice parsing config - slice separator '%s', disable separator:%v", i.sliceSeparator, i.disableSliceSeparator)
}
// Set parses the value and appends it to the list of values
func (i *SliceBase[T, C, VC]) Set(value string) error {
if !i.hasBeenSet {
*i.slice = []T{}
i.hasBeenSet = true
}
if strings.HasPrefix(value, slPfx) {
// Deserializing assumes overwrite
_ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice)
i.hasBeenSet = true
return nil
}
trimSpace := true
// hack. How do we know if we should trim spaces?
// it makes sense only for string slice flags which have
// an option to not trim spaces. So by default we trim spaces
// otherwise we let the underlying value type handle it.
var t T
if reflect.TypeOf(t).Kind() == reflect.String {
trimSpace = false
}
tracef("splitting slice value '%s', separator '%s', disable separator:%v", value, i.sliceSeparator, i.disableSliceSeparator)
for _, s := range flagSplitMultiValues(value, i.sliceSeparator, i.disableSliceSeparator) {
if trimSpace {
s = strings.TrimSpace(s)
}
if err := i.value.Set(s); err != nil {
return err
}
*i.slice = append(*i.slice, i.value.Get().(T))
}
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (i *SliceBase[T, C, VC]) String() string {
var defaultVals []string
var v VC
for _, s := range *i.slice {
defaultVals = append(defaultVals, v.ToString(s))
}
return strings.Join(defaultVals, ", ")
}
// Serialize allows SliceBase to fulfill Serializer
func (i *SliceBase[T, C, VC]) Serialize() string {
jsonBytes, _ := json.Marshal(i.slice)
return fmt.Sprintf("%s%s", slPfx, string(jsonBytes))
}
// Value returns the slice of values set by this flag
func (i *SliceBase[T, C, VC]) Value() []T {
if i.slice == nil {
return nil
}
return *i.slice
}
// Get returns the slice of values set by this flag
func (i *SliceBase[T, C, VC]) Get() any {
return *i.slice
}
func (i SliceBase[T, C, VC]) ToString(t []T) string {
i.slice = &t
return i.String()
}
+64
View File
@@ -0,0 +1,64 @@
package cli
import (
"fmt"
"strings"
)
type StringFlag = FlagBase[string, StringConfig, stringValue]
// StringConfig defines the configuration for string flags
type StringConfig struct {
// Whether to trim whitespace of parsed value
TrimSpace bool
}
// -- string Value
type stringValue struct {
destination *string
trimSpace bool
}
// Below functions are to satisfy the ValueCreator interface
func (s stringValue) Create(val string, p *string, c StringConfig) Value {
*p = val
return &stringValue{
destination: p,
trimSpace: c.TrimSpace,
}
}
func (s stringValue) ToString(val string) string {
s.destination = &val
return s.String()
}
// Below functions are to satisfy the flag.Value interface
func (s *stringValue) Set(val string) error {
if s.trimSpace {
val = strings.TrimSpace(val)
}
*s.destination = val
return nil
}
func (s *stringValue) Get() any { return *s.destination }
func (s *stringValue) String() string {
if s.destination != nil && *s.destination != "" {
return fmt.Sprintf("%q", *s.destination)
}
return ""
}
func (cmd *Command) String(name string) string {
if v, ok := cmd.Value(name).(string); ok {
tracef("string available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("string NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return ""
}
+20
View File
@@ -0,0 +1,20 @@
package cli
type (
StringMap = MapBase[string, StringConfig, stringValue]
StringMapFlag = FlagBase[map[string]string, StringConfig, StringMap]
)
var NewStringMap = NewMapBase[string, StringConfig, stringValue]
// StringMap looks up the value of a local StringMapFlag, returns
// nil if not found
func (cmd *Command) StringMap(name string) map[string]string {
if v, ok := cmd.Value(name).(map[string]string); ok {
tracef("string map available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("string map NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return nil
}
+20
View File
@@ -0,0 +1,20 @@
package cli
type (
StringSlice = SliceBase[string, StringConfig, stringValue]
StringSliceFlag = FlagBase[[]string, StringConfig, StringSlice]
)
var NewStringSlice = NewSliceBase[string, StringConfig, stringValue]
// StringSlice looks up the value of a local StringSliceFlag, returns
// nil if not found
func (cmd *Command) StringSlice(name string) []string {
if v, ok := cmd.Value(name).([]string); ok {
tracef("string slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("string slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return nil
}
+143
View File
@@ -0,0 +1,143 @@
package cli
import (
"errors"
"fmt"
"time"
)
type TimestampFlag = FlagBase[time.Time, TimestampConfig, timestampValue]
// TimestampConfig defines the config for timestamp flags
type TimestampConfig struct {
Timezone *time.Location
// Available layouts for flag value.
//
// Note that value for formats with missing year/date will be interpreted as current year/date respectively.
//
// Read more about time layouts: https://pkg.go.dev/time#pkg-constants
Layouts []string
}
// timestampValue wrap to satisfy golang's flag interface.
type timestampValue struct {
timestamp *time.Time
hasBeenSet bool
layouts []string
location *time.Location
}
var _ ValueCreator[time.Time, TimestampConfig] = timestampValue{}
// Below functions are to satisfy the ValueCreator interface
func (t timestampValue) Create(val time.Time, p *time.Time, c TimestampConfig) Value {
*p = val
return &timestampValue{
timestamp: p,
layouts: c.Layouts,
location: c.Timezone,
}
}
func (t timestampValue) ToString(b time.Time) string {
if b.IsZero() {
return ""
}
t.timestamp = &b
return t.String()
}
// Below functions are to satisfy the Value interface
// Parses the string value to timestamp
func (t *timestampValue) Set(value string) error {
var timestamp time.Time
var err error
if t.location == nil {
t.location = time.UTC
}
if len(t.layouts) == 0 {
return errors.New("got nil/empty layouts slice")
}
for _, layout := range t.layouts {
var locErr error
timestamp, locErr = time.ParseInLocation(layout, value, t.location)
if locErr != nil {
if err == nil {
err = locErr
continue
}
err = newMultiError(err, locErr)
continue
}
err = nil
break
}
if err != nil {
return err
}
defaultTS, _ := time.ParseInLocation(time.TimeOnly, time.TimeOnly, timestamp.Location())
n := time.Now().In(timestamp.Location())
// If format is missing date (or year only), set it explicitly to current
if timestamp.Truncate(time.Hour*24).UnixNano() == defaultTS.Truncate(time.Hour*24).UnixNano() {
timestamp = time.Date(
n.Year(),
n.Month(),
n.Day(),
timestamp.Hour(),
timestamp.Minute(),
timestamp.Second(),
timestamp.Nanosecond(),
timestamp.Location(),
)
} else if timestamp.Year() == 0 {
timestamp = time.Date(
n.Year(),
timestamp.Month(),
timestamp.Day(),
timestamp.Hour(),
timestamp.Minute(),
timestamp.Second(),
timestamp.Nanosecond(),
timestamp.Location(),
)
}
if t.timestamp != nil {
*t.timestamp = timestamp
}
t.hasBeenSet = true
return nil
}
// String returns a readable representation of this value (for usage defaults)
func (t *timestampValue) String() string {
return fmt.Sprintf("%v", t.timestamp)
}
// Get returns the flag structure
func (t *timestampValue) Get() any {
return *t.timestamp
}
// Timestamp gets the timestamp from a flag name
func (cmd *Command) Timestamp(name string) time.Time {
if v, ok := cmd.Value(name).(time.Time); ok {
tracef("time.Time available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("time.Time NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return time.Time{}
}
+99
View File
@@ -0,0 +1,99 @@
package cli
import (
"strconv"
"unsafe"
)
type (
UintFlag = FlagBase[uint, IntegerConfig, uintValue[uint]]
Uint8Flag = FlagBase[uint8, IntegerConfig, uintValue[uint8]]
Uint16Flag = FlagBase[uint16, IntegerConfig, uintValue[uint16]]
Uint32Flag = FlagBase[uint32, IntegerConfig, uintValue[uint32]]
Uint64Flag = FlagBase[uint64, IntegerConfig, uintValue[uint64]]
)
// -- uint Value
type uintValue[T uint | uint8 | uint16 | uint32 | uint64] struct {
val *T
base int
}
// Below functions are to satisfy the ValueCreator interface
func (i uintValue[T]) Create(val T, p *T, c IntegerConfig) Value {
*p = val
return &uintValue[T]{
val: p,
base: c.Base,
}
}
func (i uintValue[T]) ToString(b T) string {
i.val = &b
return i.String()
}
// Below functions are to satisfy the flag.Value interface
func (i *uintValue[T]) Set(s string) error {
v, err := strconv.ParseUint(s, i.base, int(unsafe.Sizeof(T(0))*8))
if err != nil {
return err
}
*i.val = T(v)
return err
}
func (i *uintValue[T]) Get() any { return *i.val }
func (i *uintValue[T]) String() string {
base := i.base
if base == 0 {
base = 10
}
return strconv.FormatUint(uint64(*i.val), base)
}
// Uint looks up the value of a local Uint64Flag, returns
// 0 if not found
func (cmd *Command) Uint(name string) uint {
return getUint[uint](cmd, name)
}
// Uint8 looks up the value of a local Uint8Flag, returns
// 0 if not found
func (cmd *Command) Uint8(name string) uint8 {
return getUint[uint8](cmd, name)
}
// Uint16 looks up the value of a local Uint16Flag, returns
// 0 if not found
func (cmd *Command) Uint16(name string) uint16 {
return getUint[uint16](cmd, name)
}
// Uint32 looks up the value of a local Uint32Flag, returns
// 0 if not found
func (cmd *Command) Uint32(name string) uint32 {
return getUint[uint32](cmd, name)
}
// Uint64 looks up the value of a local Uint64Flag, returns
// 0 if not found
func (cmd *Command) Uint64(name string) uint64 {
return getUint[uint64](cmd, name)
}
func getUint[T uint | uint8 | uint16 | uint32 | uint64](cmd *Command, name string) T {
if v, ok := cmd.Value(name).(T); ok {
tracef("uint available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("uint NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return 0
}
+63
View File
@@ -0,0 +1,63 @@
package cli
type (
UintSlice = SliceBase[uint, IntegerConfig, uintValue[uint]]
Uint8Slice = SliceBase[uint8, IntegerConfig, uintValue[uint8]]
Uint16Slice = SliceBase[uint16, IntegerConfig, uintValue[uint16]]
Uint32Slice = SliceBase[uint32, IntegerConfig, uintValue[uint32]]
Uint64Slice = SliceBase[uint64, IntegerConfig, uintValue[uint64]]
UintSliceFlag = FlagBase[[]uint, IntegerConfig, UintSlice]
Uint8SliceFlag = FlagBase[[]uint8, IntegerConfig, Uint8Slice]
Uint16SliceFlag = FlagBase[[]uint16, IntegerConfig, Uint16Slice]
Uint32SliceFlag = FlagBase[[]uint32, IntegerConfig, Uint32Slice]
Uint64SliceFlag = FlagBase[[]uint64, IntegerConfig, Uint64Slice]
)
var (
NewUintSlice = NewSliceBase[uint, IntegerConfig, uintValue[uint]]
NewUint8Slice = NewSliceBase[uint8, IntegerConfig, uintValue[uint8]]
NewUint16Slice = NewSliceBase[uint16, IntegerConfig, uintValue[uint16]]
NewUint32Slice = NewSliceBase[uint32, IntegerConfig, uintValue[uint32]]
NewUint64Slice = NewSliceBase[uint64, IntegerConfig, uintValue[uint64]]
)
// UintSlice looks up the value of a local UintSliceFlag, returns
// nil if not found
func (cmd *Command) UintSlice(name string) []uint {
return getUintSlice[uint](cmd, name)
}
// Uint8Slice looks up the value of a local Uint8SliceFlag, returns
// nil if not found
func (cmd *Command) Uint8Slice(name string) []uint8 {
return getUintSlice[uint8](cmd, name)
}
// Uint16Slice looks up the value of a local Uint16SliceFlag, returns
// nil if not found
func (cmd *Command) Uint16Slice(name string) []uint16 {
return getUintSlice[uint16](cmd, name)
}
// Uint32Slice looks up the value of a local Uint32SliceFlag, returns
// nil if not found
func (cmd *Command) Uint32Slice(name string) []uint32 {
return getUintSlice[uint32](cmd, name)
}
// Uint64Slice looks up the value of a local Uint64SliceFlag, returns
// nil if not found
func (cmd *Command) Uint64Slice(name string) []uint64 {
return getUintSlice[uint64](cmd, name)
}
func getUintSlice[T uint | uint8 | uint16 | uint32 | uint64](cmd *Command, name string) []T {
if v, ok := cmd.Value(name).([]T); ok {
tracef("uint slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name)
return v
}
tracef("uint slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name)
return nil
}
+53
View File
@@ -0,0 +1,53 @@
package cli
import "context"
// ShellCompleteFunc is an action to execute when the shell completion flag is set
type ShellCompleteFunc func(context.Context, *Command)
// BeforeFunc is an action that executes prior to any subcommands being run once
// the context is ready. If a non-nil error is returned, no subcommands are
// run.
type BeforeFunc func(context.Context, *Command) (context.Context, error)
// AfterFunc is an action that executes after any subcommands are run and have
// finished. The AfterFunc is run even if Action() panics.
type AfterFunc func(context.Context, *Command) error
// ActionFunc is the action to execute when no subcommands are specified
type ActionFunc func(context.Context, *Command) error
// CommandNotFoundFunc is executed if the proper command cannot be found
type CommandNotFoundFunc func(context.Context, *Command, string)
// ConfigureShellCompletionCommand is a function to configure a shell completion command
type ConfigureShellCompletionCommand func(*Command)
// OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying
// customized usage error messages. This function is able to replace the
// original error messages. If this function is not set, the "Incorrect usage"
// is displayed and the execution is interrupted.
type OnUsageErrorFunc func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error
// InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context.
type InvalidFlagAccessFunc func(context.Context, *Command, string)
// ExitErrHandlerFunc is executed if provided in order to handle exitError values
// returned by Actions and Before/After functions.
type ExitErrHandlerFunc func(context.Context, *Command, error)
// FlagStringFunc is used by the help generation to display a flag, which is
// expected to be a single line.
type FlagStringFunc func(Flag) string
// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix
// text for a flag's full name.
type FlagNamePrefixFunc func(fullName []string, placeholder string) string
// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help
// with the environment variable details.
type FlagEnvHintFunc func(envVars []string, str string) string
// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help
// with the file path details.
type FlagFileHintFunc func(filePath, str string) string
+1503
View File
File diff suppressed because it is too large Load Diff
+611
View File
@@ -0,0 +1,611 @@
package cli
import (
"context"
"fmt"
"io"
"os"
"slices"
"strings"
"text/tabwriter"
"text/template"
"unicode/utf8"
)
const (
helpName = "help"
helpAlias = "h"
)
// HelpPrinterFunc prints help for the Command.
type HelpPrinterFunc func(w io.Writer, templ string, data any)
// Prints help for the Command with custom template function.
type HelpPrinterCustomFunc func(w io.Writer, templ string, data any, customFunc map[string]any)
// HelpPrinter is a function that writes the help output. If not set explicitly,
// this calls HelpPrinterCustom using only the default template functions.
//
// If custom logic for printing help is required, this function can be
// overridden. If the ExtraInfo field is defined on a Command, this function
// should not be modified, as HelpPrinterCustom will be used directly in order
// to capture the extra information.
var HelpPrinter HelpPrinterFunc = DefaultPrintHelp
// HelpPrinterCustom is a function that writes the help output. It is used as
// the default implementation of HelpPrinter, and may be called directly if
// the ExtraInfo field is set on a Command.
//
// In the default implementation, if the customFuncs argument contains a
// "wrapAt" key, which is a function which takes no arguments and returns
// an int, this int value will be used to produce a "wrap" function used
// by the default template to wrap long lines.
var HelpPrinterCustom HelpPrinterCustomFunc = DefaultPrintHelpCustom
// VersionPrinter prints the version for the root Command.
var VersionPrinter = DefaultPrintVersion
// ShowRootCommandHelp is an action that displays help for the root command.
var ShowRootCommandHelp = DefaultShowRootCommandHelp
// ShowAppHelp is a backward-compatible name for ShowRootCommandHelp.
var ShowAppHelp = ShowRootCommandHelp
// ShowCommandHelp prints help for the given command
var ShowCommandHelp = DefaultShowCommandHelp
// ShowSubcommandHelp prints help for the given subcommand
var ShowSubcommandHelp = DefaultShowSubcommandHelp
// UsageCommandHelp is the text to override the USAGE section of the help command
var UsageCommandHelp = "Shows a list of commands or help for one command"
// ArgsUsageCommandHelp is a short description of the arguments of the help command
var ArgsUsageCommandHelp = "[command]"
func buildHelpCommand(withAction bool) *Command {
cmd := &Command{
Name: helpName,
Aliases: []string{helpAlias},
Usage: UsageCommandHelp,
ArgsUsage: ArgsUsageCommandHelp,
HideHelp: true,
}
if withAction {
cmd.Action = helpCommandAction
}
return cmd
}
func helpCommandAction(ctx context.Context, cmd *Command) error {
args := cmd.Args()
firstArg := args.First()
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
//
// 1 $ app
// 2 $ app help
// 3 $ app foo
// 4 $ app help foo
// 5 $ app foo help
// Case 4. when executing a help command set the context to parent
// to allow resolution of subsequent args. This will transform
// $ app help foo
// to
// $ app foo
// which will then be handled as case 3
if cmd.parent != nil && (cmd.HasName(helpName) || cmd.HasName(helpAlias)) {
tracef("setting cmd to cmd.parent")
cmd = cmd.parent
}
// Case 4. $ app help foo
// foo is the command for which help needs to be shown
if firstArg != "" {
/* if firstArg == "--" {
return nil
}*/
tracef("returning ShowCommandHelp with %[1]q", firstArg)
return ShowCommandHelp(ctx, cmd, firstArg)
}
// Case 1 & 2
// Special case when running help on main app itself as opposed to individual
// commands/subcommands
if cmd.parent == nil {
tracef("returning ShowRootCommandHelp")
_ = ShowRootCommandHelp(cmd)
return nil
}
// Case 3, 5
if len(cmd.VisibleCommands()) == 0 {
tracef("running HelpPrinter with command %[1]q", cmd.Name)
return ShowCommandHelp(ctx, cmd.parent, cmd.Name)
}
tracef("running ShowSubcommandHelp")
return ShowSubcommandHelp(cmd)
}
// ShowRootCommandHelpAndExit prints the list of subcommands and exits with exit code.
func ShowRootCommandHelpAndExit(cmd *Command, exitCode int) {
_ = ShowRootCommandHelp(cmd)
OsExiter(exitCode)
}
// ShowAppHelpAndExit is a backward-compatible name for ShowRootCommandHelp.
var ShowAppHelpAndExit = ShowRootCommandHelpAndExit
// DefaultShowRootCommandHelp is the default implementation of ShowRootCommandHelp.
func DefaultShowRootCommandHelp(cmd *Command) error {
tmpl := cmd.CustomRootCommandHelpTemplate
if tmpl == "" {
tracef("using RootCommandHelpTemplate")
tmpl = RootCommandHelpTemplate
}
if cmd.ExtraInfo == nil {
HelpPrinter(cmd.Root().Writer, tmpl, cmd.Root())
return nil
}
tracef("setting ExtraInfo in customAppData")
customAppData := func() map[string]any {
return map[string]any{
"ExtraInfo": cmd.ExtraInfo,
}
}
HelpPrinterCustom(cmd.Root().Writer, tmpl, cmd.Root(), customAppData())
return nil
}
// DefaultRootCommandComplete prints the list of subcommands as the default completion method.
func DefaultRootCommandComplete(ctx context.Context, cmd *Command) {
DefaultCompleteWithFlags(ctx, cmd)
}
// DefaultAppComplete is a backward-compatible name for DefaultRootCommandComplete.
var DefaultAppComplete = DefaultRootCommandComplete
func printCommandSuggestions(commands []*Command, writer io.Writer) {
for _, command := range commands {
if command.Hidden {
continue
}
if len(command.Usage) > 0 {
_, _ = fmt.Fprintf(writer, "%s:%s\n", command.Name, command.Usage)
} else {
_, _ = fmt.Fprintf(writer, "%s\n", command.Name)
}
}
}
func cliArgContains(flagName string, args []string) bool {
for _, name := range strings.Split(flagName, ",") {
name = strings.TrimSpace(name)
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2
}
flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
if slices.Contains(args, flag) {
return true
}
}
return false
}
func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
// Trim to handle both "-short" and "--long" flags.
cur := strings.TrimLeft(lastArg, "-")
for _, flag := range flags {
if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden {
continue
}
usage := ""
if docFlag, ok := flag.(DocGenerationFlag); ok {
usage = docFlag.GetUsage()
}
name := strings.TrimSpace(flag.Names()[0])
// this will get total count utf8 letters in flag name
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2 // reuse this count to generate single - or -- in flag completion
}
// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
// skip flag completion for short flags example -v or -x
if strings.HasPrefix(lastArg, "--") && count == 1 {
continue
}
// match if last argument matches this flag and it is not repeated
if strings.HasPrefix(name, cur) && cur != name /* && !cliArgContains(name, os.Args)*/ {
flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
if usage != "" {
flagCompletion = fmt.Sprintf("%s:%s", flagCompletion, usage)
}
fmt.Fprintln(writer, flagCompletion)
}
}
}
func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) {
args := os.Args
if cmd != nil && cmd.parent != nil {
args = cmd.Args().Slice()
tracef("running default complete with flags[%v] on command %[2]q", args, cmd.Name)
} else {
tracef("running default complete with os.Args flags[%v]", args)
}
argsLen := len(args)
lastArg := ""
// parent command will have --generate-shell-completion so we need
// to account for that
if argsLen > 1 {
lastArg = args[argsLen-2]
} else if argsLen > 0 {
lastArg = args[argsLen-1]
}
if lastArg == completionFlag {
lastArg = ""
}
if strings.HasPrefix(lastArg, "-") {
tracef("printing flag suggestion for flag[%v] on command %[1]q", lastArg, cmd.Name)
printFlagSuggestions(lastArg, cmd.Flags, cmd.Root().Writer)
return
}
if cmd != nil {
tracef("printing command suggestions on command %[1]q", cmd.Name)
printCommandSuggestions(cmd.Commands, cmd.Root().Writer)
return
}
}
// ShowCommandHelpAndExit exits with code after showing help via ShowCommandHelp.
func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) {
_ = ShowCommandHelp(ctx, cmd, command)
OsExiter(code)
}
// DefaultShowCommandHelp is the default implementation of ShowCommandHelp.
func DefaultShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error {
for _, subCmd := range cmd.Commands {
if !subCmd.HasName(commandName) {
continue
}
tmpl := subCmd.CustomHelpTemplate
if tmpl == "" {
if len(subCmd.VisibleCommands()) == 0 {
tracef("using CommandHelpTemplate")
tmpl = CommandHelpTemplate
} else {
tracef("using SubcommandHelpTemplate")
tmpl = SubcommandHelpTemplate
}
}
tracef("running HelpPrinter")
HelpPrinter(cmd.Root().Writer, tmpl, subCmd)
tracef("returning nil after printing help")
return nil
}
tracef("no matching command found")
if cmd.CommandNotFound == nil {
errMsg := fmt.Sprintf("No help topic for '%v'", commandName)
if cmd.Suggest {
if suggestion := SuggestCommand(cmd.Commands, commandName); suggestion != "" {
errMsg += ". " + suggestion
}
}
tracef("exiting 3 with errMsg %[1]q", errMsg)
return Exit(errMsg, 3)
}
tracef("running CommandNotFound func for %[1]q", commandName)
cmd.CommandNotFound(ctx, cmd, commandName)
return nil
}
// ShowSubcommandHelpAndExit prints help for the given subcommand via ShowSubcommandHelp and exits with exit code.
func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) {
_ = ShowSubcommandHelp(cmd)
OsExiter(exitCode)
}
// DefaultShowSubcommandHelp is the default implementation of ShowSubcommandHelp.
func DefaultShowSubcommandHelp(cmd *Command) error {
HelpPrinter(cmd.Root().Writer, SubcommandHelpTemplate, cmd)
return nil
}
// ShowVersion prints the version number of the root Command.
func ShowVersion(cmd *Command) {
tracef("showing version via VersionPrinter (cmd=%[1]q)", cmd.Name)
VersionPrinter(cmd)
}
// DefaultPrintVersion is the default implementation of VersionPrinter.
func DefaultPrintVersion(cmd *Command) {
_, _ = fmt.Fprintf(cmd.Root().Writer, "%v version %v\n", cmd.Name, cmd.Version)
}
func handleTemplateError(err error) {
if err != nil {
tracef("error encountered during template parse: %[1]v", err)
// If the writer is closed, t.Execute will fail, and there's nothing
// we can do to recover.
if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" {
_, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err)
}
return
}
}
// DefaultPrintHelpCustom is the default implementation of HelpPrinterCustom.
//
// The customFuncs map will be combined with a default template.FuncMap to
// allow using arbitrary functions in template rendering.
func DefaultPrintHelpCustom(out io.Writer, templ string, data any, customFuncs map[string]any) {
const maxLineLength = 10000
tracef("building default funcMap")
funcMap := template.FuncMap{
"join": strings.Join,
"subtract": subtract,
"indent": indent,
"nindent": nindent,
"trim": strings.TrimSpace,
"wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) },
"offset": offset,
"offsetCommands": offsetCommands,
}
if wa, ok := customFuncs["wrapAt"]; ok {
if wrapAtFunc, ok := wa.(func() int); ok {
wrapAt := wrapAtFunc()
customFuncs["wrap"] = func(input string, offset int) string {
return wrap(input, offset, wrapAt)
}
}
}
for key, value := range customFuncs {
funcMap[key] = value
}
w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0)
t := template.Must(template.New("help").Funcs(funcMap).Parse(templ))
if _, err := t.New("helpNameTemplate").Parse(helpNameTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("argsTemplate").Parse(argsTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("usageTemplate").Parse(usageTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("descriptionTemplate").Parse(descriptionTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("visibleCommandTemplate").Parse(visibleCommandTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("copyrightTemplate").Parse(copyrightTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("versionTemplate").Parse(versionTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("visibleFlagCategoryTemplate").Parse(visibleFlagCategoryTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("visibleFlagTemplate").Parse(visibleFlagTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("visiblePersistentFlagTemplate").Parse(visiblePersistentFlagTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("visibleGlobalFlagCategoryTemplate").Parse(strings.ReplaceAll(visibleFlagCategoryTemplate, "OPTIONS", "GLOBAL OPTIONS")); err != nil {
handleTemplateError(err)
}
if _, err := t.New("authorsTemplate").Parse(authorsTemplate); err != nil {
handleTemplateError(err)
}
if _, err := t.New("visibleCommandCategoryTemplate").Parse(visibleCommandCategoryTemplate); err != nil {
handleTemplateError(err)
}
tracef("executing template")
handleTemplateError(t.Execute(w, data))
_ = w.Flush()
}
// DefaultPrintHelp is the default implementation of HelpPrinter.
func DefaultPrintHelp(out io.Writer, templ string, data any) {
HelpPrinterCustom(out, templ, data, nil)
}
func checkVersion(cmd *Command) bool {
found := false
for _, name := range VersionFlag.Names() {
if cmd.Bool(name) {
found = true
}
}
return found
}
func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) {
if (c.parent == nil && !c.EnableShellCompletion) || (c.parent != nil && !c.Root().shellCompletion) {
return false, arguments
}
pos := len(arguments) - 1
lastArg := arguments[pos]
if lastArg != completionFlag {
return false, arguments
}
// If arguments include "--" before the token being completed, shell completion
// is disabled because after "--" only positional arguments are accepted.
// https://unix.stackexchange.com/a/11382
// Note: The token being completed is at position pos-1 (immediately before completionFlag).
// We only check arguments before that position, so completing "--" itself still works.
if pos >= 1 && slices.Contains(arguments[:pos-1], "--") {
return false, arguments[:pos]
}
return true, arguments[:pos]
}
func checkCompletions(ctx context.Context, cmd *Command) bool {
tracef("checking completions on command %[1]q", cmd.Name)
if !cmd.Root().shellCompletion {
tracef("completion not enabled skipping %[1]q", cmd.Name)
return false
}
if argsArguments := cmd.Args(); argsArguments.Present() {
name := argsArguments.First()
if cmd := cmd.Command(name); cmd != nil {
// let the command handle the completion
return false
}
}
tracef("no subcommand found for completion %[1]q", cmd.Name)
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 {
return a - b
}
func indent(spaces int, v string) string {
pad := strings.Repeat(" ", spaces)
return pad + strings.ReplaceAll(v, "\n", "\n"+pad)
}
func nindent(spaces int, v string) string {
return "\n" + indent(spaces, v)
}
func wrap(input string, offset int, wrapAt int) string {
var ss []string
lines := strings.Split(input, "\n")
padding := strings.Repeat(" ", offset)
for i, line := range lines {
if line == "" {
ss = append(ss, line)
} else {
wrapped := wrapLine(line, offset, wrapAt, padding)
if i == 0 {
ss = append(ss, wrapped)
} else {
ss = append(ss, padding+wrapped)
}
}
}
return strings.Join(ss, "\n")
}
func wrapLine(input string, offset int, wrapAt int, padding string) string {
if wrapAt <= offset || len(input) <= wrapAt-offset {
return input
}
lineWidth := wrapAt - offset
words := strings.Fields(input)
if len(words) == 0 {
return input
}
wrapped := words[0]
spaceLeft := lineWidth - len(wrapped)
for _, word := range words[1:] {
if len(word)+1 > spaceLeft {
wrapped += "\n" + padding + word
spaceLeft = lineWidth - len(word)
} else {
wrapped += " " + word
spaceLeft -= 1 + len(word)
}
}
return wrapped
}
func offset(input string, fixed int) int {
return len(input) + fixed
}
// this function tries to find the max width of the names column
// so say we have the following rows for help
//
// foo1, foo2, foo3 some string here
// bar1, b2 some other string here
//
// We want to offset the 2nd row usage by some amount so that everything
// is aligned
//
// foo1, foo2, foo3 some string here
// bar1, b2 some other string here
//
// to find that offset we find the length of all the rows and use the max
// to calculate the offset
func offsetCommands(cmds []*Command, fixed int) int {
max := 0
for _, cmd := range cmds {
s := strings.Join(cmd.Names(), ", ")
if len(s) > max {
max = len(s)
}
}
return max + fixed
}
+5
View File
@@ -0,0 +1,5 @@
mkdocs-git-revision-date-localized-plugin==1.5.1
mkdocs-material==9.7.6
mkdocs==1.6.1
mkdocs-redirects==1.2.3
pygments==2.20.0
+139
View File
@@ -0,0 +1,139 @@
# NOTE: the mkdocs dependencies will need to be installed out of
# band until this whole thing gets more automated:
#
# pip install -r mkdocs-requirements.txt
#
site_name: urfave/cli
site_url: https://cli.urfave.org/
repo_url: https://github.com/urfave/cli
edit_uri: edit/main/docs/
nav:
- Home:
- Welcome: index.md
- Contributing: CONTRIBUTING.md
- Code of Conduct: CODE_OF_CONDUCT.md
- Releasing: RELEASING.md
- Security: SECURITY.md
- Migrate v2 to v3: migrate-v2-to-v3.md
- Migrate v1 to v2: migrate-v1-to-v2.md
- v3 Manual:
- Getting Started: v3/getting-started.md
- Migrating From Older Releases: v3/migrating-from-older-releases.md
- Examples:
- Greet: v3/examples/greet.md
- Flags:
- Basics: v3/examples/flags/basics.md
- Value Sources: v3/examples/flags/value-sources.md
- Short Options: v3/examples/flags/short-options.md
- Advanced: v3/examples/flags/advanced.md
- Arguments:
- Basics: v3/examples/arguments/basics.md
- Advanced: v3/examples/arguments/advanced.md
- Subcommands:
- Basics: v3/examples/subcommands/basics.md
- Categories: v3/examples/subcommands/categories.md
- Completions:
- Shell Completions: v3/examples/completions/shell-completions.md
- Customizations: v3/examples/completions/customizations.md
- Help Text:
- Generated Help Text: v3/examples/help/generated-help-text.md
- Suggestions: v3/examples/help/suggestions.md
- Error Handling:
- Exit Codes: v3/examples/exit-codes.md
- Full API Example: v3/examples/full-api-example.md
- v2 Manual:
- Getting Started: v2/getting-started.md
- Migrating to v3: v2/migrating-to-v3.md
- Migrating From Older Releases: v2/migrating-from-older-releases.md
- Examples:
- Greet: v2/examples/greet.md
- Arguments: v2/examples/arguments.md
- Flags: v2/examples/flags.md
- Subcommands: v2/examples/subcommands.md
- Subcommands Categories: v2/examples/subcommands-categories.md
- Exit Codes: v2/examples/exit-codes.md
- Combining Short Options: v2/examples/combining-short-options.md
- Bash Completions: v2/examples/bash-completions.md
- Generated Help Text: v2/examples/generated-help-text.md
- Version Flag: v2/examples/version-flag.md
- Timestamp Flag: v2/examples/timestamp-flag.md
- Suggestions: v2/examples/suggestions.md
- Full API Example: v2/examples/full-api-example.md
- v1 Manual:
- Getting Started: v1/getting-started.md
- Migrating to v2: v1/migrating-to-v2.md
- Examples:
- Greet: v1/examples/greet.md
- Arguments: v1/examples/arguments.md
- Flags: v1/examples/flags.md
- Subcommands: v1/examples/subcommands.md
- Subcommands (Categories): v1/examples/subcommands-categories.md
- Exit Codes: v1/examples/exit-codes.md
- Combining Short Options: v1/examples/combining-short-options.md
- Bash Completions: v1/examples/bash-completions.md
- Generated Help Text: v1/examples/generated-help-text.md
- Version Flag: v1/examples/version-flag.md
theme:
name: material
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/brightness-4
name: dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/brightness-7
name: light mode
features:
- content.code.annotate
- navigation.top
- navigation.instant
- navigation.expand
- navigation.sections
- navigation.tabs
- navigation.tabs.sticky
plugins:
- git-revision-date-localized
- search
- redirects:
redirect_maps:
'v3/examples/bash-completions.md': 'v3/examples/completions/shell-completions.md'
- tags
# NOTE: this is the recommended configuration from
# https://squidfunk.github.io/mkdocs-material/setup/extensions/#recommended-configuration
markdown_extensions:
- abbr
- admonition
- attr_list
- def_list
- footnotes
- meta
- md_in_html
- toc:
permalink: true
- pymdownx.arithmatex:
generic: true
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.details
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.highlight
- pymdownx.inlinehilite
- pymdownx.keys
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde
+1
View File
@@ -0,0 +1 @@
checks=["all"]
+147
View File
@@ -0,0 +1,147 @@
package cli
import (
"math"
)
const suggestDidYouMeanTemplate = "Did you mean %q?"
var (
SuggestFlag SuggestFlagFunc = suggestFlag
SuggestCommand SuggestCommandFunc = suggestCommand
SuggestDidYouMeanTemplate string = suggestDidYouMeanTemplate
)
type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
type SuggestCommandFunc func(commands []*Command, provided string) string
// jaroDistance is the measure of similarity between two strings. It returns a
// value between 0 and 1, where 1 indicates identical strings and 0 indicates
// completely different strings.
//
// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro.go.
func jaroDistance(a, b string) float64 {
if len(a) == 0 && len(b) == 0 {
return 1
}
if len(a) == 0 || len(b) == 0 {
return 0
}
lenA := float64(len(a))
lenB := float64(len(b))
hashA := make([]bool, len(a))
hashB := make([]bool, len(b))
maxDistance := int(math.Max(0, math.Floor(math.Max(lenA, lenB)/2.0)-1))
var matches float64
for i := 0; i < len(a); i++ {
start := int(math.Max(0, float64(i-maxDistance)))
end := int(math.Min(lenB-1, float64(i+maxDistance)))
for j := start; j <= end; j++ {
if hashB[j] {
continue
}
if a[i] == b[j] {
hashA[i] = true
hashB[j] = true
matches++
break
}
}
}
if matches == 0 {
return 0
}
var transpositions float64
var j int
for i := 0; i < len(a); i++ {
if !hashA[i] {
continue
}
for !hashB[j] {
j++
}
if a[i] != b[j] {
transpositions++
}
j++
}
transpositions /= 2
return ((matches / lenA) + (matches / lenB) + ((matches - transpositions) / matches)) / 3.0
}
// jaroWinkler is more accurate when strings have a common prefix up to a
// defined maximum length.
//
// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro-winkler.go.
func jaroWinkler(a, b string) float64 {
const (
boostThreshold = 0.7
prefixSize = 4
)
jaroDist := jaroDistance(a, b)
if jaroDist <= boostThreshold {
return jaroDist
}
prefix := int(math.Min(float64(len(a)), math.Min(float64(prefixSize), float64(len(b)))))
var prefixMatch float64
for i := range prefix {
if a[i] == b[i] {
prefixMatch++
} else {
break
}
}
return jaroDist + 0.1*prefixMatch*(1.0-jaroDist)
}
func suggestFlag(flags []Flag, provided string, hideHelp bool) string {
distance := 0.0
suggestion := ""
for _, flag := range flags {
flagNames := flag.Names()
if !hideHelp && HelpFlag != nil {
flagNames = append(flagNames, HelpFlag.Names()...)
}
for _, name := range flagNames {
newDistance := jaroWinkler(name, provided)
if newDistance > distance {
distance = newDistance
suggestion = name
}
}
}
if len(suggestion) == 1 {
suggestion = "-" + suggestion
} else if len(suggestion) > 1 {
suggestion = "--" + suggestion
}
return suggestion
}
// suggestCommand takes a list of commands and a provided string to suggest a
// command name
func suggestCommand(commands []*Command, provided string) (suggestion string) {
distance := 0.0
for _, command := range commands {
for _, name := range append(command.Names(), helpName, helpAlias) {
newDistance := jaroWinkler(name, provided)
if newDistance > distance {
distance = newDistance
suggestion = name
}
}
}
return suggestion
}
+125
View File
@@ -0,0 +1,125 @@
package cli
var (
helpNameTemplate = `{{$v := offset .FullName 6}}{{wrap .FullName 3}}{{if .Usage}} - {{wrap .Usage $v}}{{end}}`
argsTemplate = `{{if .Arguments}}{{range .Arguments}}{{.Usage}} {{end}}{{end}}`
usageTemplate = `{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleFlags}} [options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} {{template "argsTemplate" .}}{{end}}{{end}}{{end}}`
descriptionTemplate = `{{wrap .Description 3}}`
authorsTemplate = `{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
{{range $index, $author := .Authors}}{{if $index}}
{{end}}{{$author}}{{end}}`
)
var visibleCommandTemplate = `{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}}
{{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}`
var visibleCommandCategoryTemplate = `{{range .VisibleCategories}}{{if .Name}}
{{.Name}}:{{range .VisibleCommands}}
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{template "visibleCommandTemplate" .}}{{end}}{{end}}`
var visibleFlagCategoryTemplate = `{{range .VisibleFlagCategories}}
{{if .Name}}{{.Name}}
{{end}}{{$flglen := len .Flags}}{{range $i, $e := .Flags}}{{if eq (subtract $flglen $i) 1}}{{$e}}
{{else}}{{$e}}
{{end}}{{end}}{{end}}`
var visibleFlagTemplate = `{{range $i, $e := .VisibleFlags}}
{{wrap $e.String 6}}{{end}}`
var visiblePersistentFlagTemplate = `{{range $i, $e := .VisiblePersistentFlags}}
{{wrap $e.String 6}}{{end}}`
var versionTemplate = `{{if .Version}}{{if not .HideVersion}}
VERSION:
{{.Version}}{{end}}{{end}}`
var copyrightTemplate = `{{wrap .Copyright 3}}`
// RootCommandHelpTemplate is the text template for the Default help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var RootCommandHelpTemplate = `NAME:
{{template "helpNameTemplate" .}}
USAGE:
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}} {{if .VisibleFlags}}[global options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
VERSION:
{{.Version}}{{end}}{{end}}{{if .Description}}
DESCRIPTION:
{{template "descriptionTemplate" .}}{{end}}
{{- if len .Authors}}
AUTHOR{{template "authorsTemplate" .}}{{end}}{{if .VisibleCommands}}
COMMANDS:{{template "visibleCommandCategoryTemplate" .}}{{end}}{{if .VisibleFlagCategories}}
GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .Copyright}}
COPYRIGHT:
{{template "copyrightTemplate" .}}{{end}}
`
// CommandHelpTemplate is the text template for the command help topic.
// cli.go uses text/template to render templates. You can
// render custom help text by setting this variable.
var CommandHelpTemplate = `NAME:
{{template "helpNameTemplate" .}}
USAGE:
{{template "usageTemplate" .}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}}
OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
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 text by setting this variable.
var SubcommandHelpTemplate = `NAME:
{{template "helpNameTemplate" .}}
USAGE:
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}}
COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategories}}
OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}
`
var FishCompletionTemplate = `# {{ .Command.Name }} fish shell completion
function __fish_{{ .Command.Name }}_no_subcommand --description 'Test if there has been any subcommand yet'
for i in (commandline -opc)
if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }}
return 1
end
end
return 0
end
{{ range $v := .Completions }}{{ $v }}
{{ end }}`
+257
View File
@@ -0,0 +1,257 @@
package cli
import (
"fmt"
"os"
"strings"
)
// ValueSource is a source which can be used to look up a value,
// typically for use with a cli.Flag
type ValueSource interface {
fmt.Stringer
fmt.GoStringer
// Lookup returns the value from the source and if it was found
// or returns an empty string and false
Lookup() (string, bool)
}
// EnvValueSource is to specifically detect env sources when
// printing help text
type EnvValueSource interface {
IsFromEnv() bool
Key() string
}
// MapSource is a source which can be used to look up a value
// based on a key
// typically for use with a cli.Flag
type MapSource interface {
fmt.Stringer
fmt.GoStringer
// Lookup returns the value from the source based on key
// and if it was found
// or returns an empty string and false
Lookup(string) (any, bool)
}
// ValueSourceChain contains an ordered series of ValueSource that
// allows for lookup where the first ValueSource to resolve is
// returned
type ValueSourceChain struct {
Chain []ValueSource
}
func NewValueSourceChain(src ...ValueSource) ValueSourceChain {
return ValueSourceChain{
Chain: src,
}
}
func (vsc *ValueSourceChain) Append(other ValueSourceChain) {
vsc.Chain = append(vsc.Chain, other.Chain...)
}
func (vsc *ValueSourceChain) EnvKeys() []string {
vals := []string{}
for _, src := range vsc.Chain {
if v, ok := src.(EnvValueSource); ok && v.IsFromEnv() {
vals = append(vals, v.Key())
}
}
return vals
}
func (vsc *ValueSourceChain) String() string {
s := []string{}
for _, vs := range vsc.Chain {
s = append(s, vs.String())
}
return strings.Join(s, ",")
}
func (vsc *ValueSourceChain) GoString() string {
s := []string{}
for _, vs := range vsc.Chain {
s = append(s, vs.GoString())
}
return fmt.Sprintf("&ValueSourceChain{Chain:{%[1]s}}", strings.Join(s, ","))
}
func (vsc *ValueSourceChain) Lookup() (string, bool) {
s, _, ok := vsc.LookupWithSource()
return s, ok
}
func (vsc *ValueSourceChain) LookupWithSource() (string, ValueSource, bool) {
for _, src := range vsc.Chain {
if value, found := src.Lookup(); found {
return value, src, true
}
}
return "", nil, false
}
// envVarValueSource encapsulates a ValueSource from an environment variable
type envVarValueSource struct {
key string
}
func (e *envVarValueSource) Lookup() (string, bool) {
return os.LookupEnv(strings.TrimSpace(string(e.key)))
}
func (e *envVarValueSource) IsFromEnv() bool {
return true
}
func (e *envVarValueSource) Key() string {
return e.key
}
func (e *envVarValueSource) String() string { return fmt.Sprintf("environment variable %[1]q", e.key) }
func (e *envVarValueSource) GoString() string {
return fmt.Sprintf("&envVarValueSource{Key:%[1]q}", e.key)
}
func EnvVar(key string) ValueSource {
return &envVarValueSource{
key: key,
}
}
// EnvVars is a helper function to encapsulate a number of
// envVarValueSource together as a ValueSourceChain
func EnvVars(keys ...string) ValueSourceChain {
vsc := ValueSourceChain{Chain: []ValueSource{}}
for _, key := range keys {
vsc.Chain = append(vsc.Chain, EnvVar(key))
}
return vsc
}
// fileValueSource encapsulates a ValueSource from a file
type fileValueSource struct {
Path string
}
func (f *fileValueSource) Lookup() (string, bool) {
data, err := os.ReadFile(f.Path)
return string(data), err == nil
}
func (f *fileValueSource) String() string { return fmt.Sprintf("file %[1]q", f.Path) }
func (f *fileValueSource) GoString() string {
return fmt.Sprintf("&fileValueSource{Path:%[1]q}", f.Path)
}
func File(path string) ValueSource {
return &fileValueSource{Path: path}
}
// Files is a helper function to encapsulate a number of
// fileValueSource together as a ValueSourceChain
func Files(paths ...string) ValueSourceChain {
vsc := ValueSourceChain{Chain: []ValueSource{}}
for _, path := range paths {
vsc.Chain = append(vsc.Chain, File(path))
}
return vsc
}
type mapSource struct {
name string
m map[any]any
}
func NewMapSource(name string, m map[any]any) MapSource {
return &mapSource{
name: name,
m: m,
}
}
func (ms *mapSource) String() string { return fmt.Sprintf("map source %[1]q", ms.name) }
func (ms *mapSource) GoString() string {
return fmt.Sprintf("&mapSource{name:%[1]q}", ms.name)
}
// Lookup returns a value from the map source. The lookup name may be a dot-separated path into the map.
// If that is the case, it will recursively traverse the map based on the '.' delimited sections to find
// a nested value for the key.
func (ms *mapSource) Lookup(name string) (any, bool) {
sections := strings.Split(name, ".")
if name == "" || len(sections) == 0 {
return nil, false
}
node := ms.m
// traverse into the map based on the dot-separated sections
if len(sections) >= 2 { // the last section is the value we want, we will return it directly at the end
for _, section := range sections[:len(sections)-1] {
child, ok := node[section]
if !ok {
return nil, false
}
switch child := child.(type) {
case map[string]any:
node = make(map[any]any, len(child))
for k, v := range child {
node[k] = v
}
case map[any]any:
node = child
default:
return nil, false
}
}
}
if val, ok := node[sections[len(sections)-1]]; ok {
return val, true
}
return nil, false
}
type mapValueSource struct {
key string
ms MapSource
}
func NewMapValueSource(key string, ms MapSource) ValueSource {
return &mapValueSource{
key: key,
ms: ms,
}
}
func (mvs *mapValueSource) String() string {
return fmt.Sprintf("key %[1]q from %[2]s", mvs.key, mvs.ms.String())
}
func (mvs *mapValueSource) GoString() string {
return fmt.Sprintf("&mapValueSource{key:%[1]q, src:%[2]s}", mvs.key, mvs.ms.GoString())
}
func (mvs *mapValueSource) Lookup() (string, bool) {
if v, ok := mvs.ms.Lookup(mvs.key); !ok {
return "", false
} else {
return fmt.Sprintf("%+v", v), true
}
}