diff --git a/build_dist.sh b/build_dist.sh index 0c757c26d..66afa8f74 100755 --- a/build_dist.sh +++ b/build_dist.sh @@ -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 diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index 2ff361b2c..2c952a1c0 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -22,6 +22,7 @@ import ( "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 @@ change in the future. whoisCmd, debugCmd, driveCmd, + idTokenCmd, }, FlagSet: rootfs, Exec: func(ctx context.Context, args []string) error { @@ -206,11 +208,6 @@ change in the future. 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 @@ change in the future. } 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) { diff --git a/cmd/tailscale/cli/configure-synology.go b/cmd/tailscale/cli/configure-synology.go index 3c189d5ef..9d674e56d 100644 --- a/cmd/tailscale/cli/configure-synology.go +++ b/cmd/tailscale/cli/configure-synology.go @@ -24,9 +24,9 @@ import ( 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 diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 88b7312c9..6793e94cc 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -48,7 +48,8 @@ var debugCmd = &ffcli.Command{ Name: "debug", Exec: runDebug, ShortUsage: "tailscale debug ", - 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") diff --git a/cmd/tailscale/cli/ffcomplete/complete.go b/cmd/tailscale/cli/ffcomplete/complete.go new file mode 100644 index 000000000..83ae8e092 --- /dev/null +++ b/cmd/tailscale/cli/ffcomplete/complete.go @@ -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 -- ", + 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 [--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=' or '--name ' 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 ` or `prog subcmd arg1 `, for example. +func Args(cmd *ffcli.Command, comp CompleteFunc) { + if internal.CompleteCmds == nil { + internal.CompleteCmds = make(map[*ffcli.Command]CompleteFunc) + } + internal.CompleteCmds[cmd] = comp +} diff --git a/cmd/tailscale/cli/ffcomplete/complete_omit.go b/cmd/tailscale/cli/ffcomplete/complete_omit.go new file mode 100644 index 000000000..bafc059e7 --- /dev/null +++ b/cmd/tailscale/cli/ffcomplete/complete_omit.go @@ -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 } diff --git a/cmd/tailscale/cli/ffcomplete/ffcomplete.go b/cmd/tailscale/cli/ffcomplete/ffcomplete.go new file mode 100644 index 000000000..4b8207ec6 --- /dev/null +++ b/cmd/tailscale/cli/ffcomplete/ffcomplete.go @@ -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 + } +} diff --git a/cmd/tailscale/cli/ffcomplete/internal/complete.go b/cmd/tailscale/cli/ffcomplete/internal/complete.go new file mode 100644 index 000000000..1eb58337b --- /dev/null +++ b/cmd/tailscale/cli/ffcomplete/internal/complete.go @@ -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() +} diff --git a/cmd/tailscale/cli/ffcomplete/internal/complete_test.go b/cmd/tailscale/cli/ffcomplete/internal/complete_test.go new file mode 100644 index 000000000..f5be74741 --- /dev/null +++ b/cmd/tailscale/cli/ffcomplete/internal/complete_test.go @@ -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) + } + }) + } +} diff --git a/cmd/tailscale/cli/ffcomplete/scripts.go b/cmd/tailscale/cli/ffcomplete/scripts.go new file mode 100644 index 000000000..8218683af --- /dev/null +++ b/cmd/tailscale/cli/ffcomplete/scripts.go @@ -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) + }, + }, + } +} diff --git a/cmd/tailscale/cli/ffcomplete/scripts_omit.go b/cmd/tailscale/cli/ffcomplete/scripts_omit.go new file mode 100644 index 000000000..b5d520c3f --- /dev/null +++ b/cmd/tailscale/cli/ffcomplete/scripts_omit.go @@ -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 +} diff --git a/cmd/tailscale/cli/file.go b/cmd/tailscale/cli/file.go index 4c1b73a1a..cd7762446 100644 --- a/cmd/tailscale/cli/file.go +++ b/cmd/tailscale/cli/file.go @@ -26,6 +26,7 @@ import ( "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 @@ var fileGetCmd = &ffcli.Command{ 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 })(), } diff --git a/cmd/tailscale/cli/id-token.go b/cmd/tailscale/cli/id-token.go index e675819bf..a4d02c95a 100644 --- a/cmd/tailscale/cli/id-token.go +++ b/cmd/tailscale/cli/id-token.go @@ -8,16 +8,21 @@ import ( "errors" "github.com/peterbourgon/ff/v3/ffcli" + "tailscale.com/envknob" ) var idTokenCmd = &ffcli.Command{ Name: "id-token", ShortUsage: "tailscale id-token ", 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 ") } diff --git a/cmd/tailscale/cli/nc.go b/cmd/tailscale/cli/nc.go index e0d681644..4ea622554 100644 --- a/cmd/tailscale/cli/nc.go +++ b/cmd/tailscale/cli/nc.go @@ -10,8 +10,10 @@ import ( "io" "os" "strconv" + "strings" "github.com/peterbourgon/ff/v3/ffcli" + "tailscale.com/cmd/tailscale/cli/ffcomplete" ) var ncCmd = &ffcli.Command{ @@ -21,6 +23,27 @@ var ncCmd = &ffcli.Command{ 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 { diff --git a/cmd/tailscale/cli/ping.go b/cmd/tailscale/cli/ping.go index bf7f2c53d..3a909f30d 100644 --- a/cmd/tailscale/cli/ping.go +++ b/cmd/tailscale/cli/ping.go @@ -17,6 +17,7 @@ import ( "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 @@ relay node. })(), } +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 diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index fedd07603..94503082f 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -10,10 +10,12 @@ import ( "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") } diff --git a/cmd/tailscale/cli/switch.go b/cmd/tailscale/cli/switch.go index e5adc2ce5..731492daa 100644 --- a/cmd/tailscale/cli/switch.go +++ b/cmd/tailscale/cli/switch.go @@ -13,6 +13,7 @@ import ( "time" "github.com/peterbourgon/ff/v3/ffcli" + "tailscale.com/cmd/tailscale/cli/ffcomplete" "tailscale.com/ipn" ) @@ -35,6 +36,34 @@ This command is currently in alpha and may change in the future.`, 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 } diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 951ed4edd..dc7fcb0c3 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -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") diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 103f2048d..9870aeb42 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -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+ diff --git a/tempfork/spf13/cobra/LICENSE.txt b/tempfork/spf13/cobra/LICENSE.txt new file mode 100644 index 000000000..298f0e266 --- /dev/null +++ b/tempfork/spf13/cobra/LICENSE.txt @@ -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. diff --git a/tempfork/spf13/cobra/README.md b/tempfork/spf13/cobra/README.md new file mode 100644 index 000000000..c1941e061 --- /dev/null +++ b/tempfork/spf13/cobra/README.md @@ -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. diff --git a/tempfork/spf13/cobra/cobra.go b/tempfork/spf13/cobra/cobra.go new file mode 100644 index 000000000..085a06be9 --- /dev/null +++ b/tempfork/spf13/cobra/cobra.go @@ -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. +` diff --git a/tempfork/spf13/cobra/comp.bash b/tempfork/spf13/cobra/comp.bash new file mode 100644 index 000000000..7d1b0d5bf --- /dev/null +++ b/tempfork/spf13/cobra/comp.bash @@ -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=) +# 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 diff --git a/tempfork/spf13/cobra/comp.bash.gz b/tempfork/spf13/cobra/comp.bash.gz new file mode 100644 index 000000000..3b8d84256 Binary files /dev/null and b/tempfork/spf13/cobra/comp.bash.gz differ diff --git a/tempfork/spf13/cobra/comp.fish b/tempfork/spf13/cobra/comp.fish new file mode 100644 index 000000000..b1e4dcccc --- /dev/null +++ b/tempfork/spf13/cobra/comp.fish @@ -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., -n=) + # 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' diff --git a/tempfork/spf13/cobra/comp.fish.gz b/tempfork/spf13/cobra/comp.fish.gz new file mode 100644 index 000000000..c37b957fe Binary files /dev/null and b/tempfork/spf13/cobra/comp.fish.gz differ diff --git a/tempfork/spf13/cobra/comp.go b/tempfork/spf13/cobra/comp.go new file mode 100644 index 000000000..4def2c068 --- /dev/null +++ b/tempfork/spf13/cobra/comp.go @@ -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 +} diff --git a/tempfork/spf13/cobra/comp.ps1 b/tempfork/spf13/cobra/comp.ps1 new file mode 100644 index 000000000..469208313 --- /dev/null +++ b/tempfork/spf13/cobra/comp.ps1 @@ -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 + + # 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} diff --git a/tempfork/spf13/cobra/comp.ps1.gz b/tempfork/spf13/cobra/comp.ps1.gz new file mode 100644 index 000000000..04edf6512 Binary files /dev/null and b/tempfork/spf13/cobra/comp.ps1.gz differ diff --git a/tempfork/spf13/cobra/comp.zsh b/tempfork/spf13/cobra/comp.zsh new file mode 100644 index 000000000..b2b5299c0 --- /dev/null +++ b/tempfork/spf13/cobra/comp.zsh @@ -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=) + # 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 diff --git a/tempfork/spf13/cobra/comp.zsh.gz b/tempfork/spf13/cobra/comp.zsh.gz new file mode 100644 index 000000000..a90d368b1 Binary files /dev/null and b/tempfork/spf13/cobra/comp.zsh.gz differ diff --git a/tempfork/spf13/cobra/gen.go b/tempfork/spf13/cobra/gen.go new file mode 100644 index 000000000..36b38e4b2 --- /dev/null +++ b/tempfork/spf13/cobra/gen.go @@ -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() +}