cmd/tailscale: add shell tab-completion
The approach is lifted from cobra: `tailscale completion bash` emits a bash
script for configuring the shell's autocomplete:
. <( tailscale completion bash )
so that typing:
tailscale st<TAB>
invokes:
tailscale completion __complete -- st
RELNOTE=tailscale CLI now supports shell tab-completion
Fixes #3793
Signed-off-by: Paul Scott <paul@tailscale.com>
This commit is contained in:
+11
-10
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,8 @@ var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
ShortUsage: "tailscale debug <debug-flags | subcommand>",
|
||||
LongHelp: `HIDDEN: "tailscale debug" contains misc debug facilities; it is not a stable interface.`,
|
||||
ShortHelp: "Debug commands",
|
||||
LongHelp: hidden + `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("debug")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build go1.19 && !ts_omit_completion
|
||||
|
||||
// Package ffcomplete provides shell tab-completion of subcommands, flags and
|
||||
// arguments for Go programs written with [ffcli].
|
||||
//
|
||||
// The shell integration scripts have been extracted from Cobra
|
||||
// (https://cobra.dev/), whose authors deserve most of the credit for this work.
|
||||
// These shell completion functions invoke `$0 completion __complete -- ...`
|
||||
// which is wired up to [Complete].
|
||||
package ffcomplete
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal"
|
||||
"tailscale.com/tempfork/spf13/cobra"
|
||||
)
|
||||
|
||||
type compOpts struct {
|
||||
showFlags bool
|
||||
showDescs bool
|
||||
}
|
||||
|
||||
func newFS(name string, opts *compOpts) *flag.FlagSet {
|
||||
fs := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||
fs.BoolVar(&opts.showFlags, "flags", true, "Suggest flag completions with subcommands")
|
||||
fs.BoolVar(&opts.showDescs, "descs", true, "Include flag, subcommand, and other descriptions in completions")
|
||||
return fs
|
||||
}
|
||||
|
||||
// Inject adds the 'completion' subcommand to the root command which provide the
|
||||
// user with shell scripts for calling `completion __command` to provide
|
||||
// tab-completion suggestions.
|
||||
//
|
||||
// root.Name needs to match the command that the user is tab-completing for the
|
||||
// shell script to work as expected by default.
|
||||
//
|
||||
// The hide function is called with the __complete Command instance to provide a
|
||||
// hook to omit it from the help output, if desired.
|
||||
func Inject(root *ffcli.Command, hide func(*ffcli.Command), usageFunc func(*ffcli.Command) string) {
|
||||
var opts compOpts
|
||||
compFS := newFS("completion", &opts)
|
||||
|
||||
completeCmd := &ffcli.Command{
|
||||
Name: "__complete",
|
||||
ShortUsage: root.Name + " completion __complete -- <args to complete...>",
|
||||
ShortHelp: "Tab-completion suggestions for interactive shells",
|
||||
UsageFunc: usageFunc,
|
||||
FlagSet: compFS,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
// Set up debug logging for the rest of this function call.
|
||||
if t := os.Getenv("BASH_COMP_DEBUG_FILE"); t != "" {
|
||||
tf, err := os.OpenFile(t, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening debug file: %w", err)
|
||||
}
|
||||
defer func(origW io.Writer, origPrefix string, origFlags int) {
|
||||
log.SetOutput(origW)
|
||||
log.SetFlags(origFlags)
|
||||
log.SetPrefix(origPrefix)
|
||||
tf.Close()
|
||||
}(log.Writer(), log.Prefix(), log.Flags())
|
||||
log.SetOutput(tf)
|
||||
log.SetFlags(log.Lshortfile)
|
||||
log.SetPrefix("debug: ")
|
||||
}
|
||||
|
||||
// Send back the results to the shell.
|
||||
words, dir, err := internal.Complete(root, args, opts.showFlags, opts.showDescs)
|
||||
if err != nil {
|
||||
dir = ShellCompDirectiveError
|
||||
}
|
||||
for _, word := range words {
|
||||
fmt.Println(word)
|
||||
}
|
||||
fmt.Println(":" + strconv.Itoa(int(dir)))
|
||||
return err
|
||||
},
|
||||
}
|
||||
if hide != nil {
|
||||
hide(completeCmd)
|
||||
}
|
||||
|
||||
root.Subcommands = append(
|
||||
root.Subcommands,
|
||||
&ffcli.Command{
|
||||
Name: "completion",
|
||||
ShortUsage: root.Name + " completion <shell> [--flags] [--descs]",
|
||||
ShortHelp: "Shell tab-completion scripts.",
|
||||
LongHelp: fmt.Sprintf(cobra.UsageTemplate, root.Name),
|
||||
|
||||
// Print help if run without args.
|
||||
Exec: func(ctx context.Context, args []string) error { return flag.ErrHelp },
|
||||
|
||||
// Omit the '__complete' subcommand from the 'completion' help.
|
||||
UsageFunc: func(c *ffcli.Command) string {
|
||||
// Filter the subcommands to omit '__complete'.
|
||||
s := make([]*ffcli.Command, 0, len(c.Subcommands))
|
||||
for _, sub := range c.Subcommands {
|
||||
if !strings.HasPrefix(sub.Name, "__") {
|
||||
s = append(s, sub)
|
||||
}
|
||||
}
|
||||
|
||||
// Swap in the filtered subcommands list for the rest of the call.
|
||||
defer func(r []*ffcli.Command) { c.Subcommands = r }(c.Subcommands)
|
||||
c.Subcommands = s
|
||||
|
||||
// Render the usage.
|
||||
if usageFunc == nil {
|
||||
return ffcli.DefaultUsageFunc(c)
|
||||
}
|
||||
return usageFunc(c)
|
||||
},
|
||||
|
||||
Subcommands: append(
|
||||
scriptCmds(root, usageFunc),
|
||||
completeCmd,
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Flag registers a completion function for the flag in fs with given name.
|
||||
// comp will always called with a 1-element slice.
|
||||
//
|
||||
// comp will be called to return suggestions when the user tries to tab-complete
|
||||
// '--name=<TAB>' or '--name <TAB>' for the commands using fs.
|
||||
func Flag(fs *flag.FlagSet, name string, comp CompleteFunc) {
|
||||
f := fs.Lookup(name)
|
||||
if f == nil {
|
||||
panic(fmt.Errorf("ffcomplete.Flag: flag %s not found", name))
|
||||
}
|
||||
if internal.CompleteFlags == nil {
|
||||
internal.CompleteFlags = make(map[*flag.Flag]CompleteFunc)
|
||||
}
|
||||
internal.CompleteFlags[f] = comp
|
||||
}
|
||||
|
||||
// Args registers a completion function for the args of cmd.
|
||||
//
|
||||
// comp will be called to return suggestions when the user tries to tab-complete
|
||||
// `prog <TAB>` or `prog subcmd arg1 <TAB>`, for example.
|
||||
func Args(cmd *ffcli.Command, comp CompleteFunc) {
|
||||
if internal.CompleteCmds == nil {
|
||||
internal.CompleteCmds = make(map[*ffcli.Command]CompleteFunc)
|
||||
}
|
||||
internal.CompleteCmds[cmd] = comp
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build go1.19 && ts_omit_completion
|
||||
|
||||
package ffcomplete
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
)
|
||||
|
||||
func Inject(root *ffcli.Command, hide func(*ffcli.Command), usageFunc func(*ffcli.Command) string) {}
|
||||
|
||||
func Flag(fs *flag.FlagSet, name string, comp CompleteFunc) {}
|
||||
func Args(cmd *ffcli.Command, comp CompleteFunc) *ffcli.Command { return cmd }
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ffcomplete
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal"
|
||||
"tailscale.com/tempfork/spf13/cobra"
|
||||
)
|
||||
|
||||
type ShellCompDirective = cobra.ShellCompDirective
|
||||
|
||||
const (
|
||||
ShellCompDirectiveError = cobra.ShellCompDirectiveError
|
||||
ShellCompDirectiveNoSpace = cobra.ShellCompDirectiveNoSpace
|
||||
ShellCompDirectiveNoFileComp = cobra.ShellCompDirectiveNoFileComp
|
||||
ShellCompDirectiveFilterFileExt = cobra.ShellCompDirectiveFilterFileExt
|
||||
ShellCompDirectiveFilterDirs = cobra.ShellCompDirectiveFilterDirs
|
||||
ShellCompDirectiveKeepOrder = cobra.ShellCompDirectiveKeepOrder
|
||||
ShellCompDirectiveDefault = cobra.ShellCompDirectiveDefault
|
||||
)
|
||||
|
||||
// CompleteFunc is used to return tab-completion suggestions to the user as they
|
||||
// are typing command-line instructions. It returns the list of things to
|
||||
// suggest and an additional directive to the shell about what extra
|
||||
// functionality to enable.
|
||||
type CompleteFunc = internal.CompleteFunc
|
||||
|
||||
// LastArg returns the last element of args, or the empty string if args is
|
||||
// empty.
|
||||
func LastArg(args []string) string {
|
||||
if len(args) == 0 {
|
||||
return ""
|
||||
}
|
||||
return args[len(args)-1]
|
||||
}
|
||||
|
||||
// Fixed returns a CompleteFunc which suggests the given words.
|
||||
func Fixed(words ...string) CompleteFunc {
|
||||
return func(args []string) ([]string, cobra.ShellCompDirective, error) {
|
||||
match := LastArg(args)
|
||||
matches := make([]string, 0, len(words))
|
||||
for _, word := range words {
|
||||
if strings.HasPrefix(word, match) {
|
||||
matches = append(matches, word)
|
||||
}
|
||||
}
|
||||
return matches, cobra.ShellCompDirectiveNoFileComp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// FilesWithExtensions returns a CompleteFunc that tells the shell to limit file
|
||||
// suggestions to those with the given extensions.
|
||||
func FilesWithExtensions(exts ...string) CompleteFunc {
|
||||
return func(args []string) ([]string, cobra.ShellCompDirective, error) {
|
||||
return exts, cobra.ShellCompDirectiveFilterFileExt, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/tempfork/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
CompleteCmds map[*ffcli.Command]CompleteFunc
|
||||
CompleteFlags map[*flag.Flag]CompleteFunc
|
||||
)
|
||||
|
||||
type CompleteFunc func([]string) ([]string, cobra.ShellCompDirective, error)
|
||||
|
||||
// Complete returns the autocomplete suggestions for the root program and args.
|
||||
//
|
||||
// The returned words do not necessarily need to be prefixed with the last arg
|
||||
// which is being completed. For example, '--bool-flag=' will have completions
|
||||
// 'true' and 'false'.
|
||||
//
|
||||
// "HIDDEN: " is trimmed from the start of Flag Usage's.
|
||||
func Complete(root *ffcli.Command, args []string, startFlags, descs bool) (words []string, dir cobra.ShellCompDirective, err error) {
|
||||
// Explicitly log panics.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if rerr, ok := err.(error); ok {
|
||||
err = fmt.Errorf("panic: %w", rerr)
|
||||
} else {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up the arguments.
|
||||
if len(args) == 0 {
|
||||
args = []string{""}
|
||||
}
|
||||
|
||||
// Completion criteria.
|
||||
completeArg := args[len(args)-1]
|
||||
args = args[:len(args)-1]
|
||||
emitFlag := startFlags || strings.HasPrefix(completeArg, "-")
|
||||
emitArgs := true
|
||||
|
||||
// Traverse the command-tree to find the cmd command whose
|
||||
// subcommand, flags, or arguments are being completed.
|
||||
cmd := root
|
||||
walk:
|
||||
for {
|
||||
// Ensure there's a flagset with ContinueOnError set.
|
||||
if cmd.FlagSet == nil {
|
||||
cmd.FlagSet = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
|
||||
}
|
||||
cmd.FlagSet.Init(cmd.FlagSet.Name(), flag.ContinueOnError)
|
||||
|
||||
// Manually split the args so we know when we're completing flags/args.
|
||||
flagArgs, argArgs, flagNeedingValue := splitFlagArgs(cmd.FlagSet, args)
|
||||
if flagNeedingValue != "" {
|
||||
completeArg = flagNeedingValue + "=" + completeArg
|
||||
emitFlag = true
|
||||
}
|
||||
args = argArgs
|
||||
|
||||
// Parse the flags.
|
||||
err := ff.Parse(cmd.FlagSet, flagArgs, cmd.Options...)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("%s flag parsing: %w", cmd.Name, err)
|
||||
}
|
||||
if cmd.FlagSet.NArg() > 0 {
|
||||
// This shouldn't happen if splitFlagArgs is accurately finding the
|
||||
// split between flags and args.
|
||||
_ = false
|
||||
}
|
||||
if len(args) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Check if the first argument is actually a subcommand.
|
||||
for _, sub := range cmd.Subcommands {
|
||||
if strings.EqualFold(sub.Name, args[0]) {
|
||||
args = args[1:]
|
||||
cmd = sub
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(args) > 0 {
|
||||
emitFlag = false
|
||||
}
|
||||
|
||||
// Complete '-flag=...'. If the args ended with '-flag ...' we will have
|
||||
// rewritten to '-flag=...' by now.
|
||||
if emitFlag && strings.HasPrefix(completeArg, "-") && strings.Contains(completeArg, "=") {
|
||||
// Don't complete '-flag' later on as the
|
||||
// flag name is terminated by a '='.
|
||||
emitFlag = false
|
||||
emitArgs = false
|
||||
|
||||
dashFlag, completeVal, _ := strings.Cut(completeArg, "=")
|
||||
_, f := cutDash(dashFlag)
|
||||
flag := cmd.FlagSet.Lookup(f)
|
||||
if flag != nil {
|
||||
if comp := CompleteFlags[flag]; comp != nil {
|
||||
// Complete custom flag values.
|
||||
var err error
|
||||
words, dir, err = comp([]string{completeVal})
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("completing %s flag %s: %w", cmd.Name, flag.Name, err)
|
||||
}
|
||||
} else if isBoolFlag(flag) {
|
||||
// Complete true/false.
|
||||
for _, vals := range [][]string{
|
||||
{"true", "TRUE", "True", "1"},
|
||||
{"false", "FALSE", "False", "0"},
|
||||
} {
|
||||
for _, val := range vals {
|
||||
if strings.HasPrefix(val, completeVal) {
|
||||
words = append(words, val)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete '-flag...'.
|
||||
if emitFlag {
|
||||
used := make(map[string]struct{})
|
||||
cmd.FlagSet.Visit(func(f *flag.Flag) {
|
||||
used[f.Name] = struct{}{}
|
||||
})
|
||||
|
||||
cd, cf := cutDash(completeArg)
|
||||
cmd.FlagSet.VisitAll(func(f *flag.Flag) {
|
||||
if !strings.HasPrefix(f.Name, cf) {
|
||||
return
|
||||
}
|
||||
// Skip flags already set by the user.
|
||||
if _, seen := used[f.Name]; seen {
|
||||
return
|
||||
}
|
||||
// Suggest single-dash '-v' for single-char flags and
|
||||
// double-dash '--verbose' for longer.
|
||||
d := cd
|
||||
if (d == "" || d == "-") && cf == "" && len(f.Name) > 1 {
|
||||
d = "--"
|
||||
}
|
||||
if descs {
|
||||
_, usage := flag.UnquoteUsage(f)
|
||||
usage = strings.TrimPrefix(usage, "HIDDEN: ")
|
||||
if usage != "" {
|
||||
words = append(words, d+f.Name+"\t"+usage)
|
||||
return
|
||||
}
|
||||
}
|
||||
words = append(words, d+f.Name)
|
||||
})
|
||||
}
|
||||
|
||||
if emitArgs {
|
||||
// Complete 'sub...'.
|
||||
for _, sub := range cmd.Subcommands {
|
||||
if strings.HasPrefix(sub.Name, completeArg) {
|
||||
if descs {
|
||||
if sub.ShortHelp != "" {
|
||||
words = append(words, sub.Name+"\t"+sub.ShortHelp)
|
||||
continue
|
||||
}
|
||||
}
|
||||
words = append(words, sub.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Complete custom args.
|
||||
if comp := CompleteCmds[cmd]; comp != nil {
|
||||
w, d, err := comp(append(args, completeArg))
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("completing %s args: %w", cmd.Name, err)
|
||||
}
|
||||
dir = d
|
||||
words = append(words, w...)
|
||||
}
|
||||
}
|
||||
|
||||
// Strip any descriptions if they were suppressed.
|
||||
if !descs {
|
||||
for i := range words {
|
||||
words[i], _, _ = strings.Cut(words[i], "\t")
|
||||
}
|
||||
}
|
||||
return words, dir, nil
|
||||
}
|
||||
|
||||
// splitFlagArgs separates a list of command-line arguments into arguments
|
||||
// comprising flags and their values, preceding arguments to be passed to the
|
||||
// command. This follows the stdlib 'flag' parsing conventions. If the final
|
||||
// argument is a flag name which takes a value but has no value specified, it is
|
||||
// omitted from flagArgs and argArgs and instead returned in needValue.
|
||||
func splitFlagArgs(fs *flag.FlagSet, args []string) (flagArgs, argArgs []string, flagNeedingValue string) {
|
||||
for i := 0; i < len(args); i++ {
|
||||
a := args[i]
|
||||
if a == "--" {
|
||||
return args[:i], args[i+1:], ""
|
||||
}
|
||||
|
||||
d, f := cutDash(a)
|
||||
if d == "" {
|
||||
return args[:i], args[i:], ""
|
||||
}
|
||||
if strings.Contains(f, "=") {
|
||||
continue
|
||||
}
|
||||
|
||||
flag := fs.Lookup(f)
|
||||
if flag == nil {
|
||||
return args[:i], args[i:], ""
|
||||
}
|
||||
if isBoolFlag(flag) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Consume an extra argument for the flag value.
|
||||
if i == len(args)-1 {
|
||||
return args[:i], nil, args[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
return args, nil, ""
|
||||
}
|
||||
|
||||
func cutDash(s string) (dashes, flag string) {
|
||||
if strings.HasPrefix(s, "-") {
|
||||
if strings.HasPrefix(s[1:], "-") {
|
||||
return "--", s[2:]
|
||||
}
|
||||
return "-", s[1:]
|
||||
}
|
||||
return "", s
|
||||
}
|
||||
|
||||
func isBoolFlag(f *flag.Flag) bool {
|
||||
bf, ok := f.Value.(interface {
|
||||
IsBoolFlag() bool
|
||||
})
|
||||
return ok && bf.IsBoolFlag()
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"flag"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete"
|
||||
"tailscale.com/cmd/tailscale/cli/ffcomplete/internal"
|
||||
)
|
||||
|
||||
func newFlagSet(name string, errh flag.ErrorHandling, flags func(fs *flag.FlagSet)) *flag.FlagSet {
|
||||
fs := flag.NewFlagSet(name, errh)
|
||||
if flags != nil {
|
||||
flags(fs)
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
func TestComplete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Build our test program in testdata.
|
||||
root := &ffcli.Command{
|
||||
Name: "prog",
|
||||
FlagSet: newFlagSet("prog", flag.ContinueOnError, func(fs *flag.FlagSet) {
|
||||
fs.Bool("v", false, "verbose")
|
||||
fs.Bool("root-bool", false, "root `bool`")
|
||||
fs.String("root-str", "", "some `text`")
|
||||
}),
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "debug",
|
||||
ShortHelp: "Debug data",
|
||||
FlagSet: newFlagSet("prog debug", flag.ExitOnError, func(fs *flag.FlagSet) {
|
||||
fs.String("cpu-profile", "", "write cpu profile to `file`")
|
||||
fs.Bool("debug-bool", false, "debug bool")
|
||||
fs.Int("level", 0, "a number")
|
||||
fs.String("enum", "", "a flag that takes several specific values")
|
||||
ffcomplete.Flag(fs, "enum", ffcomplete.Fixed("alpha", "beta", "charlie"))
|
||||
}),
|
||||
},
|
||||
func() *ffcli.Command {
|
||||
cmd := &ffcli.Command{
|
||||
Name: "ping",
|
||||
FlagSet: newFlagSet("prog ping", flag.ContinueOnError, func(fs *flag.FlagSet) {
|
||||
fs.String("until", "", "when pinging should end")
|
||||
ffcomplete.Flag(fs, "until", ffcomplete.Fixed("forever", "direct"))
|
||||
}),
|
||||
}
|
||||
ffcomplete.Args(cmd, ffcomplete.Fixed("jupiter", "neptune", "venus"))
|
||||
return cmd
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
args []string
|
||||
showFlags bool
|
||||
showDescs bool
|
||||
wantComp []string
|
||||
wantDir ffcomplete.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
args: []string{"deb"},
|
||||
wantComp: []string{"debug"},
|
||||
},
|
||||
{
|
||||
args: []string{"deb"},
|
||||
showDescs: true,
|
||||
wantComp: []string{"debug\tDebug data"},
|
||||
},
|
||||
{
|
||||
args: []string{"-"},
|
||||
wantComp: []string{"--root-bool", "--root-str", "-v"},
|
||||
},
|
||||
{
|
||||
args: []string{"--"},
|
||||
wantComp: []string{"--root-bool", "--root-str", "--v"},
|
||||
},
|
||||
{
|
||||
args: []string{"-r"},
|
||||
wantComp: []string{"-root-bool", "-root-str"},
|
||||
},
|
||||
{
|
||||
args: []string{"--r"},
|
||||
wantComp: []string{"--root-bool", "--root-str"},
|
||||
},
|
||||
{
|
||||
args: []string{"--root-str=s", "--r"},
|
||||
wantComp: []string{"--root-bool"}, // omits --root-str which is already set
|
||||
},
|
||||
{
|
||||
// '--' disables flag parsing, so we shouldn't suggest flags.
|
||||
args: []string{"--", "--root"},
|
||||
wantComp: nil,
|
||||
},
|
||||
{
|
||||
// '--' is used as the value of '--root-str'.
|
||||
args: []string{"--root-str", "--", "--r"},
|
||||
wantComp: []string{"--root-bool"},
|
||||
},
|
||||
{
|
||||
// '--' here is a flag value, so doesn't disable flag parsing.
|
||||
args: []string{"--root-str", "--", "--root"},
|
||||
wantComp: []string{"--root-bool"},
|
||||
},
|
||||
{
|
||||
// Equivalent to '--root-str=-- -- --r' meaning '--r' is not
|
||||
// a flag because it's preceded by a '--' argument:
|
||||
// https://go.dev/play/p/UCtftQqVhOD.
|
||||
args: []string{"--root-str", "--", "--", "--r"},
|
||||
wantComp: nil,
|
||||
},
|
||||
{
|
||||
args: []string{"--root-bool="},
|
||||
wantComp: []string{"true", "false"},
|
||||
},
|
||||
{
|
||||
args: []string{"--root-bool=t"},
|
||||
wantComp: []string{"true"},
|
||||
},
|
||||
{
|
||||
args: []string{"--root-bool=T"},
|
||||
wantComp: []string{"TRUE"},
|
||||
},
|
||||
{
|
||||
args: []string{"debug", "--de"},
|
||||
wantComp: []string{"--debug-bool"},
|
||||
},
|
||||
{
|
||||
args: []string{"debug", "--enum="},
|
||||
wantComp: []string{"alpha", "beta", "charlie"},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
args: []string{"debug", "--enum=al"},
|
||||
wantComp: []string{"alpha"},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
args: []string{"debug", "--level", ""},
|
||||
wantComp: nil,
|
||||
},
|
||||
{
|
||||
args: []string{"debug", "--enum", "b"},
|
||||
wantComp: []string{"beta"},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
args: []string{"debug", "--enum", "al"},
|
||||
wantComp: []string{"alpha"},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
args: []string{"ping", ""},
|
||||
showFlags: true,
|
||||
wantComp: []string{"--until", "jupiter", "neptune", "venus"},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
args: []string{"ping", ""},
|
||||
showFlags: true,
|
||||
showDescs: true,
|
||||
wantComp: []string{
|
||||
"--until\twhen pinging should end",
|
||||
"jupiter",
|
||||
"neptune",
|
||||
"venus",
|
||||
},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
args: []string{"ping", ""},
|
||||
wantComp: []string{"jupiter", "neptune", "venus"},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
args: []string{"ping", "j"},
|
||||
wantComp: []string{"jupiter"},
|
||||
wantDir: ffcomplete.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
}
|
||||
|
||||
// Run the tests.
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
name := strings.Join(test.args, "␣")
|
||||
if test.showFlags {
|
||||
name += "+flags"
|
||||
}
|
||||
if test.showDescs {
|
||||
name += "+descs"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Capture the binary
|
||||
complete, dir, err := internal.Complete(root, test.args, test.showFlags, test.showDescs)
|
||||
if err != nil {
|
||||
t.Fatalf("completion error: %s", err)
|
||||
}
|
||||
|
||||
// Test the results match our expectation.
|
||||
if test.wantComp != nil {
|
||||
if diff := cmp.Diff(test.wantComp, complete); diff != "" {
|
||||
t.Errorf("unexpected completion directives (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
if test.wantDir != dir {
|
||||
t.Errorf("got shell completion directive %[1]d (%[1]s), want %[2]d (%[2]s)", dir, test.wantDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build go1.19 && !ts_omit_completion && !ts_omit_completion_scripts
|
||||
|
||||
package ffcomplete
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/tempfork/spf13/cobra"
|
||||
)
|
||||
|
||||
func compCmd(fs *flag.FlagSet) string {
|
||||
var s strings.Builder
|
||||
s.WriteString("completion __complete")
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
s.WriteString(" --")
|
||||
s.WriteString(f.Name)
|
||||
s.WriteString("=")
|
||||
s.WriteString(f.Value.String())
|
||||
})
|
||||
s.WriteString(" --")
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func scriptCmds(root *ffcli.Command, usageFunc func(*ffcli.Command) string) []*ffcli.Command {
|
||||
nameForVar := root.Name
|
||||
nameForVar = strings.ReplaceAll(nameForVar, "-", "_")
|
||||
nameForVar = strings.ReplaceAll(nameForVar, ":", "_")
|
||||
|
||||
var (
|
||||
bashFS = newFS("bash", &compOpts{})
|
||||
zshFS = newFS("zsh", &compOpts{})
|
||||
fishFS = newFS("fish", &compOpts{})
|
||||
pwshFS = newFS("powershell", &compOpts{})
|
||||
)
|
||||
|
||||
return []*ffcli.Command{
|
||||
{
|
||||
Name: "bash",
|
||||
ShortHelp: "Generate bash shell completion script",
|
||||
ShortUsage: ". <( " + root.Name + " completion bash )",
|
||||
UsageFunc: usageFunc,
|
||||
FlagSet: bashFS,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return cobra.ScriptBash(os.Stdout, root.Name, compCmd(bashFS), nameForVar)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "zsh",
|
||||
ShortHelp: "Generate zsh shell completion script",
|
||||
ShortUsage: ". <( " + root.Name + " completion zsh )",
|
||||
UsageFunc: usageFunc,
|
||||
FlagSet: zshFS,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return cobra.ScriptZsh(os.Stdout, root.Name, compCmd(zshFS), nameForVar)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "fish",
|
||||
ShortHelp: "Generate fish shell completion script",
|
||||
ShortUsage: root.Name + " completion fish | source",
|
||||
UsageFunc: usageFunc,
|
||||
FlagSet: fishFS,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return cobra.ScriptFish(os.Stdout, root.Name, compCmd(fishFS), nameForVar)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "powershell",
|
||||
ShortHelp: "Generate powershell completion script",
|
||||
ShortUsage: root.Name + " completion powershell | Out-String | Invoke-Expression",
|
||||
UsageFunc: usageFunc,
|
||||
FlagSet: pwshFS,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return cobra.ScriptPowershell(os.Stdout, root.Name, compCmd(pwshFS), nameForVar)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build go1.19 && !ts_omit_completion && ts_omit_completion_scripts
|
||||
|
||||
package ffcomplete
|
||||
|
||||
import "github.com/peterbourgon/ff/v3/ffcli"
|
||||
|
||||
func scriptCmds(root *ffcli.Command, usageFunc func(*ffcli.Command) string) []*ffcli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
})(),
|
||||
}
|
||||
|
||||
@@ -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 <aud>",
|
||||
ShortHelp: "Fetch an OIDC id-token for the Tailscale machine",
|
||||
LongHelp: hidden,
|
||||
Exec: runIDToken,
|
||||
}
|
||||
|
||||
func runIDToken(ctx context.Context, args []string) error {
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("tailscale id-token: works-in-progress require TAILSCALE_USE_WIP_CODE=1 envvar")
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: tailscale id-token <aud>")
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ 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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "HIDDEN: install host routes to other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, hidden+"install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
|
||||
@@ -34,8 +34,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
github.com/miekg/dns from tailscale.com/net/dns/recursive
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli+
|
||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/peterbourgon/ff/v3/internal from github.com/peterbourgon/ff/v3
|
||||
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
||||
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
||||
@@ -78,6 +78,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
@@ -119,6 +121,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
|
||||
Reference in New Issue
Block a user