From 45c8a7131235ef5754f4d78a88406baa4a8fa11e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 3 May 2022 15:42:21 -0400 Subject: [PATCH] internal/tool: implement structured help command 'gopls help cmd...' is redirected to 'gopls cmd... -h'. The tool.Run operation for each subcommand already has logic to show the usage message when it parses a -h flag. Examples: $ gopls help $ gopls help remote $ gopls help remote sessions Fixes golang/go#52598 Change-Id: I5135c6e7b130d839f880d0613534ac08de199bcf Reviewed-on: https://go-review.googlesource.com/c/tools/+/403677 Run-TryBot: Alan Donovan Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Hyang-Ah Hana Kim --- internal/lsp/cmd/cmd.go | 1 + internal/lsp/cmd/info.go | 49 ++++++++++++++++++++++++++++++++ internal/lsp/cmd/subcommands.go | 15 ++++++++++ internal/lsp/cmd/usage/help.hlp | 10 +++++++ internal/lsp/cmd/usage/usage.hlp | 1 + internal/tool/tool.go | 4 +++ 6 files changed, 80 insertions(+) create mode 100644 internal/lsp/cmd/usage/help.hlp diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index 06e1d5f03c..64b0703a02 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -245,6 +245,7 @@ func (app *Application) mainCommands() []tool.Application { &app.Serve, &version{app: app}, &bug{app: app}, + &help{app: app}, &apiJSON{app: app}, &licenses{app: app}, } diff --git a/internal/lsp/cmd/info.go b/internal/lsp/cmd/info.go index 09f453e1a9..8e581a37cb 100644 --- a/internal/lsp/cmd/info.go +++ b/internal/lsp/cmd/info.go @@ -17,8 +17,57 @@ import ( "golang.org/x/tools/internal/lsp/browser" "golang.org/x/tools/internal/lsp/debug" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/tool" ) +// help implements the help command. +type help struct { + app *Application +} + +func (h *help) Name() string { return "help" } +func (h *help) Parent() string { return h.app.Name() } +func (h *help) Usage() string { return "" } +func (h *help) ShortHelp() string { return "print usage information for subcommands" } +func (h *help) DetailedHelp(f *flag.FlagSet) { + fmt.Fprint(f.Output(), ` + +Examples: +$ gopls help # main gopls help message +$ gopls help remote # help on 'remote' command +$ gopls help remote sessions # help on 'remote sessions' subcommand +`) + printFlagDefaults(f) +} + +// Run prints help information about a subcommand. +func (h *help) Run(ctx context.Context, args ...string) error { + find := func(cmds []tool.Application, name string) tool.Application { + for _, cmd := range cmds { + if cmd.Name() == name { + return cmd + } + } + return nil + } + + // Find the subcommand denoted by args (empty => h.app). + var cmd tool.Application = h.app + for i, arg := range args { + cmd = find(getSubcommands(cmd), arg) + if cmd == nil { + return tool.CommandLineErrorf( + "no such subcommand: %s", strings.Join(args[:i+1], " ")) + } + } + + // 'gopls help cmd subcmd' is equivalent to 'gopls cmd subcmd -h'. + // The flag package prints the usage information (defined by tool.Run) + // when it sees the -h flag. + fs := flag.NewFlagSet(cmd.Name(), flag.ExitOnError) + return tool.Run(ctx, fs, h.app, append(args[:len(args):len(args)], "-h")) +} + // version implements the version command. type version struct { JSON bool `flag:"json" help:"outputs in json format."` diff --git a/internal/lsp/cmd/subcommands.go b/internal/lsp/cmd/subcommands.go index deac5c822d..e30c42b85f 100644 --- a/internal/lsp/cmd/subcommands.go +++ b/internal/lsp/cmd/subcommands.go @@ -42,3 +42,18 @@ func (s subcommands) Run(ctx context.Context, args ...string) error { } return tool.CommandLineErrorf("unknown subcommand %v", command) } + +func (s subcommands) Commands() []tool.Application { return s } + +// getSubcommands returns the subcommands of a given Application. +func getSubcommands(a tool.Application) []tool.Application { + // This interface is satisfied both by tool.Applications + // that embed subcommands, and by *cmd.Application. + type hasCommands interface { + Commands() []tool.Application + } + if sub, ok := a.(hasCommands); ok { + return sub.Commands() + } + return nil +} diff --git a/internal/lsp/cmd/usage/help.hlp b/internal/lsp/cmd/usage/help.hlp new file mode 100644 index 0000000000..f0ff44a4d5 --- /dev/null +++ b/internal/lsp/cmd/usage/help.hlp @@ -0,0 +1,10 @@ +print usage information for subcommands + +Usage: + gopls [flags] help + + +Examples: +$ gopls help # main gopls help message +$ gopls help remote # help on 'remote' command +$ gopls help remote sessions # help on 'remote sessions' subcommand diff --git a/internal/lsp/cmd/usage/usage.hlp b/internal/lsp/cmd/usage/usage.hlp index 1d0fb8d4c1..eaa05c5fa3 100644 --- a/internal/lsp/cmd/usage/usage.hlp +++ b/internal/lsp/cmd/usage/usage.hlp @@ -14,6 +14,7 @@ Main serve run a server for Go code using the Language Server Protocol version print the gopls version information bug report a bug in gopls + help print usage information for subcommands api-json print json describing gopls API licenses print licenses of included software diff --git a/internal/tool/tool.go b/internal/tool/tool.go index 526b6b7ae5..ec3a6bb06b 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -92,6 +92,10 @@ func Main(ctx context.Context, app Application, args []string) { if err := Run(ctx, s, app, args); err != nil { fmt.Fprintf(s.Output(), "%s: %v\n", app.Name(), err) if _, printHelp := err.(commandLineError); printHelp { + // TODO(adonovan): refine this. It causes + // any command-line error to result in the full + // usage message, which typically obscures + // the actual error. s.Usage() } os.Exit(2)