mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
Support XDG Base Directory spec for .mlrrc location (#1271)
Adds $XDG_CONFIG_HOME/miller/mlrrc (defaulting to $HOME/.config/miller/mlrrc when $XDG_CONFIG_HOME is unset) as an additional stacking location, processed after $HOME/.mlrrc and before ./.mlrrc. Fully backward-compatible: existing $HOME/.mlrrc and ./.mlrrc behavior is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4573618433
commit
a1fc51546e
8 changed files with 155 additions and 24 deletions
|
|
@ -121,9 +121,10 @@ Semantics:
|
|||
* If the same section name appears more than once, the settings from all its blocks are applied,
|
||||
in the order they appear in the file.
|
||||
|
||||
* If both `$HOME/.mlrrc` and `./.mlrrc` are processed, each file's global settings and matching
|
||||
section settings are applied in that per-file order, `$HOME/.mlrrc` first. The selected profile
|
||||
needs to exist in only one of them.
|
||||
* If more than one of `$HOME/.mlrrc`, `$XDG_CONFIG_HOME/miller/mlrrc` (or its default
|
||||
`$HOME/.config/miller/mlrrc`), and `./.mlrrc` are processed, each file's global settings and
|
||||
matching section settings are applied in that per-file order. The selected profile needs to
|
||||
exist in only one of them.
|
||||
|
||||
* Since `--profile` selects a section of your `.mlrrc`, it can't be combined with `--norc`, or
|
||||
with `MLRRC=__none__` in the environment -- that's a fatal error.
|
||||
|
|
@ -139,14 +140,18 @@ If the environment variable `MLRRC` is set:
|
|||
|
||||
* Otherwise, its value (as a filename) is loaded and processed. If there are syntax errors, they abort `mlr` with a usage message (as if you had mistyped something on the command line). If the file can't be loaded at all, though, it is silently skipped.
|
||||
|
||||
* Any `.mlrrc` in your home directory or current directory is ignored whenever `MLRRC` is set in the environment.
|
||||
* Any `.mlrrc` in your home directory, XDG config directory, or current directory is ignored whenever `MLRRC` is set in the environment.
|
||||
|
||||
* Example line in your shell's rc file: `export MLRRC=/path/to/my/mlrrc`
|
||||
|
||||
Otherwise:
|
||||
Otherwise, each of the following which exists is processed in turn, letting them stack: the idea
|
||||
is you can have all your settings in your `$HOME/.mlrrc`, then maybe more project-specific
|
||||
settings for your current directory if you like.
|
||||
|
||||
* If `$HOME/.mlrrc` exists, it's processed as above.
|
||||
|
||||
* If `./.mlrrc` exists, it's then also processed as above.
|
||||
* If `$XDG_CONFIG_HOME/miller/mlrrc` exists, it's then also processed as above, per the
|
||||
[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
|
||||
If `$XDG_CONFIG_HOME` isn't set, `$HOME/.config/miller/mlrrc` is used instead.
|
||||
|
||||
* The idea is you can have all your settings in your `$HOME/.mlrrc`, then maybe more project-specific settings for your current directory if you like.
|
||||
* If `./.mlrrc` exists, it's then also processed as above.
|
||||
|
|
|
|||
|
|
@ -89,9 +89,10 @@ Semantics:
|
|||
* If the same section name appears more than once, the settings from all its blocks are applied,
|
||||
in the order they appear in the file.
|
||||
|
||||
* If both `$HOME/.mlrrc` and `./.mlrrc` are processed, each file's global settings and matching
|
||||
section settings are applied in that per-file order, `$HOME/.mlrrc` first. The selected profile
|
||||
needs to exist in only one of them.
|
||||
* If more than one of `$HOME/.mlrrc`, `$XDG_CONFIG_HOME/miller/mlrrc` (or its default
|
||||
`$HOME/.config/miller/mlrrc`), and `./.mlrrc` are processed, each file's global settings and
|
||||
matching section settings are applied in that per-file order. The selected profile needs to
|
||||
exist in only one of them.
|
||||
|
||||
* Since `--profile` selects a section of your `.mlrrc`, it can't be combined with `--norc`, or
|
||||
with `MLRRC=__none__` in the environment -- that's a fatal error.
|
||||
|
|
@ -107,14 +108,18 @@ If the environment variable `MLRRC` is set:
|
|||
|
||||
* Otherwise, its value (as a filename) is loaded and processed. If there are syntax errors, they abort `mlr` with a usage message (as if you had mistyped something on the command line). If the file can't be loaded at all, though, it is silently skipped.
|
||||
|
||||
* Any `.mlrrc` in your home directory or current directory is ignored whenever `MLRRC` is set in the environment.
|
||||
* Any `.mlrrc` in your home directory, XDG config directory, or current directory is ignored whenever `MLRRC` is set in the environment.
|
||||
|
||||
* Example line in your shell's rc file: `export MLRRC=/path/to/my/mlrrc`
|
||||
|
||||
Otherwise:
|
||||
Otherwise, each of the following which exists is processed in turn, letting them stack: the idea
|
||||
is you can have all your settings in your `$HOME/.mlrrc`, then maybe more project-specific
|
||||
settings for your current directory if you like.
|
||||
|
||||
* If `$HOME/.mlrrc` exists, it's processed as above.
|
||||
|
||||
* If `./.mlrrc` exists, it's then also processed as above.
|
||||
* If `$XDG_CONFIG_HOME/miller/mlrrc` exists, it's then also processed as above, per the
|
||||
[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
|
||||
If `$XDG_CONFIG_HOME` isn't set, `$HOME/.config/miller/mlrrc` is used instead.
|
||||
|
||||
* The idea is you can have all your settings in your `$HOME/.mlrrc`, then maybe more project-specific settings for your current directory if you like.
|
||||
* If `./.mlrrc` exists, it's then also processed as above.
|
||||
|
|
|
|||
|
|
@ -4173,5 +4173,5 @@ This is simply a copy of what you should see on running `man mlr` at a command p
|
|||
MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
|
||||
https://miller.readthedocs.io
|
||||
|
||||
2026-07-15 4mMILLER24m(1)
|
||||
2026-07-16 4mMILLER24m(1)
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -4152,4 +4152,4 @@
|
|||
MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
|
||||
https://miller.readthedocs.io
|
||||
|
||||
2026-07-15 4mMILLER24m(1)
|
||||
2026-07-16 4mMILLER24m(1)
|
||||
|
|
|
|||
|
|
@ -4152,4 +4152,4 @@
|
|||
MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
|
||||
https://miller.readthedocs.io
|
||||
|
||||
2026-07-15 4mMILLER24m(1)
|
||||
2026-07-16 4mMILLER24m(1)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
.\" Title: mlr
|
||||
.\" Author: [see the "AUTHOR" section]
|
||||
.\" Generator: ./mkman.rb
|
||||
.\" Date: 2026-07-15
|
||||
.\" Date: 2026-07-16
|
||||
.\" Manual: \ \&
|
||||
.\" Source: \ \&
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "MILLER" "1" "2026-07-15" "\ \&" "\ \&"
|
||||
.TH "MILLER" "1" "2026-07-16" "\ \&" "\ \&"
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * Portability definitions
|
||||
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ import (
|
|||
"github.com/johnkerl/miller/v6/pkg/cli"
|
||||
)
|
||||
|
||||
// loadMlrrcFiles rule: If $MLRRC is set, use it and only it. Otherwise try first
|
||||
// $HOME/.mlrrc and then ./.mlrrc but let them stack: e.g. $HOME/.mlrrc is
|
||||
// lots of settings and maybe in one subdir you want to override just a
|
||||
// setting or two.
|
||||
// loadMlrrcFiles rule: If $MLRRC is set, use it and only it. Otherwise try, in
|
||||
// order, $HOME/.mlrrc, then $XDG_CONFIG_HOME/miller/mlrrc (defaulting to
|
||||
// $HOME/.config/miller/mlrrc if $XDG_CONFIG_HOME is unset), then ./.mlrrc --
|
||||
// but let them stack: e.g. $HOME/.mlrrc is lots of settings and maybe in one
|
||||
// subdir you want to override just a setting or two.
|
||||
//
|
||||
// The profileName argument comes from the --profile {name} / -P {name} main
|
||||
// flag. Empty string means no profile was requested: only global (pre-section)
|
||||
|
|
@ -62,6 +63,21 @@ func loadMlrrcFiles(
|
|||
}
|
||||
}
|
||||
|
||||
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgConfigHome == "" && env_home != "" {
|
||||
xdgConfigHome = env_home + "/.config"
|
||||
}
|
||||
if xdgConfigHome != "" {
|
||||
path := xdgConfigHome + "/miller/mlrrc"
|
||||
loaded, err := tryLoadMlrrc(options, path, profileName, &foundProfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if loaded {
|
||||
loadedPaths = append(loadedPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
loaded, err := tryLoadMlrrc(options, "./.mlrrc", profileName, &foundProfile)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -128,9 +128,11 @@ func TestMlrrcProfileWithMlrrcNoneIsError(t *testing.T) {
|
|||
func TestMlrrcProfileWithNoMlrrcFileIsError(t *testing.T) {
|
||||
// Point MLRRC at a nonexistent file: an unopenable file is silently
|
||||
// skipped (the normal no-.mlrrc case), and HOME is remapped to an empty
|
||||
// temp directory, so no .mlrrc file is processed at all.
|
||||
// temp directory, so no .mlrrc file is processed at all. XDG_CONFIG_HOME
|
||||
// is cleared so a developer's real config dir doesn't leak in.
|
||||
t.Setenv("MLRRC", filepath.Join(t.TempDir(), "nonexistent"))
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
options := cli.DefaultOptions()
|
||||
err := loadMlrrcFiles(options, "j")
|
||||
if err == nil {
|
||||
|
|
@ -197,6 +199,109 @@ func TestMlrrcPrepipeStillDisallowed(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMlrrcXdgConfigHomeIsLoaded(t *testing.T) {
|
||||
// No $HOME/.mlrrc, no ./.mlrrc, no $MLRRC: only
|
||||
// $XDG_CONFIG_HOME/miller/mlrrc is processed.
|
||||
xdgConfigHome := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(xdgConfigHome, "miller"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(xdgConfigHome, "miller", "mlrrc")
|
||||
if err := os.WriteFile(path, []byte("ojson\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("MLRRC", "")
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("XDG_CONFIG_HOME", xdgConfigHome)
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(t.TempDir()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(oldwd) }()
|
||||
|
||||
options := cli.DefaultOptions()
|
||||
if err := loadMlrrcFiles(options, ""); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if options.WriterOptions.OutputFileFormat != "json" {
|
||||
t.Errorf("output format: got %s, want json", options.WriterOptions.OutputFileFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMlrrcXdgConfigHomeDefaultsToHomeConfig(t *testing.T) {
|
||||
// When $XDG_CONFIG_HOME is unset, $HOME/.config/miller/mlrrc is used.
|
||||
home := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(home, ".config", "miller"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(home, ".config", "miller", "mlrrc")
|
||||
if err := os.WriteFile(path, []byte("ojson\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("MLRRC", "")
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(t.TempDir()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(oldwd) }()
|
||||
|
||||
options := cli.DefaultOptions()
|
||||
if err := loadMlrrcFiles(options, ""); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if options.WriterOptions.OutputFileFormat != "json" {
|
||||
t.Errorf("output format: got %s, want json", options.WriterOptions.OutputFileFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMlrrcXdgConfigHomeStacksAfterHomeMlrrc(t *testing.T) {
|
||||
// Both $HOME/.mlrrc and $XDG_CONFIG_HOME/miller/mlrrc are processed, with
|
||||
// $HOME/.mlrrc applied first: a setting in the XDG file overrides one
|
||||
// from $HOME/.mlrrc.
|
||||
home := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(home, ".mlrrc"), []byte("icsv\nojson\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
xdgConfigHome := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(xdgConfigHome, "miller"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(xdgConfigHome, "miller", "mlrrc")
|
||||
if err := os.WriteFile(path, []byte("otsv\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("MLRRC", "")
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", xdgConfigHome)
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(t.TempDir()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(oldwd) }()
|
||||
|
||||
options := cli.DefaultOptions()
|
||||
if err := loadMlrrcFiles(options, ""); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if options.ReaderOptions.InputFileFormat != "csv" {
|
||||
t.Errorf("input format: got %s, want csv", options.ReaderOptions.InputFileFormat)
|
||||
}
|
||||
if options.WriterOptions.OutputFileFormat != "tsv" {
|
||||
t.Errorf("output format: got %s, want tsv (from XDG config, overriding json from $HOME/.mlrrc)", options.WriterOptions.OutputFileFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMlrrcProfileFlagDisallowedWithinMlrrc(t *testing.T) {
|
||||
// Profiles are selected on the mlr command line, not from within a
|
||||
// .mlrrc file: --profile / -P there is a parse error, like --prepipe.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue