mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 11:05:45 +00:00
cmd/tailscale: add shell tab-completion
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>
This commit is contained in:
parent
21a0fe1b9b
commit
82394debb7
@ -37,7 +37,7 @@ while [ "$#" -gt 1 ]; do
|
||||
--extra-small)
|
||||
shift
|
||||
ldflags="$ldflags -w -s"
|
||||
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube"
|
||||
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion"
|
||||
;;
|
||||
--box)
|
||||
shift
|
||||
|
@ -22,6 +22,7 @@
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/version/distro"
|
||||
@ -197,6 +198,7 @@ func newRootCmd() *ffcli.Command {
|
||||
whoisCmd,
|
||||
debugCmd,
|
||||
driveCmd,
|
||||
idTokenCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
@ -206,11 +208,6 @@ func newRootCmd() *ffcli.Command {
|
||||
return flag.ErrHelp
|
||||
},
|
||||
}
|
||||
if envknob.UseWIPCode() {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands,
|
||||
idTokenCmd,
|
||||
)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
@ -222,6 +219,8 @@ func newRootCmd() *ffcli.Command {
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
ffcomplete.Inject(rootCmd, func(c *ffcli.Command) { c.LongHelp = hidden + c.LongHelp }, usageFunc)
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
@ -303,9 +302,12 @@ func usageFunc(c *ffcli.Command) string {
|
||||
return usageFuncOpt(c, true)
|
||||
}
|
||||
|
||||
// hidden is the prefix that hides subcommands and flags from --help output when
|
||||
// found at the start of the subcommand's LongHelp or flag's Usage.
|
||||
const hidden = "HIDDEN: "
|
||||
|
||||
func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
||||
var b strings.Builder
|
||||
const hiddenPrefix = "HIDDEN: "
|
||||
|
||||
if c.ShortHelp != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", c.ShortHelp)
|
||||
@ -319,8 +321,7 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
||||
}
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
if c.LongHelp != "" {
|
||||
help, _ := strings.CutPrefix(c.LongHelp, hiddenPrefix)
|
||||
if help := strings.TrimPrefix(c.LongHelp, hidden); help != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", help)
|
||||
}
|
||||
|
||||
@ -328,7 +329,7 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
||||
fmt.Fprintf(&b, "SUBCOMMANDS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
for _, subcommand := range c.Subcommands {
|
||||
if strings.HasPrefix(subcommand.LongHelp, hiddenPrefix) {
|
||||
if strings.HasPrefix(subcommand.LongHelp, hidden) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
|
||||
@ -343,7 +344,7 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
||||
c.FlagSet.VisitAll(func(f *flag.Flag) {
|
||||
var s string
|
||||
name, usage := flag.UnquoteUsage(f)
|
||||
if strings.HasPrefix(usage, hiddenPrefix) {
|
||||
if strings.HasPrefix(usage, hidden) {
|
||||
return
|
||||
}
|
||||
if isBoolFlag(f) {
|
||||
|
@ -24,9 +24,9 @@
|
||||
var configureHostCmd = &ffcli.Command{
|
||||
Name: "configure-host",
|
||||
Exec: runConfigureSynology,
|
||||
ShortUsage: "tailscale configure-host",
|
||||
ShortUsage: "tailscale configure-host\n" + synologyConfigureCmd.ShortUsage,
|
||||
ShortHelp: synologyConfigureCmd.ShortHelp,
|
||||
LongHelp: synologyConfigureCmd.LongHelp,
|
||||
LongHelp: hidden + synologyConfigureCmd.LongHelp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("configure-host")
|
||||
return fs
|
||||
|
@ -48,7 +48,8 @@
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
ShortUsage: "tailscale debug <debug-flags | subcommand>",
|
||||
LongHelp: `HIDDEN: "tailscale debug" contains misc debug facilities; it is not a stable interface.`,
|
||||
ShortHelp: "Debug commands",
|
||||
LongHelp: hidden + `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("debug")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
|
160
cmd/tailscale/cli/ffcomplete/complete.go
Normal file
160
cmd/tailscale/cli/ffcomplete/complete.go
Normal file
@ -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
|
||||
}
|
17
cmd/tailscale/cli/ffcomplete/complete_omit.go
Normal file
17
cmd/tailscale/cli/ffcomplete/complete_omit.go
Normal file
@ -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 }
|
60
cmd/tailscale/cli/ffcomplete/ffcomplete.go
Normal file
60
cmd/tailscale/cli/ffcomplete/ffcomplete.go
Normal file
@ -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
|
||||
}
|
||||
}
|
256
cmd/tailscale/cli/ffcomplete/internal/complete.go
Normal file
256
cmd/tailscale/cli/ffcomplete/internal/complete.go
Normal file
@ -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()
|
||||
}
|
219
cmd/tailscale/cli/ffcomplete/internal/complete_test.go
Normal file
219
cmd/tailscale/cli/ffcomplete/internal/complete_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
85
cmd/tailscale/cli/ffcomplete/scripts.go
Normal file
85
cmd/tailscale/cli/ffcomplete/scripts.go
Normal file
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
12
cmd/tailscale/cli/ffcomplete/scripts_omit.go
Normal file
12
cmd/tailscale/cli/ffcomplete/scripts_omit.go
Normal file
@ -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
|
||||
}
|
@ -26,6 +26,7 @@
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
@ -418,6 +419,7 @@ func (v *onConflict) Set(s string) error {
|
||||
skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files
|
||||
overwrite: overwrite existing file
|
||||
rename: write to a new number-suffixed filename`)
|
||||
ffcomplete.Flag(fs, "conflict", ffcomplete.Fixed("skip", "overwrite", "rename"))
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
@ -8,16 +8,21 @@
|
||||
"errors"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/envknob"
|
||||
)
|
||||
|
||||
var idTokenCmd = &ffcli.Command{
|
||||
Name: "id-token",
|
||||
ShortUsage: "tailscale id-token <aud>",
|
||||
ShortHelp: "Fetch an OIDC id-token for the Tailscale machine",
|
||||
LongHelp: hidden,
|
||||
Exec: runIDToken,
|
||||
}
|
||||
|
||||
func runIDToken(ctx context.Context, args []string) error {
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("tailscale id-token: works-in-progress require TAILSCALE_USE_WIP_CODE=1 envvar")
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: tailscale id-token <aud>")
|
||||
}
|
||||
|
@ -10,8 +10,10 @@
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
)
|
||||
|
||||
var ncCmd = &ffcli.Command{
|
||||
@ -21,6 +23,27 @@
|
||||
Exec: runNC,
|
||||
}
|
||||
|
||||
func init() {
|
||||
ffcomplete.Args(ncCmd, func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
|
||||
if len(args) > 1 {
|
||||
return nil, ffcomplete.ShellCompDirectiveNoFileComp, nil
|
||||
}
|
||||
return completeHostOrIP(ffcomplete.LastArg(args))
|
||||
})
|
||||
}
|
||||
|
||||
func completeHostOrIP(arg string) ([]string, ffcomplete.ShellCompDirective, error) {
|
||||
st, err := localClient.Status(context.Background())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
nodes := make([]string, 0, len(st.Peer))
|
||||
for _, node := range st.Peer {
|
||||
nodes = append(nodes, strings.TrimSuffix(node.DNSName, "."))
|
||||
}
|
||||
return nodes, ffcomplete.ShellCompDirectiveNoFileComp, nil
|
||||
}
|
||||
|
||||
func runNC(ctx context.Context, args []string) error {
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
@ -59,6 +60,15 @@
|
||||
})(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
ffcomplete.Args(pingCmd, func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
|
||||
if len(args) > 1 {
|
||||
return nil, ffcomplete.ShellCompDirectiveNoFileComp, nil
|
||||
}
|
||||
return completeHostOrIP(ffcomplete.LastArg(args))
|
||||
})
|
||||
}
|
||||
|
||||
var pingArgs struct {
|
||||
num int
|
||||
size int
|
||||
|
@ -10,10 +10,12 @@
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/web"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tsaddr"
|
||||
@ -74,9 +76,24 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf.BoolVar(&setArgs.advertiseConnector, "advertise-connector", false, "offer to be an app connector for domain specific internet traffic for the tailnet")
|
||||
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "notify about available Tailscale updates")
|
||||
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
|
||||
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
|
||||
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, hidden+"allow management plane to gather device posture information")
|
||||
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
|
||||
|
||||
ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
|
||||
st, err := localClient.Status(context.Background())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
nodes := make([]string, 0, len(st.Peer))
|
||||
for _, node := range st.Peer {
|
||||
if !node.ExitNodeOption {
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, strings.TrimSuffix(node.DNSName, "."))
|
||||
}
|
||||
return nodes, ffcomplete.ShellCompDirectiveNoFileComp, nil
|
||||
})
|
||||
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
@ -35,6 +36,34 @@
|
||||
Exec: switchProfile,
|
||||
}
|
||||
|
||||
func init() {
|
||||
ffcomplete.Args(switchCmd, func(s []string) (words []string, dir ffcomplete.ShellCompDirective, err error) {
|
||||
_, all, err := localClient.ProfileStatus(context.Background())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
seen := make(map[string]bool, 3*len(all))
|
||||
wordfns := []func(prof ipn.LoginProfile) string{
|
||||
func(prof ipn.LoginProfile) string { return string(prof.ID) },
|
||||
func(prof ipn.LoginProfile) string { return prof.NetworkProfile.DomainName },
|
||||
func(prof ipn.LoginProfile) string { return prof.Name },
|
||||
}
|
||||
|
||||
for _, wordfn := range wordfns {
|
||||
for _, prof := range all {
|
||||
word := wordfn(prof)
|
||||
if seen[word] {
|
||||
continue
|
||||
}
|
||||
seen[word] = true
|
||||
words = append(words, fmt.Sprintf("%s\tid: %s, tailnet: %s, account: %s", word, prof.ID, prof.NetworkProfile.DomainName, prof.Name))
|
||||
}
|
||||
}
|
||||
return words, ffcomplete.ShellCompDirectiveNoFileComp, nil
|
||||
})
|
||||
}
|
||||
|
||||
var switchArgs struct {
|
||||
list bool
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "HIDDEN: install host routes to other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, hidden+"install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
|
@ -34,8 +34,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
github.com/miekg/dns from tailscale.com/net/dns/recursive
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli+
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3/internal from github.com/peterbourgon/ff/v3
|
||||
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
||||
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
||||
@ -78,6 +78,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
@ -119,6 +121,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
|
174
tempfork/spf13/cobra/LICENSE.txt
Normal file
174
tempfork/spf13/cobra/LICENSE.txt
Normal file
@ -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.
|
10
tempfork/spf13/cobra/README.md
Normal file
10
tempfork/spf13/cobra/README.md
Normal file
@ -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.
|
139
tempfork/spf13/cobra/cobra.go
Normal file
139
tempfork/spf13/cobra/cobra.go
Normal file
@ -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.
|
||||
`
|
333
tempfork/spf13/cobra/comp.bash
Normal file
333
tempfork/spf13/cobra/comp.bash
Normal file
@ -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
|
BIN
tempfork/spf13/cobra/comp.bash.gz
Normal file
BIN
tempfork/spf13/cobra/comp.bash.gz
Normal file
Binary file not shown.
248
tempfork/spf13/cobra/comp.fish
Normal file
248
tempfork/spf13/cobra/comp.fish
Normal file
@ -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'
|
BIN
tempfork/spf13/cobra/comp.fish.gz
Normal file
BIN
tempfork/spf13/cobra/comp.fish.gz
Normal file
Binary file not shown.
72
tempfork/spf13/cobra/comp.go
Normal file
72
tempfork/spf13/cobra/comp.go
Normal file
@ -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
|
||||
}
|
259
tempfork/spf13/cobra/comp.ps1
Normal file
259
tempfork/spf13/cobra/comp.ps1
Normal file
@ -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}
|
BIN
tempfork/spf13/cobra/comp.ps1.gz
Normal file
BIN
tempfork/spf13/cobra/comp.ps1.gz
Normal file
Binary file not shown.
198
tempfork/spf13/cobra/comp.zsh
Normal file
198
tempfork/spf13/cobra/comp.zsh
Normal file
@ -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
|
BIN
tempfork/spf13/cobra/comp.zsh.gz
Normal file
BIN
tempfork/spf13/cobra/comp.zsh.gz
Normal file
Binary file not shown.
42
tempfork/spf13/cobra/gen.go
Normal file
42
tempfork/spf13/cobra/gen.go
Normal file
@ -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
Block a user