The gopls Command-Line Interface (CLI) enforces strict flag ordering constraints which adds a significant usability pain point. For example,
-
Confusing Flag Application: Subcommands accept global flags that have no effect or are irrelevant. For example,
gopls -remote=auto version silently ignores -remote.
gopls remote debug -remote=.. or gopls remote -remote=.. debug are invalid which is unintuitive.
-
Flag Collision & Silent Flag Ignoring: Running gopls -listen=:8000 mcp silently ignores -listen because the global flag configures the root LSP server but gets ignored, and the mcp subcommand runs over stdio because its subcommand-specific -listen flag is not set. gopls -listen=:8000 mcp -listen=:8001 will start only MCP server on 8001.
-
Strict Ordering Constraints with Confusing Error Message: Placing a global flag (like -profile.cpu or -v) after the subcommand is invalid, but the error message is misleading. For example, gopls symbols -profile.cpu=cpu.out main.go or gopls version -v fails with flag provided but not defined: -profile.cpu or flag provided but not defined: -v, even though they are valid global flags.
As part of the gopls CLI redesign work, I propose to revise the flag parsing rule to better align with the official Go command’s flag parsing style, and make it more predictable.
- Separating global flags from subcommand flags.
- Aligning flag behavior with the
go command style.
- Maintaining compatibility with existing editor integrations.
Background
Initially, gopls only operated as an LSP server. Developers added subcommands like symbols, rename, mcp, and remote later. To maintain compatibility, running gopls without a subcommand must still start the server.
To support this default behavior, the root Application struct (defined in ./gopls/internal/cmd) embeds the Serve subcommand struct as an exported field. This design exposes all Serve flags globally.
Root Flag Pollution (Current Implementation)
The following table lists flags exposed at the root level in the current implementation:
| Flag |
Type |
Description |
Intended Scope |
-v, -verbose |
Global |
Verbose output |
Global |
-vv, -veryverbose |
Global |
Very verbose output |
Global |
-otel |
Global |
OpenTelemetry endpoint |
Global |
-profile.cpu |
Global (Profiling) |
Write CPU profile to file |
Global |
-profile.mem |
Global (Profiling) |
Write memory profile to file |
Global |
-profile.trace |
Global (Profiling) |
Write execution trace to file |
Global |
-remote |
Connection |
Forward commands to remote LSP |
Client Subcommands / Serve |
-remote.debug |
Connection |
-debug for auto-launched daemon |
Client Subcommands / Serve |
-remote.logfile |
Connection |
-logfile for auto-launched daemon |
Client Subcommands / Serve |
-remote.listen.timeout |
Connection |
timeout for auto-launched daemon |
Client Subcommands / Serve |
-listen |
Serve |
Address to listen on for LSP |
Serve |
-listen.timeout |
Serve |
Shut down server if idle |
Serve |
-debug |
Serve |
Serve debug info |
Serve |
-logfile |
Serve |
Log filename |
Serve |
-mode |
Serve |
Server mode (no effect, TODO: deprecate) |
Serve |
-rpc.trace |
Serve |
Print RPC trace |
Serve |
-mcp.listen |
Serve |
Listen for MCP connections |
Serve |
Proposed Change
We refactor flag registration and introduce an argument pre-processor to handle the case where gopls is invoked without serve subcommand, or where the serve subcommand is used but the old style of flag ordering is used. This normalization runs before flag parsing.
Flag Localization
- Unexport
serve flags: Rename the exported Serve field in the Application struct to serve. This unexports the field, which removes the serve flags from the global scope and resolves the -listen collision.
- Isolate connection flags: Define a
RemoteFlags struct for -remote and -remote.* flags. Embed this struct only in the serve subcommand and in subcommands that connect to an LSP server.
Normalization for Compatibility
Many editor plugins (for example, VS Code Go, Neovim's nvim-lspconfig, Vim's vim-lsc, and Emacs' lsp-mode) configure gopls without any arguments. They often use -remote=auto to enable daemon mode. For debugging, users frequently append -rpc.trace and -logfile.
Because these editors start gopls without a subcommand, they rely on the default serve behavior and expect these flags to be globally available. The new design must preserve this capability to avoid breaking existing integrations.
Before passing arguments to the standard flag parser, we normalize the args/flags and handle two cases:
-
Case 1: Explicit Subcommand (Go tool style): Enforce Go-style command ordering. Global flags must precede the subcommand, and subcommand flags must follow it. NormalizeArgs validates the placement of all flags.
-
Case 2: Implicit serve (Hoisting): If the command line lacks a subcommand (for example, gopls -listen=localhost:3000 -profile.cpu=cpu.out), the pre-processor separates global flags from serve flags using reflection. It moves global flags to the front, inserts the serve subcommand, and appends subcommand flags to the end:
- Before:
gopls -listen=localhost:3000 -profile.cpu=cpu.out
- After:
gopls -profile.cpu=cpu.out serve -listen=localhost:3000
This automatically preserves compatibility with existing editor integrations.
Helpful Error Messages
When validation fails in Case 1, NormalizeArgs rejects the command with a clear, actionable error message rather than the default Go flag error:
-
Misplaced Global Flag: Placing a global flag after a subcommand (for example, gopls symbols -profile.cpu=cpu.out) returns:
global flag -profile.cpu=cpu.out must be placed before subcommand symbols
-
Misplaced Subcommand Flag: Placing a subcommand flag before the subcommand (for example, gopls -remote=auto symbols) returns: flag -remote=auto belongs to subcommand but is placed before it )
Alternative Considered
- Allow flags to be specified anywhere: not great. Not go style.
- Define all global flags in the last subcommands (using struct embedding): backwards compatible change is hard.
The
goplsCommand-Line Interface (CLI) enforces strict flag ordering constraints which adds a significant usability pain point. For example,Confusing Flag Application: Subcommands accept global flags that have no effect or are irrelevant. For example,
gopls -remote=auto versionsilently ignores-remote.gopls remote debug -remote=..orgopls remote -remote=.. debugare invalid which is unintuitive.Flag Collision & Silent Flag Ignoring: Running
gopls -listen=:8000 mcpsilently ignores-listenbecause the global flag configures the root LSP server but gets ignored, and themcpsubcommand runs over stdio because its subcommand-specific-listenflag is not set.gopls -listen=:8000 mcp -listen=:8001will start only MCP server on 8001.Strict Ordering Constraints with Confusing Error Message: Placing a global flag (like
-profile.cpuor-v) after the subcommand is invalid, but the error message is misleading. For example,gopls symbols -profile.cpu=cpu.out main.goorgopls version -vfails withflag provided but not defined: -profile.cpuorflag provided but not defined: -v, even though they are valid global flags.As part of the gopls CLI redesign work, I propose to revise the flag parsing rule to better align with the official Go command’s flag parsing style, and make it more predictable.
gocommand style.Background
Initially,
goplsonly operated as an LSP server. Developers added subcommands likesymbols,rename,mcp, andremotelater. To maintain compatibility, runninggoplswithout a subcommand must still start the server.To support this default behavior, the root
Applicationstruct (defined in./gopls/internal/cmd) embeds theServesubcommand struct as an exported field. This design exposes allServeflags globally.Root Flag Pollution (Current Implementation)
The following table lists flags exposed at the root level in the current implementation:
-v,-verbose-vv,-veryverbose-otel-profile.cpu-profile.mem-profile.trace-remote-remote.debug-debugfor auto-launched daemon-remote.logfile-logfilefor auto-launched daemon-remote.listen.timeout-listen-listen.timeout-debug-logfile-mode-rpc.trace-mcp.listenProposed Change
We refactor flag registration and introduce an argument pre-processor to handle the case where
goplsis invoked withoutservesubcommand, or where theservesubcommand is used but the old style of flag ordering is used. This normalization runs before flag parsing.Flag Localization
serveflags: Rename the exportedServefield in theApplicationstruct toserve. This unexports the field, which removes theserveflags from the global scope and resolves the-listencollision.RemoteFlagsstruct for-remoteand-remote.*flags. Embed this struct only in theservesubcommand and in subcommands that connect to an LSP server.Normalization for Compatibility
Many editor plugins (for example, VS Code Go, Neovim's
nvim-lspconfig, Vim'svim-lsc, and Emacs'lsp-mode) configuregoplswithout any arguments. They often use-remote=autoto enable daemon mode. For debugging, users frequently append-rpc.traceand-logfile.Because these editors start
goplswithout a subcommand, they rely on the defaultservebehavior and expect these flags to be globally available. The new design must preserve this capability to avoid breaking existing integrations.Before passing arguments to the standard flag parser, we normalize the args/flags and handle two cases:
Case 1: Explicit Subcommand (Go tool style): Enforce Go-style command ordering. Global flags must precede the subcommand, and subcommand flags must follow it.
NormalizeArgsvalidates the placement of all flags.Case 2: Implicit
serve(Hoisting): If the command line lacks a subcommand (for example,gopls -listen=localhost:3000 -profile.cpu=cpu.out), the pre-processor separates global flags fromserveflags using reflection. It moves global flags to the front, inserts theservesubcommand, and appends subcommand flags to the end:gopls -listen=localhost:3000 -profile.cpu=cpu.outgopls -profile.cpu=cpu.out serve -listen=localhost:3000This automatically preserves compatibility with existing editor integrations.
Helpful Error Messages
When validation fails in Case 1,
NormalizeArgsrejects the command with a clear, actionable error message rather than the default Goflagerror:Misplaced Global Flag: Placing a global flag after a subcommand (for example,
gopls symbols -profile.cpu=cpu.out) returns:global flag -profile.cpu=cpu.out must be placed before subcommand symbolsMisplaced Subcommand Flag: Placing a subcommand flag before the subcommand (for example,
gopls -remote=auto symbols) returns:flag -remote=auto belongs to subcommand but is placed before it)Alternative Considered