mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-29 02:34:16 -05:00
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>
336 lines
11 KiB
Go
336 lines
11 KiB
Go
package flaggy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"text/template"
|
|
)
|
|
|
|
// Parser represents the set of flags and subcommands we are expecting
|
|
// from our input arguments. Parser is the top level struct responsible for
|
|
// parsing an entire set of subcommands and flags.
|
|
type Parser struct {
|
|
Subcommand
|
|
Version string // the optional version of the parser.
|
|
ShowHelpWithHFlag bool // display help when -h or --help passed
|
|
ShowVersionWithVersionFlag bool // display the version when --version passed
|
|
ShowHelpOnUnexpected bool // display help when an unexpected flag or subcommand is passed
|
|
TrailingArguments []string // everything after a -- is placed here
|
|
HelpTemplate *template.Template // template for Help output
|
|
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
|
|
func NewParser(name string) *Parser {
|
|
// this can not be done inline because of struct embedding
|
|
p := &Parser{}
|
|
p.Name = name
|
|
p.Version = defaultVersion
|
|
p.ShowHelpOnUnexpected = true
|
|
p.ShowHelpWithHFlag = true
|
|
p.ShowVersionWithVersionFlag = true
|
|
p.ShowCompletion = true
|
|
p.SortFlags = false
|
|
p.SortFlagsReverse = false
|
|
p.SetHelpTemplate(DefaultHelpTemplate)
|
|
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
|
|
// is returned for invalid arguments or missing require subcommands.
|
|
func (p *Parser) ParseArgs(args []string) error {
|
|
if p.parsed {
|
|
return errors.New("Parser.Parse() called twice on parser with name: " + " " + p.Name + " " + p.ShortName)
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if we are set to exit on unexpected args, look for those here
|
|
if p.ShowHelpOnUnexpected {
|
|
parsedValues := p.findAllParsedValues()
|
|
debugPrint("parsedValues:", parsedValues)
|
|
argsNotParsed := findArgsNotInParsedValues(args, parsedValues)
|
|
if len(argsNotParsed) > 0 {
|
|
// flatten out unused args for our error message
|
|
var argsNotParsedFlat string
|
|
for _, a := range argsNotParsed {
|
|
argsNotParsedFlat = argsNotParsedFlat + " " + a
|
|
}
|
|
p.ShowHelpAndExit("Unknown arguments supplied: " + argsNotParsedFlat)
|
|
}
|
|
}
|
|
|
|
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 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.
|
|
if determineArgType(a) == argIsFinal {
|
|
return argsNotUsed
|
|
}
|
|
|
|
// allow for skipping the next arg when needed
|
|
if skipNext {
|
|
skipNext = false
|
|
continue
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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
|
|
}
|
|
}
|
|
if foundArgUsed {
|
|
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
|
|
}
|
|
}
|
|
|
|
// if the arg was not used in any parsed values, then we add it to the slice
|
|
// of arguments not used
|
|
if !foundArgUsed {
|
|
argsNotUsed = append(argsNotUsed, arg)
|
|
}
|
|
}
|
|
|
|
return argsNotUsed
|
|
}
|
|
|
|
// ShowVersionAndExit shows the version of this parser
|
|
func (p *Parser) ShowVersionAndExit() {
|
|
fmt.Println("Version:", p.Version)
|
|
exitOrPanic(0)
|
|
}
|
|
|
|
// SetHelpTemplate sets the go template this parser will use when rendering
|
|
// Help.
|
|
func (p *Parser) SetHelpTemplate(tmpl string) error {
|
|
var err error
|
|
p.HelpTemplate = template.New(helpFlagLongName)
|
|
p.HelpTemplate, err = p.HelpTemplate.Parse(tmpl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Parse calculates all flags and subcommands
|
|
func (p *Parser) Parse() error {
|
|
return p.ParseArgs(os.Args[1:])
|
|
}
|
|
|
|
// ShowHelp shows Help without an error message
|
|
func (p *Parser) ShowHelp() {
|
|
debugPrint("showing help for", p.subcommandContext.Name)
|
|
p.ShowHelpWithMessage("")
|
|
}
|
|
|
|
// ShowHelpAndExit shows parser help and exits with status code 2
|
|
func (p *Parser) ShowHelpAndExit(message string) {
|
|
p.ShowHelpWithMessage(message)
|
|
exitOrPanic(2)
|
|
}
|
|
|
|
// ShowHelpWithMessage shows the Help for this parser with an optional string error
|
|
// message as a header. The supplied subcommand will be the context of Help
|
|
// displayed to the user.
|
|
func (p *Parser) ShowHelpWithMessage(message string) {
|
|
|
|
// create a new Help values template and extract values into it
|
|
help := Help{}
|
|
help.ExtractValues(p, message)
|
|
err := p.HelpTemplate.Execute(os.Stderr, help)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "Error rendering Help template:", err)
|
|
}
|
|
}
|
|
|
|
// DisableShowVersionWithVersion disables the showing of version information
|
|
// with --version. It is enabled by default.
|
|
func (p *Parser) DisableShowVersionWithVersion() {
|
|
p.ShowVersionWithVersionFlag = false
|
|
}
|