Skip to content

cli: explain misplaced flags before the subcommand - #1453

Open
ekanshul wants to merge 2 commits into
benbjohnson:mainfrom
ekanshul:fix-1405
Open

cli: explain misplaced flags before the subcommand#1453
ekanshul wants to merge 2 commits into
benbjohnson:mainfrom
ekanshul:fix-1405

Conversation

@ekanshul

@ekanshul ekanshul commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Replace the bare usage screen printed for litestream -config c.yml databases with a targeted error that says flags go after the subcommand, and points -config users at LITESTREAM_CONFIG.

Problem

Main.Run() takes args[0] as the subcommand before any flag parser runs. A leading flag falls into the m.Usage(); return flag.ErrHelp branch, so the user gets the full command list on stdout, exit 1, and nothing on stderr. Users read that as "the flag does not exist" (#1405, #1400).

Before (origin/main 63225f1):

$ litestream -config /nonexistent.yml databases

litestream is a tool for replicating SQLite databases.

Usage:

	litestream <command> [arguments]
... (full command list) ...
$ echo $?
1

After:

$ litestream -config /nonexistent.yml databases
Error: flags must come after the subcommand
Try: litestream <command> -config PATH
 or: LITESTREAM_CONFIG=PATH litestream <command>
$ echo $?
1

Any other leading flag gets the neutral form:

$ litestream -socket status info
Error: flags must come after the subcommand
Try: litestream <command> [flags]

Solution

In Run(), a first token that starts with - and is not -h/-help/--help now returns a usageError (the existing Error: / Try: mechanism from #1297) instead of printing usage. misplacedFlagError looks only at that token: if it names config it adds an or: line using LITESTREAM_CONFIG, which is what solves the shell-alias case in #1400; otherwise the hint is a fixed form.

The hint deliberately does not try to reconstruct the user's command. Run() is not the flag parser, so it cannot tell a command from a flag value: in litestream -socket status info, status may be the value of -socket and info the intended command. An earlier revision of this PR scanned for a known command name and reordered the arguments, which reversed that case and needed its own copy of the command list. Empty args, -h/-help/--help, help, unknown commands, and flags after the subcommand behave exactly as before.

Scope

In scope:

  • cmd/litestream/main.go: split the existing branch in Run(), plus misplacedFlagError
  • Unit test in main_test.go and subprocess CLI tests in main_cli_test.go

Not in scope:

  • Accepting flags before the subcommand. Making -config a real root flag with a top-level flag.FlagSet is a separate change.
  • Documenting LITESTREAM_CONFIG more prominently on the website (separate repo)

Test Plan

gofmt -l ./cmd/litestream
go vet ./cmd/litestream/...
go test -race -count=1 -run 'TestMainFlagPlacement|TestMain_RunHelp' ./cmd/litestream/... -v
go test -race -count=1 ./cmd/litestream/...

TestMainFlagPlacement covers -config PATH, --config=PATH, another leading flag, -h/-help/--help, no arguments, and a correctly positioned flag. All pass locally (macOS, Go 1.26; staticcheck and goimports clean, uvx pre-commit run --all-files clean).

Related

This change was made with AI assistance (Claude Code) and reviewed by the author.

Main.Run consumed args[0] as the subcommand before any flag parser ran,
so "litestream -config c.yml databases" printed the bare usage screen
and exited 1 with no explanation. Return a usage error that names the
problem and reorders the user's own arguments into a pasteable command,
offering LITESTREAM_CONFIG when -config is the misplaced flag.

Fixes benbjohnson#1405

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@corylanou corylanou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the bare usage output here. I agree with the behavior goal, but I don't think we should reconstruct the invocation by scanning for a hard-coded command name.

The helper is not the parser, so it cannot reliably distinguish a command from a flag value. For example:

$ litestream -socket status info
Error: flags must come after the subcommand
Try: litestream status -socket info

Here, status can be the value of -socket and info the intended command, but the suggestion reverses that interpretation. There are related correctness issues:

  • A config path containing spaces is joined without shell quoting, so the result is not pasteable.
  • litestream -config c.yml version suggests litestream version -config c.yml, although version does not accept -config.
  • The command names already exist in the dispatcher and usage text. This adds another list that must remain synchronized; #1380 is currently adding the mcp command.

Could we keep this PR focused on producing a targeted, correct diagnostic without trying to infer and rewrite the full command? Something like:

} else if strings.HasPrefix(cmd, "-") {
	return misplacedFlagError(cmd)
}
func misplacedFlagError(arg string) error {
	name := strings.TrimLeft(strings.SplitN(arg, "=", 2)[0], "-")
	if name == "config" {
		return &usageError{
			message: "flags must come after the subcommand",
			hint: "litestream <command> -config PATH\n" +
				" or: LITESTREAM_CONFIG=PATH litestream <command>",
		}
	}
	return &usageError{
		message: "flags must come after the subcommand",
		hint:    "litestream <command> [flags]",
	}
}

That removes commandNames, slices, and the token scanner while still fixing #1405 and pointing alias users to LITESTREAM_CONFIG. It also cannot emit a confidently incorrect command.

The CLI tests can cover -config PATH, --config=PATH, another leading flag, explicit help, no arguments, and a correctly positioned flag. I would avoid asserting reconstructed user arguments.

If we want litestream -config ./c.yml databases to actually work, I would prefer a separate change that makes -config a real root flag using a top-level flag.FlagSet. Go's parser stops at the first non-flag argument, so it can parse root flags, select the subcommand, and leave the remaining arguments for the subcommand parser: https://pkg.go.dev/flag#hdr-Command_line_flag_syntax

Replace the command-name scanner with a helper that only reports where a
flag belongs. The scanner could not tell a command from a flag value, so
"litestream -socket status info" was rewritten as "litestream status
-socket info", reversing the user's intent. It also needed its own copy of
the command list.

The hint is now a fixed form rather than a rewrite of the arguments, and
-config additionally points at LITESTREAM_CONFIG for shell aliases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ekanshul

Copy link
Copy Markdown
Author

Rewritten to your design in a3e8342. commandNames, the slices import and the token scanner are gone, and the helper is your snippet as written:

$ litestream -config /etc/litestream.yml databases
Error: flags must come after the subcommand
Try: litestream <command> -config PATH
 or: LITESTREAM_CONFIG=PATH litestream <command>

$ litestream -socket status info
Error: flags must come after the subcommand
Try: litestream <command> [flags]

The second one is your example, and it is now a test case. Since the hint no longer names a command, the ambiguity you pointed out cannot be resolved wrongly, and the quoting and version problems go away with it.

TestMainFlagPlacement covers your six cases: -config PATH, --config=PATH, another leading flag, explicit help, no arguments, and a correctly positioned flag. No assertion mentions reconstructed arguments. The correctly positioned case only asserts the misplaced-flag message is absent, so it does not pin another command's error text. I also folded -help back in alongside -h and --help, since each of those starts with - and has to keep reaching the help branch rather than the new one.

I have updated the PR description, which still described the scanner.

One input this PR changes that we have not discussed, in case you want it handled differently:

# main
$ litestream -version
litestream is a tool for replicating SQLite databases.
... (usage, which lists the version command)
$ echo $?
0

# this PR
$ litestream -version
Error: flags must come after the subcommand
Try: litestream <command> [flags]
$ echo $?
1

-version is not a misplaced flag, it is a subcommand spelled as one, so there is no litestream <command> -version form to try. The usage screen was arguably the better answer there. This has been in the PR since the first revision rather than something the rewrite introduced, and it applies to -v too. Happy to special-case those two tokens to the usage screen, or to leave it as is if you would rather not grow the helper.

Not addressed here, per your review: making -config a real root flag with a top-level flag.FlagSet.

go build ./..., go vet ./..., go test -race ./... and uvx pre-commit run --all-files are clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli: flags before the subcommand print bare usage with no explanation

2 participants