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:
+44
-1
@@ -34,7 +34,7 @@ import (
|
||||
|
||||
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:
|
||||
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:]
|
||||
}
|
||||
|
||||
// `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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -486,6 +497,38 @@ func cmdTUI(root string, _ []string) error {
|
||||
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
|
||||
// authentication, so it listens on loopback unless told otherwise: anyone who
|
||||
// can reach the port can rewrite rules.toml and start an import.
|
||||
|
||||
@@ -84,3 +84,27 @@ func TestUniqLimitReportsWhatItHeldBack(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user