mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-25 18:21:01 +00:00
161 lines
4.9 KiB
Go
161 lines
4.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build go1.19 && !ts_omit_completion
|
|
|
|
// Package ffcomplete provides shell tab-completion of subcommands, flags and
|
|
// arguments for Go programs written with [ffcli].
|
|
//
|
|
// The shell integration scripts have been extracted from Cobra
|
|
// (https://cobra.dev/), whose authors deserve most of the credit for this work.
|
|
// These shell completion functions invoke `$0 completion __complete -- ...`
|
|
// which is wired up to [Complete].
|
|
package ffcomplete
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal"
|
|
"tailscale.com/tempfork/spf13/cobra"
|
|
)
|
|
|
|
type compOpts struct {
|
|
showFlags bool
|
|
showDescs bool
|
|
}
|
|
|
|
func newFS(name string, opts *compOpts) *flag.FlagSet {
|
|
fs := flag.NewFlagSet(name, flag.ContinueOnError)
|
|
fs.BoolVar(&opts.showFlags, "flags", true, "Suggest flag completions with subcommands")
|
|
fs.BoolVar(&opts.showDescs, "descs", true, "Include flag, subcommand, and other descriptions in completions")
|
|
return fs
|
|
}
|
|
|
|
// Inject adds the 'completion' subcommand to the root command which provide the
|
|
// user with shell scripts for calling `completion __command` to provide
|
|
// tab-completion suggestions.
|
|
//
|
|
// root.Name needs to match the command that the user is tab-completing for the
|
|
// shell script to work as expected by default.
|
|
//
|
|
// The hide function is called with the __complete Command instance to provide a
|
|
// hook to omit it from the help output, if desired.
|
|
func Inject(root *ffcli.Command, hide func(*ffcli.Command), usageFunc func(*ffcli.Command) string) {
|
|
var opts compOpts
|
|
compFS := newFS("completion", &opts)
|
|
|
|
completeCmd := &ffcli.Command{
|
|
Name: "__complete",
|
|
ShortUsage: root.Name + " completion __complete -- <args to complete...>",
|
|
ShortHelp: "Tab-completion suggestions for interactive shells",
|
|
UsageFunc: usageFunc,
|
|
FlagSet: compFS,
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
// Set up debug logging for the rest of this function call.
|
|
if t := os.Getenv("BASH_COMP_DEBUG_FILE"); t != "" {
|
|
tf, err := os.OpenFile(t, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("opening debug file: %w", err)
|
|
}
|
|
defer func(origW io.Writer, origPrefix string, origFlags int) {
|
|
log.SetOutput(origW)
|
|
log.SetFlags(origFlags)
|
|
log.SetPrefix(origPrefix)
|
|
tf.Close()
|
|
}(log.Writer(), log.Prefix(), log.Flags())
|
|
log.SetOutput(tf)
|
|
log.SetFlags(log.Lshortfile)
|
|
log.SetPrefix("debug: ")
|
|
}
|
|
|
|
// Send back the results to the shell.
|
|
words, dir, err := internal.Complete(root, args, opts.showFlags, opts.showDescs)
|
|
if err != nil {
|
|
dir = ShellCompDirectiveError
|
|
}
|
|
for _, word := range words {
|
|
fmt.Println(word)
|
|
}
|
|
fmt.Println(":" + strconv.Itoa(int(dir)))
|
|
return err
|
|
},
|
|
}
|
|
if hide != nil {
|
|
hide(completeCmd)
|
|
}
|
|
|
|
root.Subcommands = append(
|
|
root.Subcommands,
|
|
&ffcli.Command{
|
|
Name: "completion",
|
|
ShortUsage: root.Name + " completion <shell> [--flags] [--descs]",
|
|
ShortHelp: "Shell tab-completion scripts",
|
|
LongHelp: fmt.Sprintf(cobra.UsageTemplate, root.Name),
|
|
|
|
// Print help if run without args.
|
|
Exec: func(ctx context.Context, args []string) error { return flag.ErrHelp },
|
|
|
|
// Omit the '__complete' subcommand from the 'completion' help.
|
|
UsageFunc: func(c *ffcli.Command) string {
|
|
// Filter the subcommands to omit '__complete'.
|
|
s := make([]*ffcli.Command, 0, len(c.Subcommands))
|
|
for _, sub := range c.Subcommands {
|
|
if !strings.HasPrefix(sub.Name, "__") {
|
|
s = append(s, sub)
|
|
}
|
|
}
|
|
|
|
// Swap in the filtered subcommands list for the rest of the call.
|
|
defer func(r []*ffcli.Command) { c.Subcommands = r }(c.Subcommands)
|
|
c.Subcommands = s
|
|
|
|
// Render the usage.
|
|
if usageFunc == nil {
|
|
return ffcli.DefaultUsageFunc(c)
|
|
}
|
|
return usageFunc(c)
|
|
},
|
|
|
|
Subcommands: append(
|
|
scriptCmds(root, usageFunc),
|
|
completeCmd,
|
|
),
|
|
},
|
|
)
|
|
}
|
|
|
|
// Flag registers a completion function for the flag in fs with given name.
|
|
// comp will always called with a 1-element slice.
|
|
//
|
|
// comp will be called to return suggestions when the user tries to tab-complete
|
|
// '--name=<TAB>' or '--name <TAB>' for the commands using fs.
|
|
func Flag(fs *flag.FlagSet, name string, comp CompleteFunc) {
|
|
f := fs.Lookup(name)
|
|
if f == nil {
|
|
panic(fmt.Errorf("ffcomplete.Flag: flag %s not found", name))
|
|
}
|
|
if internal.CompleteFlags == nil {
|
|
internal.CompleteFlags = make(map[*flag.Flag]CompleteFunc)
|
|
}
|
|
internal.CompleteFlags[f] = comp
|
|
}
|
|
|
|
// Args registers a completion function for the args of cmd.
|
|
//
|
|
// comp will be called to return suggestions when the user tries to tab-complete
|
|
// `prog <TAB>` or `prog subcmd arg1 <TAB>`, for example.
|
|
func Args(cmd *ffcli.Command, comp CompleteFunc) {
|
|
if internal.CompleteCmds == nil {
|
|
internal.CompleteCmds = make(map[*ffcli.Command]CompleteFunc)
|
|
}
|
|
internal.CompleteCmds[cmd] = comp
|
|
}
|