diff --git a/README.md b/README.md index 5f20d9a..3a73f9a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ build changes its shape, delete it and run `money import` again. 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` 3. the `root` key in `$XDG_CONFIG_HOME/money/config.toml`, which is `~/.config/money/config.toml` unless `XDG_CONFIG_HOME` says otherwise diff --git a/cmd/money/main.go b/cmd/money/main.go index e2937df..11ad965 100644 --- a/cmd/money/main.go +++ b/cmd/money/main.go @@ -34,7 +34,7 @@ import ( const usage = `money - statement-driven personal finance tracker -usage: money [--root DIR] [flags] +usage: money [--root DIR] [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. diff --git a/cmd/money/main_test.go b/cmd/money/main_test.go index c180f15..aad4218 100644 --- a/cmd/money/main_test.go +++ b/cmd/money/main_test.go @@ -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") + } +}