Accept --root after the subcommand

`money serve --root DIR` failed because --root was only read before the
subcommand; after it, the flag reached serve's own flag set, which knows
only --addr, and was rejected. It is too natural a thing to type to refuse,
so run now lifts --root/--root=DIR out of any subcommand's arguments before
they are parsed. A bare `--` still ends it, and given on both sides, the one
after the command wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 18:13:01 +02:00
co-authored by Claude Opus 5.5
parent 5847245638
commit 8e4df08f43
3 changed files with 69 additions and 2 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ build changes its shape, delete it and run `money import` again.
Checked in order, first one wins: Checked in order, first one wins:
1. `--root DIR` on the command line 1. `--root DIR` on the command line, before or after the command
2. `$MONEY_ROOT` 2. `$MONEY_ROOT`
3. the `root` key in `$XDG_CONFIG_HOME/money/config.toml`, which is 3. the `root` key in `$XDG_CONFIG_HOME/money/config.toml`, which is
`~/.config/money/config.toml` unless `XDG_CONFIG_HOME` says otherwise `~/.config/money/config.toml` unless `XDG_CONFIG_HOME` says otherwise
+44 -1
View File
@@ -34,7 +34,7 @@ import (
const usage = `money - statement-driven personal finance tracker const usage = `money - statement-driven personal finance tracker
usage: money [--root DIR] <command> [flags] usage: money [--root DIR] <command> [flags] (--root may also follow the command)
commands: commands:
tui browse transactions, build the rules that tag them and the tui browse transactions, build the rules that tag them and the
@@ -88,6 +88,17 @@ func run(args []string) error {
cmd, args = args[0], args[1:] cmd, args = args[0], args[1:]
} }
// `money serve --root DIR` is too natural to reject, so --root is also
// accepted after the subcommand; each subcommand's own flag set never
// sees it.
after, args, err := takeRoot(args)
if err != nil {
return err
}
if after != "" {
root = after
}
abs, source, err := config.ResolveRoot(root) abs, source, err := config.ResolveRoot(root)
if err != nil { if err != nil {
return err return err
@@ -486,6 +497,38 @@ func cmdTUI(root string, _ []string) error {
return tui.Run(root, o.db, o.accounts, o.engine, o.links) return tui.Run(root, o.db, o.accounts, o.engine, o.links)
} }
// takeRoot removes --root/-root (as `--root DIR` or `--root=DIR`) from a
// subcommand's arguments, returning the last value given. It stops at `--`,
// after which everything is positional.
func takeRoot(args []string) (root string, rest []string, err error) {
rest = make([]string, 0, len(args))
for i := 0; i < len(args); i++ {
a := args[i]
if a == "--" {
return root, append(rest, args[i:]...), nil
}
if !strings.HasPrefix(a, "-") {
rest = append(rest, a)
continue
}
name, inline, hasInline := strings.Cut(strings.TrimLeft(a, "-"), "=")
if name != "root" {
rest = append(rest, a)
continue
}
if hasInline {
root = inline
continue
}
if i+1 >= len(args) {
return "", nil, fmt.Errorf("--root needs a directory")
}
root = args[i+1]
i++
}
return root, rest, nil
}
// cmdServe serves the TUI's screens over HTTP until interrupted. There is no // cmdServe serves the TUI's screens over HTTP until interrupted. There is no
// authentication, so it listens on loopback unless told otherwise: anyone who // authentication, so it listens on loopback unless told otherwise: anyone who
// can reach the port can rewrite rules.toml and start an import. // can reach the port can rewrite rules.toml and start an import.
+24
View File
@@ -84,3 +84,27 @@ func TestUniqLimitReportsWhatItHeldBack(t *testing.T) {
t.Errorf("want the first row alphabetically:\n%s", out) t.Errorf("want the first row alphabetically:\n%s", out)
} }
} }
func TestRootAfterTheSubcommand(t *testing.T) {
for _, tc := range []struct {
args []string
root string
rest []string
}{
{[]string{"--root", "/r", "--addr", ":9"}, "/r", []string{"--addr", ":9"}},
{[]string{"--addr", ":9", "-root=/r"}, "/r", []string{"--addr", ":9"}},
{[]string{"--untagged"}, "", []string{"--untagged"}},
{[]string{"--", "--root", "/r"}, "", []string{"--", "--root", "/r"}},
} {
root, rest, err := takeRoot(tc.args)
if err != nil {
t.Fatalf("%q: %v", tc.args, err)
}
if root != tc.root || strings.Join(rest, " ") != strings.Join(tc.rest, " ") {
t.Errorf("%q: got %q %q, want %q %q", tc.args, root, rest, tc.root, tc.rest)
}
}
if _, _, err := takeRoot([]string{"--root"}); err == nil {
t.Error("--root without a directory was accepted")
}
}