diff --git a/go.mod b/go.mod index c50b0d976..0dee2b179 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/gdamore/tcell/v2 v2.13.8 github.com/go-errors/errors v1.5.1 github.com/gookit/color v1.4.2 - github.com/integrii/flaggy v1.4.0 + github.com/integrii/flaggy v1.8.0 github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c github.com/jesseduffield/gocui v0.3.1-0.20260327132312-944dab3bc980 github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 diff --git a/go.sum b/go.sum index 3f186bc6f..676fbf4d3 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM= -github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI= +github.com/integrii/flaggy v1.8.0 h1:tC1qWwg4fhF2Qdaj+MpPK04cxlOSq0+HoMZqAW6Arao= +github.com/integrii/flaggy v1.8.0/go.mod h1:QS4c80m87SXG0pmVUT/Lx2RY5EbkLvLp7IKBD2jwcFA= github.com/invopop/jsonschema v0.10.0 h1:c1ktzNLBun3LyQQhyty5WE3lulbOdIIyOVlkmDLehcE= github.com/invopop/jsonschema v0.10.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2PaiisXAC5sOjDPfMArSnbswDObtCssx+xn28edX4= diff --git a/vendor/github.com/integrii/flaggy/AGENTS.md b/vendor/github.com/integrii/flaggy/AGENTS.md new file mode 100644 index 000000000..e521055e8 --- /dev/null +++ b/vendor/github.com/integrii/flaggy/AGENTS.md @@ -0,0 +1,23 @@ +# Flaggy Contribution Guidelines for Agents + +This repository provides a zero-dependency command-line parsing library that must always rely exclusively on the Go standard library for runtime and test code. The core principles that follow preserve the project's lightweight nature and its focus on easily understandable, flat code. + +## Core Principles +- Keep the codebase dependency-free beyond the Go standard library. Adding third-party modules for any purpose, including testing, is not permitted. +- Prefer flat, straightforward control flow. Avoid `else` statements when possible and limit indentation depth so examples remain approachable for beginners. +- Optimize every change for readability. Favor descriptive names, small logical blocks, and explanatory comments that teach users how the parser behaves. + +## Documentation Expectations +- Maintain clear, beginner-friendly explanations throughout the codebase. Add comments to **every** function and test describing what they do and why they matter to the overall library. +- Annotate each stanza of code with concise comments, even when the logic appears self-explanatory. +- Keep primary documentation accurate. Update `README.md` and `CONTRIBUTING.md` whenever your modifications alter usage instructions, contribution workflows, or observable behavior. + +## Tooling Requirements +- Always run `go fmt`, `go vet`, and `goimports` over affected packages before committing. +- Favor consistent formatting and import organization that highlight the minimal surface area of each example. + +## Testing Guidance +- When writing tests, ensure the accompanying comment explains exactly what is being verified. +- Leave benchmarks and examples with clarifying comments so readers immediately understand the intent and scope of each scenario. + +Following these guidelines keeps Flaggy's codebase welcoming to newcomers and aligned with its lightweight philosophy. diff --git a/vendor/github.com/integrii/flaggy/CONTRIBUTING.md b/vendor/github.com/integrii/flaggy/CONTRIBUTING.md new file mode 100644 index 000000000..6d60d64f2 --- /dev/null +++ b/vendor/github.com/integrii/flaggy/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing to Flaggy + +Thanks for your interest in improving Flaggy! The following guide outlines the standard workflow for proposing and landing a change. Following these steps helps maintainers review contributions efficiently and keeps the project healthy. + +* **Getting Started** + - **Open an issue** describing the problem you want to fix or the feature you plan to add. This gives us a chance to discuss the proposal before you start coding. + - **Fork the repository** to your own GitHub account so you can develop the change independently of the main project. +* **Implementing Your Change** + - Make your modifications in your fork (create a topic branch if that helps keep things organized). + - Add or update tests so your change is well covered. + - Run the full test suite locally and ensure everything passes (`go test ./...`). +* **Opening a Pull Request** + - Push your updates to your fork. + - Open a pull request against the main repository, referencing the issue created earlier. Include context about what the change does and any testing performed. + - Participate in the review process and incorporate any requested changes. Keep your branch up to date with the main branch as needed. + +We appreciate your contributions and look forward to collaborating with you! diff --git a/vendor/github.com/integrii/flaggy/LICENSE b/vendor/github.com/integrii/flaggy/LICENSE index cf1ab25da..fdddb29aa 100644 --- a/vendor/github.com/integrii/flaggy/LICENSE +++ b/vendor/github.com/integrii/flaggy/LICENSE @@ -21,4 +21,4 @@ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -For more information, please refer to +For more information, please refer to diff --git a/vendor/github.com/integrii/flaggy/README.md b/vendor/github.com/integrii/flaggy/README.md index 740811fc7..8afbedfb7 100644 --- a/vendor/github.com/integrii/flaggy/README.md +++ b/vendor/github.com/integrii/flaggy/README.md @@ -1,18 +1,18 @@

- +
- - + - + +

Sensible and _fast_ command-line flag parsing with excellent support for **subcommands** and **positional values**. Flags can be at any position. Flaggy has no required project or package layout like [Cobra requires](https://github.com/spf13/cobra/issues/641), and **no external dependencies**! -Check out the [godoc](http://godoc.org/github.com/integrii/flaggy), [examples directory](https://github.com/integrii/flaggy/tree/master/examples), and [examples in this readme](https://github.com/integrii/flaggy#super-simple-example) to get started quickly. You can also read the Flaggy introduction post with helpful examples [on my weblog](https://ericgreer.info/post/a-better-flags-package-for-go/). +Check out the [go doc](http://pkg.go.dev/github.com/integrii/flaggy), [examples directory](https://github.com/integrii/flaggy/tree/master/examples), and [examples in this readme](https://github.com/integrii/flaggy#super-simple-example) to get started quickly. You can also read the Flaggy introduction post with helpful examples [on my weblog](https://ericgreer.info/post/a-better-flags-package-for-go/). # Installation @@ -38,10 +38,11 @@ Check out the [godoc](http://godoc.org/github.com/integrii/flaggy), [examples di - Flags can use a single dash or double dash (`--flag`, `-flag`, `-f`, `--f`) - Flags can have `=` assignment operators, or use a space (`--flag=value`, `--flag value`) - Flags support single quote globs with spaces (`--flag 'this is all one value'`) -- Flags of slice types can be passed multiple times (`-f one -f two -f three`) +- Flags of slice types can be passed multiple times (`-f one -f two -f three`). - Optional but default version output with `--version` - Optional but default help output with `-h` or `--help` - Optional but default help output when any invalid or unknown parameter is passed +- bash, zsh, fish, PowerShell, and Nushell shell completion generation by default - It's _fast_. All flag and subcommand parsing takes less than `1ms` in most programs. # Example Help Output @@ -153,24 +154,40 @@ print(flaggy.TrailingArguments[0]) # Supported Flag Types -Flaggy has specific flag types for all basic types included in go as well as a slice of any of those types. This includes all of the following types: +Flaggy has specific flag types for all basic Go types as well as slice variants, plus a selection of helpful standard library structures. You can target any of the following assignments when defining a flag: -- string and []string -- bool and []bool -- all int types and all []int types -- all float types and all []float types -- all uint types and all []uint types +- Text and truthy values: `string`, `[]string`, `bool`, `[]bool` +- Signed integers: `int`, `int64`, `int32`, `int16`, `int8`, and a slice form for each type +- Unsigned integers: `uint`, `uint64`, `uint32`, `uint16`, `uint8` (aka `byte`), and slice forms for each type +- Floating point numbers: `float64`, `float32`, and slices of both precisions +- Time utilities: `time.Duration`, `[]time.Duration`, `time.Time`, `time.Location`, `time.Month`, `time.Weekday` +- Network primitives: `net.IP`, `[]net.IP`, `net.HardwareAddr`, `[]net.HardwareAddr`, `net.IPMask`, `[]net.IPMask`, `net.IPNet`, `net.TCPAddr`, `net.UDPAddr` +- Modern IP types: `netip.Addr`, `netip.Prefix`, `netip.AddrPort` +- URLs and filesystem helpers: `url.URL`, `os.FileMode` +- Pattern and math types: `regexp.Regexp`, `big.Int`, `big.Rat` +- Encoded byte helpers: `Base64Bytes` (a base64-decoded `[]byte`) -Other more specific types can also be used as flag types. They will be automatically parsed using the standard parsing functions included with those types in those packages. This includes: +# Shell Completion -- net.IP -- []net.IP -- net.HardwareAddr -- []net.HardwareAddr -- net.IPMask -- []net.IPMask -- time.Duration -- []time.Duration +Flaggy generates `bash`, `zsh`, `fish`, `PowerShell`, and `Nushell` completion scripts automatically. + +```bash +# Bash +source <(./app completion bash) + +# Zsh +source <(./app completion zsh) + +# Fish +./app completion fish | source + +# PowerShell +./app completion powershell | Out-String | Invoke-Expression + +# Nushell +./app completion nushell | save --force ~/.cache/app-completions.nu +source ~/.cache/app-completions.nu +``` # An Example Program diff --git a/vendor/github.com/integrii/flaggy/byte_types.go b/vendor/github.com/integrii/flaggy/byte_types.go new file mode 100644 index 000000000..6acb7f07b --- /dev/null +++ b/vendor/github.com/integrii/flaggy/byte_types.go @@ -0,0 +1,4 @@ +package flaggy + +// Base64Bytes is a []byte interpreted as base64 when parsed from flags. +type Base64Bytes []byte diff --git a/vendor/github.com/integrii/flaggy/completion.go b/vendor/github.com/integrii/flaggy/completion.go new file mode 100644 index 000000000..de40a277e --- /dev/null +++ b/vendor/github.com/integrii/flaggy/completion.go @@ -0,0 +1,390 @@ +package flaggy + +import ( + "fmt" + "strings" +) + +// EnableCompletion enables shell autocomplete outputs to be generated. +func EnableCompletion() { + DefaultParser.ShowCompletion = true +} + +// DisableCompletion disallows shell autocomplete outputs to be generated. +func DisableCompletion() { + DefaultParser.ShowCompletion = false +} + +// GenerateBashCompletion returns a bash completion script for the parser. +func GenerateBashCompletion(p *Parser) string { + var b strings.Builder + funcName := "_" + sanitizeName(p.Name) + "_complete" + b.WriteString("# bash completion for " + p.Name + "\n") + b.WriteString(funcName + "() {\n") + b.WriteString(" local cur prev\n") + b.WriteString(" COMPREPLY=()\n") + b.WriteString(" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n") + b.WriteString(" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n") + b.WriteString(" case \"$prev\" in\n") + bashCaseEntries(&p.Subcommand, &b) + rootOpts := collectOptions(&p.Subcommand) + b.WriteString(" *)\n COMPREPLY=( $(compgen -W \"" + rootOpts + "\" -- \"$cur\") )\n return 0\n ;;\n esac\n}\n") + b.WriteString("complete -F " + funcName + " " + p.Name + "\n") + return b.String() +} + +// GenerateZshCompletion returns a zsh completion script for the parser. +func GenerateZshCompletion(p *Parser) string { + var b strings.Builder + funcName := "_" + sanitizeName(p.Name) + b.WriteString("#compdef " + p.Name + "\n\n") + b.WriteString(funcName + "() {\n") + b.WriteString(" local cur prev\n") + b.WriteString(" cur=${words[CURRENT]}\n") + b.WriteString(" prev=${words[CURRENT-1]}\n") + b.WriteString(" case \"$prev\" in\n") + zshCaseEntries(&p.Subcommand, &b) + rootOpts := collectOptions(&p.Subcommand) + b.WriteString(" *)\n compadd -- " + rootOpts + "\n ;;\n esac\n}\n") + b.WriteString("compdef " + funcName + " " + p.Name + "\n") + return b.String() +} + +// GenerateFishCompletion returns a fish completion script for the parser. +func GenerateFishCompletion(p *Parser) string { + var b strings.Builder + b.WriteString("# fish completion for " + p.Name + "\n") + writeFishEntries(&p.Subcommand, &b, p.Name, nil) + return b.String() +} + +// GeneratePowerShellCompletion returns a PowerShell completion script for the parser. +func GeneratePowerShellCompletion(p *Parser) string { + var b strings.Builder + b.WriteString("# PowerShell completion for " + p.Name + "\n") + b.WriteString("Register-ArgumentCompleter -CommandName '" + p.Name + "' -ScriptBlock {\n") + b.WriteString(" param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)\n") + b.WriteString(" $completions = @(\n") + writePowerShellEntries(&p.Subcommand, &b) + b.WriteString(" )\n") + b.WriteString(" $completions | Where-Object { $_.CompletionText -like \"$wordToComplete*\" }\n") + b.WriteString("}\n") + return b.String() +} + +// GenerateNushellCompletion returns a Nushell completion script for the parser. +func GenerateNushellCompletion(p *Parser) string { + var b strings.Builder + command := p.Name + funcName := "nu-complete " + command + b.WriteString("# nushell completion for " + command + "\n") + b.WriteString("def \"" + funcName + "\" [] {\n") + b.WriteString(" [\n") + writeNushellEntries(&p.Subcommand, &b) + b.WriteString(" ]\n") + b.WriteString("}\n\n") + b.WriteString("extern \"" + command + "\" [\n") + writeNushellFlagSignature(&p.Subcommand, &b) + b.WriteString(" command?: string@\"" + funcName + "\"\n") + b.WriteString("]\n") + return b.String() +} + +// collectOptions builds a space-delimited list of flags, subcommands, and positional values +// for the provided subcommand. +func collectOptions(sc *Subcommand) string { + var opts []string + for _, f := range sc.Flags { + if len(f.ShortName) > 0 { + opts = append(opts, "-"+f.ShortName) + } + if len(f.LongName) > 0 { + opts = append(opts, "--"+f.LongName) + } + } + for _, p := range sc.PositionalFlags { + if p.Name != "" { + opts = append(opts, p.Name) + } + } + for _, s := range sc.Subcommands { + if s.Hidden { + continue + } + if s.Name != "" { + opts = append(opts, s.Name) + } + if s.ShortName != "" { + opts = append(opts, s.ShortName) + } + } + return strings.Join(opts, " ") +} + +func bashCaseEntries(sc *Subcommand, b *strings.Builder) { + for _, s := range sc.Subcommands { + if s.Hidden { + continue + } + opts := collectOptions(s) + b.WriteString(" " + s.Name + ")\n COMPREPLY=( $(compgen -W \"" + opts + "\" -- \"$cur\") )\n return 0\n ;;\n") + if s.ShortName != "" { + b.WriteString(" " + s.ShortName + ")\n COMPREPLY=( $(compgen -W \"" + opts + "\" -- \"$cur\") )\n return 0\n ;;\n") + } + bashCaseEntries(s, b) + } +} + +func zshCaseEntries(sc *Subcommand, b *strings.Builder) { + for _, s := range sc.Subcommands { + if s.Hidden { + continue + } + opts := collectOptions(s) + b.WriteString(" " + s.Name + ")\n compadd -- " + opts + "\n return\n ;;\n") + if s.ShortName != "" { + b.WriteString(" " + s.ShortName + ")\n compadd -- " + opts + "\n return\n ;;\n") + } + zshCaseEntries(s, b) + } +} + +func sanitizeName(n string) string { + return strings.ReplaceAll(n, "-", "_") +} + +// writeFishEntries builds the fish completion statements for the provided subcommand path so +// the generated script mirrors Flaggy's flag and subcommand hierarchy for interactive use. +func writeFishEntries(sc *Subcommand, b *strings.Builder, command string, path []string) { + condition := fishConditionForFlags(path) + for _, f := range sc.Flags { + if f.Hidden { + continue + } + line := "complete -c " + command + if condition != "" { + line += " -n '" + condition + "'" + } + if f.ShortName != "" { + line += " -s " + f.ShortName + } + if f.LongName != "" { + line += " -l " + f.LongName + } + if f.Description != "" { + line += " -d '" + escapeSingleQuotes(f.Description) + "'" + } + line += "\n" + b.WriteString(line) + } + for _, p := range sc.PositionalFlags { + if p.Hidden { + continue + } + if p.Name == "" { + continue + } + line := "complete -c " + command + if condition != "" { + line += " -n '" + condition + "'" + } + line += " -a '" + escapeSingleQuotes(p.Name) + "'" + if p.Description != "" { + line += " -d '" + escapeSingleQuotes(p.Description) + "'" + } + line += "\n" + b.WriteString(line) + } + subCondition := fishConditionForSubcommands(path) + for _, sub := range sc.Subcommands { + if sub.Hidden { + continue + } + line := "complete -c " + command + if subCondition != "" { + line += " -n '" + subCondition + "'" + } + line += " -a '" + escapeSingleQuotes(sub.Name) + "'" + if sub.Description != "" { + line += " -d '" + escapeSingleQuotes(sub.Description) + "'" + } + line += "\n" + b.WriteString(line) + if sub.ShortName != "" { + aliasLine := "complete -c " + command + if subCondition != "" { + aliasLine += " -n '" + subCondition + "'" + } + aliasLine += " -a '" + escapeSingleQuotes(sub.ShortName) + "'" + if sub.Description != "" { + aliasLine += " -d '" + escapeSingleQuotes(sub.Description) + "'" + } + aliasLine += "\n" + b.WriteString(aliasLine) + } + nextPath := appendPath(path, sub.Name) + writeFishEntries(sub, b, command, nextPath) + } +} + +// fishConditionForFlags returns the fish condition needed to scope flag suggestions to the +// current subcommand path while leaving root flags globally available. +func fishConditionForFlags(path []string) string { + if len(path) == 0 { + return "" + } + return "__fish_seen_subcommand_from " + path[len(path)-1] +} + +// fishConditionForSubcommands returns the fish condition that ensures subcommand suggestions +// appear only after their parent command token has been entered. +func fishConditionForSubcommands(path []string) string { + if len(path) == 0 { + return "__fish_use_subcommand" + } + return "__fish_seen_subcommand_from " + path[len(path)-1] +} + +// appendPath creates a new slice with the next subcommand name appended so recursive +// completion builders can keep the traversal stack immutable. +func appendPath(path []string, value string) []string { + next := make([]string, len(path)+1) + copy(next, path) + next[len(path)] = value + return next +} + +// escapeSingleQuotes prepares text for inclusion in single-quoted shell strings so flag +// descriptions render safely in the generated scripts. +func escapeSingleQuotes(s string) string { + return strings.ReplaceAll(s, "'", "\\'") +} + +// escapeDoubleQuotes prepares text for inclusion in double-quoted shell strings which is +// required for PowerShell and Nushell emission. +func escapeDoubleQuotes(s string) string { + return strings.ReplaceAll(s, "\"", "\\\"") +} + +// writePowerShellEntries walks the parser tree and emits CompletionResult entries so the +// PowerShell script can surface flags, positionals, and subcommands interactively. +func writePowerShellEntries(sc *Subcommand, b *strings.Builder) { + for _, f := range sc.Flags { + if f.Hidden { + continue + } + if f.LongName != "" { + writePowerShellLine("--"+f.LongName, f.Description, "ParameterName", b) + } + if f.ShortName != "" { + writePowerShellLine("-"+f.ShortName, f.Description, "ParameterName", b) + } + } + for _, p := range sc.PositionalFlags { + if p.Hidden { + continue + } + if p.Name == "" { + continue + } + writePowerShellLine(p.Name, p.Description, "ParameterValue", b) + } + for _, sub := range sc.Subcommands { + if sub.Hidden { + continue + } + writePowerShellLine(sub.Name, sub.Description, "Command", b) + if sub.ShortName != "" { + writePowerShellLine(sub.ShortName, sub.Description, "Command", b) + } + writePowerShellEntries(sub, b) + } +} + +// writePowerShellLine emits a single CompletionResult definition with the supplied tooltip and +// completion type for consumption by Register-ArgumentCompleter. +func writePowerShellLine(value, description, kind string, b *strings.Builder) { + tooltip := description + if tooltip == "" { + tooltip = value + } + line := fmt.Sprintf(" [System.Management.Automation.CompletionResult]::new(\"%s\", \"%s\", \"%s\", \"%s\")\n", escapeDoubleQuotes(value), escapeDoubleQuotes(value), kind, escapeDoubleQuotes(tooltip)) + b.WriteString(line) +} + +// writeNushellEntries collects all completion values into Nushell's structured format so +// external commands can expose their interactive help inside the shell. +func writeNushellEntries(sc *Subcommand, b *strings.Builder) { + for _, f := range sc.Flags { + if f.Hidden { + continue + } + if f.LongName != "" { + writeNushellLine("--"+f.LongName, f.Description, b) + } + if f.ShortName != "" { + writeNushellLine("-"+f.ShortName, f.Description, b) + } + } + for _, p := range sc.PositionalFlags { + if p.Hidden { + continue + } + if p.Name == "" { + continue + } + writeNushellLine(p.Name, p.Description, b) + } + for _, sub := range sc.Subcommands { + if sub.Hidden { + continue + } + writeNushellLine(sub.Name, sub.Description, b) + if sub.ShortName != "" { + writeNushellLine(sub.ShortName, sub.Description, b) + } + writeNushellEntries(sub, b) + } +} + +// writeNushellLine emits a single structured completion item for Nushell with a value and +// friendly description. +func writeNushellLine(value, description string, b *strings.Builder) { + tooltip := description + if tooltip == "" { + tooltip = value + } + line := fmt.Sprintf(" { value: \"%s\", description: \"%s\" }\n", escapeDoubleQuotes(value), escapeDoubleQuotes(tooltip)) + b.WriteString(line) +} + +// writeNushellFlagSignature appends flag signature stubs so Nushell understands which +// switches are available when invoking the external command. +func writeNushellFlagSignature(sc *Subcommand, b *strings.Builder) { + for _, f := range sc.Flags { + if f.Hidden { + continue + } + if f.LongName != "" || f.ShortName != "" { + line := " " + if f.LongName != "" { + line += "--" + f.LongName + } + if f.ShortName != "" { + if f.LongName != "" { + line += "(-" + f.ShortName + ")" + } else { + line += "-" + f.ShortName + } + } + line += "\n" + b.WriteString(line) + } + } + for _, sub := range sc.Subcommands { + if sub.Hidden { + continue + } + writeNushellFlagSignature(sub, b) + } +} diff --git a/vendor/github.com/integrii/flaggy/flag.go b/vendor/github.com/integrii/flaggy/flag.go index 409ddf94d..c1a33006d 100644 --- a/vendor/github.com/integrii/flaggy/flag.go +++ b/vendor/github.com/integrii/flaggy/flag.go @@ -1,10 +1,16 @@ package flaggy import ( + "encoding/base64" "errors" "fmt" + "math/big" "net" + netip "net/netip" + "net/url" + "os" "reflect" + "regexp" "strconv" "strings" "time" @@ -62,8 +68,7 @@ func (f *Flag) identifyAndAssignValue(value string) error { *v = value case *[]string: v := f.AssignmentVar.(*[]string) - splitString := strings.Split(value, ",") - new := append(*v, splitString...) + new := append(*v, value) *v = new case *bool: v, err := strconv.ParseBool(value) @@ -327,6 +332,164 @@ func (f *Flag) identifyAndAssignValue(value string) error { existing := f.AssignmentVar.(*[]net.IPMask) new := append(*existing, v) *existing = new + case *time.Time: + // Support unix seconds if numeric, else try common layouts + if isAllDigits(value) { + sec, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + t := time.Unix(sec, 0).UTC() + a := f.AssignmentVar.(*time.Time) + *a = t + return nil + } + var parsed time.Time + var err error + layouts := []string{time.RFC3339Nano, time.RFC3339, time.RFC1123Z, time.RFC1123} + for _, layout := range layouts { + parsed, err = time.Parse(layout, value) + if err == nil { + a := f.AssignmentVar.(*time.Time) + *a = parsed + return nil + } + } + return err + case *url.URL: + u, err := url.Parse(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*url.URL) + *a = *u + case *net.IPNet: + _, ipnet, err := net.ParseCIDR(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*net.IPNet) + *a = *ipnet + case *net.TCPAddr: + host, portStr, err := net.SplitHostPort(value) + if err != nil { + return err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return err + } + var ip net.IP + if len(host) > 0 { + ip = net.ParseIP(host) + } + addr := net.TCPAddr{IP: ip, Port: port} + a := f.AssignmentVar.(*net.TCPAddr) + *a = addr + case *net.UDPAddr: + host, portStr, err := net.SplitHostPort(value) + if err != nil { + return err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return err + } + var ip net.IP + if len(host) > 0 { + ip = net.ParseIP(host) + } + addr := net.UDPAddr{IP: ip, Port: port} + a := f.AssignmentVar.(*net.UDPAddr) + *a = addr + case *os.FileMode: + v, err := strconv.ParseUint(value, 0, 32) + if err != nil { + return err + } + a := f.AssignmentVar.(*os.FileMode) + *a = os.FileMode(v) + case *regexp.Regexp: + r, err := regexp.Compile(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*regexp.Regexp) + *a = *r + case *time.Location: + // Try IANA name, with fallback to UTC offset like +02:00 or -0700 + if loc, err := time.LoadLocation(value); err == nil { + a := f.AssignmentVar.(*time.Location) + *a = *loc + return nil + } + if off, ok := parseUTCOffset(value); ok { + name := offsetName(off) + loc := time.FixedZone(name, off) + a := f.AssignmentVar.(*time.Location) + *a = *loc + return nil + } + return fmt.Errorf("invalid time.Location: %s", value) + case *time.Month: + if m, ok := parseMonth(value); ok { + a := f.AssignmentVar.(*time.Month) + *a = m + return nil + } + return fmt.Errorf("invalid time.Month: %s", value) + case *time.Weekday: + if d, ok := parseWeekday(value); ok { + a := f.AssignmentVar.(*time.Weekday) + *a = d + return nil + } + return fmt.Errorf("invalid time.Weekday: %s", value) + case *big.Int: + bi := f.AssignmentVar.(*big.Int) + if _, ok := bi.SetString(value, 0); !ok { + return fmt.Errorf("invalid big.Int: %s", value) + } + case *big.Rat: + br := f.AssignmentVar.(*big.Rat) + if _, ok := br.SetString(value); !ok { + return fmt.Errorf("invalid big.Rat: %s", value) + } + case *Base64Bytes: + // Try standard then URL encoding + decoded, err := base64.StdEncoding.DecodeString(value) + if err == nil { + a := f.AssignmentVar.(*Base64Bytes) + *a = Base64Bytes(decoded) + return nil + } + if decodedURL, errURL := base64.URLEncoding.DecodeString(value); errURL == nil { + a := f.AssignmentVar.(*Base64Bytes) + *a = Base64Bytes(decodedURL) + return nil + } + return err + case *netip.Addr: + addr, err := netip.ParseAddr(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*netip.Addr) + *a = addr + case *netip.Prefix: + pfx, err := netip.ParsePrefix(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*netip.Prefix) + *a = pfx + case *netip.AddrPort: + ap, err := netip.ParseAddrPort(value) + if err != nil { + return err + } + a := f.AssignmentVar.(*netip.AddrPort) + *a = ap default: return errors.New("Unknown flag assignmentVar supplied in flag " + f.LongName + " " + f.ShortName) } @@ -398,8 +561,9 @@ func parseArgWithValue(arg string) (key string, value string) { } // parseFlagToName parses a flag with space value down to a key name: -// --path -> path -// -p -> p +// +// --path -> path +// -p -> p func parseFlagToName(arg string) string { // remove minus from start arg = strings.TrimLeft(arg, "-") @@ -407,10 +571,32 @@ func parseFlagToName(arg string) string { return arg } +// collectAllNestedFlags recurses through the command tree to get all +// +// flags specified on a subcommand and its descending subcommands +func collectAllNestedFlags(sc *Subcommand) []*Flag { + fullList := sc.Flags + for _, sc := range sc.Subcommands { + fullList = append(fullList, sc.Flags...) + fullList = append(fullList, collectAllNestedFlags(sc)...) + } + return fullList +} + // flagIsBool determines if the flag is a bool within the specified parser // and subcommand's context func flagIsBool(sc *Subcommand, p *Parser, key string) bool { - for _, f := range append(sc.Flags, p.Flags...) { + for _, f := range sc.Flags { + if f.HasName(key) { + _, isBool := f.AssignmentVar.(*bool) + _, isBoolSlice := f.AssignmentVar.(*[]bool) + if isBool || isBoolSlice { + return true + } + } + } + + for _, f := range p.Flags { if f.HasName(key) { _, isBool := f.AssignmentVar.(*bool) _, isBoolSlice := f.AssignmentVar.(*[]bool) @@ -424,6 +610,24 @@ func flagIsBool(sc *Subcommand, p *Parser, key string) bool { return false } +// flagIsDefined reports whether a flag with the provided key is registered on +// the supplied subcommand or parser. +func flagIsDefined(sc *Subcommand, p *Parser, key string) bool { + for _, f := range sc.Flags { + if f.HasName(key) { + return true + } + } + + for _, f := range p.Flags { + if f.HasName(key) { + return true + } + } + + return false +} + // returnAssignmentVarValueAsString returns the value of the flag's // assignment variable as a string. This is used to display the // default value of flags before they are assigned (like when help is output). @@ -616,7 +820,171 @@ func (f *Flag) returnAssignmentVarValueAsString() (string, error) { strSlice = append(strSlice, m.String()) } return strings.Join(strSlice, ","), err + case *time.Time: + v := f.AssignmentVar.(*time.Time) + if v.IsZero() { + return "", err + } + return v.UTC().Format(time.RFC3339Nano), err + case *url.URL: + v := f.AssignmentVar.(*url.URL) + return v.String(), err + case *net.IPNet: + v := f.AssignmentVar.(*net.IPNet) + return v.String(), err + case *net.TCPAddr: + v := f.AssignmentVar.(*net.TCPAddr) + return v.String(), err + case *net.UDPAddr: + v := f.AssignmentVar.(*net.UDPAddr) + return v.String(), err + case *os.FileMode: + v := f.AssignmentVar.(*os.FileMode) + return fmt.Sprintf("%#o", *v), err + case *regexp.Regexp: + v := f.AssignmentVar.(*regexp.Regexp) + return v.String(), err + case *time.Location: + v := f.AssignmentVar.(*time.Location) + return v.String(), err + case *time.Month: + v := f.AssignmentVar.(*time.Month) + if *v == 0 { + return "", err + } + return v.String(), err + case *time.Weekday: + v := f.AssignmentVar.(*time.Weekday) + return v.String(), err + case *big.Int: + v := f.AssignmentVar.(*big.Int) + return v.String(), err + case *big.Rat: + v := f.AssignmentVar.(*big.Rat) + return v.RatString(), err + case *Base64Bytes: + v := f.AssignmentVar.(*Base64Bytes) + if v == nil || len(*v) == 0 { + return "", err + } + return base64.StdEncoding.EncodeToString([]byte(*v)), err + case *netip.Addr: + v := f.AssignmentVar.(*netip.Addr) + return v.String(), err + case *netip.Prefix: + v := f.AssignmentVar.(*netip.Prefix) + return v.String(), err + case *netip.AddrPort: + v := f.AssignmentVar.(*netip.AddrPort) + return v.String(), err default: return "", errors.New("Unknown flag assignmentVar found in flag " + f.LongName + " " + f.ShortName + ". Type not supported: " + reflect.TypeOf(f.AssignmentVar).String()) } } + +// helpers +func isAllDigits(s string) bool { + if len(s) == 0 { + return false + } + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} + +func parseUTCOffset(s string) (int, bool) { + // Supports formats: +HH, -HH, +HHMM, -HHMM, +HH:MM, -HH:MM, Z + if s == "Z" || s == "z" || strings.EqualFold(s, "UTC") { + return 0, true + } + if len(s) < 2 { + return 0, false + } + sign := 1 + switch s[0] { + case '+': + sign = 1 + case '-': + sign = -1 + default: + return 0, false + } + rest := s[1:] + rest = strings.ReplaceAll(rest, ":", "") + if len(rest) != 2 && len(rest) != 4 { + return 0, false + } + hh, err := strconv.Atoi(rest[:2]) + if err != nil { + return 0, false + } + mm := 0 + if len(rest) == 4 { + mm, err = strconv.Atoi(rest[2:]) + if err != nil { + return 0, false + } + } + if hh < 0 || hh > 23 || mm < 0 || mm > 59 { + return 0, false + } + return sign * (hh*3600 + mm*60), true +} + +func offsetName(offset int) string { + if offset == 0 { + return "UTC" + } + sign := "+" + if offset < 0 { + sign = "-" + offset = -offset + } + hh := offset / 3600 + mm := (offset % 3600) / 60 + return fmt.Sprintf("UTC%s%02d:%02d", sign, hh, mm) +} + +func parseMonth(s string) (time.Month, bool) { + // Try name + names := map[string]time.Month{ + "january": time.January, "february": time.February, "march": time.March, "april": time.April, + "may": time.May, "june": time.June, "july": time.July, "august": time.August, + "september": time.September, "october": time.October, "november": time.November, "december": time.December, + } + if m, ok := names[strings.ToLower(s)]; ok { + return m, true + } + // Try number 1-12 + n, err := strconv.Atoi(s) + if err == nil && n >= 1 && n <= 12 { + return time.Month(n), true + } + return 0, false +} + +func parseWeekday(s string) (time.Weekday, bool) { + names := map[string]time.Weekday{ + "sunday": time.Sunday, "monday": time.Monday, "tuesday": time.Tuesday, "wednesday": time.Wednesday, + "thursday": time.Thursday, "friday": time.Friday, "saturday": time.Saturday, + } + if d, ok := names[strings.ToLower(s)]; ok { + return d, true + } + n, err := strconv.Atoi(s) + if err == nil { + // Accept 0-6 as Sunday-Saturday + if n >= 0 && n <= 6 { + return time.Weekday(n), true + } + // Also accept 1-7 as Monday-Sunday + if n >= 1 && n <= 7 { + v := (n % 7) // 7->0 + return time.Weekday(v), true + } + } + return 0, false +} diff --git a/vendor/github.com/integrii/flaggy/main.go b/vendor/github.com/integrii/flaggy/flaggy.go similarity index 75% rename from vendor/github.com/integrii/flaggy/main.go rename to vendor/github.com/integrii/flaggy/flaggy.go index b242cb61d..72ddd49bd 100644 --- a/vendor/github.com/integrii/flaggy/main.go +++ b/vendor/github.com/integrii/flaggy/flaggy.go @@ -9,8 +9,12 @@ package flaggy // import "github.com/integrii/flaggy" import ( "fmt" "log" + "math/big" "net" + netip "net/netip" + "net/url" "os" + "regexp" "strconv" "strings" "time" @@ -52,12 +56,25 @@ func ResetParser() { if len(os.Args) > 0 { chunks := strings.Split(os.Args[0], "/") DefaultParser = NewParser(chunks[len(chunks)-1]) - } else { - DefaultParser = NewParser("default") + return } + DefaultParser = NewParser("default") } -// Parse parses flags as requested in the default package parser +// SortFlagsByLongName enables alphabetical sorting of flags by long name +// in help output on the default parser. +func SortFlagsByLongName() { + DefaultParser.SortFlagsByLongName() +} + +// SortFlagsByLongNameReversed enables reverse alphabetical sorting of flags +// by long name in help output on the default parser. +func SortFlagsByLongNameReversed() { + DefaultParser.SortFlagsByLongNameReversed() +} + +// Parse parses flags as requested in the default package parser. All trailing arguments +// that result from parsing are placed in the global TrailingArguments variable. func Parse() { err := DefaultParser.Parse() TrailingArguments = DefaultParser.TrailingArguments @@ -67,7 +84,8 @@ func Parse() { } // ParseArgs parses the passed args as if they were the arguments to the -// running binary. Targets the default main parser for the package. +// running binary. Targets the default main parser for the package. All trailing +// arguments are set in the global TrailingArguments variable. func ParseArgs(args []string) { err := DefaultParser.ParseArgs(args) TrailingArguments = DefaultParser.TrailingArguments @@ -104,6 +122,11 @@ func ByteSlice(assignmentVar *[]byte, shortName string, longName string, descrip DefaultParser.add(assignmentVar, shortName, longName, description) } +// BytesBase64 adds a new []byte flag parsed from base64 input. +func BytesBase64(assignmentVar *Base64Bytes, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + // Duration adds a new time.Duration flag. // Input format is described in time.ParseDuration(). // Example values: 1h, 1h50m, 32s @@ -284,6 +307,81 @@ func IPMaskSlice(assignmentVar *[]net.IPMask, shortName string, longName string, DefaultParser.add(assignmentVar, shortName, longName, description) } +// Time adds a new time.Time flag. Supports RFC3339/RFC3339Nano, RFC1123, and unix seconds. +func Time(assignmentVar *time.Time, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// URL adds a new url.URL flag. +func URL(assignmentVar *url.URL, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// IPNet adds a new net.IPNet flag parsed from CIDR. +func IPNet(assignmentVar *net.IPNet, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// TCPAddr adds a new net.TCPAddr flag parsed from host:port. +func TCPAddr(assignmentVar *net.TCPAddr, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// UDPAddr adds a new net.UDPAddr flag parsed from host:port. +func UDPAddr(assignmentVar *net.UDPAddr, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// FileMode adds a new os.FileMode flag parsed from octal/decimal (base auto-detected). +func FileMode(assignmentVar *os.FileMode, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// Regexp adds a new regexp.Regexp flag. +func Regexp(assignmentVar *regexp.Regexp, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// Location adds a new time.Location flag. +func Location(assignmentVar *time.Location, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// Month adds a new time.Month flag. +func Month(assignmentVar *time.Month, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// Weekday adds a new time.Weekday flag. +func Weekday(assignmentVar *time.Weekday, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// BigInt adds a new big.Int flag. +func BigInt(assignmentVar *big.Int, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// BigRat adds a new big.Rat flag. +func BigRat(assignmentVar *big.Rat, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// NetipAddr adds a new netip.Addr flag. +func NetipAddr(assignmentVar *netip.Addr, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// NetipPrefix adds a new netip.Prefix flag. +func NetipPrefix(assignmentVar *netip.Prefix, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + +// NetipAddrPort adds a new netip.AddrPort flag. +func NetipAddrPort(assignmentVar *netip.AddrPort, shortName string, longName string, description string) { + DefaultParser.add(assignmentVar, shortName, longName, description) +} + // AttachSubcommand adds a subcommand for parsing func AttachSubcommand(subcommand *Subcommand, relativePosition int) { DefaultParser.AttachSubcommand(subcommand, relativePosition) diff --git a/vendor/github.com/integrii/flaggy/help.go b/vendor/github.com/integrii/flaggy/help.go index 353be7d4f..19d042ff3 100644 --- a/vendor/github.com/integrii/flaggy/help.go +++ b/vendor/github.com/integrii/flaggy/help.go @@ -3,21 +3,5 @@ package flaggy // defaultHelpTemplate is the help template used by default // {{if (or (or (gt (len .StringFlags) 0) (gt (len .IntFlags) 0)) (gt (len .BoolFlags) 0))}} // {{if (or (gt (len .StringFlags) 0) (gt (len .BoolFlags) 0))}} -const defaultHelpTemplate = `{{.CommandName}}{{if .Description}} - {{.Description}}{{end}}{{if .PrependMessage}} -{{.PrependMessage}}{{end}} -{{if .UsageString}} - Usage: - {{.UsageString}}{{end}}{{if .Positionals}} - - Positional Variables: {{range .Positionals}} - {{.Name}} {{.Spacer}}{{if .Description}} {{.Description}}{{end}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{else}}{{if .Required}} (Required){{end}}{{end}}{{end}}{{end}}{{if .Subcommands}} - - Subcommands: {{range .Subcommands}} - {{.LongName}}{{if .ShortName}} ({{.ShortName}}){{end}}{{if .Position}}{{if gt .Position 1}} (position {{.Position}}){{end}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{end}}{{end}} -{{end}}{{if (gt (len .Flags) 0)}} - Flags: {{if .Flags}}{{range .Flags}} - {{if .ShortName}}-{{.ShortName}} {{else}} {{end}}{{if .LongName}}--{{.LongName}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{end}} -{{end}}{{if .AppendMessage}}{{.AppendMessage}} -{{end}}{{if .Message}} -{{.Message}}{{end}} -` +const defaultHelpTemplate = `{{range $idx, $line := .Lines}}{{if gt $idx 0}} +{{end}}{{$line}}{{end}}` diff --git a/vendor/github.com/integrii/flaggy/helpValues.go b/vendor/github.com/integrii/flaggy/helpValues.go index df2f679e9..28d5cbb27 100644 --- a/vendor/github.com/integrii/flaggy/helpValues.go +++ b/vendor/github.com/integrii/flaggy/helpValues.go @@ -3,6 +3,8 @@ package flaggy import ( "log" "reflect" + "sort" + "strconv" "strings" "unicode/utf8" ) @@ -12,12 +14,15 @@ type Help struct { Subcommands []HelpSubcommand Positionals []HelpPositional Flags []HelpFlag + GlobalFlags []HelpFlag UsageString string CommandName string PrependMessage string AppendMessage string + ShowCompletion bool Message string Description string + Lines []string } // HelpSubcommand is used to template subcommand Help output @@ -45,31 +50,47 @@ type HelpFlag struct { LongName string Description string DefaultValue string - Spacer string + ShortDisplay string + LongDisplay string } // ExtractValues extracts Help template values from a subcommand and its parent // parser. The parser is required in order to detect default flag settings // for help and version output. func (h *Help) ExtractValues(p *Parser, message string) { - // accept message string for output h.Message = message + ctx := p.subcommandContext + if ctx == nil || ctx == p.initialSubcommandContext { + ctx = &p.Subcommand + } + isRootContext := ctx == &p.Subcommand + // extract Help values from the current subcommand in context // prependMessage string - h.PrependMessage = p.subcommandContext.AdditionalHelpPrepend + h.PrependMessage = ctx.AdditionalHelpPrepend // appendMessage string - h.AppendMessage = p.subcommandContext.AdditionalHelpAppend + h.AppendMessage = ctx.AdditionalHelpAppend // command name - h.CommandName = p.subcommandContext.Name + h.CommandName = ctx.Name // description - h.Description = p.subcommandContext.Description + h.Description = ctx.Description + // shell completion + showCompletion := p.ShowCompletion && p.isTopLevelHelpContext() + h.ShowCompletion = showCompletion - maxLength := getLongestNameLength(p.subcommandContext.Subcommands, 0) + // determine the max length of subcommand names for spacer calculation. + maxLength := getLongestNameLength(ctx.Subcommands, 0) + // include the synthetic completion subcommand in spacer calculation + if showCompletion { + if l := len("completion"); l > maxLength { + maxLength = l + } + } // subcommands []HelpSubcommand - for _, cmd := range p.subcommandContext.Subcommands { + for _, cmd := range ctx.Subcommands { if cmd.Hidden { continue } @@ -83,10 +104,23 @@ func (h *Help) ExtractValues(p *Parser, message string) { h.Subcommands = append(h.Subcommands, newHelpSubcommand) } - maxLength = getLongestNameLength(p.subcommandContext.PositionalFlags, 0) + // Append a synthetic completion subcommand at the end when enabled. + // This shows users the correct invocation: "./appName completion [bash|zsh]". + if showCompletion { + completionHelp := HelpSubcommand{ + ShortName: "", + LongName: "completion", + Description: "Generate shell completion script for bash or zsh.", + Position: 0, + Spacer: makeSpacer("completion", maxLength), + } + h.Subcommands = append(h.Subcommands, completionHelp) + } + + maxLength = getLongestNameLength(ctx.PositionalFlags, 0) // parse positional flags into help output structs - for _, pos := range p.subcommandContext.PositionalFlags { + for _, pos := range ctx.PositionalFlags { if pos.Hidden { continue } @@ -101,23 +135,19 @@ func (h *Help) ExtractValues(p *Parser, message string) { h.Positionals = append(h.Positionals, newHelpPositional) } - maxLength = len(versionFlagLongName) - if len(helpFlagLongName) > maxLength { - maxLength = len(helpFlagLongName) - } - maxLength = getLongestNameLength(p.subcommandContext.Flags, maxLength) - maxLength = getLongestNameLength(p.Flags, maxLength) - - // if the built-in version flag is enabled, then add it as a help flag + // if the built-in version flag is enabled, then add it to the appropriate help collection if p.ShowVersionWithVersionFlag { defaultVersionFlag := HelpFlag{ ShortName: "", LongName: versionFlagLongName, Description: "Displays the program version string.", DefaultValue: "", - Spacer: makeSpacer(versionFlagLongName, maxLength), } - h.Flags = append(h.Flags, defaultVersionFlag) + if isRootContext { + h.addFlagToSlice(&h.Flags, defaultVersionFlag) + } else { + h.addFlagToSlice(&h.GlobalFlags, defaultVersionFlag) + } } // if the built-in help flag exists, then add it as a help flag @@ -127,21 +157,64 @@ func (h *Help) ExtractValues(p *Parser, message string) { LongName: helpFlagLongName, Description: "Displays help with available flag, subcommand, and positional value parameters.", DefaultValue: "", - Spacer: makeSpacer(helpFlagLongName, maxLength), } - h.Flags = append(h.Flags, defaultHelpFlag) + if isRootContext { + h.addFlagToSlice(&h.Flags, defaultHelpFlag) + } else { + h.addFlagToSlice(&h.GlobalFlags, defaultHelpFlag) + } } // go through every flag in the subcommand and add it to help output - h.parseFlagsToHelpFlags(p.subcommandContext.Flags, maxLength) + h.parseFlagsToHelpFlags(ctx.Flags, &h.Flags) // go through every flag in the parent parser and add it to help output - h.parseFlagsToHelpFlags(p.Flags, maxLength) + if isRootContext { + h.parseFlagsToHelpFlags(p.Flags, &h.Flags) + } else { + h.parseFlagsToHelpFlags(p.Flags, &h.GlobalFlags) + } + + // Optionally sort flags alphabetically by long name (fallback to short name) + if p.SortFlags { + sort.SliceStable(h.Flags, func(i, j int) bool { + a := h.Flags[i] + b := h.Flags[j] + aName := strings.ToLower(strings.TrimSpace(a.LongName)) + bName := strings.ToLower(strings.TrimSpace(b.LongName)) + if aName == "" { + aName = strings.ToLower(strings.TrimSpace(a.ShortName)) + } + if bName == "" { + bName = strings.ToLower(strings.TrimSpace(b.ShortName)) + } + if p.SortFlagsReverse { + return aName > bName + } + return aName < bName + }) + sort.SliceStable(h.GlobalFlags, func(i, j int) bool { + a := h.GlobalFlags[i] + b := h.GlobalFlags[j] + aName := strings.ToLower(strings.TrimSpace(a.LongName)) + bName := strings.ToLower(strings.TrimSpace(b.LongName)) + if aName == "" { + aName = strings.ToLower(strings.TrimSpace(a.ShortName)) + } + if bName == "" { + bName = strings.ToLower(strings.TrimSpace(b.ShortName)) + } + if p.SortFlagsReverse { + return aName > bName + } + return aName < bName + }) + } // formulate the usage string // first, we capture all the command and positional names by position commandsByPosition := make(map[int]string) - for _, pos := range p.subcommandContext.PositionalFlags { + for _, pos := range ctx.PositionalFlags { if pos.Hidden { continue } @@ -151,7 +224,7 @@ func (h *Help) ExtractValues(p *Parser, message string) { commandsByPosition[pos.Position] = pos.Name } } - for _, cmd := range p.subcommandContext.Subcommands { + for _, cmd := range ctx.Subcommands { if cmd.Hidden { continue } @@ -174,7 +247,7 @@ func (h *Help) ExtractValues(p *Parser, message string) { var usageString string if highestPosition > 0 { // find each positional value and make our final string - usageString = p.subcommandContext.Name + usageString = ctx.Name for i := 1; i <= highestPosition; i++ { if len(commandsByPosition[i]) > 0 { usageString = usageString + " [" + commandsByPosition[i] + "]" @@ -188,12 +261,14 @@ func (h *Help) ExtractValues(p *Parser, message string) { h.UsageString = usageString + alignHelpFlags(h.Flags) + alignHelpFlags(h.GlobalFlags) + h.composeLines() } // parseFlagsToHelpFlags parses the specified slice of flags into // help flags on the the calling help command -func (h *Help) parseFlagsToHelpFlags(flags []*Flag, maxLength int) { - +func (h *Help) parseFlagsToHelpFlags(flags []*Flag, dest *[]HelpFlag) { for _, f := range flags { if f.Hidden { continue @@ -218,7 +293,7 @@ func (h *Help) parseFlagsToHelpFlags(flags []*Flag, maxLength int) { _, isBool := f.AssignmentVar.(*bool) if isBool { b := f.AssignmentVar.(*bool) - if *b == false { + if !*b { defaultValue = "" } } @@ -228,15 +303,14 @@ func (h *Help) parseFlagsToHelpFlags(flags []*Flag, maxLength int) { LongName: f.LongName, Description: f.Description, DefaultValue: defaultValue, - Spacer: makeSpacer(f.LongName, maxLength), } - h.AddFlagToHelp(newHelpFlag) + h.addFlagToSlice(dest, newHelpFlag) } } -// AddFlagToHelp adds a flag to help output if it does not exist -func (h *Help) AddFlagToHelp(f HelpFlag) { - for _, existingFlag := range h.Flags { +// addFlagToSlice adds a flag to the provided slice if it does not exist already. +func (h *Help) addFlagToSlice(dest *[]HelpFlag, f HelpFlag) { + for _, existingFlag := range *dest { if len(existingFlag.ShortName) > 0 && existingFlag.ShortName == f.ShortName { return } @@ -244,7 +318,7 @@ func (h *Help) AddFlagToHelp(f HelpFlag) { return } } - h.Flags = append(h.Flags, f) + *dest = append(*dest, f) } // getLongestNameLength takes a slice of any supported flag and returns the length of the longest of their names @@ -253,7 +327,7 @@ func getLongestNameLength(slice interface{}, min int) int { s := reflect.ValueOf(slice) if s.Kind() != reflect.Slice { - log.Panicf("Paremeter given to getLongestNameLength() is of type %s. Expected slice", s.Kind()) + log.Panicf("Parameter given to getLongestNameLength() is of type %s. Expected slice", s.Kind()) } for i := 0; i < s.Len(); i++ { @@ -287,3 +361,214 @@ func makeSpacer(name string, maxLength int) string { } return strings.Repeat(" ", length) } + +func alignHelpFlags(flags []HelpFlag) { + if len(flags) == 0 { + return + } + + shortWidth := 0 + longWidth := 0 + + for _, flag := range flags { + shortCol := flagShortColumn(flag.ShortName) + longCol := flagLongColumn(flag.LongName) + if l := utf8.RuneCountInString(shortCol); l > shortWidth { + shortWidth = l + } + if l := utf8.RuneCountInString(longCol); l > longWidth { + longWidth = l + } + } + + const shortGap = " " + const descGap = " " + + for i := range flags { + shortCol := flagShortColumn(flags[i].ShortName) + longCol := flagLongColumn(flags[i].LongName) + + if shortWidth > 0 { + flags[i].ShortDisplay = padRight(shortCol, shortWidth) + shortGap + } else { + flags[i].ShortDisplay = shortGap + } + + if longWidth > 0 { + flags[i].LongDisplay = padRight(longCol, longWidth) + descGap + } else { + flags[i].LongDisplay = descGap + } + } +} + +func flagShortColumn(shortName string) string { + if shortName == "" { + return "" + } + return "-" + shortName +} + +func flagLongColumn(longName string) string { + if longName == "" { + return "" + } + return "--" + longName +} + +func padRight(input string, width int) string { + delta := width - utf8.RuneCountInString(input) + if delta <= 0 { + return input + } + return input + strings.Repeat(" ", delta) +} + +func (h *Help) composeLines() { + lines := make([]string, 0, 16) + + appendBlank := func() { + if len(lines) > 0 && lines[len(lines)-1] != "" { + lines = append(lines, "") + } + } + + if h.CommandName != "" || h.Description != "" { + header := h.CommandName + if h.Description != "" { + if header != "" { + header += " - " + } + header += h.Description + } + lines = append(lines, header) + } + + if h.PrependMessage != "" { + lines = append(lines, splitLines(h.PrependMessage)...) + } + + appendSection := func(section []string) { + if len(section) == 0 { + return + } + appendBlank() + lines = append(lines, section...) + } + + if h.UsageString != "" { + section := []string{ + " Usage:", + " " + h.UsageString, + } + appendSection(section) + } + + if len(h.Positionals) > 0 { + section := []string{" Positional Variables:"} + for _, pos := range h.Positionals { + line := " " + pos.Name + " " + pos.Spacer + if pos.Description != "" { + line += " " + pos.Description + } + if pos.DefaultValue != "" { + line += " (default: " + pos.DefaultValue + ")" + } else if pos.Required { + line += " (Required)" + } + section = append(section, line) + } + appendSection(section) + } + + if len(h.Subcommands) > 0 { + section := []string{" Subcommands:"} + for _, sub := range h.Subcommands { + line := " " + sub.LongName + if sub.ShortName != "" { + line += " (" + sub.ShortName + ")" + } + if sub.Position > 1 { + line += " (position " + strconv.Itoa(sub.Position) + ")" + } + if sub.Description != "" { + line += " " + sub.Spacer + sub.Description + } + section = append(section, line) + } + appendSection(section) + } + + if len(h.Flags) > 0 { + section := []string{" Flags:"} + for _, flag := range h.Flags { + line := " " + flag.ShortDisplay + flag.LongDisplay + descAdded := false + if flag.Description != "" { + line += flag.Description + descAdded = true + } + if flag.DefaultValue != "" { + if descAdded { + line += " (default: " + flag.DefaultValue + ")" + } else { + line += "(default: " + flag.DefaultValue + ")" + } + } + section = append(section, line) + } + appendSection(section) + } + + if len(h.GlobalFlags) > 0 { + section := []string{" Global Flags:"} + for _, flag := range h.GlobalFlags { + line := " " + flag.ShortDisplay + flag.LongDisplay + descAdded := false + if flag.Description != "" { + line += flag.Description + descAdded = true + } + if flag.DefaultValue != "" { + if descAdded { + line += " (default: " + flag.DefaultValue + ")" + } else { + line += "(default: " + flag.DefaultValue + ")" + } + } + section = append(section, line) + } + appendSection(section) + } + + appendText := func(text string) { + if text == "" { + return + } + appendBlank() + lines = append(lines, splitLines(text)...) + } + + appendText(h.AppendMessage) + appendText(h.Message) + + if len(lines) == 0 { + lines = append(lines, "") + } else { + if lines[0] != "" { + lines = append([]string{""}, lines...) + } + if lines[len(lines)-1] != "" { + lines = append(lines, "") + } + } + + h.Lines = lines +} + +func splitLines(input string) []string { + if input == "" { + return nil + } + return strings.Split(input, "\n") +} diff --git a/vendor/github.com/integrii/flaggy/logo.png b/vendor/github.com/integrii/flaggy/logo.png deleted file mode 100644 index d5ebabfb7..000000000 Binary files a/vendor/github.com/integrii/flaggy/logo.png and /dev/null differ diff --git a/vendor/github.com/integrii/flaggy/parsedValue.go b/vendor/github.com/integrii/flaggy/parsedValue.go index 04ada324a..a6cf024e5 100644 --- a/vendor/github.com/integrii/flaggy/parsedValue.go +++ b/vendor/github.com/integrii/flaggy/parsedValue.go @@ -1,23 +1,25 @@ package flaggy -// parsedValue represents a flag or subcommand that was parsed. Primairily used +// parsedValue represents a flag or subcommand that was parsed. Primarily used // to account for all parsed values in order to determine if unknown values were // passed to the root parser after all subcommands have been parsed. type parsedValue struct { Key string Value string IsPositional bool // indicates that this value was positional and not a key/value + ConsumesNext bool // indicates that parsing this value consumed the following CLI token } // newParsedValue creates and returns a new parsedValue struct with the // supplied values set -func newParsedValue(key string, value string, isPositional bool) parsedValue { +func newParsedValue(key string, value string, isPositional bool, consumesNext bool) parsedValue { if len(key) == 0 && len(value) == 0 { - panic("cant add parsed value with no key or value") + panic("can't add parsed value with no key or value") } return parsedValue{ Key: key, Value: value, IsPositional: isPositional, + ConsumesNext: consumesNext, } } diff --git a/vendor/github.com/integrii/flaggy/parser.go b/vendor/github.com/integrii/flaggy/parser.go index 41ab76e60..b26538a24 100644 --- a/vendor/github.com/integrii/flaggy/parser.go +++ b/vendor/github.com/integrii/flaggy/parser.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "text/template" ) @@ -23,6 +24,33 @@ type Parser struct { trailingArgumentsExtracted bool // indicates that trailing args have been parsed and should not be appended again parsed bool // indicates this parser has parsed subcommandContext *Subcommand // points to the most specific subcommand being used + initialSubcommandContext *Subcommand // points to the initial help context prior to parsing + ShowCompletion bool // indicates that bash and zsh completion output is possible + SortFlags bool // when true, help output flags are sorted alphabetically + SortFlagsReverse bool // when true with SortFlags, sort order is reversed (Z..A) +} + +// supportedCompletionShells lists every shell that can receive generated completion output. +var supportedCompletionShells = []string{"bash", "zsh", "fish", "powershell", "nushell"} + +// completionShellList joins the supported completion shell names into a space separated string. +func completionShellList() string { + return strings.Join(supportedCompletionShells, " ") +} + +// isSupportedCompletionShell reports whether the provided shell is eligible for generated completions. +func isSupportedCompletionShell(shell string) bool { + for _, supported := range supportedCompletionShells { + if shell == supported { + return true + } + } + return false +} + +// TrailingSubcommand returns the last and most specific subcommand invoked. +func (p *Parser) TrailingSubcommand() *Subcommand { + return p.subcommandContext } // NewParser creates a new ArgumentParser ready to parse inputs @@ -34,11 +62,45 @@ func NewParser(name string) *Parser { p.ShowHelpOnUnexpected = true p.ShowHelpWithHFlag = true p.ShowVersionWithVersionFlag = true + p.ShowCompletion = true + p.SortFlags = false + p.SortFlagsReverse = false p.SetHelpTemplate(DefaultHelpTemplate) - p.subcommandContext = &Subcommand{} + initialContext := &Subcommand{} + p.subcommandContext = initialContext + p.initialSubcommandContext = initialContext return p } +// isTopLevelHelpContext returns true when help output should be shown for the top +// level parser instead of a specific subcommand. +func (p *Parser) isTopLevelHelpContext() bool { + if p.subcommandContext == nil { + return true + } + if p.subcommandContext == &p.Subcommand { + return true + } + if p.initialSubcommandContext != nil && p.subcommandContext == p.initialSubcommandContext { + return true + } + return false +} + +// SortFlagsByLongName enables alphabetical sorting by long flag name +// (case-insensitive) for help output on this parser. +func (p *Parser) SortFlagsByLongName() { + p.SortFlags = true + p.SortFlagsReverse = false +} + +// SortFlagsByLongNameReversed enables reverse alphabetical sorting by +// long flag name (case-insensitive) for help output on this parser. +func (p *Parser) SortFlagsByLongNameReversed() { + p.SortFlags = true + p.SortFlagsReverse = true +} + // ParseArgs parses as if the passed args were the os.Args, but without the // binary at the 0 position in the array. An error is returned if there // is a low level issue converting flags to their proper type. No error @@ -49,13 +111,32 @@ func (p *Parser) ParseArgs(args []string) error { } p.parsed = true + // Handle shell completion before any parsing to avoid unknown-argument exits. + if p.ShowCompletion { + if len(args) >= 1 && strings.EqualFold(args[0], "completion") { + // no shell provided + if len(args) < 2 { + fmt.Fprintf(os.Stderr, "Please specify a shell for completion. Supported shells: %s\n", completionShellList()) + exitOrPanic(2) + } + + shell := strings.ToLower(args[1]) + if isSupportedCompletionShell(shell) { + p.Completion(shell) + exitOrPanic(0) + } + fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\nSupported shells: %s\n", args[1], completionShellList()) + exitOrPanic(2) + } + } + debugPrint("Kicking off parsing with args:", args) - err := p.parse(p, args, 0) + err := p.parse(p, args) if err != nil { return err } - // if we are set to crash on unexpected args, look for those here TODO + // if we are set to exit on unexpected args, look for those here if p.ShowHelpOnUnexpected { parsedValues := p.findAllParsedValues() debugPrint("parsedValues:", parsedValues) @@ -73,13 +154,39 @@ func (p *Parser) ParseArgs(args []string) error { return nil } +// Completion takes in a shell type and outputs the completion script for +// that shell. +func (p *Parser) Completion(completionType string) { + switch strings.ToLower(completionType) { + case "bash": + fmt.Print(GenerateBashCompletion(p)) + case "zsh": + fmt.Print(GenerateZshCompletion(p)) + case "fish": + fmt.Print(GenerateFishCompletion(p)) + case "powershell": + fmt.Print(GeneratePowerShellCompletion(p)) + case "nushell": + fmt.Print(GenerateNushellCompletion(p)) + default: + fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\nSupported shells: %s\n", completionType, completionShellList()) + } +} + // findArgsNotInParsedValues finds arguments not used in parsed values. The // incoming args should be in the order supplied by the user and should not // include the invoked binary, which is normally the first thing in os.Args. func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []string { + // DebugMode = true + // defer func() { + // DebugMode = false + // }() + var argsNotUsed []string var skipNext bool - for _, a := range args { + + for i := 0; i < len(args); i++ { + a := args[i] // if the final argument (--) is seen, then we stop checking because all // further values are trailing arguments. @@ -93,33 +200,72 @@ func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []stri continue } - // strip flag slashes from incoming arguments so they match up with the - // keys from parsedValues. + // Determine token type and normalized key/value arg := parseFlagToName(a) + isFlagToken := strings.HasPrefix(a, "-") + + // skip args that start with 'test.' because they are injected with go test + debugPrint("flagsNotParsed: checking arg for test prefix:", arg) + if strings.HasPrefix(arg, "test.") { + debugPrint("skipping test. prefixed arg has test prefix:", arg) + continue + } + debugPrint("flagsNotParsed: flag is not a test. flag:", arg) // indicates that we found this arg used in one of the parsed values. Used // to indicate which values should be added to argsNotUsed. var foundArgUsed bool - // search all args for a corresponding parsed value - for _, pv := range parsedValues { - // this argumenet was a key - // debugPrint(pv.Key, "==", arg) - debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") - if pv.Key == arg || (pv.IsPositional && pv.Value == arg) { - debugPrint("Found matching parsed arg for " + pv.Key) - foundArgUsed = true // the arg was used in this parsedValues set - // if the value is not a positional value and the parsed value had a - // value that was not blank, we skip the next value in the argument list - if !pv.IsPositional && len(pv.Value) > 0 { - skipNext = true + // For flag tokens, only allow non-positional (flag) matches. + if isFlagToken { + for _, pv := range parsedValues { + debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") + if !pv.IsPositional && pv.Key == arg { + debugPrint("Found matching parsed flag for " + pv.Key) + foundArgUsed = true + if pv.ConsumesNext { + skipNext = true + } else if i+1 < len(args) && pv.Value == args[i+1] { + skipNext = true + } break } } - // this prevents excessive parsed values from being checked after we find - // the arg used for the first time if foundArgUsed { - break + continue + } + } + + // For non-flag tokens, prefer positional matches first. + if !isFlagToken { + for _, pv := range parsedValues { + debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") + if pv.IsPositional && pv.Value == arg { + debugPrint("Found matching parsed positional for " + pv.Value) + foundArgUsed = true + break + } + } + if foundArgUsed { + continue + } + + // Fallback for non-flag tokens: allow matching a non-positional flag by bare name. + for _, pv := range parsedValues { + debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") + if !pv.IsPositional && pv.Key == arg { + debugPrint("Found matching parsed flag for " + pv.Key) + foundArgUsed = true + if pv.ConsumesNext { + skipNext = true + } else if i+1 < len(args) && pv.Value == args[i+1] { + skipNext = true + } + break + } + } + if foundArgUsed { + continue } } @@ -153,13 +299,7 @@ func (p *Parser) SetHelpTemplate(tmpl string) error { // Parse calculates all flags and subcommands func (p *Parser) Parse() error { - - err := p.ParseArgs(os.Args[1:]) - if err != nil { - return err - } - return nil - + return p.ParseArgs(os.Args[1:]) } // ShowHelp shows Help without an error message diff --git a/vendor/github.com/integrii/flaggy/scan_result.go b/vendor/github.com/integrii/flaggy/scan_result.go new file mode 100644 index 000000000..485fb6c6e --- /dev/null +++ b/vendor/github.com/integrii/flaggy/scan_result.go @@ -0,0 +1,39 @@ +package flaggy + +// flagScanResult summarizes the outcome of scanning arguments for a parser. +type flagScanResult struct { + // Positionals lists positional tokens (subcommands or positional args) in + // the order they were encountered, along with their indexes in the source + // argument slice. + Positionals []positionalToken + // ForwardArgs contains arguments that were intentionally left untouched so + // that downstream parsers can process them. These tokens maintain their + // original order. + ForwardArgs []string + // HelpRequested reports whether a help flag (-h/--help) was encountered + // while scanning this parser. + HelpRequested bool + // Subcommand holds the first subcommand encountered while scanning. When + // non-nil, scanning stops and the remaining arguments are handed off to the + // referenced parser. + Subcommand *subcommandMatch +} + +// positionalToken tracks a positional argument's value and the index it was +// read from in the source slice. +type positionalToken struct { + Value string + Index int +} + +// subcommandMatch captures the metadata necessary to hand control over to a +// downstream subcommand parser. +type subcommandMatch struct { + // Command references the subcommand that matched the positional token. + Command *Subcommand + // Token points to the positional token that triggered the match. + Token positionalToken + // RelativeDepth tracks the positional depth (1-based) where the match was + // found. This mirrors how subcommand positions are configured. + RelativeDepth int +} diff --git a/vendor/github.com/integrii/flaggy/subCommand.go b/vendor/github.com/integrii/flaggy/subCommand.go index 7f99e3e8a..9bae18fc9 100644 --- a/vendor/github.com/integrii/flaggy/subCommand.go +++ b/vendor/github.com/integrii/flaggy/subCommand.go @@ -3,8 +3,12 @@ package flaggy import ( "fmt" "log" + "math/big" "net" + netip "net/netip" + "net/url" "os" + "regexp" "strconv" "strings" "time" @@ -47,154 +51,142 @@ func NewSubcommand(name string) *Subcommand { // out of the supplied args and returns the resulting positional items in order, // all the flag names found (without values), a bool to indicate if help was // requested, and any errors found during parsing -func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, bool, error) { +func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) (flagScanResult, error) { - var positionalOnlyArguments []string - var helpRequested bool // indicates the user has supplied -h and we - // should render help if we are the last subcommand + result := flagScanResult{} + positionalCount := 0 - // indicates we should skip the next argument, like when parsing a flag - // that separates key and value by space - var skipNext bool - - // endArgfound indicates that a -- was found and everything - // remaining should be added to the trailing arguments slices - var endArgFound bool - - // find all the normal flags (not positional) and parse them out - for i, a := range args { + for i := 0; i < len(args); i++ { + a := args[i] debugPrint("parsing arg:", a) - // evaluate if there is a following arg to avoid panics - var nextArgExists bool - var nextArg string - if len(args)-1 >= i+1 { - nextArgExists = true - nextArg = args[i+1] - } - - // if end arg -- has been found, just add everything to TrailingArguments - if endArgFound { - if !p.trailingArgumentsExtracted { - p.TrailingArguments = append(p.TrailingArguments, a) - } - continue - } - - // skip this run if specified - if skipNext { - skipNext = false - debugPrint("skipping flag because it is an arg:", a) - continue - } - - // parse the flag into its name for consideration without dashes - flagName := parseFlagToName(a) - - // if the flag being passed is version or v and the option to display - // version with version flags, then display version - if p.ShowVersionWithVersionFlag { - if flagName == versionFlagLongName { - p.ShowVersionAndExit() - } - } - - // if the show Help on h flag option is set, then show Help when h or Help - // is passed as an option - if p.ShowHelpWithHFlag { - if flagName == helpFlagShortName || flagName == helpFlagLongName { - // Ensure this is the last subcommand passed so we give the correct - // help output - helpRequested = true - continue - } - } - - // determine what kind of flag this is argType := determineArgType(a) - // strip flags from arg - // debugPrint("Parsing flag named", a, "of type", argType) + if argType == argIsFinal { + if !p.trailingArgumentsExtracted { + p.TrailingArguments = append(p.TrailingArguments, args[i+1:]...) + } + break + } + + flagName := parseFlagToName(a) + + if p.ShowVersionWithVersionFlag && flagName == versionFlagLongName { + p.ShowVersionAndExit() + } + + if p.ShowHelpWithHFlag && (flagName == helpFlagShortName || flagName == helpFlagLongName) { + result.HelpRequested = true + continue + } + + debugPrint("Parsing flag named", a, "of type", argType) - // depending on the flag type, parse the key and value out, then apply it switch argType { - case argIsFinal: - // debugPrint("Arg", i, "is final:", a) - endArgFound = true case argIsPositional: - // debugPrint("Arg is positional or subcommand:", a) - // this positional argument into a slice of their own, so that - // we can determine if its a subcommand or positional value later - positionalOnlyArguments = append(positionalOnlyArguments, a) - // track this as a parsed value with the subcommand + positionalCount++ + token := positionalToken{Value: a, Index: i} + result.Positionals = append(result.Positionals, token) sc.addParsedPositionalValue(a) - case argIsFlagWithSpace: // a flag with a space. ex) -k v or --key value - a = parseFlagToName(a) - // debugPrint("Arg", i, "is flag with space:", a) - // parse next arg as value to this flag and apply to subcommand flags - // if the flag is a bool flag, then we check for a following positional - // and skip it if necessary - if flagIsBool(sc, p, a) { - debugPrint(sc.Name, "bool flag", a, "next var is:", nextArg) - // set the value in this subcommand and its root parser - valueSet, err := setValueForParsers(a, "true", p, sc) + // Detect subcommands early so we avoid parsing child flags at this level. + var matched *Subcommand + for _, cmd := range sc.Subcommands { + if a == cmd.Name || a == cmd.ShortName { + // Prefer an exact positional match when available. + if cmd.Position == positionalCount { + matched = cmd + break + } + if matched == nil { + matched = cmd + } + } + } + if matched != nil { + // Ignore the tentative match when a positional value is already defined at this depth. + if matched.Position != positionalCount && hasPositionalAtDepth(sc, positionalCount) { + matched = nil + } + } + if matched != nil { + // Drop the provisional positional bookkeeping because the token actually belongs to the child. + if len(result.Positionals) > 0 { + result.Positionals = result.Positionals[:len(result.Positionals)-1] + } + if len(sc.ParsedValues) > 0 { + lastIdx := len(sc.ParsedValues) - 1 + if sc.ParsedValues[lastIdx].IsPositional { + sc.ParsedValues = sc.ParsedValues[:lastIdx] + } + } + // Record which subcommand will own the remainder of the arguments. + result.Subcommand = &subcommandMatch{ + Command: matched, + Token: token, + RelativeDepth: matched.Position, + } + // Stop scanning so the child can handle the remainder. + return result, nil + } + case argIsFlagWithSpace: + key := flagName - // if an error occurs, just return it and quit parsing + if flagIsBool(sc, p, key) { + valueSet, err := setValueForParsers(key, "true", p, sc) if err != nil { - return []string{}, false, err + return result, err } - - // log all values parsed by this subcommand. We leave the value blank - // because the bool value had no explicit true or false supplied if valueSet { - sc.addParsedFlag(a, "") + sc.addParsedFlag(key, "", false) } - - // we've found and set a standalone bool flag, so we move on to the next - // argument in the list of arguments continue } - skipNext = true - // debugPrint(sc.Name, "NOT bool flag", a) + if !flagIsDefined(sc, p, key) { + result.ForwardArgs = append(result.ForwardArgs, args[i]) + if i+1 < len(args) && shouldReserveNextArgForChild(sc, positionalCount, args[i+1]) { + result.ForwardArgs = append(result.ForwardArgs, args[i+1]) + i++ + } + continue + } - // if the next arg was not found, then show a Help message - if !nextArgExists { - p.ShowHelpWithMessage("Expected a following arg for flag " + a + ", but it did not exist.") + if i+1 >= len(args) { + p.ShowHelpWithMessage("Expected a following arg for flag " + key + ", but it did not exist.") exitOrPanic(2) } - valueSet, err := setValueForParsers(a, nextArg, p, sc) + + nextArg := args[i+1] + valueSet, err := setValueForParsers(key, nextArg, p, sc) if err != nil { - return []string{}, false, err + return result, err } - - // log all parsed values in the subcommand if valueSet { - sc.addParsedFlag(a, nextArg) + sc.addParsedFlag(key, nextArg, true) } - case argIsFlagWithValue: // a flag with an equals sign. ex) -k=v or --key=value - // debugPrint("Arg", i, "is flag with value:", a) - a = parseFlagToName(a) + i++ + case argIsFlagWithValue: + keyWithValue := flagName + key, val := parseArgWithValue(keyWithValue) - // parse flag into key and value and apply to subcommand flags - key, val := parseArgWithValue(a) + if !flagIsDefined(sc, p, key) { + result.ForwardArgs = append(result.ForwardArgs, args[i]) + continue + } - // set the value in this subcommand and its root parser valueSet, err := setValueForParsers(key, val, p, sc) if err != nil { - return []string{}, false, err + return result, err } - - // log all values parsed by the subcommand if valueSet { - sc.addParsedFlag(a, val) + sc.addParsedFlag(keyWithValue, val, false) } } } - return positionalOnlyArguments, helpRequested, nil + return result, nil } // findAllParsedValues finds all values parsed by all subcommands and this @@ -211,13 +203,46 @@ func (sc *Subcommand) findAllParsedValues() []parsedValue { return parsedValues } -// parse causes the argument parser to parse based on the supplied []string. -// depth specifies the non-flag subcommand positional depth. A slice of flags -// and subcommands parsed is returned so that the parser can ultimately decide -// if there were any unexpected values supplied by the user -func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { +// shouldReserveNextArgForChild determines if the following argument should be +// left untouched so that a downstream subcommand can parse it as its own flag +// or positional value. +func shouldReserveNextArgForChild(sc *Subcommand, positionalCount int, nextArg string) bool { + if determineArgType(nextArg) == argIsFinal { + return false + } - debugPrint("- Parsing subcommand", sc.Name, "with depth of", depth, "and args", args) + position := positionalCount + 1 + for _, cmd := range sc.Subcommands { + if cmd.Position == position && (cmd.Name == nextArg || cmd.ShortName == nextArg) { + return false + } + } + + for _, pos := range sc.PositionalFlags { + if pos.Position == position { + return false + } + } + + return true +} + +func hasPositionalAtDepth(sc *Subcommand, depth int) bool { + for _, pos := range sc.PositionalFlags { + if pos.Position == depth { + return true + } + } + return false +} + +// parse causes the argument parser to parse based on the supplied []string. +// The args slice should contain only values that have not already been +// consumed by parent parsers. The parser records any values it parses so that +// the root parser can detect unexpected arguments after parsing is complete. +func (sc *Subcommand) parse(p *Parser, args []string) error { + + debugPrint("- Parsing subcommand", sc.Name, "with args", args) // if a command is parsed, its used sc.Used = true @@ -242,103 +267,79 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { sc.ensureNoConflictWithBuiltinVersion() } - // Parse the normal flags out of the argument list and return the positionals - // (subcommands and positional values), along with the flags used. - // Then the flag values are applied to the parent parser and the current - // subcommand being parsed. - positionalOnlyArguments, helpRequested, err := sc.parseAllFlagsFromArgs(p, args) + scan, err := sc.parseAllFlagsFromArgs(p, args) if err != nil { return err } - // indicate that trailing arguments have been extracted, so that they aren't - // appended a second time - p.trailingArgumentsExtracted = true + for idx, token := range scan.Positionals { + relativeDepth := idx + 1 + value := token.Value - // loop over positional values and look for their matching positional - // parameter, or their positional command. If neither are found, then - // we throw an error - var parsedArgCount int - for pos, v := range positionalOnlyArguments { - - // the first relative positional argument will be human natural at position 1 - // but offset for the depth of relative commands being parsed for currently. - relativeDepth := pos - depth + 1 - // debugPrint("Parsing positional only position", relativeDepth, "with value", v) - - if relativeDepth < 1 { - // debugPrint(sc.Name, "skipped value:", v) - continue - } - parsedArgCount++ - - // determine subcommands and parse them by positional value and name - for _, cmd := range sc.Subcommands { - // debugPrint("Subcommand being compared", relativeDepth, "==", cmd.Position, "and", v, "==", cmd.Name, "==", cmd.ShortName) - if relativeDepth == cmd.Position && (v == cmd.Name || v == cmd.ShortName) { - debugPrint("Decending into positional subcommand", cmd.Name, "at relativeDepth", relativeDepth, "and absolute depth", depth+1) - return cmd.parse(p, args, depth+parsedArgCount) // continue recursive positional parsing - } + if scan.Subcommand != nil && token.Index == scan.Subcommand.Token.Index { + debugPrint("Descending into positional subcommand", scan.Subcommand.Command.Name, "at relativeDepth", scan.Subcommand.RelativeDepth) + childArgs := append([]string{}, scan.ForwardArgs...) + childArgs = append(childArgs, args[token.Index+1:]...) + return scan.Subcommand.Command.parse(p, childArgs) } - // determine positional args and parse them by positional value and name var foundPositional bool for _, val := range sc.PositionalFlags { if relativeDepth == val.Position { - debugPrint("Found a positional value at relativePos:", relativeDepth, "value:", v) + debugPrint("Found a positional value at relativePos:", relativeDepth, "value:", value) - // set original value for help output val.defaultValue = *val.AssignmentVar - - // defrerence the struct pointer, then set the pointer property within it - *val.AssignmentVar = v - // debugPrint("set positional to value", *val.AssignmentVar) + *val.AssignmentVar = value foundPositional = true val.Found = true break } } - // if there aren't any positional flags but there are subcommands that - // were not used, display a useful message with subcommand options. - if !foundPositional && p.ShowHelpOnUnexpected { - debugPrint("No positional at position", relativeDepth) - var foundSubcommandAtDepth bool - for _, cmd := range sc.Subcommands { - if cmd.Position == relativeDepth { - foundSubcommandAtDepth = true - } - } - - // if there is a subcommand here but it was not specified, display them all - // as a suggestion to the user before exiting. - if foundSubcommandAtDepth { - // determine which name to use in upcoming help output - fmt.Fprintln(os.Stderr, sc.Name+":", "No subcommand or positional value found at position", strconv.Itoa(relativeDepth)+".") - var output string + if !foundPositional { + if p.ShowHelpOnUnexpected { + debugPrint("No positional at position", relativeDepth) + var foundSubcommandAtDepth bool for _, cmd := range sc.Subcommands { - if cmd.Hidden { - continue + if cmd.Position == relativeDepth { + foundSubcommandAtDepth = true } - output = output + " " + cmd.Name } - // if there are available subcommands, let the user know - if len(output) > 0 { - output = strings.TrimLeft(output, " ") - fmt.Println("Available subcommands:", output) - } - exitOrPanic(2) - } - // if there were not any flags or subcommands at this position at all, then - // throw an error (display Help if necessary) - p.ShowHelpWithMessage("Unexpected argument: " + v) - exitOrPanic(2) + if foundSubcommandAtDepth { + fmt.Fprintln(os.Stderr, sc.Name+":", "No subcommand or positional value found at position", strconv.Itoa(relativeDepth)+".") + var output string + for _, cmd := range sc.Subcommands { + if cmd.Hidden { + continue + } + output = output + " " + cmd.Name + } + if len(output) > 0 { + output = strings.TrimLeft(output, " ") + fmt.Println("Available subcommands:", output) + } + exitOrPanic(2) + } + + p.ShowHelpWithMessage("Unexpected argument: " + value) + exitOrPanic(2) + } else { + p.TrailingArguments = append(p.TrailingArguments, value) + } } } - // if help was requested and we should show help when h is passed, - if helpRequested && p.ShowHelpWithHFlag { + if scan.Subcommand != nil { + // If we recorded a subcommand but didn't descend, ensure the remaining + // arguments are handed off now. + debugPrint("Descending into positional subcommand", scan.Subcommand.Command.Name, "at relativeDepth", scan.Subcommand.RelativeDepth) + childArgs := append([]string{}, scan.ForwardArgs...) + childArgs = append(childArgs, args[scan.Subcommand.Token.Index+1:]...) + return scan.Subcommand.Command.parse(p, childArgs) + } + + if scan.HelpRequested && p.ShowHelpWithHFlag { p.ShowHelp() exitOrPanic(0) } @@ -358,18 +359,22 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { } } + // indicate that trailing arguments have been extracted, so that they aren't + // appended a second time by parent parsers. + p.trailingArgumentsExtracted = true + return nil } // addParsedFlag makes it easy to append flag values parsed by the subcommand -func (sc *Subcommand) addParsedFlag(key string, value string) { - sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false)) +func (sc *Subcommand) addParsedFlag(key string, value string, consumesNext bool) { + sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false, consumesNext)) } // addParsedPositionalValue makes it easy to append positionals parsed by the // subcommand func (sc *Subcommand) addParsedPositionalValue(value string) { - sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true)) + sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true, false)) } // FlagExists lets you know if the flag name exists as either a short or long @@ -469,6 +474,11 @@ func (sc *Subcommand) ByteSlice(assignmentVar *[]byte, shortName string, longNam sc.add(assignmentVar, shortName, longName, description) } +// BytesBase64 adds a new []byte flag parsed from base64 input. +func (sc *Subcommand) BytesBase64(assignmentVar *Base64Bytes, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + // Duration adds a new time.Duration flag. // Input format is described in time.ParseDuration(). // Example values: 1h, 1h50m, 32s @@ -649,6 +659,81 @@ func (sc *Subcommand) IPMaskSlice(assignmentVar *[]net.IPMask, shortName string, sc.add(assignmentVar, shortName, longName, description) } +// Time adds a new time.Time flag. Supports RFC3339/RFC3339Nano, RFC1123, and unix seconds. +func (sc *Subcommand) Time(assignmentVar *time.Time, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// URL adds a new url.URL flag. +func (sc *Subcommand) URL(assignmentVar *url.URL, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// IPNet adds a new net.IPNet flag parsed from CIDR. +func (sc *Subcommand) IPNet(assignmentVar *net.IPNet, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// TCPAddr adds a new net.TCPAddr flag parsed from host:port. +func (sc *Subcommand) TCPAddr(assignmentVar *net.TCPAddr, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// UDPAddr adds a new net.UDPAddr flag parsed from host:port. +func (sc *Subcommand) UDPAddr(assignmentVar *net.UDPAddr, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// FileMode adds a new os.FileMode flag parsed from octal/decimal (base auto-detected). +func (sc *Subcommand) FileMode(assignmentVar *os.FileMode, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// Regexp adds a new regexp.Regexp flag. +func (sc *Subcommand) Regexp(assignmentVar *regexp.Regexp, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// Location adds a new time.Location flag. +func (sc *Subcommand) Location(assignmentVar *time.Location, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// Month adds a new time.Month flag. +func (sc *Subcommand) Month(assignmentVar *time.Month, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// Weekday adds a new time.Weekday flag. +func (sc *Subcommand) Weekday(assignmentVar *time.Weekday, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// BigInt adds a new big.Int flag. +func (sc *Subcommand) BigInt(assignmentVar *big.Int, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// BigRat adds a new big.Rat flag. +func (sc *Subcommand) BigRat(assignmentVar *big.Rat, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// NetipAddr adds a new netip.Addr flag. +func (sc *Subcommand) NetipAddr(assignmentVar *netip.Addr, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// NetipPrefix adds a new netip.Prefix flag. +func (sc *Subcommand) NetipPrefix(assignmentVar *netip.Prefix, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + +// NetipAddrPort adds a new netip.AddrPort flag. +func (sc *Subcommand) NetipAddrPort(assignmentVar *netip.AddrPort, shortName string, longName string, description string) { + sc.add(assignmentVar, shortName, longName, description) +} + // AddPositionalValue adds a positional value to the subcommand. the // relativePosition starts at 1 and is relative to the subcommand it belongs to func (sc *Subcommand) AddPositionalValue(assignmentVar *string, name string, relativePosition int, required bool, description string) { @@ -656,14 +741,14 @@ func (sc *Subcommand) AddPositionalValue(assignmentVar *string, name string, rel // ensure no other positionals are at this depth for _, other := range sc.PositionalFlags { if relativePosition == other.Position { - log.Panicln("Unable to add positional value because one already exists at position: " + strconv.Itoa(relativePosition)) + log.Panicln("Unable to add positional value " + name + " because " + other.Name + " already exists at position: " + strconv.Itoa(relativePosition)) } } // ensure no subcommands at this depth for _, other := range sc.Subcommands { if relativePosition == other.Position { - log.Panicln("Unable to add positional value a subcommand already exists at position: " + strconv.Itoa(relativePosition)) + log.Panicln("Unable to add positional value " + name + "because a subcommand, " + other.Name + ", already exists at position: " + strconv.Itoa(relativePosition)) } } @@ -689,7 +774,9 @@ func (sc *Subcommand) SetValueForKey(key string, value string) (bool, error) { // debugPrint("Evaluating string flag", f.ShortName, "==", key, "||", f.LongName, "==", key) if f.ShortName == key || f.LongName == key { // debugPrint("Setting string value for", key, "to", value) - f.identifyAndAssignValue(value) + if err := f.identifyAndAssignValue(value); err != nil { + return false, err + } return true, nil } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 417f41d2e..dd8c3747f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -89,8 +89,8 @@ github.com/go-logfmt/logfmt github.com/gookit/color # github.com/hpcloud/tail v1.0.0 ## explicit -# github.com/integrii/flaggy v1.4.0 -## explicit; go 1.12 +# github.com/integrii/flaggy v1.8.0 +## explicit; go 1.25 github.com/integrii/flaggy # github.com/invopop/jsonschema v0.10.0 ## explicit; go 1.18