From 40fbdc87ce985e2edb349b616a361cc828ec99e1 Mon Sep 17 00:00:00 2001 From: Simon Frei Date: Wed, 17 Mar 2021 09:04:50 +0100 Subject: [PATCH] cmd/syncthing: Refactor cli subcommand to allow flags (#7454) --- cmd/syncthing/cli/main.go | 92 +++++++++++++++++++++++++-------------- cmd/syncthing/main.go | 18 +++++--- 2 files changed, 73 insertions(+), 37 deletions(-) diff --git a/cmd/syncthing/cli/main.go b/cmd/syncthing/cli/main.go index 22e8d0003..f7f172991 100644 --- a/cmd/syncthing/cli/main.go +++ b/cmd/syncthing/cli/main.go @@ -10,33 +10,39 @@ import ( "bufio" "crypto/tls" "encoding/json" - "log" + "io/ioutil" "os" "reflect" + "strings" "github.com/AudriusButkevicius/recli" + "github.com/alecthomas/kong" "github.com/flynn-archive/go-shlex" "github.com/mattn/go-isatty" "github.com/pkg/errors" - "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/protocol" - "github.com/syncthing/syncthing/lib/svcutil" - "github.com/urfave/cli" ) -type CLI struct { - GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"` - GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"` - HomeDir string `name:"home" placeholder:"PATH" help:"Set configuration and data directory"` - ConfDir string `name:"conf" placeholder:"PATH" help:"Set configuration directory (config and keys)"` - Args []string `arg:"" optional:""` +type preCli struct { + GUIAddress string `name:"gui-address"` + GUIAPIKey string `name:"gui-apikey"` + HomeDir string `name:"home"` + ConfDir string `name:"conf"` } -func (c *CLI) Run() error { +func Run() error { + // This is somewhat a hack around a chicken and egg problem. We need to set + // the home directory and potentially other flags to know where the + // syncthing instance is running in order to get it's config ... which we + // then use to construct the actual CLI ... at which point it's too late to + // add flags there... + c := preCli{} + parseFlags(&c) + // Not set as default above because the strings can be really long. var err error homeSet := c.HomeDir != "" @@ -50,8 +56,7 @@ func (c *CLI) Run() error { err = locations.SetBaseDir(locations.ConfigBaseDir, c.ConfDir) } if err != nil { - log.Println("Command line options:", err) - os.Exit(svcutil.ExitError.AsInt()) + return errors.Wrap(err, "Command line options:") } guiCfg := config.GUIConfiguration{ RawAddress: c.GUIAddress, @@ -136,28 +141,26 @@ func (c *CLI) Run() error { // Construct the actual CLI app := cli.NewApp() - app.Name = "syncthing cli" - app.HelpName = app.Name app.Author = "The Syncthing Authors" - app.Usage = "Syncthing command line interface" - app.Flags = fakeFlags app.Metadata = map[string]interface{}{ "client": client, } - app.Commands = []cli.Command{ - { - Name: "config", - HideHelp: true, - Usage: "Configuration modification command group", - Subcommands: commands, + app.Commands = []cli.Command{{ + Name: "cli", + Usage: "Syncthing command line interface", + Flags: fakeFlags, + Subcommands: []cli.Command{ + { + Name: "config", + HideHelp: true, + Usage: "Configuration modification command group", + Subcommands: commands, + }, + showCommand, + operationCommand, + errorsCommand, }, - showCommand, - operationCommand, - errorsCommand, - } - - // It expects to be give os.Args which has argv[0] set to executable name, so fake it. - c.Args = append([]string{"cli"}, c.Args...) + }} tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) if !tty { @@ -171,7 +174,7 @@ func (c *CLI) Run() error { if len(input) == 0 { continue } - err = app.Run(append(c.Args, input...)) + err = app.Run(append(os.Args, input...)) if err != nil { return err } @@ -181,7 +184,7 @@ func (c *CLI) Run() error { return err } } else { - err = app.Run(c.Args) + err = app.Run(os.Args) if err != nil { return err } @@ -206,3 +209,28 @@ func (c *CLI) Run() error { } return nil } + +func parseFlags(c *preCli) error { + // kong only needs to parse the global arguments after "cli" and before the + // subcommand (if any). + if len(os.Args) <= 2 { + return nil + } + args := os.Args[2:] + for i := 0; i < len(args); i++ { + if !strings.HasPrefix(args[i], "--") { + args = args[:i] + break + } + if !strings.Contains(args[i], "=") { + i++ + } + } + // We don't want kong to print anything nor os.Exit (e.g. on -h) + parser, err := kong.New(c, kong.Writers(ioutil.Discard, ioutil.Discard), kong.Exit(func(int) {})) + if err != nil { + return err + } + _, err = parser.Parse(args) + return err +} diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index d29ab2723..a8fbf0ace 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -131,10 +131,11 @@ var ( // The entrypoint struct is the main entry point for the command line parser. The // commands and options here are top level commands to syncthing. +// Cli is just a placeholder for the help text (see main). var entrypoint struct { Serve serveOptions `cmd:"" help:"Run Syncthing"` Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"` - Cli cli.CLI `cmd:"" help:"Command line interface for Syncthing"` + Cli struct{} `cmd:"" help:"Command line interface for Syncthing"` } // serveOptions are the options for the `syncthing serve` command. @@ -211,6 +212,17 @@ func defaultVars() kong.Vars { } func main() { + // The "cli" subcommand uses a different command line parser, and e.g. help + // gets mangled when integrating it as a subcommand -> detect it here at the + // beginning. + if os.Args[1] == "cli" { + if err := cli.Run(); err != nil { + fmt.Println(err) + os.Exit(1) + } + return + } + // First some massaging of the raw command line to fit the new model. // Basically this means adding the default command at the front, and // converting -options to --options. @@ -248,10 +260,6 @@ func main() { } func helpHandler(options kong.HelpOptions, ctx *kong.Context) error { - // If we're looking for CLI help, pass the arguments down to the CLI library to print it's own help. - if ctx.Command() == "cli" { - return ctx.Run() - } if err := kong.DefaultHelpPrinter(options, ctx); err != nil { return err }