diff --git a/cmd/config.go b/cmd/config.go index 52c9e09fc23..174074a1999 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -22,8 +22,8 @@ import ( "github.com/Masterminds/sprig" "github.com/pelletier/go-toml" "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/twpayne/chezmoi/internal/chezmoi" + "github.com/twpayne/chezmoi/internal/configparser" vfs "github.com/twpayne/go-vfs" xdg "github.com/twpayne/go-xdg/v3" bolt "go.etcd.io/bbolt" @@ -31,41 +31,42 @@ import ( ) type sourceVCSConfig struct { - Command string - AutoCommit bool - AutoPush bool - Init interface{} - Pull interface{} + Command string `json:"command" toml:"command" yaml:"command"` + AutoCommit bool `json:"autoCommit" toml:"autoCommit" yaml:"autoCommit"` + AutoPush bool `json:"autoPush" toml:"autoPush" yaml:"autoPush"` + Init interface{} `json:"init" toml:"init" yaml:"init"` + Pull interface{} `json:"pull" toml:"pull" yaml:"pull"` } // A Config represents a configuration. type Config struct { - configFile string + configFileName string + configFile *os.File err error fs vfs.FS mutator chezmoi.Mutator - SourceDir string - DestDir string - Umask permValue - DryRun bool - Follow bool - Remove bool - Verbose bool - Color string - Debug bool - GPG chezmoi.GPG - GPGRecipient string - SourceVCS sourceVCSConfig - Merge mergeConfig - Bitwarden bitwardenCmdConfig - GenericSecret genericSecretCmdConfig - Gopass gopassCmdConfig - KeePassXC keePassXCCmdConfig - Lastpass lastpassCmdConfig - Onepassword onepasswordCmdConfig - Vault vaultCmdConfig - Pass passCmdConfig - Data map[string]interface{} + SourceDir string `json:"sourceDir" toml:"sourceDir" yaml:"sourceDir"` + DestDir string `json:"destDir" toml:"destDir" yaml:"destDir"` + Umask permValue `json:"umask" toml:"umask" yaml:"umask"` + DryRun bool `json:"dryRun" toml:"dryRun" yaml:"dryRun"` + Follow bool `json:"follow" toml:"follow" yaml:"follow"` + Remove bool `json:"remove" toml:"remove" yaml:"remove"` + Verbose bool `json:"verbose" toml:"verbose" yaml:"verbose"` + Color string `json:"color" toml:"color" yaml:"color"` + Debug bool `json:"debug" toml:"debug" yaml:"debug"` + GPG chezmoi.GPG `json:"gpg" toml:"gpg" yaml:"gpg"` + GPGRecipient string `json:"gpgRecipient" toml:"gpgRecipient" yaml:"gpgRecipient"` + SourceVCS sourceVCSConfig `json:"sourceVCS" toml:"sourceVCS" yaml:"sourceVCS"` + Merge mergeConfig `json:"merge" toml:"merge" yaml:"merge"` + Bitwarden bitwardenCmdConfig `json:"bitwarden" toml:"bitwarden" yaml:"bitwarden"` + GenericSecret genericSecretCmdConfig `json:"genericSecret" toml:"genericSecret" yaml:"genericSecret"` + Gopass gopassCmdConfig `json:"gopass" toml:"gopass" yaml:"gopass"` + KeePassXC keePassXCCmdConfig `json:"keePassXC" toml:"keePassXC" yaml:"keePassXC"` + Lastpass lastpassCmdConfig `json:"lastpass" toml:"lastpass" yaml:"lastpass"` + Onepassword onepasswordCmdConfig `json:"onepassword" toml:"onepassword" yaml:"onepassword"` + Vault vaultCmdConfig `json:"vault" toml:"vault" yaml:"vault"` + Pass passCmdConfig `json:"pass" toml:"pass" yaml:"pass"` + Data map[string]interface{} `json:"data" toml:"data" yaml:"data"` colored bool maxDiffDataSize int templateFuncs template.FuncMap @@ -277,6 +278,13 @@ func (c *Config) ensureSourceDirectory() error { } } +func (c *Config) getConfigFileName() string { + if c.configFile != nil { + return c.configFile.Name() + } + return c.getDefaultConfigFileName(c.bds) +} + func (c *Config) getData() (map[string]interface{}, error) { defaultData, err := c.getDefaultData() if err != nil { @@ -291,6 +299,17 @@ func (c *Config) getData() (map[string]interface{}, error) { return data, nil } +func (c *Config) getDefaultConfigFileName(bds *xdg.BaseDirectorySpecification) string { + for _, configDir := range bds.ConfigDirs { + configFileName, _ := configparser.FindConfig(c.fs, filepath.Join(configDir, "chezmoi", "chezmoi")) + if configFileName != "" { + return configFileName + } + } + // Fallback to XDG Base Directory Specification default. + return filepath.Join(bds.ConfigHome, "chezmoi", "chezmoi.toml") +} + func (c *Config) getDefaultData() (map[string]interface{}, error) { data := map[string]interface{}{ "arch": runtime.GOARCH, @@ -381,7 +400,7 @@ func (c *Config) getEntries(ts *chezmoi.TargetState, args []string) ([]chezmoi.E } func (c *Config) getPersistentState(options *bolt.Options) (chezmoi.PersistentState, error) { - persistentStateFile := c.getPersistentStateFile() + persistentStateFile := c.getPersistentStateFileName() if c.DryRun { if options == nil { options = &bolt.Options{} @@ -391,17 +410,8 @@ func (c *Config) getPersistentState(options *bolt.Options) (chezmoi.PersistentSt return chezmoi.NewBoltPersistentState(c.fs, persistentStateFile, options) } -func (c *Config) getPersistentStateFile() string { - if c.configFile != "" { - return filepath.Join(filepath.Dir(c.configFile), "chezmoistate.boltdb") - } - for _, configDir := range c.bds.ConfigDirs { - persistentStateFile := filepath.Join(configDir, "chezmoi", "chezmoistate.boltdb") - if _, err := os.Stat(persistentStateFile); err == nil { - return persistentStateFile - } - } - return filepath.Join(filepath.Dir(getDefaultConfigFile(c.bds)), "chezmoistate.boltdb") +func (c *Config) getPersistentStateFileName() string { + return filepath.Join(filepath.Dir(c.getConfigFileName()), "chezmoistate.boltdb") } func (c *Config) getTargetState(populateOptions *chezmoi.PopulateOptions) (*chezmoi.TargetState, error) { @@ -514,20 +524,6 @@ func getAsset(name string) ([]byte, error) { return ioutil.ReadAll(r) } -func getDefaultConfigFile(bds *xdg.BaseDirectorySpecification) string { - // Search XDG Base Directory Specification config directories first. - for _, configDir := range bds.ConfigDirs { - for _, extension := range viper.SupportedExts { - configFilePath := filepath.Join(configDir, "chezmoi", "chezmoi."+extension) - if _, err := os.Stat(configFilePath); err == nil { - return configFilePath - } - } - } - // Fallback to XDG Base Directory Specification default. - return filepath.Join(bds.ConfigHome, "chezmoi", "chezmoi.toml") -} - func getDefaultSourceDir(bds *xdg.BaseDirectorySpecification) string { // Check for XDG Base Directory Specification data directories first. for _, dataDir := range bds.DataDirs { diff --git a/cmd/doctor.go b/cmd/doctor.go index 7ad30ffa2a9..347fc2cec96 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -131,7 +131,7 @@ func (c *Config) runDoctorCmd(cmd *cobra.Command, args []string) error { }, &doctorFileCheck{ name: "configuration file", - path: c.configFile, + path: c.getConfigFileName(), }, &doctorBinaryCheck{ name: "shell", diff --git a/cmd/editconfig.go b/cmd/editconfig.go index fe3ce260082..7b3cf0a2b0e 100644 --- a/cmd/editconfig.go +++ b/cmd/editconfig.go @@ -2,12 +2,12 @@ package cmd import ( "fmt" + "os" "path/filepath" "github.com/spf13/cobra" - "github.com/spf13/viper" + "github.com/twpayne/chezmoi/internal/configparser" vfs "github.com/twpayne/go-vfs" - vfsafero "github.com/twpayne/go-vfsafero" ) var editConfigCommand = &cobra.Command{ @@ -24,24 +24,24 @@ func init() { } func (c *Config) runEditConfigCmd(cmd *cobra.Command, args []string) error { - if err := vfs.MkdirAll(c.mutator, filepath.Dir(c.configFile), 0777); err != nil { + configFileName := c.getConfigFileName() + if err := vfs.MkdirAll(c.mutator, filepath.Dir(configFileName), 0777); err != nil { return err } - if err := c.runEditor(c.configFile); err != nil { + if err := c.runEditor(configFileName); err != nil { return err } // Warn the user of any errors reading the config file. - v := viper.New() - v.SetFs(vfsafero.NewAferoFS(c.fs)) - v.SetConfigFile(c.configFile) - err := v.ReadInConfig() - if err == nil { - err = v.Unmarshal(&Config{}) - } + configFile, err := os.Open(configFileName) if err != nil { - c.warn(fmt.Sprintf("%s: %v", c.configFile, err)) + c.warn(fmt.Sprintf("%s: %v", configFileName, err)) + return nil + } + defer configFile.Close() + if err := configparser.ParseConfig(configFile, &Config{}); err != nil { + c.warn(fmt.Sprintf("%s: %v", configFileName, err)) } return nil diff --git a/cmd/init.go b/cmd/init.go index ab315f2263a..be771264fcd 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -10,8 +10,8 @@ import ( "text/template" "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/twpayne/chezmoi/internal/chezmoi" + "github.com/twpayne/chezmoi/internal/configparser" vfs "github.com/twpayne/go-vfs" ) @@ -110,7 +110,7 @@ func (c *Config) runInitCmd(cmd *cobra.Command, args []string) error { } func (c *Config) createConfigFile() error { - filename, ext, data, err := c.findConfigTemplate() + filename, data, err := c.findConfigTemplate() if err != nil { return err } @@ -152,25 +152,26 @@ func (c *Config) createConfigFile() error { return err } - viper.SetConfigType(ext) - if err := viper.ReadConfig(contents); err != nil { + configFile, err := os.Open(configPath) + if err != nil { return err } - return viper.Unmarshal(c) + defer configFile.Close() + return configparser.ParseConfig(configFile, c) } -func (c *Config) findConfigTemplate() (string, string, string, error) { - for _, ext := range viper.SupportedExts { - contents, err := c.fs.ReadFile(filepath.Join(c.SourceDir, ".chezmoi."+ext+chezmoi.TemplateSuffix)) +func (c *Config) findConfigTemplate() (string, string, error) { + for _, ext := range configparser.Extensions() { + contents, err := c.fs.ReadFile(filepath.Join(c.SourceDir, ".chezmoi"+ext+chezmoi.TemplateSuffix)) switch { case os.IsNotExist(err): continue case err != nil: - return "", "", "", err + return "", "", err } - return "chezmoi." + ext, ext, string(contents), nil + return "chezmoi" + ext, string(contents), nil } - return "", "", "", nil + return "", "", nil } func (c *Config) promptString(field string) string { diff --git a/cmd/merge.go b/cmd/merge.go index b69201d3259..ae8267075bd 100644 --- a/cmd/merge.go +++ b/cmd/merge.go @@ -21,8 +21,8 @@ var mergeCmd = &cobra.Command{ } type mergeConfig struct { - Command string - Args []string + Command string `json:"command" toml:"command" yaml:"command"` + Args []string `json:"args" toml:"args" yaml:"args"` } func init() { diff --git a/cmd/root.go b/cmd/root.go index ba42b9b592f..32c94d43fe4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/twpayne/chezmoi/internal/chezmoi" + "github.com/twpayne/chezmoi/internal/configparser" vfs "github.com/twpayne/go-vfs" xdg "github.com/twpayne/go-xdg/v3" "golang.org/x/crypto/ssh/terminal" @@ -64,7 +65,7 @@ func init() { persistentFlags := rootCmd.PersistentFlags() - persistentFlags.StringVarP(&config.configFile, "config", "c", getDefaultConfigFile(config.bds), "config file") + persistentFlags.StringVarP(&config.configFileName, "config", "c", config.getDefaultConfigFileName(config.bds), "config file") persistentFlags.BoolVarP(&config.DryRun, "dry-run", "n", false, "dry run") panicOnError(viper.BindPFlag("dry-run", persistentFlags.Lookup("dry-run"))) @@ -91,19 +92,15 @@ func init() { panicOnError(viper.BindPFlag("debug", persistentFlags.Lookup("debug"))) cobra.OnInitialize(func() { - _, err := os.Stat(config.configFile) + config.configFile, config.err = config.fs.Open(config.configFileName) switch { - case err == nil: - viper.SetConfigFile(config.configFile) - config.err = viper.ReadInConfig() - if config.err == nil { - config.err = viper.Unmarshal(&config) - } + case config.err == nil: + config.err = configparser.ParseConfig(config.configFile, &config) if config.err == nil { config.err = config.validateData() } if config.err != nil { - config.warn(fmt.Sprintf("%s: %v", config.configFile, config.err)) + config.warn(fmt.Sprintf("%s: %v", config.configFileName, config.err)) } case os.IsNotExist(err): default: diff --git a/cmd/secretbitwarden.go b/cmd/secretbitwarden.go index 86d4d70d8a9..bbabec2e701 100644 --- a/cmd/secretbitwarden.go +++ b/cmd/secretbitwarden.go @@ -19,7 +19,7 @@ var bitwardenCmd = &cobra.Command{ } type bitwardenCmdConfig struct { - Command string + Command string `json:"command" toml:"command" yaml:"command"` } var bitwardenCache = make(map[string]interface{}) diff --git a/cmd/secretgeneric.go b/cmd/secretgeneric.go index 67b96b17a6b..e34030a518c 100644 --- a/cmd/secretgeneric.go +++ b/cmd/secretgeneric.go @@ -20,7 +20,7 @@ var genericSecretCmd = &cobra.Command{ } type genericSecretCmdConfig struct { - Command string + Command string `json:"command" toml:"command" yaml:"command"` } var ( diff --git a/cmd/secretgopass.go b/cmd/secretgopass.go index f459e9efe21..4252d11fd74 100644 --- a/cmd/secretgopass.go +++ b/cmd/secretgopass.go @@ -17,7 +17,7 @@ var gopassCmd = &cobra.Command{ } type gopassCmdConfig struct { - Command string + Command string `json:"command" toml:"command" yaml:"command"` } var gopassCache = make(map[string]string) diff --git a/cmd/secretkeepassxc.go b/cmd/secretkeepassxc.go index fd0951ae14a..1c39e579808 100644 --- a/cmd/secretkeepassxc.go +++ b/cmd/secretkeepassxc.go @@ -24,9 +24,9 @@ var keePassXCCmd = &cobra.Command{ } type keePassXCCmdConfig struct { - Command string - Database string - Args []string + Command string `json:"command" toml:"command" yaml:"command"` + Database string `json:"database" toml:"database" yaml:"database"` + Args []string `json:"args" toml:"args" yaml:"args"` } type keePassXCAttributeCacheKey struct { diff --git a/cmd/secretlastpass.go b/cmd/secretlastpass.go index 47b42cb749d..35a2ca58f5c 100644 --- a/cmd/secretlastpass.go +++ b/cmd/secretlastpass.go @@ -34,7 +34,7 @@ var ( ) type lastpassCmdConfig struct { - Command string + Command string `json:"command" toml:"command" yaml:"command"` versionCheckOnce sync.Once } diff --git a/cmd/secretonepassword.go b/cmd/secretonepassword.go index 7b511a875ed..1f6b30cd531 100644 --- a/cmd/secretonepassword.go +++ b/cmd/secretonepassword.go @@ -18,7 +18,7 @@ var onepasswordCmd = &cobra.Command{ } type onepasswordCmdConfig struct { - Command string + Command string `json:"command" toml:"command" yaml:"command"` } var ( diff --git a/cmd/secretpass.go b/cmd/secretpass.go index 1a64318208d..78dc7306e53 100644 --- a/cmd/secretpass.go +++ b/cmd/secretpass.go @@ -17,7 +17,7 @@ var passCmd = &cobra.Command{ } type passCmdConfig struct { - Command string + Command string `json:"command" toml:"command" yaml:"command"` } var passCache = make(map[string]string) diff --git a/cmd/secretvault.go b/cmd/secretvault.go index d00ed0953f5..36ce4e5f64a 100644 --- a/cmd/secretvault.go +++ b/cmd/secretvault.go @@ -18,7 +18,7 @@ var vaultCmd = &cobra.Command{ } type vaultCmdConfig struct { - Command string + Command string `json:"command" toml:"command" yaml:"command"` } var vaultCache = make(map[string]interface{}) diff --git a/docs/HOWTO.md b/docs/HOWTO.md index 93718c28a9f..1eeff1c4d5a 100644 --- a/docs/HOWTO.md +++ b/docs/HOWTO.md @@ -115,10 +115,8 @@ to machine. For example, for your home machine: If you intend to store private data (e.g. access tokens) in `~/.config/chezmoi/chezmoi.toml`, make sure it has permissions `0600`. -If you prefer, you can use any format supported by -[Viper](https://github.com/spf13/viper) for your configuration file. This -includes JSON, YAML, and TOML. Variable names must start with a letter and be -followed by zero or more letters or digits. +You can use JSON, YAML, or TOML for your configuration file. Variable names must +start with a letter and be followed by zero or more letters or digits. Then, add `~/.gitconfig` to chezmoi using the `-T` flag to automatically turn it in to a template: diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 31f3f0ab52c..6de13180cdb 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -159,10 +159,8 @@ timestamp. chezmoi searches for its configuration file according to the [XDG Base Directory Specification](https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html) -and supports all formats supported by -[`github.com/spf13/viper`](https://github.com/spf13/viper), namely JSON, TOML, -YAML, macOS property file format, and HCL. The basename of the config file is -chezmoi, and the first config file found is used. +and supports JSON, TOML, YAML. The basename of the config file is chezmoi, and +the first config file found is used. ### Configuration variables diff --git a/go.mod b/go.mod index 55599f99b31..2c835a64710 100644 --- a/go.mod +++ b/go.mod @@ -24,17 +24,13 @@ require ( github.com/mitchellh/reflectwalk v1.0.1 // indirect github.com/pelletier/go-toml v1.6.0 github.com/pkg/diff v0.0.0-20190930165518-531926345625 - github.com/spf13/afero v1.2.2 // indirect - github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v0.0.5 - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.6.2 + github.com/spf13/viper v1.4.0 github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.4.0 github.com/twpayne/go-shell v0.1.1 github.com/twpayne/go-vfs v1.3.6 - github.com/twpayne/go-vfsafero v1.0.0 github.com/twpayne/go-xdg/v3 v3.1.0 github.com/yuin/goldmark v1.1.21 // indirect github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717 @@ -44,8 +40,8 @@ require ( golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 google.golang.org/appengine v1.6.5 // indirect - gopkg.in/ini.v1 v1.51.1 // indirect gopkg.in/yaml.v2 v2.2.7 + gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 ) // Temporary while waiting for https://github.com/spf13/cobra/pull/754 to be merged diff --git a/go.sum b/go.sum index b6fb2cc189e..e762c36d8e0 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,6 @@ github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -110,8 +108,6 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -130,8 +126,6 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= @@ -185,31 +179,20 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= -github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= @@ -220,19 +203,14 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twpayne/cobra v0.0.6 h1:zJ26aak/ChId/jCdBCgrqJR6hixAOGLC1fo+ONtZPQc= github.com/twpayne/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/twpayne/go-shell v0.1.1 h1:Kr1hSEFrPBTRmhOW8woaj7ZxV3/OH7Qefg86OFJ5DFc= github.com/twpayne/go-shell v0.1.1/go.mod h1:H/gzux0DOH5jsjQSHXs6rs2Onxy+V4j6ycZTOulC0l8= -github.com/twpayne/go-vfs v1.0.1/go.mod h1:OIXA6zWkcn7Jk46XT7ceYqBMeIkfzJ8WOBhGJM0W4y8= github.com/twpayne/go-vfs v1.0.5/go.mod h1:OIXA6zWkcn7Jk46XT7ceYqBMeIkfzJ8WOBhGJM0W4y8= github.com/twpayne/go-vfs v1.3.6 h1:dKR5suwT6WnNweNNQjts3Wih/sBTHLt4XbbEbapnWEE= github.com/twpayne/go-vfs v1.3.6/go.mod h1:BH2oQurpkb3roQDR7hZH+9DITZidl6JHOEfHlCModXY= -github.com/twpayne/go-vfsafero v1.0.0 h1:ZlH32HF4OoVX/aRqc5bZa+2+M+/ezmJ4XYpT0ShtZNc= -github.com/twpayne/go-vfsafero v1.0.0/go.mod h1:rs2H15b2z0euJzwyoBS63eUHZgBhNXVQfIFfRp8DKEk= github.com/twpayne/go-xdg/v3 v3.1.0 h1:AxX5ZLJIzqYHJh+4uGxWT97ySh1ND1bJLjqMxdYF+xs= github.com/twpayne/go-xdg/v3 v3.1.0/go.mod h1:z6/LkoG2gtuzrsxEqPRoEjccS5Q35GK+lguVP0K3L9o= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -303,7 +281,6 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -317,10 +294,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -330,4 +303,6 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo= +gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/chezmoi/gpg.go b/internal/chezmoi/gpg.go index ec88afecef2..c514da0fcdd 100644 --- a/internal/chezmoi/gpg.go +++ b/internal/chezmoi/gpg.go @@ -9,8 +9,8 @@ import ( // GPG interfaces with gpg. type GPG struct { - Recipient string - Symmetric bool + Recipient string `json:"recipient" toml:"recipient" yaml:"recipient"` + Symmetric bool `json:"symmetric" toml:"symmetric" yaml:"symmetric"` } // Decrypt decrypts ciphertext. filename is used as a hint for naming temporary diff --git a/internal/configparser/configparser.go b/internal/configparser/configparser.go new file mode 100644 index 00000000000..6104e9bd41b --- /dev/null +++ b/internal/configparser/configparser.go @@ -0,0 +1,61 @@ +package configparser + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + + "github.com/twpayne/go-vfs" +) + +type parser func(io.Reader, interface{}) error + +var parsers = make(map[string]parser) + +// Extensions returns all the supported extensions in alphabetical order. +func Extensions() []string { + extensions := make([]string, 0, len(parsers)) + for extension := range parsers { + extensions = append(extensions, extension) + } + sort.Strings(extensions) + return extensions +} + +// FindConfig finds the first config file named filename or with basename +// filename. +func FindConfig(fs vfs.Stater, filename string) (string, error) { + info, err := fs.Stat(filename) + switch { + case err == nil && info.Mode().IsRegular(): + return filename, err + case !os.IsNotExist(err): + return "", err + } + + for _, extension := range Extensions() { + info, err := fs.Stat(filename + extension) + switch { + case err == nil && info.Mode().IsRegular(): + return filename + extension, err + case !os.IsNotExist(err): + return "", err + } + } + + return "", nil +} + +// ParseConfig parses r into value. +func ParseConfig(f *os.File, value interface{}) error { + if f == nil { + return nil + } + parser, ok := parsers[filepath.Ext(f.Name())] + if !ok { + return fmt.Errorf("%s: unsupported format", f.Name()) + } + return parser(f, value) +} diff --git a/internal/configparser/configparser_test.go b/internal/configparser/configparser_test.go new file mode 100644 index 00000000000..19d5e34cd71 --- /dev/null +++ b/internal/configparser/configparser_test.go @@ -0,0 +1,135 @@ +package configparser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/twpayne/go-vfs/vfst" +) + +func TestExtensions(t *testing.T) { + expectedExtensions := []string{ + ".json", + ".toml", + ".yaml", + ".yml", + } + assert.Equal(t, expectedExtensions, Extensions()) +} + +func TestParseConfig(t *testing.T) { + type Config struct { + Format string `json:"format" toml:"format" yaml:"format"` + } + for _, tc := range []struct { + name string + root interface{} + filename string + expectedFindConfigError bool + expectedParseConfigError bool + expectedConfig *Config + }{ + { + name: "no_config", + root: nil, + filename: "/home/user/.config/chezmoi/chezmoi", + expectedConfig: &Config{}, + }, + { + name: "json_file", + root: map[string]interface{}{ + "/home/user/.config/chezmoi/chezmoi.json": `{"format":"json"}`, + }, + filename: "/home/user/.config/chezmoi/chezmoi.json", + expectedConfig: &Config{ + Format: "json", + }, + }, + { + name: "json", + root: map[string]interface{}{ + "/home/user/.config/chezmoi/chezmoi.json": `{"format":"json"}`, + }, + filename: "/home/user/.config/chezmoi/chezmoi", + expectedConfig: &Config{ + Format: "json", + }, + }, + { + name: "toml", + root: map[string]interface{}{ + "/home/user/.config/chezmoi/chezmoi.toml": "format = \"toml\"\n", + }, + filename: "/home/user/.config/chezmoi/chezmoi", + expectedConfig: &Config{ + Format: "toml", + }, + }, + { + name: "yaml", + root: map[string]interface{}{ + "/home/user/.config/chezmoi/chezmoi.yaml": "format: yaml\n", + }, + filename: "/home/user/.config/chezmoi/chezmoi", + expectedConfig: &Config{ + Format: "yaml", + }, + }, + { + name: "yml", + root: map[string]interface{}{ + "/home/user/.config/chezmoi/chezmoi.yml": "format: yaml\n", + }, + filename: "/home/user/.config/chezmoi/chezmoi", + expectedConfig: &Config{ + Format: "yaml", + }, + }, + { + name: "file_does_not_exist", + filename: "/home/user/.config/chezmoi/chezmoi.json", + expectedConfig: &Config{}, + }, + { + name: "json_file_is_directory", + root: map[string]interface{}{ + "/home/user/.config/chezmoi/chezmoi.json/foo": "bar", + }, + filename: "/home/user/.config/chezmoi/chezmoi.json", + expectedConfig: &Config{}, + }, + { + name: "unsupported_format", + root: map[string]interface{}{ + "/home/user/.config/chezmoi/chezmoi.properties": ``, + }, + filename: "/home/user/.config/chezmoi/chezmoi.properties", + expectedParseConfigError: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + fs, cleanup, err := vfst.NewTestFS(tc.root) + require.NoError(t, err) + defer cleanup() + configFileName, err := FindConfig(fs, tc.filename) + if tc.expectedFindConfigError { + require.Error(t, err) + return + } + actualConfig := &Config{} + if configFileName != "" { + configFile, err := fs.Open(configFileName) + require.NoError(t, err) + defer configFile.Close() + err = ParseConfig(configFile, actualConfig) + if tc.expectedParseConfigError { + require.Error(t, err) + return + } + } + require.NoError(t, err) + assert.Equal(t, tc.expectedConfig, actualConfig) + }) + } +} diff --git a/internal/configparser/json.go b/internal/configparser/json.go new file mode 100644 index 00000000000..96ff445fd0f --- /dev/null +++ b/internal/configparser/json.go @@ -0,0 +1,14 @@ +package configparser + +import ( + "encoding/json" + "io" +) + +func init() { + parsers[".json"] = parseJSON +} + +func parseJSON(r io.Reader, value interface{}) error { + return json.NewDecoder(r).Decode(value) +} diff --git a/internal/configparser/toml.go b/internal/configparser/toml.go new file mode 100644 index 00000000000..e9a69838d0f --- /dev/null +++ b/internal/configparser/toml.go @@ -0,0 +1,15 @@ +package configparser + +import ( + "io" + + "github.com/pelletier/go-toml" +) + +func init() { + parsers[".toml"] = parseTOML +} + +func parseTOML(r io.Reader, value interface{}) error { + return toml.NewDecoder(r).Decode(value) +} diff --git a/internal/configparser/yaml.go b/internal/configparser/yaml.go new file mode 100644 index 00000000000..55b3385596c --- /dev/null +++ b/internal/configparser/yaml.go @@ -0,0 +1,16 @@ +package configparser + +import ( + "io" + + "gopkg.in/yaml.v3" +) + +func init() { + parsers[".yaml"] = parseYAML + parsers[".yml"] = parseYAML +} + +func parseYAML(r io.Reader, value interface{}) error { + return yaml.NewDecoder(r).Decode(value) +}