cmd/syncthing: Refactor cli subcommand to allow flags (#7454)

This commit is contained in:
Simon Frei 2021-03-17 09:04:50 +01:00 committed by GitHub
parent 1814f4693d
commit 40fbdc87ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 73 additions and 37 deletions

View File

@ -10,33 +10,39 @@ import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"log" "io/ioutil"
"os" "os"
"reflect" "reflect"
"strings"
"github.com/AudriusButkevicius/recli" "github.com/AudriusButkevicius/recli"
"github.com/alecthomas/kong"
"github.com/flynn-archive/go-shlex" "github.com/flynn-archive/go-shlex"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
type CLI struct { type preCli struct {
GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"` GUIAddress string `name:"gui-address"`
GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"` GUIAPIKey string `name:"gui-apikey"`
HomeDir string `name:"home" placeholder:"PATH" help:"Set configuration and data directory"` HomeDir string `name:"home"`
ConfDir string `name:"conf" placeholder:"PATH" help:"Set configuration directory (config and keys)"` ConfDir string `name:"conf"`
Args []string `arg:"" optional:""`
} }
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. // Not set as default above because the strings can be really long.
var err error var err error
homeSet := c.HomeDir != "" homeSet := c.HomeDir != ""
@ -50,8 +56,7 @@ func (c *CLI) Run() error {
err = locations.SetBaseDir(locations.ConfigBaseDir, c.ConfDir) err = locations.SetBaseDir(locations.ConfigBaseDir, c.ConfDir)
} }
if err != nil { if err != nil {
log.Println("Command line options:", err) return errors.Wrap(err, "Command line options:")
os.Exit(svcutil.ExitError.AsInt())
} }
guiCfg := config.GUIConfiguration{ guiCfg := config.GUIConfiguration{
RawAddress: c.GUIAddress, RawAddress: c.GUIAddress,
@ -136,15 +141,15 @@ func (c *CLI) Run() error {
// Construct the actual CLI // Construct the actual CLI
app := cli.NewApp() app := cli.NewApp()
app.Name = "syncthing cli"
app.HelpName = app.Name
app.Author = "The Syncthing Authors" app.Author = "The Syncthing Authors"
app.Usage = "Syncthing command line interface"
app.Flags = fakeFlags
app.Metadata = map[string]interface{}{ app.Metadata = map[string]interface{}{
"client": client, "client": client,
} }
app.Commands = []cli.Command{ app.Commands = []cli.Command{{
Name: "cli",
Usage: "Syncthing command line interface",
Flags: fakeFlags,
Subcommands: []cli.Command{
{ {
Name: "config", Name: "config",
HideHelp: true, HideHelp: true,
@ -154,10 +159,8 @@ func (c *CLI) Run() error {
showCommand, showCommand,
operationCommand, operationCommand,
errorsCommand, 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()) tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
if !tty { if !tty {
@ -171,7 +174,7 @@ func (c *CLI) Run() error {
if len(input) == 0 { if len(input) == 0 {
continue continue
} }
err = app.Run(append(c.Args, input...)) err = app.Run(append(os.Args, input...))
if err != nil { if err != nil {
return err return err
} }
@ -181,7 +184,7 @@ func (c *CLI) Run() error {
return err return err
} }
} else { } else {
err = app.Run(c.Args) err = app.Run(os.Args)
if err != nil { if err != nil {
return err return err
} }
@ -206,3 +209,28 @@ func (c *CLI) Run() error {
} }
return nil 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
}

View File

@ -131,10 +131,11 @@ var (
// The entrypoint struct is the main entry point for the command line parser. The // The entrypoint struct is the main entry point for the command line parser. The
// commands and options here are top level commands to syncthing. // commands and options here are top level commands to syncthing.
// Cli is just a placeholder for the help text (see main).
var entrypoint struct { var entrypoint struct {
Serve serveOptions `cmd:"" help:"Run Syncthing"` Serve serveOptions `cmd:"" help:"Run Syncthing"`
Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"` 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. // serveOptions are the options for the `syncthing serve` command.
@ -211,6 +212,17 @@ func defaultVars() kong.Vars {
} }
func main() { 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. // First some massaging of the raw command line to fit the new model.
// Basically this means adding the default command at the front, and // Basically this means adding the default command at the front, and
// converting -options to --options. // converting -options to --options.
@ -248,10 +260,6 @@ func main() {
} }
func helpHandler(options kong.HelpOptions, ctx *kong.Context) error { 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 { if err := kong.DefaultHelpPrinter(options, ctx); err != nil {
return err return err
} }