Bump github.com/integrii/flaggy from 1.4.0 to 1.8.0

Bumps [github.com/integrii/flaggy](https://github.com/integrii/flaggy) from 1.4.0 to 1.8.0.
- [Release notes](https://github.com/integrii/flaggy/releases)
- [Commits](https://github.com/integrii/flaggy/compare/v1.4.0...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/integrii/flaggy
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot] 2026-04-01 09:17:06 +00:00 committed by GitHub
parent 03e75bd3ed
commit dbb1ff4f24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1771 additions and 317 deletions

2
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

23
vendor/github.com/integrii/flaggy/AGENTS.md generated vendored Normal file
View file

@ -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.

17
vendor/github.com/integrii/flaggy/CONTRIBUTING.md generated vendored Normal file
View file

@ -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!

View file

@ -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 <http://unlicense.org>
For more information, please refer to <https://unlicense.org>

View file

@ -1,18 +1,18 @@
<p align="center">
<img src="/logo.png" />
<img width=240 src="https://raw.githubusercontent.com/integrii/flaggy/master/assets/flaggy-gopher.png" />
<br />
<a href="https://goreportcard.com/report/github.com/integrii/flaggy"><img src="https://goreportcard.com/badge/github.com/integrii/flaggy"></a>
<a href="https://travis-ci.org/integrii/flaggy"><img src="https://travis-ci.org/integrii/flaggy.svg?branch=master"></a>
<a href="http://godoc.org/github.com/integrii/flaggy"><img src="https://camo.githubusercontent.com/d48cccd1ce67ddf8ba7fc356ec1087f3f7aa6d12/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f6c696c65696f2f6c696c653f7374617475732e737667"></a>
<a href="https://pkg.go.dev/github.com/integrii/flaggy"> <img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white"></a>
<a href="http://unlicense.org/"><img src="https://img.shields.io/badge/license-Unlicense-blue.svg"></a>
<a href="https://cover.run/go?repo=github.com%2Fintegrii%2Fflaggy&tag=golang-1.10"><img src="https://cover.run/go/github.com/integrii/flaggy.svg?style=flat&tag=golang-1.10"></a>
<a href="https://github.com/avelino/awesome-go"><img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"></a>
<a href="https://gophers.slack.com/messages/CBMUGQYRH" target="_blank"><img src="https://img.shields.io/badge/slack-@gophers/flaggy-blue.svg?logo=slack"></a>
</p>
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

4
vendor/github.com/integrii/flaggy/byte_types.go generated vendored Normal file
View file

@ -0,0 +1,4 @@
package flaggy
// Base64Bytes is a []byte interpreted as base64 when parsed from flags.
type Base64Bytes []byte

390
vendor/github.com/integrii/flaggy/completion.go generated vendored Normal file
View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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}}`

View file

@ -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")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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,
}
}

View file

@ -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

39
vendor/github.com/integrii/flaggy/scan_result.go generated vendored Normal file
View file

@ -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
}

View file

@ -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
}
}

4
vendor/modules.txt vendored
View file

@ -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