The approach is lifted from cobra: `tailscale completion bash` emits a bash
script for configuring the shell's autocomplete:
. <( tailscale completion bash )
so that typing:
tailscale st<TAB>
invokes:
tailscale completion __complete -- st
RELNOTE=tailscale CLI now supports shell tab-completion
Fixes #3793
Signed-off-by: Paul Scott <paul@tailscale.com>
main
parent
21a0fe1b9b
commit
82394debb7
@ -0,0 +1,160 @@ |
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build go1.19 && !ts_omit_completion
|
||||||
|
|
||||||
|
// Package ffcomplete provides shell tab-completion of subcommands, flags and
|
||||||
|
// arguments for Go programs written with [ffcli].
|
||||||
|
//
|
||||||
|
// The shell integration scripts have been extracted from Cobra
|
||||||
|
// (https://cobra.dev/), whose authors deserve most of the credit for this work.
|
||||||
|
// These shell completion functions invoke `$0 completion __complete -- ...`
|
||||||
|
// which is wired up to [Complete].
|
||||||
|
package ffcomplete |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli" |
||||||
|
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal" |
||||||
|
"tailscale.com/tempfork/spf13/cobra" |
||||||
|
) |
||||||
|
|
||||||
|
type compOpts struct { |
||||||
|
showFlags bool |
||||||
|
showDescs bool |
||||||
|
} |
||||||
|
|
||||||
|
func newFS(name string, opts *compOpts) *flag.FlagSet { |
||||||
|
fs := flag.NewFlagSet(name, flag.ContinueOnError) |
||||||
|
fs.BoolVar(&opts.showFlags, "flags", true, "Suggest flag completions with subcommands") |
||||||
|
fs.BoolVar(&opts.showDescs, "descs", true, "Include flag, subcommand, and other descriptions in completions") |
||||||
|
return fs |
||||||
|
} |
||||||
|
|
||||||
|
// Inject adds the 'completion' subcommand to the root command which provide the
|
||||||
|
// user with shell scripts for calling `completion __command` to provide
|
||||||
|
// tab-completion suggestions.
|
||||||
|
//
|
||||||
|
// root.Name needs to match the command that the user is tab-completing for the
|
||||||
|
// shell script to work as expected by default.
|
||||||
|
//
|
||||||
|
// The hide function is called with the __complete Command instance to provide a
|
||||||
|
// hook to omit it from the help output, if desired.
|
||||||
|
func Inject(root *ffcli.Command, hide func(*ffcli.Command), usageFunc func(*ffcli.Command) string) { |
||||||
|
var opts compOpts |
||||||
|
compFS := newFS("completion", &opts) |
||||||
|
|
||||||
|
completeCmd := &ffcli.Command{ |
||||||
|
Name: "__complete", |
||||||
|
ShortUsage: root.Name + " completion __complete -- <args to complete...>", |
||||||
|
ShortHelp: "Tab-completion suggestions for interactive shells", |
||||||
|
UsageFunc: usageFunc, |
||||||
|
FlagSet: compFS, |
||||||
|
Exec: func(ctx context.Context, args []string) error { |
||||||
|
// Set up debug logging for the rest of this function call.
|
||||||
|
if t := os.Getenv("BASH_COMP_DEBUG_FILE"); t != "" { |
||||||
|
tf, err := os.OpenFile(t, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("opening debug file: %w", err) |
||||||
|
} |
||||||
|
defer func(origW io.Writer, origPrefix string, origFlags int) { |
||||||
|
log.SetOutput(origW) |
||||||
|
log.SetFlags(origFlags) |
||||||
|
log.SetPrefix(origPrefix) |
||||||
|
tf.Close() |
||||||
|
}(log.Writer(), log.Prefix(), log.Flags()) |
||||||
|
log.SetOutput(tf) |
||||||
|
log.SetFlags(log.Lshortfile) |
||||||
|
log.SetPrefix("debug: ") |
||||||
|
} |
||||||
|
|
||||||
|
// Send back the results to the shell.
|
||||||
|
words, dir, err := internal.Complete(root, args, opts.showFlags, opts.showDescs) |
||||||
|
if err != nil { |
||||||
|
dir = ShellCompDirectiveError |
||||||
|
} |
||||||
|
for _, word := range words { |
||||||
|
fmt.Println(word) |
||||||
|
} |
||||||
|
fmt.Println(":" + strconv.Itoa(int(dir))) |
||||||
|
return err |
||||||
|
}, |
||||||
|
} |
||||||
|
if hide != nil { |
||||||
|
hide(completeCmd) |
||||||
|
} |
||||||
|
|
||||||
|
root.Subcommands = append( |
||||||
|
root.Subcommands, |
||||||
|
&ffcli.Command{ |
||||||
|
Name: "completion", |
||||||
|
ShortUsage: root.Name + " completion <shell> [--flags] [--descs]", |
||||||
|
ShortHelp: "Shell tab-completion scripts.", |
||||||
|
LongHelp: fmt.Sprintf(cobra.UsageTemplate, root.Name), |
||||||
|
|
||||||
|
// Print help if run without args.
|
||||||
|
Exec: func(ctx context.Context, args []string) error { return flag.ErrHelp }, |
||||||
|
|
||||||
|
// Omit the '__complete' subcommand from the 'completion' help.
|
||||||
|
UsageFunc: func(c *ffcli.Command) string { |
||||||
|
// Filter the subcommands to omit '__complete'.
|
||||||
|
s := make([]*ffcli.Command, 0, len(c.Subcommands)) |
||||||
|
for _, sub := range c.Subcommands { |
||||||
|
if !strings.HasPrefix(sub.Name, "__") { |
||||||
|
s = append(s, sub) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Swap in the filtered subcommands list for the rest of the call.
|
||||||
|
defer func(r []*ffcli.Command) { c.Subcommands = r }(c.Subcommands) |
||||||
|
c.Subcommands = s |
||||||
|
|
||||||
|
// Render the usage.
|
||||||
|
if usageFunc == nil { |
||||||
|
return ffcli.DefaultUsageFunc(c) |
||||||
|
} |
||||||
|
return usageFunc(c) |
||||||
|
}, |
||||||
|
|
||||||
|
Subcommands: append( |
||||||
|
scriptCmds(root, usageFunc), |
||||||
|
completeCmd, |
||||||
|
), |
||||||
|
}, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// Flag registers a completion function for the flag in fs with given name.
|
||||||
|
// comp will always called with a 1-element slice.
|
||||||
|
//
|
||||||
|
// comp will be called to return suggestions when the user tries to tab-complete
|
||||||
|
// '--name=<TAB>' or '--name <TAB>' for the commands using fs.
|
||||||
|
func Flag(fs *flag.FlagSet, name string, comp CompleteFunc) { |
||||||
|
f := fs.Lookup(name) |
||||||
|
if f == nil { |
||||||
|
panic(fmt.Errorf("ffcomplete.Flag: flag %s not found", name)) |
||||||
|
} |
||||||
|
if internal.CompleteFlags == nil { |
||||||
|
internal.CompleteFlags = make(map[*flag.Flag]CompleteFunc) |
||||||
|
} |
||||||
|
internal.CompleteFlags[f] = comp |
||||||
|
} |
||||||
|
|
||||||
|
// Args registers a completion function for the args of cmd.
|
||||||
|
//
|
||||||
|
// comp will be called to return suggestions when the user tries to tab-complete
|
||||||
|
// `prog <TAB>` or `prog subcmd arg1 <TAB>`, for example.
|
||||||
|
func Args(cmd *ffcli.Command, comp CompleteFunc) { |
||||||
|
if internal.CompleteCmds == nil { |
||||||
|
internal.CompleteCmds = make(map[*ffcli.Command]CompleteFunc) |
||||||
|
} |
||||||
|
internal.CompleteCmds[cmd] = comp |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build go1.19 && ts_omit_completion
|
||||||
|
|
||||||
|
package ffcomplete |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli" |
||||||
|
) |
||||||
|
|
||||||
|
func Inject(root *ffcli.Command, hide func(*ffcli.Command), usageFunc func(*ffcli.Command) string) {} |
||||||
|
|
||||||
|
func Flag(fs *flag.FlagSet, name string, comp CompleteFunc) {} |
||||||
|
func Args(cmd *ffcli.Command, comp CompleteFunc) *ffcli.Command { return cmd } |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package ffcomplete |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
|
||||||
|
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal" |
||||||
|
"tailscale.com/tempfork/spf13/cobra" |
||||||
|
) |
||||||
|
|
||||||
|
type ShellCompDirective = cobra.ShellCompDirective |
||||||
|
|
||||||
|
const ( |
||||||
|
ShellCompDirectiveError = cobra.ShellCompDirectiveError |
||||||
|
ShellCompDirectiveNoSpace = cobra.ShellCompDirectiveNoSpace |
||||||
|
ShellCompDirectiveNoFileComp = cobra.ShellCompDirectiveNoFileComp |
||||||
|
ShellCompDirectiveFilterFileExt = cobra.ShellCompDirectiveFilterFileExt |
||||||
|
ShellCompDirectiveFilterDirs = cobra.ShellCompDirectiveFilterDirs |
||||||
|
ShellCompDirectiveKeepOrder = cobra.ShellCompDirectiveKeepOrder |
||||||
|
ShellCompDirectiveDefault = cobra.ShellCompDirectiveDefault |
||||||
|
) |
||||||
|
|
||||||
|
// CompleteFunc is used to return tab-completion suggestions to the user as they
|
||||||
|
// are typing command-line instructions. It returns the list of things to
|
||||||
|
// suggest and an additional directive to the shell about what extra
|
||||||
|
// functionality to enable.
|
||||||
|
type CompleteFunc = internal.CompleteFunc |
||||||
|
|
||||||
|
// LastArg returns the last element of args, or the empty string if args is
|
||||||
|
// empty.
|
||||||
|
func LastArg(args []string) string { |
||||||
|
if len(args) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
return args[len(args)-1] |
||||||
|
} |
||||||
|
|
||||||
|
// Fixed returns a CompleteFunc which suggests the given words.
|
||||||
|
func Fixed(words ...string) CompleteFunc { |
||||||
|
return func(args []string) ([]string, cobra.ShellCompDirective, error) { |
||||||
|
match := LastArg(args) |
||||||
|
matches := make([]string, 0, len(words)) |
||||||
|
for _, word := range words { |
||||||
|
if strings.HasPrefix(word, match) { |
||||||
|
matches = append(matches, word) |
||||||
|
} |
||||||
|
} |
||||||
|
return matches, cobra.ShellCompDirectiveNoFileComp, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// FilesWithExtensions returns a CompleteFunc that tells the shell to limit file
|
||||||
|
// suggestions to those with the given extensions.
|
||||||
|
func FilesWithExtensions(exts ...string) CompleteFunc { |
||||||
|
return func(args []string) ([]string, cobra.ShellCompDirective, error) { |
||||||
|
return exts, cobra.ShellCompDirectiveFilterFileExt, nil |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,256 @@ |
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package internal |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3" |
||||||
|
"github.com/peterbourgon/ff/v3/ffcli" |
||||||
|
"tailscale.com/tempfork/spf13/cobra" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
CompleteCmds map[*ffcli.Command]CompleteFunc |
||||||
|
CompleteFlags map[*flag.Flag]CompleteFunc |
||||||
|
) |
||||||
|
|
||||||
|
type CompleteFunc func([]string) ([]string, cobra.ShellCompDirective, error) |
||||||
|
|
||||||
|
// Complete returns the autocomplete suggestions for the root program and args.
|
||||||
|
//
|
||||||
|
// The returned words do not necessarily need to be prefixed with the last arg
|
||||||
|
// which is being completed. For example, '--bool-flag=' will have completions
|
||||||
|
// 'true' and 'false'.
|
||||||
|
//
|
||||||
|
// "HIDDEN: " is trimmed from the start of Flag Usage's.
|
||||||
|
func Complete(root *ffcli.Command, args []string, startFlags, descs bool) (words []string, dir cobra.ShellCompDirective, err error) { |
||||||
|
// Explicitly log panics.
|
||||||
|
defer func() { |
||||||
|
if r := recover(); r != nil { |
||||||
|
if rerr, ok := err.(error); ok { |
||||||
|
err = fmt.Errorf("panic: %w", rerr) |
||||||
|
} else { |
||||||
|
err = fmt.Errorf("panic: %v", r) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
// Set up the arguments.
|
||||||
|
if len(args) == 0 { |
||||||
|
args = []string{""} |
||||||
|
} |
||||||
|
|
||||||
|
// Completion criteria.
|
||||||
|
completeArg := args[len(args)-1] |
||||||
|
args = args[:len(args)-1] |
||||||
|
emitFlag := startFlags || strings.HasPrefix(completeArg, "-") |
||||||
|
emitArgs := true |
||||||
|
|
||||||
|
// Traverse the command-tree to find the cmd command whose
|
||||||
|
// subcommand, flags, or arguments are being completed.
|
||||||
|
cmd := root |
||||||
|
walk: |
||||||
|
for { |
||||||
|
// Ensure there's a flagset with ContinueOnError set.
|
||||||
|
if cmd.FlagSet == nil { |
||||||
|
cmd.FlagSet = flag.NewFlagSet(cmd.Name, flag.ContinueOnError) |
||||||
|
} |
||||||
|
cmd.FlagSet.Init(cmd.FlagSet.Name(), flag.ContinueOnError) |
||||||
|
|
||||||
|
// Manually split the args so we know when we're completing flags/args.
|
||||||
|
flagArgs, argArgs, flagNeedingValue := splitFlagArgs(cmd.FlagSet, args) |
||||||
|
if flagNeedingValue != "" { |
||||||
|
completeArg = flagNeedingValue + "=" + completeArg |
||||||
|
emitFlag = true |
||||||
|
} |
||||||
|
args = argArgs |
||||||
|
|
||||||
|
// Parse the flags.
|
||||||
|
err := ff.Parse(cmd.FlagSet, flagArgs, cmd.Options...) |
||||||
|
if err != nil { |
||||||
|
return nil, 0, fmt.Errorf("%s flag parsing: %w", cmd.Name, err) |
||||||
|
} |
||||||
|
if cmd.FlagSet.NArg() > 0 { |
||||||
|
// This shouldn't happen if splitFlagArgs is accurately finding the
|
||||||
|
// split between flags and args.
|
||||||
|
_ = false |
||||||
|
} |
||||||
|
if len(args) == 0 { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the first argument is actually a subcommand.
|
||||||
|
for _, sub := range cmd.Subcommands { |
||||||
|
if strings.EqualFold(sub.Name, args[0]) { |
||||||
|
args = args[1:] |
||||||
|
cmd = sub |
||||||
|
continue walk |
||||||
|
} |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
if len(args) > 0 { |
||||||
|
emitFlag = false |
||||||
|
} |
||||||
|
|
||||||
|
// Complete '-flag=...'. If the args ended with '-flag ...' we will have
|
||||||
|
// rewritten to '-flag=...' by now.
|
||||||
|
if emitFlag && strings.HasPrefix(completeArg, "-") && strings.Contains(completeArg, "=") { |
||||||
|
// Don't complete '-flag' later on as the
|
||||||
|
// flag name is terminated by a '='.
|
||||||
|
emitFlag = false |
||||||
|
emitArgs = false |
||||||
|
|
||||||
|
dashFlag, completeVal, _ := strings.Cut(completeArg, "=") |
||||||
|
_, f := cutDash(dashFlag) |
||||||
|
flag := cmd.FlagSet.Lookup(f) |
||||||
|
if flag != nil { |
||||||
|
if comp := CompleteFlags[flag]; comp != nil { |
||||||
|
// Complete custom flag values.
|
||||||
|
var err error |
||||||
|
words, dir, err = comp([]string{completeVal}) |
||||||
|
if err != nil { |
||||||
|
return nil, 0, fmt.Errorf("completing %s flag %s: %w", cmd.Name, flag.Name, err) |
||||||
|
} |
||||||
|
} else if isBoolFlag(flag) { |
||||||
|
// Complete true/false.
|
||||||
|
for _, vals := range [][]string{ |
||||||
|
{"true", "TRUE", "True", "1"}, |
||||||
|
{"false", "FALSE", "False", "0"}, |
||||||
|
} { |
||||||
|
for _, val := range vals { |
||||||
|
if strings.HasPrefix(val, completeVal) { |
||||||
|
words = append(words, val) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Complete '-flag...'.
|
||||||
|
if emitFlag { |
||||||
|
used := make(map[string]struct{}) |
||||||
|
cmd.FlagSet.Visit(func(f *flag.Flag) { |
||||||
|
used[f.Name] = struct{}{} |
||||||
|
}) |
||||||
|
|
||||||
|
cd, cf := cutDash(completeArg) |
||||||
|
cmd.FlagSet.VisitAll(func(f *flag.Flag) { |
||||||
|
if !strings.HasPrefix(f.Name, cf) { |
||||||
|
return |
||||||
|
} |
||||||
|
// Skip flags already set by the user.
|
||||||
|
if _, seen := used[f.Name]; seen { |
||||||
|
return |
||||||
|
} |
||||||
|
// Suggest single-dash '-v' for single-char flags and
|
||||||
|
// double-dash '--verbose' for longer.
|
||||||
|
d := cd |
||||||
|
if (d == "" || d == "-") && cf == "" && len(f.Name) > 1 { |
||||||
|
d = "--" |
||||||
|
} |
||||||
|
if descs { |
||||||
|
_, usage := flag.UnquoteUsage(f) |
||||||
|
usage = strings.TrimPrefix(usage, "HIDDEN: ") |
||||||
|
if usage != "" { |
||||||
|
words = append(words, d+f.Name+"\t"+usage) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
words = append(words, d+f.Name) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if emitArgs { |
||||||
|
// Complete 'sub...'.
|
||||||
|
for _, sub := range cmd.Subcommands { |
||||||
|
if strings.HasPrefix(sub.Name, completeArg) { |
||||||
|
if descs { |
||||||
|
if sub.ShortHelp != "" { |
||||||
|
words = append(words, sub.Name+"\t"+sub.ShortHelp) |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
words = append(words, sub.Name) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Complete custom args.
|
||||||
|
if comp := CompleteCmds[cmd]; comp != nil { |
||||||
|
w, d, err := comp(append(args, completeArg)) |
||||||
|
if err != nil { |
||||||
|
return nil, 0, fmt.Errorf("completing %s args: %w", cmd.Name, err) |
||||||
|
} |
||||||
|
dir = d |
||||||
|
words = append(words, w...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Strip any descriptions if they were suppressed.
|
||||||
|
if !descs { |
||||||
|
for i := range words { |
||||||
|
words[i], _, _ = strings.Cut(words[i], "\t") |
||||||
|
} |
||||||
|
} |
||||||
|
return words, dir, nil |
||||||
|
} |
||||||
|
|
||||||
|
// splitFlagArgs separates a list of command-line arguments into arguments
|
||||||
|
// comprising flags and their values, preceding arguments to be passed to the
|
||||||
|
// command. This follows the stdlib 'flag' parsing conventions. If the final
|
||||||
|
// argument is a flag name which takes a value but has no value specified, it is
|
||||||
|
// omitted from flagArgs and argArgs and instead returned in needValue.
|
||||||
|
func splitFlagArgs(fs *flag.FlagSet, args []string) (flagArgs, argArgs []string, flagNeedingValue string) { |
||||||
|
for i := 0; i < len(args); i++ { |
||||||
|
a := args[i] |
||||||
|
if a == "--" { |
||||||
|
return args[:i], args[i+1:], "" |
||||||
|
} |
||||||
|
|
||||||
|
d, f := cutDash(a) |
||||||
|
if d == "" { |
||||||
|
return args[:i], args[i:], "" |
||||||
|
} |
||||||
|
if strings.Contains(f, "=") { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
flag := fs.Lookup(f) |
||||||
|
if flag == nil { |
||||||
|
return args[:i], args[i:], "" |
||||||
|
} |
||||||
|
if isBoolFlag(flag) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Consume an extra argument for the flag value.
|
||||||
|
if i == len(args)-1 { |
||||||
|
return args[:i], nil, args[i] |
||||||
|
} |
||||||
|
i++ |
||||||
|
} |
||||||
|
return args, nil, "" |
||||||
|
} |
||||||
|
|
||||||
|
func cutDash(s string) (dashes, flag string) { |
||||||
|
if strings.HasPrefix(s, "-") { |
||||||
|
if strings.HasPrefix(s[1:], "-") { |
||||||
|
return "--", s[2:] |
||||||
|
} |
||||||
|
return "-", s[1:] |
||||||
|
} |
||||||
|
return "", s |
||||||
|
} |
||||||
|
|
||||||
|
func isBoolFlag(f *flag.Flag) bool { |
||||||
|
bf, ok := f.Value.(interface { |
||||||
|
IsBoolFlag() bool |
||||||
|
}) |
||||||
|
return ok && bf.IsBoolFlag() |
||||||
|
} |
||||||
@ -0,0 +1,219 @@ |
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package internal_test |
||||||
|
|
||||||
|
import ( |
||||||
|
_ "embed" |
||||||
|
"flag" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp" |
||||||
|
"github.com/peterbourgon/ff/v3/ffcli" |
||||||
|
"tailscale.com/cmd/tailscale/cli/ffcomplete" |
||||||
|
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal" |
||||||
|
) |
||||||
|
|
||||||
|
func newFlagSet(name string, errh flag.ErrorHandling, flags func(fs *flag.FlagSet)) *flag.FlagSet { |
||||||
|
fs := flag.NewFlagSet(name, errh) |
||||||
|
if flags != nil { |
||||||
|
flags(fs) |
||||||
|
} |
||||||
|
return fs |
||||||
|
} |
||||||
|
|
||||||
|
func TestComplete(t *testing.T) { |
||||||
|
t.Parallel() |
||||||
|
|
||||||
|
// Build our test program in testdata.
|
||||||
|
root := &ffcli.Command{ |
||||||
|
Name: "prog", |
||||||
|
FlagSet: newFlagSet("prog", flag.ContinueOnError, func(fs *flag.FlagSet) { |
||||||
|
fs.Bool("v", false, "verbose") |
||||||
|
fs.Bool("root-bool", false, "root `bool`") |
||||||
|
fs.String("root-str", "", "some `text`") |
||||||
|
}), |
||||||
|
Subcommands: []*ffcli.Command{ |
||||||
|
{ |
||||||
|
Name: "debug", |
||||||
|
ShortHelp: "Debug data", |
||||||
|
FlagSet: newFlagSet("prog debug", flag.ExitOnError, func(fs *flag.FlagSet) { |
||||||
|
fs.String("cpu-profile", "", "write cpu profile to `file`") |
||||||
|
fs.Bool("debug-bool", false, "debug bool") |
||||||
|
fs.Int("level", 0, "a number") |
||||||
|
fs.String("enum", "", "a flag that takes several specific values") |
||||||
|
ffcomplete.Flag(fs, "enum", ffcomplete.Fixed("alpha", "beta", "charlie")) |
||||||
|
}), |
||||||
|
}, |
||||||
|
func() *ffcli.Command { |
||||||
|
cmd := &ffcli.Command{ |
||||||
|
Name: "ping", |
||||||
|
FlagSet: newFlagSet("prog ping", flag.ContinueOnError, func(fs *flag.FlagSet) { |
||||||
|
fs.String("until", "", "when pinging should end") |
||||||
|
ffcomplete.Flag(fs, "until", ffcomplete.Fixed("forever", "direct")) |
||||||
|
}), |
||||||
|
} |
||||||
|
ffcomplete.Args(cmd, ffcomplete.Fixed("jupiter", "neptune", "venus")) |
||||||
|
return cmd |
||||||
|
}(), |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
tests := []struct { |
||||||
|
args []string |
||||||
|
showFlags bool |
||||||
|
showDescs bool |
||||||
|
wantComp []string |
||||||
|
wantDir ffcomplete.ShellCompDirective |
||||||
|
}{ |
||||||
|
{ |
||||||
|
args: []string{"deb"}, |
||||||
|
wantComp: []string{"debug"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"deb"}, |
||||||
|
showDescs: true, |
||||||
|
wantComp: []string{"debug\tDebug data"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"-"}, |
||||||
|
wantComp: []string{"--root-bool", "--root-str", "-v"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"--"}, |
||||||
|
wantComp: []string{"--root-bool", "--root-str", "--v"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"-r"}, |
||||||
|
wantComp: []string{"-root-bool", "-root-str"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"--r"}, |
||||||
|
wantComp: []string{"--root-bool", "--root-str"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"--root-str=s", "--r"}, |
||||||
|
wantComp: []string{"--root-bool"}, // omits --root-str which is already set
|
||||||
|
}, |
||||||
|
{ |
||||||
|
// '--' disables flag parsing, so we shouldn't suggest flags.
|
||||||
|
args: []string{"--", "--root"}, |
||||||
|
wantComp: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
// '--' is used as the value of '--root-str'.
|
||||||
|
args: []string{"--root-str", "--", "--r"}, |
||||||
|
wantComp: []string{"--root-bool"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
// '--' here is a flag value, so doesn't disable flag parsing.
|
||||||
|
args: []string{"--root-str", "--", "--root"}, |
||||||
|
wantComp: []string{"--root-bool"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
// Equivalent to '--root-str=-- -- --r' meaning '--r' is not
|
||||||
|
// a flag because it's preceded by a '--' argument:
|
||||||
|
// https://go.dev/play/p/UCtftQqVhOD.
|
||||||
|
args: []string{"--root-str", "--", "--", "--r"}, |
||||||
|
wantComp: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"--root-bool="}, |
||||||
|
wantComp: []string{"true", "false"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"--root-bool=t"}, |
||||||
|
wantComp: []string{"true"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"--root-bool=T"}, |
||||||
|
wantComp: []string{"TRUE"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"debug", "--de"}, |
||||||
|
wantComp: []string{"--debug-bool"}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"debug", "--enum="}, |
||||||
|
wantComp: []string{"alpha", "beta", "charlie"}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"debug", "--enum=al"}, |
||||||
|
wantComp: []string{"alpha"}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"debug", "--level", ""}, |
||||||
|
wantComp: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"debug", "--enum", "b"}, |
||||||
|
wantComp: []string{"beta"}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"debug", "--enum", "al"}, |
||||||
|
wantComp: []string{"alpha"}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"ping", ""}, |
||||||
|
showFlags: true, |
||||||
|
wantComp: []string{"--until", "jupiter", "neptune", "venus"}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"ping", ""}, |
||||||
|
showFlags: true, |
||||||
|
showDescs: true, |
||||||
|
wantComp: []string{ |
||||||
|
"--until\twhen pinging should end", |
||||||
|
"jupiter", |
||||||
|
"neptune", |
||||||
|
"venus", |
||||||
|
}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"ping", ""}, |
||||||
|
wantComp: []string{"jupiter", "neptune", "venus"}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
{ |
||||||
|
args: []string{"ping", "j"}, |
||||||
|
wantComp: []string{"jupiter"}, |
||||||
|
wantDir: ffcomplete.ShellCompDirectiveNoFileComp, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
// Run the tests.
|
||||||
|
for _, test := range tests { |
||||||
|
test := test |
||||||
|
name := strings.Join(test.args, "␣") |
||||||
|
if test.showFlags { |
||||||
|
name += "+flags" |
||||||
|
} |
||||||
|
if test.showDescs { |
||||||
|
name += "+descs" |
||||||
|
} |
||||||
|
t.Run(name, func(t *testing.T) { |
||||||
|
// Capture the binary
|
||||||
|
complete, dir, err := internal.Complete(root, test.args, test.showFlags, test.showDescs) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("completion error: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Test the results match our expectation.
|
||||||
|
if test.wantComp != nil { |
||||||
|
if diff := cmp.Diff(test.wantComp, complete); diff != "" { |
||||||
|
t.Errorf("unexpected completion directives (-want +got):\n%s", diff) |
||||||
|
} |
||||||
|
} |
||||||
|
if test.wantDir != dir { |
||||||
|
t.Errorf("got shell completion directive %[1]d (%[1]s), want %[2]d (%[2]s)", dir, test.wantDir) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,85 @@ |
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build go1.19 && !ts_omit_completion && !ts_omit_completion_scripts
|
||||||
|
|
||||||
|
package ffcomplete |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"flag" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli" |
||||||
|
"tailscale.com/tempfork/spf13/cobra" |
||||||
|
) |
||||||
|
|
||||||
|
func compCmd(fs *flag.FlagSet) string { |
||||||
|
var s strings.Builder |
||||||
|
s.WriteString("completion __complete") |
||||||
|
fs.VisitAll(func(f *flag.Flag) { |
||||||
|
s.WriteString(" --") |
||||||
|
s.WriteString(f.Name) |
||||||
|
s.WriteString("=") |
||||||
|
s.WriteString(f.Value.String()) |
||||||
|
}) |
||||||
|
s.WriteString(" --") |
||||||
|
return s.String() |
||||||
|
} |
||||||
|
|
||||||
|
func scriptCmds(root *ffcli.Command, usageFunc func(*ffcli.Command) string) []*ffcli.Command { |
||||||
|
nameForVar := root.Name |
||||||
|
nameForVar = strings.ReplaceAll(nameForVar, "-", "_") |
||||||
|
nameForVar = strings.ReplaceAll(nameForVar, ":", "_") |
||||||
|
|
||||||
|
var ( |
||||||
|
bashFS = newFS("bash", &compOpts{}) |
||||||
|
zshFS = newFS("zsh", &compOpts{}) |
||||||
|
fishFS = newFS("fish", &compOpts{}) |
||||||
|
pwshFS = newFS("powershell", &compOpts{}) |
||||||
|
) |
||||||
|
|
||||||
|
return []*ffcli.Command{ |
||||||
|
{ |
||||||
|
Name: "bash", |
||||||
|
ShortHelp: "Generate bash shell completion script", |
||||||
|
ShortUsage: ". <( " + root.Name + " completion bash )", |
||||||
|
UsageFunc: usageFunc, |
||||||
|
FlagSet: bashFS, |
||||||
|
Exec: func(ctx context.Context, args []string) error { |
||||||
|
return cobra.ScriptBash(os.Stdout, root.Name, compCmd(bashFS), nameForVar) |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
Name: "zsh", |
||||||
|
ShortHelp: "Generate zsh shell completion script", |
||||||
|
ShortUsage: ". <( " + root.Name + " completion zsh )", |
||||||
|
UsageFunc: usageFunc, |
||||||
|
FlagSet: zshFS, |
||||||
|
Exec: func(ctx context.Context, args []string) error { |
||||||
|
return cobra.ScriptZsh(os.Stdout, root.Name, compCmd(zshFS), nameForVar) |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
Name: "fish", |
||||||
|
ShortHelp: "Generate fish shell completion script", |
||||||
|
ShortUsage: root.Name + " completion fish | source", |
||||||
|
UsageFunc: usageFunc, |
||||||
|
FlagSet: fishFS, |
||||||
|
Exec: func(ctx context.Context, args []string) error { |
||||||
|
return cobra.ScriptFish(os.Stdout, root.Name, compCmd(fishFS), nameForVar) |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
Name: "powershell", |
||||||
|
ShortHelp: "Generate powershell completion script", |
||||||
|
ShortUsage: root.Name + " completion powershell | Out-String | Invoke-Expression", |
||||||
|
UsageFunc: usageFunc, |
||||||
|
FlagSet: pwshFS, |
||||||
|
Exec: func(ctx context.Context, args []string) error { |
||||||
|
return cobra.ScriptPowershell(os.Stdout, root.Name, compCmd(pwshFS), nameForVar) |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build go1.19 && !ts_omit_completion && ts_omit_completion_scripts
|
||||||
|
|
||||||
|
package ffcomplete |
||||||
|
|
||||||
|
import "github.com/peterbourgon/ff/v3/ffcli" |
||||||
|
|
||||||
|
func scriptCmds(root *ffcli.Command, usageFunc func(*ffcli.Command) string) []*ffcli.Command { |
||||||
|
return nil |
||||||
|
} |
||||||
@ -0,0 +1,174 @@ |
|||||||
|
Apache License |
||||||
|
Version 2.0, January 2004 |
||||||
|
http://www.apache.org/licenses/ |
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||||
|
|
||||||
|
1. Definitions. |
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, |
||||||
|
and distribution as defined by Sections 1 through 9 of this document. |
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by |
||||||
|
the copyright owner that is granting the License. |
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all |
||||||
|
other entities that control, are controlled by, or are under common |
||||||
|
control with that entity. For the purposes of this definition, |
||||||
|
"control" means (i) the power, direct or indirect, to cause the |
||||||
|
direction or management of such entity, whether by contract or |
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity. |
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity |
||||||
|
exercising permissions granted by this License. |
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, |
||||||
|
including but not limited to software source code, documentation |
||||||
|
source, and configuration files. |
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical |
||||||
|
transformation or translation of a Source form, including but |
||||||
|
not limited to compiled object code, generated documentation, |
||||||
|
and conversions to other media types. |
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or |
||||||
|
Object form, made available under the License, as indicated by a |
||||||
|
copyright notice that is included in or attached to the work |
||||||
|
(an example is provided in the Appendix below). |
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object |
||||||
|
form, that is based on (or derived from) the Work and for which the |
||||||
|
editorial revisions, annotations, elaborations, or other modifications |
||||||
|
represent, as a whole, an original work of authorship. For the purposes |
||||||
|
of this License, Derivative Works shall not include works that remain |
||||||
|
separable from, or merely link (or bind by name) to the interfaces of, |
||||||
|
the Work and Derivative Works thereof. |
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including |
||||||
|
the original version of the Work and any modifications or additions |
||||||
|
to that Work or Derivative Works thereof, that is intentionally |
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner |
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of |
||||||
|
the copyright owner. For the purposes of this definition, "submitted" |
||||||
|
means any form of electronic, verbal, or written communication sent |
||||||
|
to the Licensor or its representatives, including but not limited to |
||||||
|
communication on electronic mailing lists, source code control systems, |
||||||
|
and issue tracking systems that are managed by, or on behalf of, the |
||||||
|
Licensor for the purpose of discussing and improving the Work, but |
||||||
|
excluding communication that is conspicuously marked or otherwise |
||||||
|
designated in writing by the copyright owner as "Not a Contribution." |
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||||
|
on behalf of whom a Contribution has been received by Licensor and |
||||||
|
subsequently incorporated within the Work. |
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of |
||||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||||
|
copyright license to reproduce, prepare Derivative Works of, |
||||||
|
publicly display, publicly perform, sublicense, and distribute the |
||||||
|
Work and such Derivative Works in Source or Object form. |
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of |
||||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||||
|
(except as stated in this section) patent license to make, have made, |
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||||
|
where such license applies only to those patent claims licensable |
||||||
|
by such Contributor that are necessarily infringed by their |
||||||
|
Contribution(s) alone or by combination of their Contribution(s) |
||||||
|
with the Work to which such Contribution(s) was submitted. If You |
||||||
|
institute patent litigation against any entity (including a |
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||||
|
or a Contribution incorporated within the Work constitutes direct |
||||||
|
or contributory patent infringement, then any patent licenses |
||||||
|
granted to You under this License for that Work shall terminate |
||||||
|
as of the date such litigation is filed. |
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the |
||||||
|
Work or Derivative Works thereof in any medium, with or without |
||||||
|
modifications, and in Source or Object form, provided that You |
||||||
|
meet the following conditions: |
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or |
||||||
|
Derivative Works a copy of this License; and |
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices |
||||||
|
stating that You changed the files; and |
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works |
||||||
|
that You distribute, all copyright, patent, trademark, and |
||||||
|
attribution notices from the Source form of the Work, |
||||||
|
excluding those notices that do not pertain to any part of |
||||||
|
the Derivative Works; and |
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its |
||||||
|
distribution, then any Derivative Works that You distribute must |
||||||
|
include a readable copy of the attribution notices contained |
||||||
|
within such NOTICE file, excluding those notices that do not |
||||||
|
pertain to any part of the Derivative Works, in at least one |
||||||
|
of the following places: within a NOTICE text file distributed |
||||||
|
as part of the Derivative Works; within the Source form or |
||||||
|
documentation, if provided along with the Derivative Works; or, |
||||||
|
within a display generated by the Derivative Works, if and |
||||||
|
wherever such third-party notices normally appear. The contents |
||||||
|
of the NOTICE file are for informational purposes only and |
||||||
|
do not modify the License. You may add Your own attribution |
||||||
|
notices within Derivative Works that You distribute, alongside |
||||||
|
or as an addendum to the NOTICE text from the Work, provided |
||||||
|
that such additional attribution notices cannot be construed |
||||||
|
as modifying the License. |
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and |
||||||
|
may provide additional or different license terms and conditions |
||||||
|
for use, reproduction, or distribution of Your modifications, or |
||||||
|
for any such Derivative Works as a whole, provided Your use, |
||||||
|
reproduction, and distribution of the Work otherwise complies with |
||||||
|
the conditions stated in this License. |
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||||
|
any Contribution intentionally submitted for inclusion in the Work |
||||||
|
by You to the Licensor shall be under the terms and conditions of |
||||||
|
this License, without any additional terms or conditions. |
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify |
||||||
|
the terms of any separate license agreement you may have executed |
||||||
|
with Licensor regarding such Contributions. |
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade |
||||||
|
names, trademarks, service marks, or product names of the Licensor, |
||||||
|
except as required for reasonable and customary use in describing the |
||||||
|
origin of the Work and reproducing the content of the NOTICE file. |
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or |
||||||
|
agreed to in writing, Licensor provides the Work (and each |
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||||
|
implied, including, without limitation, any warranties or conditions |
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||||
|
appropriateness of using or redistributing the Work and assume any |
||||||
|
risks associated with Your exercise of permissions under this License. |
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, |
||||||
|
whether in tort (including negligence), contract, or otherwise, |
||||||
|
unless required by applicable law (such as deliberate and grossly |
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be |
||||||
|
liable to You for damages, including any direct, indirect, special, |
||||||
|
incidental, or consequential damages of any character arising as a |
||||||
|
result of this License or out of the use or inability to use the |
||||||
|
Work (including but not limited to damages for loss of goodwill, |
||||||
|
work stoppage, computer failure or malfunction, or any and all |
||||||
|
other commercial damages or losses), even if such Contributor |
||||||
|
has been advised of the possibility of such damages. |
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing |
||||||
|
the Work or Derivative Works thereof, You may choose to offer, |
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity, |
||||||
|
or other liability obligations and/or rights consistent with this |
||||||
|
License. However, in accepting such obligations, You may act only |
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf |
||||||
|
of any other Contributor, and only if You agree to indemnify, |
||||||
|
defend, and hold each Contributor harmless for any liability |
||||||
|
incurred by, or claims asserted against, such Contributor by reason |
||||||
|
of your accepting any such warranty or additional liability. |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
# github.com/spf13/cobra |
||||||
|
|
||||||
|
This package contains a copy of the Apache 2.0-licensed shell scripts that Cobra |
||||||
|
uses to integrate tab-completion into bash, zsh, fish and powershell, and the |
||||||
|
constants that interface with them. We are re-using these scripts to implement |
||||||
|
similar tab-completion for ffcli and the standard library flag package. |
||||||
|
|
||||||
|
The shell scripts were Go constants in the Cobra code, but we have extracted |
||||||
|
them into separate files to facilitate gzipping them, and have removed the |
||||||
|
activeHelp functionality from them. |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
// Copyright 2013-2023 The Cobra Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package cobra contains shell scripts and constants copied from
|
||||||
|
// https://github.com/spf13/cobra for use in our own shell tab-completion logic.
|
||||||
|
package cobra |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// ShellCompDirective is a bit map representing the different behaviors the shell
|
||||||
|
// can be instructed to have once completions have been provided.
|
||||||
|
type ShellCompDirective int |
||||||
|
|
||||||
|
const ( |
||||||
|
// ShellCompDirectiveError indicates an error occurred and completions should be ignored.
|
||||||
|
ShellCompDirectiveError ShellCompDirective = 1 << iota |
||||||
|
|
||||||
|
// ShellCompDirectiveNoSpace indicates that the shell should not add a space
|
||||||
|
// after the completion even if there is a single completion provided.
|
||||||
|
ShellCompDirectiveNoSpace |
||||||
|
|
||||||
|
// ShellCompDirectiveNoFileComp indicates that the shell should not provide
|
||||||
|
// file completion even when no completion is provided.
|
||||||
|
ShellCompDirectiveNoFileComp |
||||||
|
|
||||||
|
// ShellCompDirectiveFilterFileExt indicates that the provided completions
|
||||||
|
// should be used as file extension filters.
|
||||||
|
ShellCompDirectiveFilterFileExt |
||||||
|
|
||||||
|
// ShellCompDirectiveFilterDirs indicates that only directory names should
|
||||||
|
// be provided in file completion. To request directory names within another
|
||||||
|
// directory, the returned completions should specify the directory within
|
||||||
|
// which to search.
|
||||||
|
ShellCompDirectiveFilterDirs |
||||||
|
|
||||||
|
// ShellCompDirectiveKeepOrder indicates that the shell should preserve the order
|
||||||
|
// in which the completions are provided
|
||||||
|
ShellCompDirectiveKeepOrder |
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// All directives using iota should be above this one.
|
||||||
|
// For internal use.
|
||||||
|
shellCompDirectiveMaxValue |
||||||
|
|
||||||
|
// ShellCompDirectiveDefault indicates to let the shell perform its default
|
||||||
|
// behavior after completions have been provided.
|
||||||
|
// This one must be last to avoid messing up the iota count.
|
||||||
|
ShellCompDirectiveDefault ShellCompDirective = 0 |
||||||
|
) |
||||||
|
|
||||||
|
// Returns a string listing the different directive enabled in the specified parameter
|
||||||
|
func (d ShellCompDirective) String() string { |
||||||
|
var directives []string |
||||||
|
if d&ShellCompDirectiveError != 0 { |
||||||
|
directives = append(directives, "ShellCompDirectiveError") |
||||||
|
} |
||||||
|
if d&ShellCompDirectiveNoSpace != 0 { |
||||||
|
directives = append(directives, "ShellCompDirectiveNoSpace") |
||||||
|
} |
||||||
|
if d&ShellCompDirectiveNoFileComp != 0 { |
||||||
|
directives = append(directives, "ShellCompDirectiveNoFileComp") |
||||||
|
} |
||||||
|
if d&ShellCompDirectiveFilterFileExt != 0 { |
||||||
|
directives = append(directives, "ShellCompDirectiveFilterFileExt") |
||||||
|
} |
||||||
|
if d&ShellCompDirectiveFilterDirs != 0 { |
||||||
|
directives = append(directives, "ShellCompDirectiveFilterDirs") |
||||||
|
} |
||||||
|
if d&ShellCompDirectiveKeepOrder != 0 { |
||||||
|
directives = append(directives, "ShellCompDirectiveKeepOrder") |
||||||
|
} |
||||||
|
if len(directives) == 0 { |
||||||
|
directives = append(directives, "ShellCompDirectiveDefault") |
||||||
|
} |
||||||
|
|
||||||
|
if d >= shellCompDirectiveMaxValue { |
||||||
|
return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d) |
||||||
|
} |
||||||
|
return strings.Join(directives, " | ") |
||||||
|
} |
||||||
|
|
||||||
|
const UsageTemplate = `To load completions: |
||||||
|
|
||||||
|
Bash: |
||||||
|
|
||||||
|
$ source <(%[1]s completion bash) |
||||||
|
|
||||||
|
# To load completions for each session, execute once: |
||||||
|
# Linux: |
||||||
|
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s |
||||||
|
# macOS: |
||||||
|
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s |
||||||
|
|
||||||
|
Zsh: |
||||||
|
|
||||||
|
# If shell completion is not already enabled in your environment, |
||||||
|
# you will need to enable it. You can execute the following once: |
||||||
|
|
||||||
|
$ echo "autoload -U compinit; compinit" >> ~/.zshrc |
||||||
|
|
||||||
|
# To load completions for each session, execute once: |
||||||
|
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s" |
||||||
|
|
||||||
|
# You will need to start a new shell for this setup to take effect. |
||||||
|
|
||||||
|
fish: |
||||||
|
|
||||||
|
$ %[1]s completion fish | source |
||||||
|
|
||||||
|
# To load completions for each session, execute once: |
||||||
|
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish |
||||||
|
|
||||||
|
PowerShell: |
||||||
|
|
||||||
|
PS> %[1]s completion powershell | Out-String | Invoke-Expression |
||||||
|
|
||||||
|
# To load completions for every new session, run: |
||||||
|
PS> %[1]s completion powershell > %[1]s.ps1 |
||||||
|
# and source this file from your PowerShell profile. |
||||||
|
|
||||||
|
The shell scripts and this help message have been adapted from the |
||||||
|
Cobra project (https://cobra.dev, https://github.com/spf13/cobra)
|
||||||
|
under the Apache-2.0 license. Thank you for making these available. |
||||||
|
` |
||||||
@ -0,0 +1,333 @@ |
|||||||
|
# Copyright 2013-2023 The Cobra Authors |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
# bash completion V2 for %-36[1]s -*- shell-script -*- |
||||||
|
|
||||||
|
__%[1]s_debug() |
||||||
|
{ |
||||||
|
if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then |
||||||
|
echo "$*" >> "${BASH_COMP_DEBUG_FILE}" |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
# Macs have bash3 for which the bash-completion package doesn't include |
||||||
|
# _init_completion. This is a minimal version of that function. |
||||||
|
__%[1]s_init_completion() |
||||||
|
{ |
||||||
|
COMPREPLY=() |
||||||
|
_get_comp_words_by_ref "$@" cur prev words cword |
||||||
|
} |
||||||
|
|
||||||
|
# This function calls the %[1]s program to obtain the completion |
||||||
|
# results and the directive. It fills the 'out' and 'directive' vars. |
||||||
|
__%[1]s_get_completion_results() { |
||||||
|
local requestComp lastParam lastChar args |
||||||
|
|
||||||
|
# Prepare the command to request completions for the program. |
||||||
|
# Calling ${words[0]} instead of directly %[1]s allows handling aliases |
||||||
|
args=("${words[@]:1}") |
||||||
|
requestComp="${words[0]} %[2]s ${args[*]}" |
||||||
|
|
||||||
|
lastParam=${words[$((${#words[@]}-1))]} |
||||||
|
lastChar=${lastParam:$((${#lastParam}-1)):1} |
||||||
|
__%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}" |
||||||
|
|
||||||
|
if [[ -z ${cur} && ${lastChar} != = ]]; then |
||||||
|
# If the last parameter is complete (there is a space following it) |
||||||
|
# We add an extra empty parameter so we can indicate this to the go method. |
||||||
|
__%[1]s_debug "Adding extra empty parameter" |
||||||
|
requestComp="${requestComp} ''" |
||||||
|
fi |
||||||
|
|
||||||
|
# When completing a flag with an = (e.g., %[1]s -n=<TAB>) |
||||||
|
# bash focuses on the part after the =, so we need to remove |
||||||
|
# the flag part from $cur |
||||||
|
if [[ ${cur} == -*=* ]]; then |
||||||
|
cur="${cur#*=}" |
||||||
|
fi |
||||||
|
|
||||||
|
__%[1]s_debug "Calling ${requestComp}" |
||||||
|
# Use eval to handle any environment variables and such |
||||||
|
out=$(eval "${requestComp}" 2>/dev/null) |
||||||
|
|
||||||
|
# Extract the directive integer at the very end of the output following a colon (:) |
||||||
|
directive=${out##*:} |
||||||
|
# Remove the directive |
||||||
|
out=${out%%:*} |
||||||
|
if [[ ${directive} == "${out}" ]]; then |
||||||
|
# There is not directive specified |
||||||
|
directive=0 |
||||||
|
fi |
||||||
|
__%[1]s_debug "The completion directive is: ${directive}" |
||||||
|
__%[1]s_debug "The completions are: ${out}" |
||||||
|
} |
||||||
|
|
||||||
|
__%[1]s_process_completion_results() { |
||||||
|
local shellCompDirectiveError=%[3]d |
||||||
|
local shellCompDirectiveNoSpace=%[4]d |
||||||
|
local shellCompDirectiveNoFileComp=%[5]d |
||||||
|
local shellCompDirectiveFilterFileExt=%[6]d |
||||||
|
local shellCompDirectiveFilterDirs=%[7]d |
||||||
|
local shellCompDirectiveKeepOrder=%[8]d |
||||||
|
|
||||||
|
if (((directive & shellCompDirectiveError) != 0)); then |
||||||
|
# Error code. No completion. |
||||||
|
__%[1]s_debug "Received error from custom completion go code" |
||||||
|
return |
||||||
|
else |
||||||
|
if (((directive & shellCompDirectiveNoSpace) != 0)); then |
||||||
|
if [[ $(type -t compopt) == builtin ]]; then |
||||||
|
__%[1]s_debug "Activating no space" |
||||||
|
compopt -o nospace |
||||||
|
else |
||||||
|
__%[1]s_debug "No space directive not supported in this version of bash" |
||||||
|
fi |
||||||
|
fi |
||||||
|
if (((directive & shellCompDirectiveKeepOrder) != 0)); then |
||||||
|
if [[ $(type -t compopt) == builtin ]]; then |
||||||
|
# no sort isn't supported for bash less than < 4.4 |
||||||
|
if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then |
||||||
|
__%[1]s_debug "No sort directive not supported in this version of bash" |
||||||
|
else |
||||||
|
__%[1]s_debug "Activating keep order" |
||||||
|
compopt -o nosort |
||||||
|
fi |
||||||
|
else |
||||||
|
__%[1]s_debug "No sort directive not supported in this version of bash" |
||||||
|
fi |
||||||
|
fi |
||||||
|
if (((directive & shellCompDirectiveNoFileComp) != 0)); then |
||||||
|
if [[ $(type -t compopt) == builtin ]]; then |
||||||
|
__%[1]s_debug "Activating no file completion" |
||||||
|
compopt +o default |
||||||
|
else |
||||||
|
__%[1]s_debug "No file completion directive not supported in this version of bash" |
||||||
|
fi |
||||||
|
fi |
||||||
|
fi |
||||||
|
|
||||||
|
# Separate activeHelp from normal completions |
||||||
|
local completions=() |
||||||
|
while IFS='' read -r comp; do |
||||||
|
completions+=("$comp") |
||||||
|
done <<<"${out}" |
||||||
|
|
||||||
|
if (((directive & shellCompDirectiveFilterFileExt) != 0)); then |
||||||
|
# File extension filtering |
||||||
|
local fullFilter filter filteringCmd |
||||||
|
|
||||||
|
# Do not use quotes around the $completions variable or else newline |
||||||
|
# characters will be kept. |
||||||
|
for filter in ${completions[*]}; do |
||||||
|
fullFilter+="$filter|" |
||||||
|
done |
||||||
|
|
||||||
|
filteringCmd="_filedir $fullFilter" |
||||||
|
__%[1]s_debug "File filtering command: $filteringCmd" |
||||||
|
$filteringCmd |
||||||
|
elif (((directive & shellCompDirectiveFilterDirs) != 0)); then |
||||||
|
# File completion for directories only |
||||||
|
|
||||||
|
local subdir |
||||||
|
subdir=${completions[0]} |
||||||
|
if [[ -n $subdir ]]; then |
||||||
|
__%[1]s_debug "Listing directories in $subdir" |
||||||
|
pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return |
||||||
|
else |
||||||
|
__%[1]s_debug "Listing directories in ." |
||||||
|
_filedir -d |
||||||
|
fi |
||||||
|
else |
||||||
|
__%[1]s_handle_completion_types |
||||||
|
fi |
||||||
|
|
||||||
|
__%[1]s_handle_special_char "$cur" : |
||||||
|
__%[1]s_handle_special_char "$cur" = |
||||||
|
|
||||||
|
# Print the activeHelp statements before we finish |
||||||
|
if ((${#activeHelp[*]} != 0)); then |
||||||
|
printf "\n"; |
||||||
|
printf "%%s\n" "${activeHelp[@]}" |
||||||
|
printf "\n" |
||||||
|
|
||||||
|
# The prompt format is only available from bash 4.4. |
||||||
|
# We test if it is available before using it. |
||||||
|
if (x=${PS1@P}) 2> /dev/null; then |
||||||
|
printf "%%s" "${PS1@P}${COMP_LINE[@]}" |
||||||
|
else |
||||||
|
# Can't print the prompt. Just print the |
||||||
|
# text the user had typed, it is workable enough. |
||||||
|
printf "%%s" "${COMP_LINE[@]}" |
||||||
|
fi |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
__%[1]s_handle_completion_types() { |
||||||
|
__%[1]s_debug "__%[1]s_handle_completion_types: COMP_TYPE is $COMP_TYPE" |
||||||
|
|
||||||
|
case $COMP_TYPE in |
||||||
|
37|42) |
||||||
|
# Type: menu-complete/menu-complete-backward and insert-completions |
||||||
|
# If the user requested inserting one completion at a time, or all |
||||||
|
# completions at once on the command-line we must remove the descriptions. |
||||||
|
# https://github.com/spf13/cobra/issues/1508 |
||||||
|
local tab=$'\t' comp |
||||||
|
while IFS='' read -r comp; do |
||||||
|
[[ -z $comp ]] && continue |
||||||
|
# Strip any description |
||||||
|
comp=${comp%%%%$tab*} |
||||||
|
# Only consider the completions that match |
||||||
|
if [[ $comp == "$cur"* ]]; then |
||||||
|
COMPREPLY+=("$comp") |
||||||
|
fi |
||||||
|
done < <(printf "%%s\n" "${completions[@]}") |
||||||
|
;; |
||||||
|
|
||||||
|
*) |
||||||
|
# Type: complete (normal completion) |
||||||
|
__%[1]s_handle_standard_completion_case |
||||||
|
;; |
||||||
|
esac |
||||||
|
} |
||||||
|
|
||||||
|
__%[1]s_handle_standard_completion_case() { |
||||||
|
local tab=$'\t' comp |
||||||
|
|
||||||
|
# Short circuit to optimize if we don't have descriptions |
||||||
|
if [[ "${completions[*]}" != *$tab* ]]; then |
||||||
|
IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") |
||||||
|
return 0 |
||||||
|
fi |
||||||
|
|
||||||
|
local longest=0 |
||||||
|
local compline |
||||||
|
# Look for the longest completion so that we can format things nicely |
||||||
|
while IFS='' read -r compline; do |
||||||
|
[[ -z $compline ]] && continue |
||||||
|
# Strip any description before checking the length |
||||||
|
comp=${compline%%%%$tab*} |
||||||
|
# Only consider the completions that match |
||||||
|
[[ $comp == "$cur"* ]] || continue |
||||||
|
COMPREPLY+=("$compline") |
||||||
|
if ((${#comp}>longest)); then |
||||||
|
longest=${#comp} |
||||||
|
fi |
||||||
|
done < <(printf "%%s\n" "${completions[@]}") |
||||||
|
|
||||||
|
# If there is a single completion left, remove the description text |
||||||
|
if ((${#COMPREPLY[*]} == 1)); then |
||||||
|
__%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" |
||||||
|
comp="${COMPREPLY[0]%%%%$tab*}" |
||||||
|
__%[1]s_debug "Removed description from single completion, which is now: ${comp}" |
||||||
|
COMPREPLY[0]=$comp |
||||||
|
else # Format the descriptions |
||||||
|
__%[1]s_format_comp_descriptions $longest |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
__%[1]s_handle_special_char() |
||||||
|
{ |
||||||
|
local comp="$1" |
||||||
|
local char=$2 |
||||||
|
if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then |
||||||
|
local word=${comp%%"${comp##*${char}}"} |
||||||
|
local idx=${#COMPREPLY[*]} |
||||||
|
while ((--idx >= 0)); do |
||||||
|
COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} |
||||||
|
done |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
__%[1]s_format_comp_descriptions() |
||||||
|
{ |
||||||
|
local tab=$'\t' |
||||||
|
local comp desc maxdesclength |
||||||
|
local longest=$1 |
||||||
|
|
||||||
|
local i ci |
||||||
|
for ci in ${!COMPREPLY[*]}; do |
||||||
|
comp=${COMPREPLY[ci]} |
||||||
|
# Properly format the description string which follows a tab character if there is one |
||||||
|
if [[ "$comp" == *$tab* ]]; then |
||||||
|
__%[1]s_debug "Original comp: $comp" |
||||||
|
desc=${comp#*$tab} |
||||||
|
comp=${comp%%%%$tab*} |
||||||
|
|
||||||
|
# $COLUMNS stores the current shell width. |
||||||
|
# Remove an extra 4 because we add 2 spaces and 2 parentheses. |
||||||
|
maxdesclength=$(( COLUMNS - longest - 4 )) |
||||||
|
|
||||||
|
# Make sure we can fit a description of at least 8 characters |
||||||
|
# if we are to align the descriptions. |
||||||
|
if ((maxdesclength > 8)); then |
||||||
|
# Add the proper number of spaces to align the descriptions |
||||||
|
for ((i = ${#comp} ; i < longest ; i++)); do |
||||||
|
comp+=" " |
||||||
|
done |
||||||
|
else |
||||||
|
# Don't pad the descriptions so we can fit more text after the completion |
||||||
|
maxdesclength=$(( COLUMNS - ${#comp} - 4 )) |
||||||
|
fi |
||||||
|
|
||||||
|
# If there is enough space for any description text, |
||||||
|
# truncate the descriptions that are too long for the shell width |
||||||
|
if ((maxdesclength > 0)); then |
||||||
|
if ((${#desc} > maxdesclength)); then |
||||||
|
desc=${desc:0:$(( maxdesclength - 1 ))} |
||||||
|
desc+="…" |
||||||
|
fi |
||||||
|
comp+=" ($desc)" |
||||||
|
fi |
||||||
|
COMPREPLY[ci]=$comp |
||||||
|
__%[1]s_debug "Final comp: $comp" |
||||||
|
fi |
||||||
|
done |
||||||
|
} |
||||||
|
|
||||||
|
__start_%[1]s() |
||||||
|
{ |
||||||
|
local cur prev words cword split |
||||||
|
|
||||||
|
COMPREPLY=() |
||||||
|
|
||||||
|
# Call _init_completion from the bash-completion package |
||||||
|
# to prepare the arguments properly |
||||||
|
if declare -F _init_completion >/dev/null 2>&1; then |
||||||
|
_init_completion -n =: || return |
||||||
|
else |
||||||
|
__%[1]s_init_completion -n =: || return |
||||||
|
fi |
||||||
|
|
||||||
|
__%[1]s_debug |
||||||
|
__%[1]s_debug "========= starting completion logic ==========" |
||||||
|
__%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" |
||||||
|
|
||||||
|
# The user could have moved the cursor backwards on the command-line. |
||||||
|
# We need to trigger completion from the $cword location, so we need |
||||||
|
# to truncate the command-line ($words) up to the $cword location. |
||||||
|
words=("${words[@]:0:$cword+1}") |
||||||
|
__%[1]s_debug "Truncated words[*]: ${words[*]}," |
||||||
|
|
||||||
|
local out directive |
||||||
|
__%[1]s_get_completion_results |
||||||
|
__%[1]s_process_completion_results |
||||||
|
} |
||||||
|
|
||||||
|
if [[ $(type -t compopt) = "builtin" ]]; then |
||||||
|
complete -o default -F __start_%[1]s %[1]s |
||||||
|
else |
||||||
|
complete -o default -o nospace -F __start_%[1]s %[1]s |
||||||
|
fi |
||||||
|
|
||||||
|
# ex: ts=4 sw=4 et filetype=sh |
||||||
Binary file not shown.
@ -0,0 +1,248 @@ |
|||||||
|
# Copyright 2013-2023 The Cobra Authors |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
# fish completion for %-36[1]s -*- shell-script -*- |
||||||
|
|
||||||
|
function __%[1]s_debug |
||||||
|
set -l file "$BASH_COMP_DEBUG_FILE" |
||||||
|
if test -n "$file" |
||||||
|
echo "$argv" >> $file |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
function __%[1]s_perform_completion |
||||||
|
__%[1]s_debug "Starting __%[1]s_perform_completion" |
||||||
|
|
||||||
|
# Extract all args except the last one |
||||||
|
set -l args (commandline -opc) |
||||||
|
# Extract the last arg and escape it in case it is a space |
||||||
|
set -l lastArg (string escape -- (commandline -ct)) |
||||||
|
|
||||||
|
__%[1]s_debug "args: $args" |
||||||
|
__%[1]s_debug "last arg: $lastArg" |
||||||
|
|
||||||
|
set -l requestComp "$args[1] %[3]s $args[2..-1] $lastArg" |
||||||
|
|
||||||
|
__%[1]s_debug "Calling $requestComp" |
||||||
|
set -l results (eval $requestComp 2> /dev/null) |
||||||
|
|
||||||
|
# Some programs may output extra empty lines after the directive. |
||||||
|
# Let's ignore them or else it will break completion. |
||||||
|
# Ref: https://github.com/spf13/cobra/issues/1279 |
||||||
|
for line in $results[-1..1] |
||||||
|
if test (string trim -- $line) = "" |
||||||
|
# Found an empty line, remove it |
||||||
|
set results $results[1..-2] |
||||||
|
else |
||||||
|
# Found non-empty line, we have our proper output |
||||||
|
break |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
set -l comps $results[1..-2] |
||||||
|
set -l directiveLine $results[-1] |
||||||
|
|
||||||
|
# For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>) |
||||||
|
# completions must be prefixed with the flag |
||||||
|
set -l flagPrefix (string match -r -- '-.*=' "$lastArg") |
||||||
|
|
||||||
|
__%[1]s_debug "Comps: $comps" |
||||||
|
__%[1]s_debug "DirectiveLine: $directiveLine" |
||||||
|
__%[1]s_debug "flagPrefix: $flagPrefix" |
||||||
|
|
||||||
|
for comp in $comps |
||||||
|
printf "%%s%%s\n" "$flagPrefix" "$comp" |
||||||
|
end |
||||||
|
|
||||||
|
printf "%%s\n" "$directiveLine" |
||||||
|
end |
||||||
|
|
||||||
|
# this function limits calls to __%[1]s_perform_completion, by caching the result behind $__%[1]s_perform_completion_once_result |
||||||
|
function __%[1]s_perform_completion_once |
||||||
|
__%[1]s_debug "Starting __%[1]s_perform_completion_once" |
||||||
|
|
||||||
|
if test -n "$__%[1]s_perform_completion_once_result" |
||||||
|
__%[1]s_debug "Seems like a valid result already exists, skipping __%[1]s_perform_completion" |
||||||
|
return 0 |
||||||
|
end |
||||||
|
|
||||||
|
set --global __%[1]s_perform_completion_once_result (__%[1]s_perform_completion) |
||||||
|
if test -z "$__%[1]s_perform_completion_once_result" |
||||||
|
__%[1]s_debug "No completions, probably due to a failure" |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
__%[1]s_debug "Performed completions and set __%[1]s_perform_completion_once_result" |
||||||
|
return 0 |
||||||
|
end |
||||||
|
|
||||||
|
# this function is used to clear the $__%[1]s_perform_completion_once_result variable after completions are run |
||||||
|
function __%[1]s_clear_perform_completion_once_result |
||||||
|
__%[1]s_debug "" |
||||||
|
__%[1]s_debug "========= clearing previously set __%[1]s_perform_completion_once_result variable ==========" |
||||||
|
set --erase __%[1]s_perform_completion_once_result |
||||||
|
__%[1]s_debug "Successfully erased the variable __%[1]s_perform_completion_once_result" |
||||||
|
end |
||||||
|
|
||||||
|
function __%[1]s_requires_order_preservation |
||||||
|
__%[1]s_debug "" |
||||||
|
__%[1]s_debug "========= checking if order preservation is required ==========" |
||||||
|
|
||||||
|
__%[1]s_perform_completion_once |
||||||
|
if test -z "$__%[1]s_perform_completion_once_result" |
||||||
|
__%[1]s_debug "Error determining if order preservation is required" |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) |
||||||
|
__%[1]s_debug "Directive is: $directive" |
||||||
|
|
||||||
|
set -l shellCompDirectiveKeepOrder %[9]d |
||||||
|
set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) %% 2) |
||||||
|
__%[1]s_debug "Keeporder is: $keeporder" |
||||||
|
|
||||||
|
if test $keeporder -ne 0 |
||||||
|
__%[1]s_debug "This does require order preservation" |
||||||
|
return 0 |
||||||
|
end |
||||||
|
|
||||||
|
__%[1]s_debug "This doesn't require order preservation" |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# This function does two things: |
||||||
|
# - Obtain the completions and store them in the global __%[1]s_comp_results |
||||||
|
# - Return false if file completion should be performed |
||||||
|
function __%[1]s_prepare_completions |
||||||
|
__%[1]s_debug "" |
||||||
|
__%[1]s_debug "========= starting completion logic ==========" |
||||||
|
|
||||||
|
# Start fresh |
||||||
|
set --erase __%[1]s_comp_results |
||||||
|
|
||||||
|
__%[1]s_perform_completion_once |
||||||
|
__%[1]s_debug "Completion results: $__%[1]s_perform_completion_once_result" |
||||||
|
|
||||||
|
if test -z "$__%[1]s_perform_completion_once_result" |
||||||
|
__%[1]s_debug "No completion, probably due to a failure" |
||||||
|
# Might as well do file completion, in case it helps |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
set -l directive (string sub --start 2 $__%[1]s_perform_completion_once_result[-1]) |
||||||
|
set --global __%[1]s_comp_results $__%[1]s_perform_completion_once_result[1..-2] |
||||||
|
|
||||||
|
__%[1]s_debug "Completions are: $__%[1]s_comp_results" |
||||||
|
__%[1]s_debug "Directive is: $directive" |
||||||
|
|
||||||
|
set -l shellCompDirectiveError %[4]d |
||||||
|
set -l shellCompDirectiveNoSpace %[5]d |
||||||
|
set -l shellCompDirectiveNoFileComp %[6]d |
||||||
|
set -l shellCompDirectiveFilterFileExt %[7]d |
||||||
|
set -l shellCompDirectiveFilterDirs %[8]d |
||||||
|
|
||||||
|
if test -z "$directive" |
||||||
|
set directive 0 |
||||||
|
end |
||||||
|
|
||||||
|
set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2) |
||||||
|
if test $compErr -eq 1 |
||||||
|
__%[1]s_debug "Received error directive: aborting." |
||||||
|
# Might as well do file completion, in case it helps |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) |
||||||
|
set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) |
||||||
|
if test $filefilter -eq 1; or test $dirfilter -eq 1 |
||||||
|
__%[1]s_debug "File extension filtering or directory filtering not supported" |
||||||
|
# Do full file completion instead |
||||||
|
return 1 |
||||||
|
end |
||||||
|
|
||||||
|
set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2) |
||||||
|
set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) |
||||||
|
|
||||||
|
__%[1]s_debug "nospace: $nospace, nofiles: $nofiles" |
||||||
|
|
||||||
|
# If we want to prevent a space, or if file completion is NOT disabled, |
||||||
|
# we need to count the number of valid completions. |
||||||
|
# To do so, we will filter on prefix as the completions we have received |
||||||
|
# may not already be filtered so as to allow fish to match on different |
||||||
|
# criteria than the prefix. |
||||||
|
if test $nospace -ne 0; or test $nofiles -eq 0 |
||||||
|
set -l prefix (commandline -t | string escape --style=regex) |
||||||
|
__%[1]s_debug "prefix: $prefix" |
||||||
|
|
||||||
|
set -l completions (string match -r -- "^$prefix.*" $__%[1]s_comp_results) |
||||||
|
set --global __%[1]s_comp_results $completions |
||||||
|
__%[1]s_debug "Filtered completions are: $__%[1]s_comp_results" |
||||||
|
|
||||||
|
# Important not to quote the variable for count to work |
||||||
|
set -l numComps (count $__%[1]s_comp_results) |
||||||
|
__%[1]s_debug "numComps: $numComps" |
||||||
|
|
||||||
|
if test $numComps -eq 1; and test $nospace -ne 0 |
||||||
|
# We must first split on \t to get rid of the descriptions to be |
||||||
|
# able to check what the actual completion will be. |
||||||
|
# We don't need descriptions anyway since there is only a single |
||||||
|
# real completion which the shell will expand immediately. |
||||||
|
set -l split (string split --max 1 \t $__%[1]s_comp_results[1]) |
||||||
|
|
||||||
|
# Fish won't add a space if the completion ends with any |
||||||
|
# of the following characters: @=/:., |
||||||
|
set -l lastChar (string sub -s -1 -- $split) |
||||||
|
if not string match -r -q "[@=/:.,]" -- "$lastChar" |
||||||
|
# In other cases, to support the "nospace" directive we trick the shell |
||||||
|
# by outputting an extra, longer completion. |
||||||
|
__%[1]s_debug "Adding second completion to perform nospace directive" |
||||||
|
set --global __%[1]s_comp_results $split[1] $split[1]. |
||||||
|
__%[1]s_debug "Completions are now: $__%[1]s_comp_results" |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
if test $numComps -eq 0; and test $nofiles -eq 0 |
||||||
|
# To be consistent with bash and zsh, we only trigger file |
||||||
|
# completion when there are no other completions |
||||||
|
__%[1]s_debug "Requesting file completion" |
||||||
|
return 1 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
return 0 |
||||||
|
end |
||||||
|
|
||||||
|
# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves |
||||||
|
# so we can properly delete any completions provided by another script. |
||||||
|
# Only do this if the program can be found, or else fish may print some errors; besides, |
||||||
|
# the existing completions will only be loaded if the program can be found. |
||||||
|
if type -q "%[2]s" |
||||||
|
# The space after the program name is essential to trigger completion for the program |
||||||
|
# and not completion of the program name itself. |
||||||
|
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. |
||||||
|
complete --do-complete "%[2]s " > /dev/null 2>&1 |
||||||
|
end |
||||||
|
|
||||||
|
# Remove any pre-existing completions for the program since we will be handling all of them. |
||||||
|
complete -c %[2]s -e |
||||||
|
|
||||||
|
# this will get called after the two calls below and clear the $__%[1]s_perform_completion_once_result global |
||||||
|
complete -c %[2]s -n '__%[1]s_clear_perform_completion_once_result' |
||||||
|
# The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results |
||||||
|
# which provides the program's completion choices. |
||||||
|
# If this doesn't require order preservation, we don't use the -k flag |
||||||
|
complete -c %[2]s -n 'not __%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' |
||||||
|
# otherwise we use the -k flag |
||||||
|
complete -k -c %[2]s -n '__%[1]s_requires_order_preservation && __%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' |
||||||
Binary file not shown.
@ -0,0 +1,72 @@ |
|||||||
|
package cobra |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"compress/gzip" |
||||||
|
_ "embed" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
) |
||||||
|
|
||||||
|
//go:generate go run gen.go
|
||||||
|
|
||||||
|
//go:embed comp.bash.gz
|
||||||
|
var compBash string |
||||||
|
|
||||||
|
func ScriptBash(w io.Writer, name, compCmd, nameForVar string) error { |
||||||
|
return fmtgz( |
||||||
|
w, compBash, |
||||||
|
name, compCmd, |
||||||
|
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
||||||
|
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
//go:embed comp.zsh.gz
|
||||||
|
var compZsh string |
||||||
|
|
||||||
|
func ScriptZsh(w io.Writer, name, compCmd, nameForVar string) error { |
||||||
|
return fmtgz( |
||||||
|
w, compZsh, |
||||||
|
name, compCmd, |
||||||
|
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
||||||
|
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
//go:embed comp.fish.gz
|
||||||
|
var compFish string |
||||||
|
|
||||||
|
func ScriptFish(w io.Writer, name, compCmd, nameForVar string) error { |
||||||
|
return fmtgz( |
||||||
|
w, compFish, |
||||||
|
nameForVar, name, compCmd, |
||||||
|
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
||||||
|
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
//go:embed comp.ps1.gz
|
||||||
|
var compPowershell string |
||||||
|
|
||||||
|
func ScriptPowershell(w io.Writer, name, compCmd, nameForVar string) error { |
||||||
|
return fmtgz( |
||||||
|
w, compPowershell, |
||||||
|
name, nameForVar, compCmd, |
||||||
|
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, |
||||||
|
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func fmtgz(w io.Writer, formatgz string, args ...any) error { |
||||||
|
f, err := gzip.NewReader(bytes.NewBufferString(formatgz)) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("decompressing script: %w", err) |
||||||
|
} |
||||||
|
format, err := io.ReadAll(f) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("decompressing script: %w", err) |
||||||
|
} |
||||||
|
_, err = fmt.Fprintf(w, string(format), args...) |
||||||
|
return err |
||||||
|
} |
||||||
@ -0,0 +1,259 @@ |
|||||||
|
# Copyright 2013-2023 The Cobra Authors |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
# powershell completion for %-36[1]s -*- shell-script -*- |
||||||
|
|
||||||
|
function __%[1]s_debug { |
||||||
|
if ($env:BASH_COMP_DEBUG_FILE) { |
||||||
|
"$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
filter __%[1]s_escapeStringWithSpecialChars { |
||||||
|
$_ -replace '\s|#|@|\$|;|,|''|\{|\}|\(|\)|"|`|\||<|>|&','`$&' |
||||||
|
} |
||||||
|
|
||||||
|
[scriptblock]${__%[2]sCompleterBlock} = { |
||||||
|
param( |
||||||
|
$WordToComplete, |
||||||
|
$CommandAst, |
||||||
|
$CursorPosition |
||||||
|
) |
||||||
|
|
||||||
|
# Get the current command line and convert into a string |
||||||
|
$Command = $CommandAst.CommandElements |
||||||
|
$Command = "$Command" |
||||||
|
|
||||||
|
__%[1]s_debug "" |
||||||
|
__%[1]s_debug "========= starting completion logic ==========" |
||||||
|
__%[1]s_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" |
||||||
|
|
||||||
|
# The user could have moved the cursor backwards on the command-line. |
||||||
|
# We need to trigger completion from the $CursorPosition location, so we need |
||||||
|
# to truncate the command-line ($Command) up to the $CursorPosition location. |
||||||
|
# Make sure the $Command is longer then the $CursorPosition before we truncate. |
||||||
|
# This happens because the $Command does not include the last space. |
||||||
|
if ($Command.Length -gt $CursorPosition) { |
||||||
|
$Command=$Command.Substring(0,$CursorPosition) |
||||||
|
} |
||||||
|
__%[1]s_debug "Truncated command: $Command" |
||||||
|
|
||||||
|
$ShellCompDirectiveError=%[4]d |
||||||
|
$ShellCompDirectiveNoSpace=%[5]d |
||||||
|
$ShellCompDirectiveNoFileComp=%[6]d |
||||||
|
$ShellCompDirectiveFilterFileExt=%[7]d |
||||||
|
$ShellCompDirectiveFilterDirs=%[8]d |
||||||
|
$ShellCompDirectiveKeepOrder=%[9]d |
||||||
|
|
||||||
|
# Prepare the command to request completions for the program. |
||||||
|
# Split the command at the first space to separate the program and arguments. |
||||||
|
$Program,$Arguments = $Command.Split(" ",2) |
||||||
|
|
||||||
|
$RequestComp="$Program %[3]s $Arguments" |
||||||
|
__%[1]s_debug "RequestComp: $RequestComp" |
||||||
|
|
||||||
|
# we cannot use $WordToComplete because it |
||||||
|
# has the wrong values if the cursor was moved |
||||||
|
# so use the last argument |
||||||
|
if ($WordToComplete -ne "" ) { |
||||||
|
$WordToComplete = $Arguments.Split(" ")[-1] |
||||||
|
} |
||||||
|
__%[1]s_debug "New WordToComplete: $WordToComplete" |
||||||
|
|
||||||
|
|
||||||
|
# Check for flag with equal sign |
||||||
|
$IsEqualFlag = ($WordToComplete -Like "--*=*" ) |
||||||
|
if ( $IsEqualFlag ) { |
||||||
|
__%[1]s_debug "Completing equal sign flag" |
||||||
|
# Remove the flag part |
||||||
|
$Flag,$WordToComplete = $WordToComplete.Split("=",2) |
||||||
|
} |
||||||
|
|
||||||
|
if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { |
||||||
|
# If the last parameter is complete (there is a space following it) |
||||||
|
# We add an extra empty parameter so we can indicate this to the go method. |
||||||
|
__%[1]s_debug "Adding extra empty parameter" |
||||||
|
# PowerShell 7.2+ changed the way how the arguments are passed to executables, |
||||||
|
# so for pre-7.2 or when Legacy argument passing is enabled we need to use |
||||||
|
# `"`" to pass an empty argument, a "" or '' does not work!!! |
||||||
|
if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or |
||||||
|
($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or |
||||||
|
(($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and |
||||||
|
$PSNativeCommandArgumentPassing -eq 'Legacy')) { |
||||||
|
$RequestComp="$RequestComp" + ' `"`"' |
||||||
|
} else { |
||||||
|
$RequestComp="$RequestComp" + ' ""' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
__%[1]s_debug "Calling $RequestComp" |
||||||
|
# First disable ActiveHelp which is not supported for Powershell |
||||||
|
${env:%[10]s}=0 |
||||||
|
|
||||||
|
#call the command store the output in $out and redirect stderr and stdout to null |
||||||
|
# $Out is an array contains each line per element |
||||||
|
Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null |
||||||
|
|
||||||
|
# get directive from last line |
||||||
|
[int]$Directive = $Out[-1].TrimStart(':') |
||||||
|
if ($Directive -eq "") { |
||||||
|
# There is no directive specified |
||||||
|
$Directive = 0 |
||||||
|
} |
||||||
|
__%[1]s_debug "The completion directive is: $Directive" |
||||||
|
|
||||||
|
# remove directive (last element) from out |
||||||
|
$Out = $Out | Where-Object { $_ -ne $Out[-1] } |
||||||
|
__%[1]s_debug "The completions are: $Out" |
||||||
|
|
||||||
|
if (($Directive -band $ShellCompDirectiveError) -ne 0 ) { |
||||||
|
# Error code. No completion. |
||||||
|
__%[1]s_debug "Received error from custom completion go code" |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
$Longest = 0 |
||||||
|
[Array]$Values = $Out | ForEach-Object { |
||||||
|
#Split the output in name and description |
||||||
|
$Name, $Description = $_.Split("`t",2) |
||||||
|
__%[1]s_debug "Name: $Name Description: $Description" |
||||||
|
|
||||||
|
# Look for the longest completion so that we can format things nicely |
||||||
|
if ($Longest -lt $Name.Length) { |
||||||
|
$Longest = $Name.Length |
||||||
|
} |
||||||
|
|
||||||
|
# Set the description to a one space string if there is none set. |
||||||
|
# This is needed because the CompletionResult does not accept an empty string as argument |
||||||
|
if (-Not $Description) { |
||||||
|
$Description = " " |
||||||
|
} |
||||||
|
@{Name="$Name";Description="$Description"} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
$Space = " " |
||||||
|
if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) { |
||||||
|
# remove the space here |
||||||
|
__%[1]s_debug "ShellCompDirectiveNoSpace is called" |
||||||
|
$Space = "" |
||||||
|
} |
||||||
|
|
||||||
|
if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or |
||||||
|
(($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) { |
||||||
|
__%[1]s_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported" |
||||||
|
|
||||||
|
# return here to prevent the completion of the extensions |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
$Values = $Values | Where-Object { |
||||||
|
# filter the result |
||||||
|
$_.Name -like "$WordToComplete*" |
||||||
|
|
||||||
|
# Join the flag back if we have an equal sign flag |
||||||
|
if ( $IsEqualFlag ) { |
||||||
|
__%[1]s_debug "Join the equal sign flag back to the completion value" |
||||||
|
$_.Name = $Flag + "=" + $_.Name |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
# we sort the values in ascending order by name if keep order isn't passed |
||||||
|
if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) { |
||||||
|
$Values = $Values | Sort-Object -Property Name |
||||||
|
} |
||||||
|
|
||||||
|
if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) { |
||||||
|
__%[1]s_debug "ShellCompDirectiveNoFileComp is called" |
||||||
|
|
||||||
|
if ($Values.Length -eq 0) { |
||||||
|
# Just print an empty string here so the |
||||||
|
# shell does not start to complete paths. |
||||||
|
# We cannot use CompletionResult here because |
||||||
|
# it does not accept an empty string as argument. |
||||||
|
"" |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
# Get the current mode |
||||||
|
$Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function |
||||||
|
__%[1]s_debug "Mode: $Mode" |
||||||
|
|
||||||
|
$Values | ForEach-Object { |
||||||
|
|
||||||
|
# store temporary because switch will overwrite $_ |
||||||
|
$comp = $_ |
||||||
|
|
||||||
|
# PowerShell supports three different completion modes |
||||||
|
# - TabCompleteNext (default windows style - on each key press the next option is displayed) |
||||||
|
# - Complete (works like bash) |
||||||
|
# - MenuComplete (works like zsh) |
||||||
|
# You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode> |
||||||
|
|
||||||
|
# CompletionResult Arguments: |
||||||
|
# 1) CompletionText text to be used as the auto completion result |
||||||
|
# 2) ListItemText text to be displayed in the suggestion list |
||||||
|
# 3) ResultType type of completion result |
||||||
|
# 4) ToolTip text for the tooltip with details about the object |
||||||
|
|
||||||
|
switch ($Mode) { |
||||||
|
|
||||||
|
# bash like |
||||||
|
"Complete" { |
||||||
|
|
||||||
|
if ($Values.Length -eq 1) { |
||||||
|
__%[1]s_debug "Only one completion left" |
||||||
|
|
||||||
|
# insert space after value |
||||||
|
[System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") |
||||||
|
|
||||||
|
} else { |
||||||
|
# Add the proper number of spaces to align the descriptions |
||||||
|
while($comp.Name.Length -lt $Longest) { |
||||||
|
$comp.Name = $comp.Name + " " |
||||||
|
} |
||||||
|
|
||||||
|
# Check for empty description and only add parentheses if needed |
||||||
|
if ($($comp.Description) -eq " " ) { |
||||||
|
$Description = "" |
||||||
|
} else { |
||||||
|
$Description = " ($($comp.Description))" |
||||||
|
} |
||||||
|
|
||||||
|
[System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
# zsh like |
||||||
|
"MenuComplete" { |
||||||
|
# insert space after value |
||||||
|
# MenuComplete will automatically show the ToolTip of |
||||||
|
# the highlighted value at the bottom of the suggestions. |
||||||
|
[System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") |
||||||
|
} |
||||||
|
|
||||||
|
# TabCompleteNext and in case we get something unknown |
||||||
|
Default { |
||||||
|
# Like MenuComplete but we don't want to add a space here because |
||||||
|
# the user need to press space anyway to get the completion. |
||||||
|
# Description will not be shown because that's not possible with TabCompleteNext |
||||||
|
[System.Management.Automation.CompletionResult]::new($($comp.Name | __%[1]s_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Register-ArgumentCompleter -CommandName '%[1]s' -ScriptBlock ${__%[2]sCompleterBlock} |
||||||
Binary file not shown.
@ -0,0 +1,198 @@ |
|||||||
|
#compdef %[1]s |
||||||
|
compdef _%[1]s %[1]s |
||||||
|
|
||||||
|
# Copyright 2013-2023 The Cobra Authors |
||||||
|
# |
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
# you may not use this file except in compliance with the License. |
||||||
|
# You may obtain a copy of the License at |
||||||
|
# |
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
# |
||||||
|
# Unless required by applicable law or agreed to in writing, software |
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
# See the License for the specific language governing permissions and |
||||||
|
# limitations under the License. |
||||||
|
|
||||||
|
# zsh completion for %-36[1]s -*- shell-script -*- |
||||||
|
|
||||||
|
__%[1]s_debug() |
||||||
|
{ |
||||||
|
local file="$BASH_COMP_DEBUG_FILE" |
||||||
|
if [[ -n ${file} ]]; then |
||||||
|
echo "$*" >> "${file}" |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
_%[1]s() |
||||||
|
{ |
||||||
|
local shellCompDirectiveError=%[3]d |
||||||
|
local shellCompDirectiveNoSpace=%[4]d |
||||||
|
local shellCompDirectiveNoFileComp=%[5]d |
||||||
|
local shellCompDirectiveFilterFileExt=%[6]d |
||||||
|
local shellCompDirectiveFilterDirs=%[7]d |
||||||
|
local shellCompDirectiveKeepOrder=%[8]d |
||||||
|
|
||||||
|
local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder |
||||||
|
local -a completions |
||||||
|
|
||||||
|
__%[1]s_debug "\n========= starting completion logic ==========" |
||||||
|
__%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" |
||||||
|
|
||||||
|
# The user could have moved the cursor backwards on the command-line. |
||||||
|
# We need to trigger completion from the $CURRENT location, so we need |
||||||
|
# to truncate the command-line ($words) up to the $CURRENT location. |
||||||
|
# (We cannot use $CURSOR as its value does not work when a command is an alias.) |
||||||
|
words=("${=words[1,CURRENT]}") |
||||||
|
__%[1]s_debug "Truncated words[*]: ${words[*]}," |
||||||
|
|
||||||
|
lastParam=${words[-1]} |
||||||
|
lastChar=${lastParam[-1]} |
||||||
|
__%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" |
||||||
|
|
||||||
|
# For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>) |
||||||
|
# completions must be prefixed with the flag |
||||||
|
setopt local_options BASH_REMATCH |
||||||
|
if [[ "${lastParam}" =~ '-.*=' ]]; then |
||||||
|
# We are dealing with a flag with an = |
||||||
|
flagPrefix="-P ${BASH_REMATCH}" |
||||||
|
fi |
||||||
|
|
||||||
|
# Prepare the command to obtain completions |
||||||
|
requestComp="${words[1]} %[2]s ${words[2,-1]}" |
||||||
|
if [ "${lastChar}" = "" ]; then |
||||||
|
# If the last parameter is complete (there is a space following it) |
||||||
|
# We add an extra empty parameter so we can indicate this to the go completion code. |
||||||
|
__%[1]s_debug "Adding extra empty parameter" |
||||||
|
requestComp="${requestComp} \"\"" |
||||||
|
fi |
||||||
|
|
||||||
|
__%[1]s_debug "About to call: eval ${requestComp}" |
||||||
|
|
||||||
|
# Use eval to handle any environment variables and such |
||||||
|
out=$(eval ${requestComp} 2>/dev/null) |
||||||
|
__%[1]s_debug "completion output: ${out}" |
||||||
|
|
||||||
|
# Extract the directive integer following a : from the last line |
||||||
|
local lastLine |
||||||
|
while IFS='\n' read -r line; do |
||||||
|
lastLine=${line} |
||||||
|
done < <(printf "%%s\n" "${out[@]}") |
||||||
|
__%[1]s_debug "last line: ${lastLine}" |
||||||
|
|
||||||
|
if [ "${lastLine[1]}" = : ]; then |
||||||
|
directive=${lastLine[2,-1]} |
||||||
|
# Remove the directive including the : and the newline |
||||||
|
local suffix |
||||||
|
(( suffix=${#lastLine}+2)) |
||||||
|
out=${out[1,-$suffix]} |
||||||
|
else |
||||||
|
# There is no directive specified. Leave $out as is. |
||||||
|
__%[1]s_debug "No directive found. Setting do default" |
||||||
|
directive=0 |
||||||
|
fi |
||||||
|
|
||||||
|
__%[1]s_debug "directive: ${directive}" |
||||||
|
__%[1]s_debug "completions: ${out}" |
||||||
|
__%[1]s_debug "flagPrefix: ${flagPrefix}" |
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then |
||||||
|
__%[1]s_debug "Completion received error. Ignoring completions." |
||||||
|
return |
||||||
|
fi |
||||||
|
|
||||||
|
while IFS='\n' read -r comp; do |
||||||
|
if [ -n "$comp" ]; then |
||||||
|
# If requested, completions are returned with a description. |
||||||
|
# The description is preceded by a TAB character. |
||||||
|
# For zsh's _describe, we need to use a : instead of a TAB. |
||||||
|
# We first need to escape any : as part of the completion itself. |
||||||
|
comp=${comp//:/\\:} |
||||||
|
|
||||||
|
local tab="$(printf '\t')" |
||||||
|
comp=${comp//$tab/:} |
||||||
|
|
||||||
|
__%[1]s_debug "Adding completion: ${comp}" |
||||||
|
completions+=${comp} |
||||||
|
lastComp=$comp |
||||||
|
fi |
||||||
|
done < <(printf "%%s\n" "${out[@]}") |
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then |
||||||
|
__%[1]s_debug "Activating nospace." |
||||||
|
noSpace="-S ''" |
||||||
|
fi |
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then |
||||||
|
__%[1]s_debug "Activating keep order." |
||||||
|
keepOrder="-V" |
||||||
|
fi |
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then |
||||||
|
# File extension filtering |
||||||
|
local filteringCmd |
||||||
|
filteringCmd='_files' |
||||||
|
for filter in ${completions[@]}; do |
||||||
|
if [ ${filter[1]} != '*' ]; then |
||||||
|
# zsh requires a glob pattern to do file filtering |
||||||
|
filter="\*.$filter" |
||||||
|
fi |
||||||
|
filteringCmd+=" -g $filter" |
||||||
|
done |
||||||
|
filteringCmd+=" ${flagPrefix}" |
||||||
|
|
||||||
|
__%[1]s_debug "File filtering command: $filteringCmd" |
||||||
|
_arguments '*:filename:'"$filteringCmd" |
||||||
|
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then |
||||||
|
# File completion for directories only |
||||||
|
local subdir |
||||||
|
subdir="${completions[1]}" |
||||||
|
if [ -n "$subdir" ]; then |
||||||
|
__%[1]s_debug "Listing directories in $subdir" |
||||||
|
pushd "${subdir}" >/dev/null 2>&1 |
||||||
|
else |
||||||
|
__%[1]s_debug "Listing directories in ." |
||||||
|
fi |
||||||
|
|
||||||
|
local result |
||||||
|
_arguments '*:dirname:_files -/'" ${flagPrefix}" |
||||||
|
result=$? |
||||||
|
if [ -n "$subdir" ]; then |
||||||
|
popd >/dev/null 2>&1 |
||||||
|
fi |
||||||
|
return $result |
||||||
|
else |
||||||
|
__%[1]s_debug "Calling _describe" |
||||||
|
if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then |
||||||
|
__%[1]s_debug "_describe found some completions" |
||||||
|
|
||||||
|
# Return the success of having called _describe |
||||||
|
return 0 |
||||||
|
else |
||||||
|
__%[1]s_debug "_describe did not find completions." |
||||||
|
__%[1]s_debug "Checking if we should do file completion." |
||||||
|
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then |
||||||
|
__%[1]s_debug "deactivating file completion" |
||||||
|
|
||||||
|
# We must return an error code here to let zsh know that there were no |
||||||
|
# completions found by _describe; this is what will trigger other |
||||||
|
# matching algorithms to attempt to find completions. |
||||||
|
# For example zsh can match letters in the middle of words. |
||||||
|
return 1 |
||||||
|
else |
||||||
|
# Perform file completion |
||||||
|
__%[1]s_debug "Activating file completion" |
||||||
|
|
||||||
|
# We must return the result of this command, so it must be the |
||||||
|
# last command, or else we must store its result to return it. |
||||||
|
_arguments '*:filename:_files'" ${flagPrefix}" |
||||||
|
fi |
||||||
|
fi |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
# don't run the completion function when being source-ed or eval-ed |
||||||
|
if [ "$funcstack[1]" = "_%[1]s" ]; then |
||||||
|
_%[1]s |
||||||
|
fi |
||||||
Binary file not shown.
@ -0,0 +1,42 @@ |
|||||||
|
//go:build gen
|
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"compress/gzip" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
for _, name := range []string{"comp.bash", "comp.zsh", "comp.fish", "comp.ps1"} { |
||||||
|
err := compress(name) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintln(os.Stderr, "compressing "+name+":", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func compress(name string) error { |
||||||
|
src, err := os.Open(name) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer src.Close() |
||||||
|
|
||||||
|
dst, err := os.Create(name + ".gz") |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer dst.Close() |
||||||
|
|
||||||
|
z := gzip.NewWriter(dst) |
||||||
|
_, err = io.Copy(z, src) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return z.Close() |
||||||
|
} |
||||||
Loading…
Reference in new issue