diff options
| author | Jeff Carr <[email protected]> | 2025-09-24 22:17:40 -0500 |
|---|---|---|
| committer | Jeff Carr <[email protected]> | 2025-09-24 22:17:40 -0500 |
| commit | 9f4fc6ecddd88c9d2d84bc1960f1b78ea56cc85c (patch) | |
| tree | d0371789aa93c27e550d764317661894d24ef29e | |
| parent | 8d5a5220c9b2dfc7c6c1e69d1eaed7bb41079bf5 (diff) | |
start autocomplete support
| -rw-r--r-- | complete.go | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/complete.go b/complete.go new file mode 100644 index 0000000..72f964d --- /dev/null +++ b/complete.go @@ -0,0 +1,222 @@ +package arg + +import ( + "fmt" + "io" + "os" + "strings" +) + +// has the variables for autocomplete +type Complete struct { + Subcommand []string // this will be sent from the shell autocomplete scheme + Partial string // whatever the user has partially entered on the commandline + Stderr io.Writer // this is where Stderr + Stdout io.Writer // this is where Stdout +} + +// same as WriteHelpForSubcommand above, but can flip to STDERR and STDOUT +// most shell autocomplete behavior usually wants things that way +func (p *Parser) WriteHelpForAutocomplete(stderr io.Writer, stdout io.Writer, partial string, subcommand ...string) error { + var automatch []string + if stderr == nil { + stderr = os.Stderr + } + cmd, err := p.lookupCommand(subcommand...) + if err != nil { + return err + } + + var positionals, longOptions, shortOptions, envOnlyOptions []*spec + var hasVersionOption bool + for _, spec := range cmd.specs { + switch { + case spec.positional: + positionals = append(positionals, spec) + case spec.long != "": + longOptions = append(longOptions, spec) + if spec.long == "version" { + hasVersionOption = true + } + case spec.short != "": + shortOptions = append(shortOptions, spec) + case spec.short == "" && spec.long == "": + envOnlyOptions = append(envOnlyOptions, spec) + } + } + + // obtain a flattened list of options from all ancestors + // also determine if any ancestor has a version option spec + var globals []*spec + ancestor := cmd.parent + for ancestor != nil { + for _, spec := range ancestor.specs { + if spec.long == "version" { + hasVersionOption = true + break + } + } + globals = append(globals, ancestor.specs...) + ancestor = ancestor.parent + } + + if p.description != "" { + fmt.Fprintln(stderr, p.description) + } + + if !hasVersionOption && p.version != "" { + fmt.Fprintln(stderr, p.version) + } + + p.WriteUsageForSubcommand(stderr, subcommand...) + + // write the list of positionals + if len(positionals) > 0 { + fmt.Fprint(stderr, "\nPositional arguments:\n") + for _, spec := range positionals { + print(stderr, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env)) + } + } + + // write the list of options with the short-only ones first to match the usage string + if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil { + fmt.Fprint(stderr, "\nOptions:\n") + for _, spec := range shortOptions { + p.printOption(stderr, spec) + } + for _, spec := range longOptions { + p.printOption(stderr, spec) + //jwc + if strings.HasPrefix(spec.long, partial) { + automatch = append(automatch, "--"+spec.long) + } + } + } + + // write the list of global options + if len(globals) > 0 { + fmt.Fprint(stderr, "\nGlobal options:\n") + for _, spec := range globals { + p.printOption(stderr, spec) + } + } + + // write the list of built in options + p.printOption(stderr, &spec{ + cardinality: zero, + long: "help", + short: "h", + help: "display this help and exit", + }) + if !hasVersionOption && p.version != "" { + p.printOption(stderr, &spec{ + cardinality: zero, + long: "version", + help: "display version and exit", + }) + } + + // write the list of environment only variables + if len(envOnlyOptions) > 0 { + fmt.Fprint(stderr, "\nEnvironment variables:\n") + for _, spec := range envOnlyOptions { + p.printEnvOnlyVar(stderr, spec) + } + } + + // write the list of subcommands + if len(cmd.subcommands) > 0 { + fmt.Fprint(stderr, "\nCommands:\n") + for _, subcmd := range cmd.subcommands { + names := append([]string{subcmd.name}, subcmd.aliases...) + print(stderr, strings.Join(names, ", "), subcmd.help) + if strings.HasPrefix(subcmd.name, partial) { + automatch = append(automatch, subcmd.name) + } + } + } + + if p.epilogue != "" { + fmt.Fprintln(stderr, "\n"+p.epilogue) + } + + if stdout == nil { + fmt.Fprintf(os.Stdout, "err foo") + } else { + // writes out the shell autocomplete matches + if len(automatch) > 0 { + fmt.Fprintf(stdout, "%s", strings.Join(automatch, " ")) + } + } + return nil +} + +// GetUsageForSubcommand gets the commands for bash shell completetion +func (p *Parser) GetUsageForSubcommand(stdout io.Writer, stderr io.Writer, partial string, s string) (string, error) { + var log []string + var final []string + cmd, err := p.lookupCommand(s) + if err != nil { + return "", err + } + + var positionals, longOptions, shortOptions []*spec + for _, spec := range cmd.specs { + switch { + case spec.positional: + positionals = append(positionals, spec) + case spec.long != "": + longOptions = append(longOptions, spec) + case spec.short != "": + shortOptions = append(shortOptions, spec) + } + } + + // write the option component of the usage message + for _, spec := range shortOptions { + // prefix with a space + log = append(log, fmt.Sprintf(" ")) + if !spec.required { + // log = append(log, fmt.Sprintf("[")) + } + log = append(log, synopsis(spec, "-"+spec.short)) + if !spec.required { + // log = append(log, fmt.Sprintf("]")) + } + } + + for _, spec := range longOptions { + // prefix with a space + if !spec.required { + // log = append(log, fmt.Sprintf("[")) + } + log = append(log, synopsis(spec, "--"+spec.long)) + if !spec.required { + // log = append(log, fmt.Sprintf("]")) + } + } + + for _, spec := range positionals { + if !spec.required { + // log = append(log, fmt.Sprintf("[")) + } + if spec.cardinality == multiple { + log = append(log, fmt.Sprintf("%s [%s ...]", spec.placeholder, spec.placeholder)) + } else { + log = append(log, spec.placeholder) + } + } + for _, subcmd := range cmd.subcommands { + names := append([]string{subcmd.name}, subcmd.aliases...) + if strings.HasPrefix(subcmd.name, partial) { + final = append(final, subcmd.name) + } + log = append(log, strings.Join(names, ", "), subcmd.help) + if stderr != nil { + print(stderr, strings.Join(names, ", "), subcmd.help) + } + } + fmt.Fprintf(stdout, "%s", strings.Join(final, " ")) + + return strings.Join(final, " "), nil +} |
