Find the data root via the XDG config directory

Running the tool from anywhere required either a --root flag or an exported
MONEY_ROOT. Read the data root from $XDG_CONFIG_HOME/money/config.toml
(~/.config/money/config.toml) instead, so it can be configured once.

Resolution order is --root, then $MONEY_ROOT, then the config file, then
~/money. A leading tilde in the config file is expanded, since no shell has
done it for us, and relative paths are made absolute.

The new "money config" subcommand prints the resolved root and which rule
chose it, which is the first thing to check when the tool reads the wrong
directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 00:40:59 +02:00
co-authored by Claude Opus 5
parent b0026c5a79
commit cf1753fb0a
4 changed files with 316 additions and 11 deletions
+33 -10
View File
@@ -9,7 +9,6 @@ import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
@@ -35,8 +34,10 @@ commands:
report spending by tag
accounts list accounts with balances
parsers list available statement parsers
config show which data root is in use and why
The data root defaults to $MONEY_ROOT, or ~/money if that is unset.
The data root is taken from --root, else $MONEY_ROOT, else the "root" key in
$XDG_CONFIG_HOME/money/config.toml (~/.config/money/config.toml), else ~/money.
`
func main() {
@@ -47,7 +48,7 @@ func main() {
}
func run(args []string) error {
root := defaultRoot()
var root string // empty means "not given on the command line"
// A leading --root/-root applies to every subcommand.
for len(args) > 0 && strings.HasPrefix(args[0], "-") {
flagName := strings.TrimLeft(args[0], "-")
@@ -75,12 +76,14 @@ func run(args []string) error {
cmd, args = args[0], args[1:]
}
abs, err := filepath.Abs(root)
abs, source, err := config.ResolveRoot(root)
if err != nil {
return err
}
switch cmd {
case "config":
return cmdConfig(abs, source)
case "import":
return cmdImport(abs, args)
case "retag":
@@ -104,14 +107,34 @@ func run(args []string) error {
}
}
func defaultRoot() string {
if r := os.Getenv("MONEY_ROOT"); r != "" {
return r
// cmdConfig explains which data root is in use and where that came from,
// which is the first thing to check when the tool looks at the wrong place.
func cmdConfig(root string, source config.RootSource) error {
path, err := config.UserConfigPath()
if err != nil {
return err
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, "money")
exists := "not present"
if _, err := os.Stat(path); err == nil {
exists = "present"
}
return "."
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "data root\t%s\n", root)
fmt.Fprintf(w, "chosen by\t%s\n", source)
fmt.Fprintf(w, "config file\t%s (%s)\n", path, exists)
if _, err := os.Stat(root); err != nil {
fmt.Fprintf(w, "status\tdoes not exist yet\n")
} else {
fmt.Fprintf(w, "index\t%s\n", config.IndexPath(root))
}
w.Flush()
if source != config.RootFromConfig && exists == "not present" {
fmt.Printf("\nTo point money at a different directory, create %s with:\n\n root = %q\n",
path, root)
}
return nil
}
// open loads the config and index that every command needs.