2020-07-15 07:56:48 -07:00
|
|
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
2020-07-15 12:48:35 -04:00
|
|
|
// license that can be found in the LICENSE file.
|
2020-07-15 07:56:48 -07:00
|
|
|
|
|
|
|
// Package cli contains the cmd/tailscale CLI code in a package that can be included
|
|
|
|
// in other wrapper binaries such as the Mac and Windows clients.
|
|
|
|
package cli
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-05-04 07:49:29 -07:00
|
|
|
"errors"
|
2020-07-15 07:56:48 -07:00
|
|
|
"flag"
|
2021-03-19 13:09:10 -07:00
|
|
|
"fmt"
|
2021-05-04 07:49:29 -07:00
|
|
|
"io"
|
2020-07-15 07:56:48 -07:00
|
|
|
"log"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"runtime"
|
2021-04-20 09:10:17 -07:00
|
|
|
"strconv"
|
2020-07-15 07:56:48 -07:00
|
|
|
"strings"
|
2021-08-31 15:21:27 -07:00
|
|
|
"sync"
|
2020-07-15 07:56:48 -07:00
|
|
|
"syscall"
|
2021-03-19 13:09:10 -07:00
|
|
|
"text/tabwriter"
|
2020-07-15 07:56:48 -07:00
|
|
|
|
2021-08-19 11:10:27 -07:00
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
2021-03-30 09:21:22 -07:00
|
|
|
"tailscale.com/client/tailscale"
|
2022-04-12 11:57:46 -07:00
|
|
|
"tailscale.com/envknob"
|
2020-07-15 07:56:48 -07:00
|
|
|
"tailscale.com/ipn"
|
|
|
|
"tailscale.com/paths"
|
|
|
|
"tailscale.com/safesocket"
|
2022-02-07 15:56:56 -08:00
|
|
|
"tailscale.com/version/distro"
|
2020-07-15 07:56:48 -07:00
|
|
|
)
|
|
|
|
|
2021-10-27 14:53:46 -07:00
|
|
|
var Stderr io.Writer = os.Stderr
|
|
|
|
var Stdout io.Writer = os.Stdout
|
|
|
|
|
2022-03-16 16:27:57 -07:00
|
|
|
func printf(format string, a ...any) {
|
2021-10-27 14:53:46 -07:00
|
|
|
fmt.Fprintf(Stdout, format, a...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// outln is like fmt.Println in the common case, except when Stdout is
|
|
|
|
// changed (as in js/wasm).
|
|
|
|
//
|
|
|
|
// It's not named println because that looks like the Go built-in
|
|
|
|
// which goes to stderr and formats slightly differently.
|
2022-03-16 16:27:57 -07:00
|
|
|
func outln(a ...any) {
|
2021-10-27 14:53:46 -07:00
|
|
|
fmt.Fprintln(Stdout, a...)
|
|
|
|
}
|
|
|
|
|
2020-07-15 18:56:07 -07:00
|
|
|
// ActLikeCLI reports whether a GUI application should act like the
|
|
|
|
// CLI based on os.Args, GOOS, the context the process is running in
|
|
|
|
// (pty, parent PID), etc.
|
|
|
|
func ActLikeCLI() bool {
|
2021-04-20 09:10:17 -07:00
|
|
|
// This function is only used on macOS.
|
|
|
|
if runtime.GOOS != "darwin" {
|
2020-07-15 18:56:07 -07:00
|
|
|
return false
|
|
|
|
}
|
2021-04-20 09:10:17 -07:00
|
|
|
|
|
|
|
// Escape hatch to let people force running the macOS
|
|
|
|
// GUI Tailscale binary as the CLI.
|
|
|
|
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_BE_CLI")); v {
|
2020-07-15 18:56:07 -07:00
|
|
|
return true
|
|
|
|
}
|
2021-04-20 09:10:17 -07:00
|
|
|
|
|
|
|
// If our parent is launchd, we're definitely not
|
|
|
|
// being run as a CLI.
|
|
|
|
if os.Getppid() == 1 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-05-04 11:00:35 -07:00
|
|
|
// Xcode adds the -NSDocumentRevisionsDebugMode flag on execution.
|
|
|
|
// If present, we are almost certainly being run as a GUI.
|
|
|
|
for _, arg := range os.Args {
|
|
|
|
if arg == "-NSDocumentRevisionsDebugMode" {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-20 09:10:17 -07:00
|
|
|
// Looking at the environment of the GUI Tailscale app (ps eww
|
|
|
|
// $PID), empirically none of these environment variables are
|
|
|
|
// present. But all or some of these should be present with
|
|
|
|
// Terminal.all and bash or zsh.
|
|
|
|
for _, e := range []string{
|
|
|
|
"SHLVL",
|
|
|
|
"TERM",
|
|
|
|
"TERM_PROGRAM",
|
|
|
|
"PS1",
|
|
|
|
} {
|
|
|
|
if os.Getenv(e) != "" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2020-07-15 18:56:07 -07:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-10-27 13:57:05 -07:00
|
|
|
func newFlagSet(name string) *flag.FlagSet {
|
|
|
|
onError := flag.ExitOnError
|
|
|
|
if runtime.GOOS == "js" {
|
|
|
|
onError = flag.ContinueOnError
|
|
|
|
}
|
2021-10-27 14:53:46 -07:00
|
|
|
fs := flag.NewFlagSet(name, onError)
|
|
|
|
fs.SetOutput(Stderr)
|
|
|
|
return fs
|
2021-10-27 13:57:05 -07:00
|
|
|
}
|
|
|
|
|
2022-02-16 17:46:17 -08:00
|
|
|
// CleanUpArgs rewrites command line arguments for simplicity and backwards compatibility.
|
|
|
|
// In particular, it rewrites --authkey to --auth-key.
|
|
|
|
func CleanUpArgs(args []string) []string {
|
|
|
|
out := make([]string, 0, len(args))
|
|
|
|
for _, arg := range args {
|
|
|
|
// Rewrite --authkey to --auth-key, and --authkey=x to --auth-key=x,
|
|
|
|
// and the same for the -authkey variant.
|
|
|
|
switch {
|
|
|
|
case arg == "--authkey", arg == "-authkey":
|
|
|
|
arg = "--auth-key"
|
|
|
|
case strings.HasPrefix(arg, "--authkey="), strings.HasPrefix(arg, "-authkey="):
|
|
|
|
arg = strings.TrimLeft(arg, "-")
|
|
|
|
arg = strings.TrimPrefix(arg, "authkey=")
|
|
|
|
arg = "--auth-key=" + arg
|
|
|
|
}
|
|
|
|
out = append(out, arg)
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
2022-04-29 11:20:11 -07:00
|
|
|
var localClient tailscale.LocalClient
|
|
|
|
|
2020-07-20 14:23:50 -07:00
|
|
|
// Run runs the CLI. The args do not include the binary name.
|
2022-03-09 09:50:38 +02:00
|
|
|
func Run(args []string) (err error) {
|
2022-06-27 14:56:25 -07:00
|
|
|
args = CleanUpArgs(args)
|
|
|
|
|
2020-07-20 20:54:35 -07:00
|
|
|
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
|
|
|
args = []string{"version"}
|
|
|
|
}
|
|
|
|
|
2021-08-31 15:21:27 -07:00
|
|
|
var warnOnce sync.Once
|
|
|
|
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
|
|
|
|
warnOnce.Do(func() {
|
2021-10-27 14:53:46 -07:00
|
|
|
fmt.Fprintf(Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
|
2021-08-31 15:21:27 -07:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2021-10-27 13:57:05 -07:00
|
|
|
rootfs := newFlagSet("tailscale")
|
2020-07-15 07:56:48 -07:00
|
|
|
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
|
|
|
|
|
|
|
|
rootCmd := &ffcli.Command{
|
|
|
|
Name: "tailscale",
|
2021-03-09 12:04:12 -08:00
|
|
|
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
|
2020-07-15 07:56:48 -07:00
|
|
|
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
|
|
|
LongHelp: strings.TrimSpace(`
|
2021-03-19 13:09:10 -07:00
|
|
|
For help on subcommands, add --help after: "tailscale status --help".
|
2021-03-09 12:04:12 -08:00
|
|
|
|
2020-07-15 07:56:48 -07:00
|
|
|
This CLI is still under active development. Commands and flags will
|
|
|
|
change in the future.
|
|
|
|
`),
|
|
|
|
Subcommands: []*ffcli.Command{
|
|
|
|
upCmd,
|
2020-08-10 19:42:04 -07:00
|
|
|
downCmd,
|
2022-10-25 18:02:58 -07:00
|
|
|
setCmd,
|
2021-04-07 21:06:31 -07:00
|
|
|
logoutCmd,
|
2020-07-15 07:56:48 -07:00
|
|
|
netcheckCmd,
|
2021-03-24 09:24:25 -07:00
|
|
|
ipCmd,
|
2020-07-15 07:56:48 -07:00
|
|
|
statusCmd,
|
2020-08-09 14:49:42 -07:00
|
|
|
pingCmd,
|
2022-03-24 09:04:01 -07:00
|
|
|
ncCmd,
|
2022-03-24 12:22:36 -07:00
|
|
|
sshCmd,
|
2020-07-20 20:54:35 -07:00
|
|
|
versionCmd,
|
2021-03-25 08:21:31 -07:00
|
|
|
webCmd,
|
2021-04-27 13:52:26 -07:00
|
|
|
fileCmd,
|
2021-03-30 15:59:44 -07:00
|
|
|
bugReportCmd,
|
2021-08-17 15:03:28 -07:00
|
|
|
certCmd,
|
2022-08-11 10:43:09 -07:00
|
|
|
netlockCmd,
|
2022-08-25 16:39:14 -07:00
|
|
|
licensesCmd,
|
2020-07-15 07:56:48 -07:00
|
|
|
},
|
2021-03-19 13:09:10 -07:00
|
|
|
FlagSet: rootfs,
|
|
|
|
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
|
|
|
UsageFunc: usageFunc,
|
|
|
|
}
|
|
|
|
for _, c := range rootCmd.Subcommands {
|
2022-10-25 18:02:58 -07:00
|
|
|
if c.UsageFunc == nil {
|
|
|
|
c.UsageFunc = usageFunc
|
|
|
|
}
|
2020-07-15 07:56:48 -07:00
|
|
|
}
|
2022-04-12 11:57:46 -07:00
|
|
|
if envknob.UseWIPCode() {
|
|
|
|
rootCmd.Subcommands = append(rootCmd.Subcommands, idTokenCmd)
|
|
|
|
}
|
2020-07-15 07:56:48 -07:00
|
|
|
|
2021-03-05 12:08:20 -08:00
|
|
|
// Don't advertise the debug command, but it exists.
|
|
|
|
if strSliceContains(args, "debug") {
|
|
|
|
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
|
|
|
}
|
2022-02-07 15:56:56 -08:00
|
|
|
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
|
|
|
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
|
|
|
}
|
2021-03-05 12:08:20 -08:00
|
|
|
|
2020-07-15 07:56:48 -07:00
|
|
|
if err := rootCmd.Parse(args); err != nil {
|
2021-10-27 14:06:22 -07:00
|
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
|
|
return nil
|
|
|
|
}
|
2020-07-15 07:56:48 -07:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-04-29 11:20:11 -07:00
|
|
|
localClient.Socket = rootArgs.socket
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 13:55:55 -08:00
|
|
|
rootfs.Visit(func(f *flag.Flag) {
|
|
|
|
if f.Name == "socket" {
|
2022-04-29 11:20:11 -07:00
|
|
|
localClient.UseSocketOnly = true
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 13:55:55 -08:00
|
|
|
}
|
|
|
|
})
|
2021-03-30 09:21:22 -07:00
|
|
|
|
2022-03-09 09:50:38 +02:00
|
|
|
err = rootCmd.Run(context.Background())
|
2022-01-25 09:58:21 -08:00
|
|
|
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
|
|
|
|
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
|
|
|
|
}
|
2021-10-27 14:06:22 -07:00
|
|
|
if errors.Is(err, flag.ErrHelp) {
|
2020-07-15 07:56:48 -07:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-03-16 16:27:57 -07:00
|
|
|
func fatalf(format string, a ...any) {
|
2021-10-27 15:18:48 -07:00
|
|
|
if Fatalf != nil {
|
|
|
|
Fatalf(format, a...)
|
|
|
|
return
|
|
|
|
}
|
2020-08-10 08:10:15 -07:00
|
|
|
log.SetFlags(0)
|
|
|
|
log.Fatalf(format, a...)
|
|
|
|
}
|
|
|
|
|
2021-10-27 15:18:48 -07:00
|
|
|
// Fatalf, if non-nil, is used instead of log.Fatalf.
|
2022-03-16 16:27:57 -07:00
|
|
|
var Fatalf func(format string, a ...any)
|
2021-10-27 15:18:48 -07:00
|
|
|
|
2020-07-15 07:56:48 -07:00
|
|
|
var rootArgs struct {
|
|
|
|
socket string
|
|
|
|
}
|
|
|
|
|
|
|
|
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
safesocket: add ConnectionStrategy, provide control over fallbacks
fee2d9fad added support for cmd/tailscale to connect to IPNExtension.
It came in two parts: If no socket was provided, dial IPNExtension first,
and also, if dialing the socket failed, fall back to IPNExtension.
The second half of that support caused the integration tests to fail
when run on a machine that was also running IPNExtension.
The integration tests want to wait until the tailscaled instances
that they spun up are listening. They do that by dialing the new
instance. But when that dial failed, it was falling back to IPNExtension,
so it appeared (incorrectly) that tailscaled was running.
Hilarity predictably ensued.
If a user (or a test) explicitly provides a socket to dial,
it is a reasonable assumption that they have a specific tailscaled
in mind and don't want to fall back to IPNExtension.
It is certainly true of the integration tests.
Instead of adding a bool to Connect, split out the notion of a
connection strategy. For now, the implementation remains the same,
but with the details hidden a bit. Later, we can improve that.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-12-08 13:55:55 -08:00
|
|
|
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
|
|
|
c, err := safesocket.Connect(s)
|
2020-07-15 07:56:48 -07:00
|
|
|
if err != nil {
|
|
|
|
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
2020-08-10 08:10:15 -07:00
|
|
|
fatalf("--socket cannot be empty")
|
2020-07-15 07:56:48 -07:00
|
|
|
}
|
2020-12-25 15:32:37 +00:00
|
|
|
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
2020-07-15 07:56:48 -07:00
|
|
|
}
|
|
|
|
clientToServer := func(b []byte) {
|
|
|
|
ipn.WriteMsg(c, b)
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
interrupt := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
2021-04-13 08:34:37 -07:00
|
|
|
select {
|
|
|
|
case <-interrupt:
|
|
|
|
case <-ctx.Done():
|
|
|
|
// Context canceled elsewhere.
|
|
|
|
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
return
|
|
|
|
}
|
2020-07-15 07:56:48 -07:00
|
|
|
c.Close()
|
|
|
|
cancel()
|
|
|
|
}()
|
|
|
|
|
|
|
|
bc := ipn.NewBackendClient(log.Printf, clientToServer)
|
|
|
|
return c, bc, ctx, cancel
|
|
|
|
}
|
|
|
|
|
|
|
|
// pump receives backend messages on conn and pushes them into bc.
|
2021-05-04 07:49:29 -07:00
|
|
|
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
|
2020-07-15 07:56:48 -07:00
|
|
|
defer conn.Close()
|
|
|
|
for ctx.Err() == nil {
|
|
|
|
msg, err := ipn.ReadMsg(conn)
|
|
|
|
if err != nil {
|
|
|
|
if ctx.Err() != nil {
|
2021-05-04 07:49:29 -07:00
|
|
|
return ctx.Err()
|
2020-07-15 07:56:48 -07:00
|
|
|
}
|
2021-05-04 07:49:29 -07:00
|
|
|
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
|
|
|
return fmt.Errorf("%w (tailscaled stopped running?)", err)
|
2021-04-16 08:00:31 -07:00
|
|
|
}
|
2021-05-04 07:49:29 -07:00
|
|
|
return err
|
2020-07-15 07:56:48 -07:00
|
|
|
}
|
|
|
|
bc.GotNotifyMsg(msg)
|
|
|
|
}
|
2021-05-04 07:49:29 -07:00
|
|
|
return ctx.Err()
|
2020-07-15 07:56:48 -07:00
|
|
|
}
|
2021-03-05 12:08:20 -08:00
|
|
|
|
|
|
|
func strSliceContains(ss []string, s string) bool {
|
|
|
|
for _, v := range ss {
|
|
|
|
if v == s {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2021-03-19 13:09:10 -07:00
|
|
|
|
2022-10-25 18:02:58 -07:00
|
|
|
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
|
|
|
|
func usageFuncNoDefaultValues(c *ffcli.Command) string {
|
2022-10-30 14:54:02 -07:00
|
|
|
return usageFuncOpt(c, false)
|
2022-10-25 18:02:58 -07:00
|
|
|
}
|
|
|
|
|
2021-03-19 13:09:10 -07:00
|
|
|
func usageFunc(c *ffcli.Command) string {
|
2022-10-30 14:54:02 -07:00
|
|
|
return usageFuncOpt(c, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
|
2021-03-19 13:09:10 -07:00
|
|
|
var b strings.Builder
|
|
|
|
|
|
|
|
fmt.Fprintf(&b, "USAGE\n")
|
|
|
|
if c.ShortUsage != "" {
|
|
|
|
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
|
|
|
|
} else {
|
|
|
|
fmt.Fprintf(&b, " %s\n", c.Name)
|
|
|
|
}
|
|
|
|
fmt.Fprintf(&b, "\n")
|
|
|
|
|
|
|
|
if c.LongHelp != "" {
|
|
|
|
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(c.Subcommands) > 0 {
|
|
|
|
fmt.Fprintf(&b, "SUBCOMMANDS\n")
|
|
|
|
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
|
|
|
for _, subcommand := range c.Subcommands {
|
|
|
|
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
|
|
|
|
}
|
|
|
|
tw.Flush()
|
|
|
|
fmt.Fprintf(&b, "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
if countFlags(c.FlagSet) > 0 {
|
|
|
|
fmt.Fprintf(&b, "FLAGS\n")
|
|
|
|
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
|
|
|
c.FlagSet.VisitAll(func(f *flag.Flag) {
|
|
|
|
var s string
|
|
|
|
name, usage := flag.UnquoteUsage(f)
|
|
|
|
if isBoolFlag(f) {
|
|
|
|
s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name)
|
|
|
|
} else {
|
|
|
|
s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments.
|
|
|
|
if len(name) > 0 {
|
|
|
|
s += " " + name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Four spaces before the tab triggers good alignment
|
|
|
|
// for both 4- and 8-space tab stops.
|
|
|
|
s += "\n \t"
|
|
|
|
s += strings.ReplaceAll(usage, "\n", "\n \t")
|
|
|
|
|
2022-10-30 14:54:02 -07:00
|
|
|
if f.DefValue != "" && withDefaults {
|
2021-03-19 13:09:10 -07:00
|
|
|
s += fmt.Sprintf(" (default %s)", f.DefValue)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintln(&b, s)
|
|
|
|
})
|
|
|
|
tw.Flush()
|
|
|
|
fmt.Fprintf(&b, "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.TrimSpace(b.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
func isBoolFlag(f *flag.Flag) bool {
|
|
|
|
bf, ok := f.Value.(interface {
|
|
|
|
IsBoolFlag() bool
|
|
|
|
})
|
|
|
|
return ok && bf.IsBoolFlag()
|
|
|
|
}
|
|
|
|
|
|
|
|
func countFlags(fs *flag.FlagSet) (n int) {
|
|
|
|
fs.VisitAll(func(*flag.Flag) { n++ })
|
|
|
|
return n
|
|
|
|
}
|