From 44bc97d336631222a329283cf6909a857b945631 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Fri, 1 Mar 2024 03:45:49 -0700 Subject: [PATCH 1/9] Refactoring the config file and flag handling - The flags will now receive and be able to display the default values. - The config file is now searched across multiple paths: - One that respects the XDG Base Directory Specification. - The old default of `~/.define.conf.json`, for fallback. --- define.go | 7 ++-- go.mod | 1 - go.sum | 2 - internal/config/config.go | 87 +++++++++++++++----------------------- internal/config/file.go | 88 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 61 deletions(-) create mode 100644 internal/config/file.go diff --git a/define.go b/define.go index faead59..b5516d7 100644 --- a/define.go +++ b/define.go @@ -26,9 +26,8 @@ import ( const ( // Configuration defaults - defaultConfigFileLocation = "~/.define.conf.json" - defaultIndentationSize = 2 - defaultPreferredSource = oxford.JSONKey + defaultIndentationSize = 2 + defaultPreferredSource = oxford.JSONKey fallbackSearchResultLimit = 5 ) @@ -67,7 +66,7 @@ func init() { providerConfsList = append(providerConfsList, providerConf) } - conf, err = config.NewFromRuntime(flags, providerConfs, defaultConfigFileLocation, config.Configuration{ + conf, err = config.NewFromRuntime(flags, providerConfs, config.Configuration{ IndentationSize: defaultIndentationSize, PreferredSource: defaultPreferredSource, }) diff --git a/go.mod b/go.mod index 10e679f..a8865dc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.22.0 require ( dario.cat/mergo v1.0.0 github.com/fatih/structs v1.1.0 - github.com/mitchellh/go-homedir v1.1.0 github.com/ogier/pflag v0.0.0-20160129220114-45c278ab3607 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index de4922a..de6a660 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/ogier/pflag v0.0.0-20160129220114-45c278ab3607 h1:db+rES1EpSjP45xOU3hgS41oawQiZzqfnl6dUgBdFjY= github.com/ogier/pflag v0.0.0-20160129220114-45c278ab3607/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/internal/config/config.go b/internal/config/config.go index f6466b6..5ee0981 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,6 @@ import ( "github.com/Rican7/define/registry" "github.com/fatih/structs" - homedir "github.com/mitchellh/go-homedir" flag "github.com/ogier/pflag" "dario.cat/mergo" @@ -25,42 +24,25 @@ type Configuration struct { Source string // Private fields that shouldn't be externally set or output - providerConfigs map[string]registry.Configuration - configFileLocation string - noConfigFile bool + providerConfigs map[string]registry.Configuration + configFilePath string + noConfigFile bool } // initializeCommandLineConfig initializes the command line configuration. -func initializeCommandLineConfig(flags *flag.FlagSet) *Configuration { +func initializeCommandLineConfig(flags *flag.FlagSet, defaults Configuration) *Configuration { var conf Configuration // Define our flags - flags.StringVarP(&conf.configFileLocation, "config-file", "c", "", "The location of the config file to use") + flags.StringVarP(&conf.configFilePath, "config-file", "c", defaults.configFilePath, "The path of the config file to use") flags.BoolVar(&conf.noConfigFile, "no-config-file", false, "To not load any config file") - flags.UintVar(&conf.IndentationSize, "indent-size", 0, "The number of spaces to indent output by") - flags.StringVar(&conf.PreferredSource, "preferred-source", "", "The preferred source to use, if available and able to be provided") - flags.StringVarP(&conf.Source, "source", "s", "", "The source to use (will error if unavailable or unable to be provided)") + flags.UintVar(&conf.IndentationSize, "indent-size", defaults.IndentationSize, "The number of spaces to indent output by") + flags.StringVar(&conf.PreferredSource, "preferred-source", defaults.PreferredSource, "The preferred source to use, if available and able to be provided") + flags.StringVarP(&conf.Source, "source", "s", defaults.Source, "The source to use (will error if unavailable or unable to be provided)") return &conf } -// initializeFileConfig initializes the file configuration by loading the -// configuration from a file at the given location. -func initializeFileConfig(fileLocation string) (Configuration, error) { - var conf Configuration - - fileContents, err := os.ReadFile(tryExpandPath(fileLocation)) - if err != nil { - return conf, err - } - - if len(fileContents) > 0 { - err = json.Unmarshal(fileContents, &conf) - } - - return conf, err -} - // initializeEnvironmentConfig initializes the environment configuration from // the application's environment. func initializeEnvironmentConfig() Configuration { @@ -76,6 +58,23 @@ func initializeEnvironmentConfig() Configuration { return conf } +// initializeFileConfig initializes the file configuration by loading the +// configuration from a file at the given path. +func initializeFileConfig(filePath string) (Configuration, error) { + var conf Configuration + + fileContents, err := os.ReadFile(tryExpandUserPath(filePath)) + if err != nil { + return conf, err + } + + if len(fileContents) > 0 { + err = json.Unmarshal(fileContents, &conf) + } + + return conf, err +} + // mergeConfigurations merges multiple configurations values together, from left // to right argument position, by filling any of the left arguments zero-values // with any non-zero-values from the right. @@ -91,16 +90,6 @@ func mergeConfigurations(confs ...Configuration) (Configuration, error) { return merged, nil } -// tryExpandPath attempts to expand a given path and returns the expanded path -// if successful. Otherwise, if expansion failed, the original path is returned. -func tryExpandPath(path string) string { - if expanded, err := homedir.Expand(path); err == nil { - path = expanded - } - - return path -} - // NewFromRuntime builds a Configuration by merging values from multiple // different sources. It accepts a Configuration containing default values to // fill in any empty/blank configuration values found when merging from the @@ -114,7 +103,6 @@ func tryExpandPath(path string) string { func NewFromRuntime( flags *flag.FlagSet, providerConfigs map[string]registry.Configuration, - defaultConfigFileLocation string, defaults Configuration, ) (Configuration, error) { var conf Configuration @@ -122,31 +110,22 @@ func NewFromRuntime( var fileConfig Configuration - // Set our config file location - defaults.configFileLocation = tryExpandPath(defaultConfigFileLocation) + // Set our config file path based on our first found default location. + defaults.configFilePath = findConfigFile() - commandLineConfig := initializeCommandLineConfig(flags) + commandLineConfig := initializeCommandLineConfig(flags, defaults) // Parse our flag set, as we need the values from the commandLineConfig err = flags.Parse(os.Args[1:]) if err == nil && !commandLineConfig.noConfigFile { - configFileLocation := tryExpandPath(commandLineConfig.configFileLocation) - - if configFileLocation == "" && defaults.configFileLocation != "" { - // If we haven't passed a config file flag, and our default exists - if _, err := os.Stat(defaults.configFileLocation); !os.IsNotExist(err) { - // Set our location to the default, since it exists - // (if there are problems reading the file, we'll handle later) - configFileLocation = defaults.configFileLocation - } - } + configFilePath := tryExpandUserPath(commandLineConfig.configFilePath) // If we have a config file to load - if configFileLocation != "" { - fileConfig, err = initializeFileConfig(configFileLocation) + if configFilePath != "" { + fileConfig, err = initializeFileConfig(configFilePath) if err != nil { - err = fmt.Errorf("error reading config file %q with error: %s", configFileLocation, err) + err = fmt.Errorf("error reading config file %q with error: %s", configFilePath, err) } } } @@ -154,8 +133,8 @@ func NewFromRuntime( if err == nil { conf, err = mergeConfigurations( *commandLineConfig, - fileConfig, initializeEnvironmentConfig(), + fileConfig, defaults, ) } diff --git a/internal/config/file.go b/internal/config/file.go new file mode 100644 index 0000000..652217f --- /dev/null +++ b/internal/config/file.go @@ -0,0 +1,88 @@ +package config + +import ( + "errors" + "io/fs" + "os" + "path/filepath" +) + +const ( + xdgBaseName = "define" + defaultXDGConfigFileName = "config.json" + + oldDefaultConfigFilePath = "~/.define.conf.json" +) + +var ( + userHomeDirPath string + userConfigDirPath string +) + +// userHomeDir returns the user's home directory, caching the value upon first +// calculation, and without worrying about errors about detection. +func userHomeDir() string { + if userHomeDirPath == "" { + // Ignore errors here. We only need the value, even if it's empty. + userHomeDirPath, _ = os.UserHomeDir() + } + + return userHomeDirPath +} + +// userConfigDir returns the user's config directory, caching the value upon +// first calculation, and without worrying about errors about detection. +func userConfigDir() string { + if userConfigDirPath == "" { + // Ignore errors here. We only need the value, even if it's empty. + userConfigDirPath, _ = os.UserConfigDir() + } + + return userConfigDirPath +} + +// tryExpandUserPath takes a path and expands it if it's user home prefixed (~). +// If the path isn't user home prefixed, then the original path is returned. +func tryExpandUserPath(path string) string { + if len(path) > 1 && path[0] == '~' && path[1] == filepath.Separator { + path = filepath.Join(userHomeDir(), path[1:]) + } + + return path +} + +// tryExpandConfigPath takes a path and expands it under the user's config home +// path, and the app's config home path within. If those paths aren't known, +// the path is returned empty. +func tryExpandConfigPath(path string) string { + userConfPath := userConfigDir() + + if userConfPath == "" { + // If we have no known config path for the user, then we don't have a + // path we can use. + return "" + } + + return filepath.Join(userConfPath, xdgBaseName, path) +} + +// findConfigFile attempts to find the current environment user's config file, +// by scanning possible known locations. It returns the path to the config file, +// if any was found. +func findConfigFile() string { + searchFilePaths := []string{ + tryExpandConfigPath(defaultXDGConfigFileName), + tryExpandUserPath(oldDefaultConfigFilePath), + } + + for _, filePath := range searchFilePaths { + // Check if the file exists + if _, err := os.Stat(filePath); !errors.Is(err, fs.ErrNotExist) { + // Return the first file that exists + // (if there are problems reading the file, we'll handle later) + return filePath + } + } + + return "" +} From 58717d2066ae9fca0b2fa15ddf712a71067ef17d Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Sun, 3 Mar 2024 01:54:51 -0700 Subject: [PATCH 2/9] Adding scanning of all XDG config dirs --- go.mod | 3 +++ go.sum | 13 +++++++++ internal/config/config.go | 1 + internal/config/file.go | 55 ++++++++++++--------------------------- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index a8865dc..7cfd34a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,10 @@ go 1.22.0 require ( dario.cat/mergo v1.0.0 + github.com/adrg/xdg v0.4.0 github.com/fatih/structs v1.1.0 github.com/ogier/pflag v0.0.0-20160129220114-45c278ab3607 golang.org/x/text v0.14.0 ) + +require golang.org/x/sys v0.5.0 // indirect diff --git a/go.sum b/go.sum index de6a660..c18b781 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,24 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/ogier/pflag v0.0.0-20160129220114-45c278ab3607 h1:db+rES1EpSjP45xOU3hgS41oawQiZzqfnl6dUgBdFjY= github.com/ogier/pflag v0.0.0-20160129220114-45c278ab3607/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 5ee0981..86287df 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -119,6 +119,7 @@ func NewFromRuntime( err = flags.Parse(os.Args[1:]) if err == nil && !commandLineConfig.noConfigFile { + // This path should have either the user-passed value or a found default configFilePath := tryExpandUserPath(commandLineConfig.configFilePath) // If we have a config file to load diff --git a/internal/config/file.go b/internal/config/file.go index 652217f..d3ba926 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -5,6 +5,8 @@ import ( "io/fs" "os" "path/filepath" + + "github.com/adrg/xdg" ) const ( @@ -15,8 +17,7 @@ const ( ) var ( - userHomeDirPath string - userConfigDirPath string + userHomeDirPath string ) // userHomeDir returns the user's home directory, caching the value upon first @@ -30,17 +31,6 @@ func userHomeDir() string { return userHomeDirPath } -// userConfigDir returns the user's config directory, caching the value upon -// first calculation, and without worrying about errors about detection. -func userConfigDir() string { - if userConfigDirPath == "" { - // Ignore errors here. We only need the value, even if it's empty. - userConfigDirPath, _ = os.UserConfigDir() - } - - return userConfigDirPath -} - // tryExpandUserPath takes a path and expands it if it's user home prefixed (~). // If the path isn't user home prefixed, then the original path is returned. func tryExpandUserPath(path string) string { @@ -51,37 +41,26 @@ func tryExpandUserPath(path string) string { return path } -// tryExpandConfigPath takes a path and expands it under the user's config home -// path, and the app's config home path within. If those paths aren't known, -// the path is returned empty. -func tryExpandConfigPath(path string) string { - userConfPath := userConfigDir() - - if userConfPath == "" { - // If we have no known config path for the user, then we don't have a - // path we can use. - return "" - } - - return filepath.Join(userConfPath, xdgBaseName, path) -} - // findConfigFile attempts to find the current environment user's config file, // by scanning possible known locations. It returns the path to the config file, // if any was found. func findConfigFile() string { - searchFilePaths := []string{ - tryExpandConfigPath(defaultXDGConfigFileName), - tryExpandUserPath(oldDefaultConfigFilePath), + defaultXDGConfigRelPath := filepath.Join(xdgBaseName, defaultXDGConfigFileName) + + filePath, err := xdg.SearchConfigFile(defaultXDGConfigRelPath) + if filePath != "" && err == nil { + // We found a config! Return it's path. + return filePath } - for _, filePath := range searchFilePaths { - // Check if the file exists - if _, err := os.Stat(filePath); !errors.Is(err, fs.ErrNotExist) { - // Return the first file that exists - // (if there are problems reading the file, we'll handle later) - return filePath - } + oldDefaultConfigFullPath := tryExpandUserPath(oldDefaultConfigFilePath) + + // Check if a file exists at the old default path + _, err = os.Stat(oldDefaultConfigFullPath) + if err == nil || errors.Is(err, fs.ErrExist) { + // Return the file path if it exists + // (if there are problems reading the file, we'll handle later) + return oldDefaultConfigFullPath } return "" From 64f0eea012fb32c3e13a55395ed4e2f0b88b4c55 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Sun, 3 Mar 2024 04:01:26 -0700 Subject: [PATCH 3/9] Improving documentation for the config locations --- README.md | 7 ++++++- internal/config/config.go | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5babb07..0af1cb7 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,12 @@ The list of command line flags is easily discovered via the `--help` flag. Any p ### Configuration file -A configuration file can be stored at `~/.define.conf.json` and **define** will automatically load the values specified there. +A configuration file can be stored that **define** will automatically load the values from. + +The path of the configuration file to load can be specified via the `--config-file` flag. If no config file path is specified, **define** will search for a config file in your OS's standard config directory paths. While these paths are OS-specific, there are two locations that are searched for that are shared among all platforms: + +1. `$XDG_CONFIG_HOME/define/config.json` (This is only searched for when the `$XDG_CONFIG_HOME` env variable is set) +2. `~/.define.conf.json` (Where `~` is equal to your `$HOME` or user directory for your OS) To print the default values of the configuration, simply use the `--print-config` flag. This can also be used to initialize a configuration file, for example: diff --git a/internal/config/config.go b/internal/config/config.go index 86287df..7fc9d51 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -97,8 +97,8 @@ func mergeConfigurations(confs ...Configuration) (Configuration, error) { // // The merging of values from different sources will take this priority: // 1. Command line arguments -// 2. A loaded config file, if available -// 3. Environment variables +// 2. Environment variables +// 3. A loaded config file, if available // 4. Passed in default values func NewFromRuntime( flags *flag.FlagSet, From bda6fe2d3d5752baecb8db465d155dad9cd4f5f8 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Sun, 3 Mar 2024 04:09:14 -0700 Subject: [PATCH 4/9] Improving documentation on config value precedence --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0af1cb7..bccd655 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,27 @@ go install github.com/Rican7/define@latest The **define** app allows configuration through multiple means. You can either set configuration via: -- Command line flags (good for one-off use) -- A configuration file (good for your "dotfiles") -- Environment variables (especially useful for API keys) +1. Command line flags (good for one-off use) +2. Environment variables (good for API keys) +3. A configuration file (good for your "dotfiles") + +When multiple means of configuration are used, the values will take precedence in the aforementioned priority. ### Command line flags The list of command line flags is easily discovered via the `--help` flag. Any passed command line flag will take precedence over any other configuration mechanism. +### Environment variables + +Some configuration values can also be specified via environment variables. This is especially useful for API keys of different sources. + +The following environment variables are read by **define**'s sources: + +- `MERRIAM_WEBSTER_DICTIONARY_APP_KEY` +- `OXFORD_DICTIONARY_APP_ID` +- `OXFORD_DICTIONARY_APP_KEY` + ### Configuration file A configuration file can be stored that **define** will automatically load the values from. @@ -50,16 +62,6 @@ To print the default values of the configuration, simply use the `--print-config define --print-config > ~/.define.conf.json ``` -### Environment variables - -Some configuration values can also be specified via environment variables. This is especially useful for API keys of different sources. - -The following environment variables are read by **define**'s sources: - -- `MERRIAM_WEBSTER_DICTIONARY_APP_KEY` -- `OXFORD_DICTIONARY_APP_ID` -- `OXFORD_DICTIONARY_APP_KEY` - ## Sources From a2b55474bb508bdf1bd1d94053f39f1a455ce02f Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Tue, 5 Mar 2024 01:55:56 -0700 Subject: [PATCH 5/9] Switching to using a custom config file precedence We now check for a user's XDG config home path, then the old default path, and then finally the more "system" level XDG config dirs. --- internal/config/file.go | 45 +++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/internal/config/file.go b/internal/config/file.go index d3ba926..a7828fa 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -45,23 +45,42 @@ func tryExpandUserPath(path string) string { // by scanning possible known locations. It returns the path to the config file, // if any was found. func findConfigFile() string { + for _, filePath := range FilePaths() { + // Check if the file exists + _, err := os.Stat(filePath) + if err == nil || errors.Is(err, fs.ErrExist) { + // Return the file path if it exists + // (if there are problems reading the file, we'll handle later) + return filePath + } + } + + return "" +} + +// FilePaths returns the paths of config files that may be searched for in the +// current environment. +// +// This is useful for self-documentation, to provide clarity to users for where +// their config file may be loaded from. +func FilePaths() []string { + // Length of filePaths is the XDG config home, plus config home, plus the + // old default file path. + filePathsLen := len(xdg.ConfigDirs) + 2 + filePaths := make([]string, 0, filePathsLen) + defaultXDGConfigRelPath := filepath.Join(xdgBaseName, defaultXDGConfigFileName) - filePath, err := xdg.SearchConfigFile(defaultXDGConfigRelPath) - if filePath != "" && err == nil { - // We found a config! Return it's path. - return filePath - } + // First we try the user's XDG config home + filePaths = append(filePaths, filepath.Join(xdg.ConfigHome, defaultXDGConfigRelPath)) - oldDefaultConfigFullPath := tryExpandUserPath(oldDefaultConfigFilePath) + // Then we fall back to the old default path + filePaths = append(filePaths, tryExpandUserPath(oldDefaultConfigFilePath)) - // Check if a file exists at the old default path - _, err = os.Stat(oldDefaultConfigFullPath) - if err == nil || errors.Is(err, fs.ErrExist) { - // Return the file path if it exists - // (if there are problems reading the file, we'll handle later) - return oldDefaultConfigFullPath + // Finally, we defer to the XDG config dirs (as those are likely global) + for _, configDir := range xdg.ConfigDirs { + filePaths = append(filePaths, filepath.Join(configDir, defaultXDGConfigRelPath)) } - return "" + return filePaths } From 6324d996c6b6739ed8331279bb5761e123b2e06c Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Tue, 5 Mar 2024 02:00:06 -0700 Subject: [PATCH 6/9] Improving config merging and value setting Config merging now handles private values. --- internal/config/config.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index 7fc9d51..6103f7d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ package config import ( + "cmp" "encoding/json" "fmt" "os" @@ -63,7 +64,9 @@ func initializeEnvironmentConfig() Configuration { func initializeFileConfig(filePath string) (Configuration, error) { var conf Configuration - fileContents, err := os.ReadFile(tryExpandUserPath(filePath)) + filePath = tryExpandUserPath(filePath) + + fileContents, err := os.ReadFile(filePath) if err != nil { return conf, err } @@ -72,6 +75,8 @@ func initializeFileConfig(filePath string) (Configuration, error) { err = json.Unmarshal(fileContents, &conf) } + conf.configFilePath = filePath + return conf, err } @@ -85,6 +90,10 @@ func mergeConfigurations(confs ...Configuration) (Configuration, error) { if err := mergo.Merge(&merged, conf); err != nil { return merged, err } + + // Set private (unexported values), as mergo can't handle those. + merged.configFilePath = cmp.Or(merged.configFilePath, conf.configFilePath) + merged.noConfigFile = cmp.Or(merged.noConfigFile, conf.noConfigFile) } return merged, nil @@ -156,6 +165,11 @@ func (c Configuration) ProviderConfigs() []registry.Configuration { return list } +// FilePath returns the path of the file that was loaded for the configuration. +func (c Configuration) FilePath() string { + return c.configFilePath +} + // MarshalJSON defines how the configuration should be JSON marshalled. func (c Configuration) MarshalJSON() ([]byte, error) { configMap := structs.Map(c) From 51658779e5cb65820686fed5d1c7354678363333 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Tue, 5 Mar 2024 02:01:13 -0700 Subject: [PATCH 7/9] Adding a `--debug-config` flag to print conf info --- define.go | 23 +++++++++++++++++++++++ internal/action/action.go | 5 +++++ 2 files changed, 28 insertions(+) diff --git a/define.go b/define.go index b5516d7..096da92 100644 --- a/define.go +++ b/define.go @@ -150,6 +150,27 @@ func printConfig() { stdOutWriter.WriteStringLine(string(encoded)) } +func printConfigDebug() { + stdOutWriter.IndentWrites(func(writer *defineio.PanicWriter) { + writer.WriteNewLine() + + switch configFilePath := conf.FilePath(); configFilePath { + case "": + writer.WriteStringLine("No config file was loaded.") + default: + writer.WriteStringLine(fmt.Sprintf("A config file was loaded from %q", configFilePath)) + } + + writer.WritePaddedStringLine("The following locations are searched for config files (in this order):", 1) + + for i, filePath := range config.FilePaths() { + writer.WriteStringLine(fmt.Sprintf("%d. %s", i+1, filePath)) + } + + writer.WriteNewLine() + }) +} + func printSources() { var sourceStrings []string @@ -237,6 +258,8 @@ func main() { switch act.Type() { case action.PrintConfig: printConfig() + case action.DebugConfig: + printConfigDebug() case action.ListSources: printSources() case action.PrintVersion: diff --git a/internal/action/action.go b/internal/action/action.go index ea9c111..8ea344c 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -12,6 +12,7 @@ import ( const ( DefineWord Type = iota PrintConfig + DebugConfig ListSources PrintVersion ) @@ -24,6 +25,7 @@ type Action struct { flagSet *flag.FlagSet flag struct { printConfig bool + debugConfig bool listSources bool printVersion bool } @@ -38,6 +40,7 @@ func Setup(flags *flag.FlagSet) *Action { // Define our flags flags.BoolVar(&act.flag.printConfig, "print-config", false, "To print the current configuration") + flags.BoolVar(&act.flag.debugConfig, "debug-config", false, "To print debug info about the configuration") flags.BoolVar(&act.flag.listSources, "list-sources", false, "To print the available sources") flags.BoolVar(&act.flag.printVersion, "version", false, "To print the app's version info") @@ -61,6 +64,8 @@ func (a *Action) Type() Type { switch { case a.flag.printConfig: return PrintConfig + case a.flag.debugConfig: + return DebugConfig case a.flag.listSources: return ListSources case a.flag.printVersion: From 0891fc555d7137441897cb7c1d17e66b27b3a498 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Tue, 5 Mar 2024 02:04:44 -0700 Subject: [PATCH 8/9] Adding documentation for the `--debug-config` flag --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bccd655..d0cc297 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ The path of the configuration file to load can be specified via the `--config-fi 1. `$XDG_CONFIG_HOME/define/config.json` (This is only searched for when the `$XDG_CONFIG_HOME` env variable is set) 2. `~/.define.conf.json` (Where `~` is equal to your `$HOME` or user directory for your OS) +To see which config file has been loaded, and to check what paths are searched for config files, use the `--debug-config` flag. + To print the default values of the configuration, simply use the `--print-config` flag. This can also be used to initialize a configuration file, for example: ```shell From dff05a28f963e021d0fdb7920922dd47efa49d96 Mon Sep 17 00:00:00 2001 From: Trevor Suarez Date: Tue, 5 Mar 2024 21:24:58 -0700 Subject: [PATCH 9/9] Fixing style formatting --- internal/config/file.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/config/file.go b/internal/config/file.go index a7828fa..7bb9136 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -16,9 +16,7 @@ const ( oldDefaultConfigFilePath = "~/.define.conf.json" ) -var ( - userHomeDirPath string -) +var userHomeDirPath string // userHomeDir returns the user's home directory, caching the value upon first // calculation, and without worrying about errors about detection.