From 4320db494e7663e57281b0cb7b964ff4ae107ce1 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Tue, 5 Nov 2024 18:14:25 +0000 Subject: [PATCH 1/5] chore: Tidy up completion test --- internal/cmd/testdata/scripts/completion.txtar | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/cmd/testdata/scripts/completion.txtar b/internal/cmd/testdata/scripts/completion.txtar index fa766869e9d..affabeff533 100644 --- a/internal/cmd/testdata/scripts/completion.txtar +++ b/internal/cmd/testdata/scripts/completion.txtar @@ -34,14 +34,14 @@ cmp stdout golden/secrets exec chezmoi __complete apply --exclude= cmp stdout golden/entry-type-set -# test that apply --refresh-externals flags are completed -exec chezmoi __complete apply --refresh-externals= -cmp stdout golden/refresh-externals - -# test that entry type set values are completed +# test that apply --include values are completed exec chezmoi __complete apply --include= cmp stdout golden/entry-type-set +# test that apply --refresh-externals values are completed +exec chezmoi __complete apply --refresh-externals= +cmp stdout golden/refresh-externals + # test that archive --format values are completed exec chezmoi __complete archive --format= cmp stdout golden/archive-format From 8e5c0e9fe085311a96dba5bba455ed6947e0dfe7 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Tue, 5 Nov 2024 17:59:14 +0000 Subject: [PATCH 2/5] chore: Add choiceFlag type --- internal/cmd/choiceflag.go | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 internal/cmd/choiceflag.go diff --git a/internal/cmd/choiceflag.go b/internal/cmd/choiceflag.go new file mode 100644 index 00000000000..b75b33e5326 --- /dev/null +++ b/internal/cmd/choiceflag.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/twpayne/chezmoi/v2/internal/chezmoi" + "github.com/twpayne/chezmoi/v2/internal/chezmoimaps" + "github.com/twpayne/chezmoi/v2/internal/chezmoiset" +) + +// A choiceFlag is a flag which accepts a limited set of allowed values. +type choiceFlag struct { + value string + allowedValues chezmoiset.Set[string] + uniqueAbbreviations map[string]string +} + +// newChoiceFlag returns a new choiceFlag with the given value and allowed +// values. If value is not allowed then it panics. +func newChoiceFlag(value string, allowedValues []string) *choiceFlag { + allowedValuesSet := chezmoiset.New(allowedValues...) + if !allowedValuesSet.Contains(value) { + panic("value not allowed") + } + return &choiceFlag{ + value: value, + allowedValues: allowedValuesSet, + uniqueAbbreviations: chezmoi.UniqueAbbreviations(allowedValues), + } +} + +// FlagCompletionFunc returns f's flag completion function. +func (f *choiceFlag) FlagCompletionFunc() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return chezmoi.FlagCompletionFunc(chezmoimaps.SortedKeys(f.allowedValues)) +} + +// MarshalJSON implements encoding/json.Marshaler.MarshalJSON. +func (f *choiceFlag) MarshalJSON() ([]byte, error) { + return []byte(strconv.Quote(f.value)), nil +} + +// MarshalText implements encoding.TextMarshaler.MarshalText. +func (f *choiceFlag) MarshalText() ([]byte, error) { + return []byte(f.value), nil +} + +// Set implements github.com/spf13/pflag.Value.Set. +func (f *choiceFlag) Set(s string) error { + value, ok := f.uniqueAbbreviations[s] + if !ok { + return errors.New("invalid value") + } + f.value = value + return nil +} + +func (f *choiceFlag) String() string { + return f.value +} + +// Type implements github.com/spf13/pflag.Value.Type. +func (f *choiceFlag) Type() string { + sortedKeys := chezmoimaps.SortedKeys(f.allowedValues) + if len(sortedKeys) > 0 && sortedKeys[0] == "" { + sortedKeys[0] = "" + } + return strings.Join(sortedKeys, "|") +} + +// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON. +func (f *choiceFlag) UnmarshalJSON(data []byte) error { + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + if !f.allowedValues.Contains(value) { + return fmt.Errorf("%s: invalid value", value) + } + f.value = value + return nil +} + +// UnmarshalText implements encoding.TextUnmarshaler.UnmarshalText. +func (f *choiceFlag) UnmarshalText(text []byte) error { + value := string(text) + if !f.allowedValues.Contains(value) { + return fmt.Errorf("%s: invalid value", value) + } + f.value = value + return nil +} From f53e13ef2b869bda4cb8d7a38961a9042a515358 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Tue, 5 Nov 2024 18:01:22 +0000 Subject: [PATCH 3/5] chore: Use choiceFlag for data formats --- internal/cmd/config.go | 56 ++++++++--- internal/cmd/datacmd.go | 13 ++- internal/cmd/dataformat.go | 98 ------------------- internal/cmd/dumpcmd.go | 8 +- internal/cmd/dumpconfigcmd.go | 16 ++- internal/cmd/statecmd.go | 27 +++-- .../cmd/testdata/scripts/completion.txtar | 25 ++++- internal/cmd/testdata/scripts/edgecases.txtar | 4 +- 8 files changed, 118 insertions(+), 129 deletions(-) delete mode 100644 internal/cmd/dataformat.go diff --git a/internal/cmd/config.go b/internal/cmd/config.go index a0acd7619fc..031fe091bb9 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -103,7 +103,7 @@ type ConfigFile struct { Color autoBool `json:"color" mapstructure:"color" yaml:"color"` Data map[string]any `json:"data" mapstructure:"data" yaml:"data"` Env map[string]string `json:"env" mapstructure:"env" yaml:"env"` - Format writeDataFormat `json:"format" mapstructure:"format" yaml:"format"` + Format string `json:"format" mapstructure:"format" yaml:"format"` DestDirAbsPath chezmoi.AbsPath `json:"destDir" mapstructure:"destDir" yaml:"destDir"` GitHub gitHubConfig `json:"gitHub" mapstructure:"gitHub" yaml:"gitHub"` Hooks map[string]hookConfig `json:"hooks" mapstructure:"hooks" yaml:"hooks"` @@ -171,7 +171,7 @@ type Config struct { ConfigFile // Global configuration. - configFormat readDataFormat + configFormat *choiceFlag cpuProfile chezmoi.AbsPath debug bool dryRun bool @@ -196,9 +196,11 @@ type Config struct { apply applyCmdConfig archive archiveCmdConfig chattr chattrCmdConfig + data dataCmdConfig destroy destroyCmdConfig doctor doctorCmdConfig dump dumpCmdConfig + dumpConfig dumpConfigCmdConfig executeTemplate executeTemplateCmdConfig ignored ignoredCmdConfig _import importCmdConfig @@ -313,7 +315,6 @@ var ( commonFlagCompletionFuncs = map[string]func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective){ "exclude": chezmoi.EntryTypeSetFlagCompletionFunc, - "format": writeDataFormatFlagCompletionFunc, "include": chezmoi.EntryTypeSetFlagCompletionFunc, "path-style": chezmoi.PathStyleFlagCompletionFunc, "secrets": severityFlagCompletionFunc, @@ -342,6 +343,7 @@ func newConfig(options ...configOption) (*Config, error) { ConfigFile: newConfigFile(bds), // Global configuration. + configFormat: newChoiceFlag("", []string{"", "json", "toml", "yaml"}), homeDir: userHomeDir, templateFuncs: sprig.TxtFuncMap(), @@ -354,10 +356,17 @@ func newConfig(options ...configOption) (*Config, error) { filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), recursive: true, }, + data: dataCmdConfig{ + format: newChoiceFlag("", []string{"", "json", "yaml"}), + }, dump: dumpCmdConfig{ filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), + format: newChoiceFlag("", []string{"", "json", "yaml"}), recursive: true, }, + dumpConfig: dumpConfigCmdConfig{ + format: newChoiceFlag("", []string{"", "json", "yaml"}), + }, executeTemplate: executeTemplateCmdConfig{ stdinIsATTY: true, }, @@ -382,6 +391,17 @@ func newConfig(options ...configOption) (*Config, error) { filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), recursive: true, }, + state: stateCmdConfig{ + data: stateDataCmdConfig{ + format: newChoiceFlag("json", []string{"", "json", "yaml"}), + }, + dump: stateDumpCmdConfig{ + format: newChoiceFlag("json", []string{"", "json", "yaml"}), + }, + getBucket: stateGetBucketCmdConfig{ + format: newChoiceFlag("json", []string{"", "json", "yaml"}), + }, + }, unmanaged: unmanagedCmdConfig{ pathStyle: chezmoi.PathStyleSimple(chezmoi.PathStyleRelative), }, @@ -933,14 +953,19 @@ func (c *Config) decodeConfigBytes(format chezmoi.Format, data []byte, configFil // configFile. func (c *Config) decodeConfigFile(configFileAbsPath chezmoi.AbsPath, configFile *ConfigFile) error { var format chezmoi.Format - if c.configFormat == "" { + switch c.configFormat.String() { + case "": var err error format, err = chezmoi.FormatFromAbsPath(configFileAbsPath) if err != nil { return err } - } else { - format = c.configFormat.Format() + case "json": + format = chezmoi.FormatJSON + case "toml": + format = chezmoi.FormatTOML + case "yaml": + format = chezmoi.FormatYAML } configFileContents, err := c.fileSystem.ReadFile(configFileAbsPath.String()) @@ -1624,8 +1649,17 @@ func (c *Config) makeRunEWithSourceState( } // marshal formats data in dataFormat and writes it to the standard output. -func (c *Config) marshal(dataFormat writeDataFormat, data any) error { - marshaledData, err := dataFormat.Format().Marshal(data) +func (c *Config) marshal(dataFormat string, data any) error { + var format chezmoi.Format + switch dataFormat { + case "json": + format = chezmoi.FormatJSON + case "yaml": + format = chezmoi.FormatYAML + default: + return fmt.Errorf("%s: invalid format", dataFormat) + } + marshaledData, err := format.Marshal(data) if err != nil { return err } @@ -1660,7 +1694,7 @@ func (c *Config) newRootCmd() (*cobra.Command, error) { persistentFlags.VarP(&c.WorkingTreeAbsPath, "working-tree", "W", "Set working tree directory") persistentFlags.VarP(&c.customConfigFileAbsPath, "config", "c", "Set config file") - persistentFlags.Var(&c.configFormat, "config-format", "Set config file format") + persistentFlags.Var(c.configFormat, "config-format", "Set config file format") persistentFlags.Var(&c.cpuProfile, "cpu-profile", "Write a CPU profile to path") persistentFlags.BoolVar(&c.debug, "debug", c.debug, "Include debug information in output") persistentFlags.BoolVarP(&c.dryRun, "dry-run", "n", c.dryRun, "Do not make any modifications to the destination directory") @@ -1684,7 +1718,7 @@ func (c *Config) newRootCmd() (*cobra.Command, error) { persistentFlags.MarkHidden("safe"), rootCmd.MarkPersistentFlagDirname("source"), rootCmd.RegisterFlagCompletionFunc("color", autoBoolFlagCompletionFunc), - rootCmd.RegisterFlagCompletionFunc("config-format", readDataFormatFlagCompletionFunc), + rootCmd.RegisterFlagCompletionFunc("config-format", c.configFormat.FlagCompletionFunc()), rootCmd.RegisterFlagCompletionFunc("mode", chezmoi.ModeFlagCompletionFunc), rootCmd.RegisterFlagCompletionFunc("refresh-externals", chezmoi.RefreshExternalsFlagCompletionFunc), rootCmd.RegisterFlagCompletionFunc("use-builtin-age", autoBoolFlagCompletionFunc), @@ -2891,7 +2925,7 @@ func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile { MinDuration: 1 * time.Second, filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), }, - Format: writeDataFormatJSON, + Format: "json", Git: gitCmdConfig{ Command: "git", }, diff --git a/internal/cmd/datacmd.go b/internal/cmd/datacmd.go index 45176248832..de5c5271aa5 100644 --- a/internal/cmd/datacmd.go +++ b/internal/cmd/datacmd.go @@ -1,11 +1,17 @@ package cmd import ( + "cmp" + "github.com/spf13/cobra" "github.com/twpayne/chezmoi/v2/internal/chezmoi" ) +type dataCmdConfig struct { + format *choiceFlag +} + func (c *Config) newDataCmd() *cobra.Command { dataCmd := &cobra.Command{ Use: "data", @@ -19,7 +25,10 @@ func (c *Config) newDataCmd() *cobra.Command { ), } - dataCmd.Flags().VarP(&c.Format, "format", "f", "Output format") + dataCmd.Flags().VarP(c.data.format, "format", "f", "Output format") + if err := dataCmd.RegisterFlagCompletionFunc("format", c.data.format.FlagCompletionFunc()); err != nil { + panic(err) + } return dataCmd } @@ -31,5 +40,5 @@ func (c *Config) runDataCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - return c.marshal(c.Format, sourceState.TemplateData()) + return c.marshal(cmp.Or(c.data.format.String(), c.Format), sourceState.TemplateData()) } diff --git a/internal/cmd/dataformat.go b/internal/cmd/dataformat.go deleted file mode 100644 index 292ba142d28..00000000000 --- a/internal/cmd/dataformat.go +++ /dev/null @@ -1,98 +0,0 @@ -package cmd - -import ( - "errors" - "strings" - - "github.com/twpayne/chezmoi/v2/internal/chezmoi" -) - -// A readDataFormat is a format that chezmoi uses for reading (JSON, TOML, or -// YAML) and implements the github.com/spf13/pflag.Value interface. -type readDataFormat string - -// A writeDataFormat is format that chezmoi uses for writing (JSON or YAML) and -// implements the github.com/spf13/pflag.Value interface. -// -// TOML is not included as write format as it requires the top level value to be -// an object, and chezmoi occasionally requires the top level value to be a -// simple value or array. -type writeDataFormat string - -const ( - readDataFormatJSON readDataFormat = "json" - readDataFormatTOML readDataFormat = "toml" - readDataFormatYAML readDataFormat = "yaml" - - writeDataFormatJSON writeDataFormat = "json" - writeDataFormatYAML writeDataFormat = "yaml" -) - -var readDataFormatFlagCompletionFunc = chezmoi.FlagCompletionFunc([]string{ - string(readDataFormatJSON), - string(readDataFormatTOML), - string(readDataFormatYAML), -}) - -var writeDataFormatFlagCompletionFunc = chezmoi.FlagCompletionFunc([]string{ - string(writeDataFormatJSON), - string(writeDataFormatYAML), -}) - -// Set implements github.com/spf13/pflag.Value.Set. -func (f *readDataFormat) Set(s string) error { - switch strings.ToLower(s) { - case "json": - *f = readDataFormatJSON - case "toml": - *f = readDataFormatTOML - case "yaml": - *f = readDataFormatYAML - default: - return errors.New("invalid or unsupported data format") - } - return nil -} - -// Format returns f's format. -func (f readDataFormat) Format() chezmoi.Format { - return chezmoi.FormatsByName[string(f)] -} - -// String implements github.com/spf13/pflag.Value.String. -func (f readDataFormat) String() string { - return string(f) -} - -// Type implements github.com/spf13/pflag.Value.Type. -func (f readDataFormat) Type() string { - return "json|toml|yaml" -} - -// Format returns f's format. -func (f writeDataFormat) Format() chezmoi.Format { - return chezmoi.FormatsByName[string(f)] -} - -// Set implements github.com/spf13/pflag.Value.Set. -func (f *writeDataFormat) Set(s string) error { - switch strings.ToLower(s) { - case "json": - *f = writeDataFormatJSON - case "yaml": - *f = writeDataFormatYAML - default: - return errors.New("invalid or unsupported data format") - } - return nil -} - -// String implements github.com/spf13/pflag.Value.String. -func (f writeDataFormat) String() string { - return string(f) -} - -// Type implements github.com/spf13/pflag.Value.Type. -func (f writeDataFormat) Type() string { - return "json|yaml" -} diff --git a/internal/cmd/dumpcmd.go b/internal/cmd/dumpcmd.go index 69aa853ad00..4b5821db639 100644 --- a/internal/cmd/dumpcmd.go +++ b/internal/cmd/dumpcmd.go @@ -1,6 +1,8 @@ package cmd import ( + "cmp" + "github.com/spf13/cobra" "github.com/twpayne/chezmoi/v2/internal/chezmoi" @@ -8,6 +10,7 @@ import ( type dumpCmdConfig struct { filter *chezmoi.EntryTypeFilter + format *choiceFlag init bool parentDirs bool recursive bool @@ -28,7 +31,8 @@ func (c *Config) newDumpCmd() *cobra.Command { } dumpCmd.Flags().VarP(c.dump.filter.Exclude, "exclude", "x", "Exclude entry types") - dumpCmd.Flags().VarP(&c.Format, "format", "f", "Output format") + dumpCmd.Flags().VarP(c.dump.format, "format", "f", "Output format") + must(dumpCmd.RegisterFlagCompletionFunc("format", c.dump.format.FlagCompletionFunc())) dumpCmd.Flags().VarP(c.dump.filter.Include, "include", "i", "Include entry types") dumpCmd.Flags().BoolVar(&c.dump.init, "init", c.dump.init, "Recreate config file from template") dumpCmd.Flags().BoolVarP(&c.dump.parentDirs, "parent-dirs", "P", c.dump.parentDirs, "Dump all parent directories") @@ -49,5 +53,5 @@ func (c *Config) runDumpCmd(cmd *cobra.Command, args []string) error { }); err != nil { return err } - return c.marshal(c.Format, dumpSystem.Data()) + return c.marshal(cmp.Or(c.dump.format.String(), c.Format), dumpSystem.Data()) } diff --git a/internal/cmd/dumpconfigcmd.go b/internal/cmd/dumpconfigcmd.go index 428db1105c3..9f3c30a0ca4 100644 --- a/internal/cmd/dumpconfigcmd.go +++ b/internal/cmd/dumpconfigcmd.go @@ -1,6 +1,14 @@ package cmd -import "github.com/spf13/cobra" +import ( + "cmp" + + "github.com/spf13/cobra" +) + +type dumpConfigCmdConfig struct { + format *choiceFlag +} func (c *Config) newDumpConfigCmd() *cobra.Command { dumpConfigCmd := &cobra.Command{ @@ -15,11 +23,13 @@ func (c *Config) newDumpConfigCmd() *cobra.Command { ), } - dumpConfigCmd.Flags().VarP(&c.Format, "format", "f", "Output format") + dumpConfigCmd.Flags().VarP(c.dumpConfig.format, "format", "f", "Output format") + + must(dumpConfigCmd.RegisterFlagCompletionFunc("format", c.dumpConfig.format.FlagCompletionFunc())) return dumpConfigCmd } func (c *Config) runDumpConfigCmd(cmd *cobra.Command, args []string) error { - return c.marshal(c.Format, c) + return c.marshal(cmp.Or(c.dumpConfig.format.String(), c.Format), c) } diff --git a/internal/cmd/statecmd.go b/internal/cmd/statecmd.go index 41caaf34a0c..8de93a146c7 100644 --- a/internal/cmd/statecmd.go +++ b/internal/cmd/statecmd.go @@ -1,6 +1,7 @@ package cmd import ( + "cmp" "errors" "fmt" "io/fs" @@ -11,13 +12,19 @@ import ( ) type stateCmdConfig struct { + data stateDataCmdConfig delete stateDeleteCmdConfig deleteBucket stateDeleteBucketCmdConfig + dump stateDumpCmdConfig get stateGetCmdConfig getBucket stateGetBucketCmdConfig set stateSetCmdConfig } +type stateDataCmdConfig struct { + format *choiceFlag +} + type stateDeleteCmdConfig struct { bucket string key string @@ -27,6 +34,10 @@ type stateDeleteBucketCmdConfig struct { bucket string } +type stateDumpCmdConfig struct { + format *choiceFlag +} + type stateGetCmdConfig struct { bucket string key string @@ -34,6 +45,7 @@ type stateGetCmdConfig struct { type stateGetBucketCmdConfig struct { bucket string + format *choiceFlag } type stateSetCmdConfig struct { @@ -62,7 +74,8 @@ func (c *Config) newStateCmd() *cobra.Command { persistentStateModeReadOnly, ), } - stateDataCmd.Flags().VarP(&c.Format, "format", "f", "Output format") + stateDataCmd.Flags().VarP(c.state.data.format, "format", "f", "Output format") + must(stateDataCmd.RegisterFlagCompletionFunc("format", c.state.data.format.FlagCompletionFunc())) stateCmd.AddCommand(stateDataCmd) stateDeleteCmd := &cobra.Command{ @@ -99,7 +112,8 @@ func (c *Config) newStateCmd() *cobra.Command { persistentStateModeReadOnly, ), } - stateDumpCmd.Flags().VarP(&c.Format, "format", "f", "Output format") + stateDumpCmd.Flags().VarP(c.state.dump.format, "format", "f", "Output format") + must(stateDumpCmd.RegisterFlagCompletionFunc("format", c.state.dump.format.FlagCompletionFunc())) stateCmd.AddCommand(stateDumpCmd) stateGetCmd := &cobra.Command{ @@ -125,7 +139,8 @@ func (c *Config) newStateCmd() *cobra.Command { ), } stateGetBucketCmd.Flags().StringVar(&c.state.getBucket.bucket, "bucket", c.state.getBucket.bucket, "bucket") - stateGetBucketCmd.Flags().VarP(&c.Format, "format", "f", "Output format") + stateGetBucketCmd.Flags().VarP(c.state.getBucket.format, "format", "f", "Output format") + must(stateGetBucketCmd.RegisterFlagCompletionFunc("format", c.state.getBucket.format.FlagCompletionFunc())) stateCmd.AddCommand(stateGetBucketCmd) stateResetCmd := &cobra.Command{ @@ -162,7 +177,7 @@ func (c *Config) runStateDataCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - return c.marshal(c.Format, data) + return c.marshal(cmp.Or(c.state.data.format.String(), c.Format), data) } func (c *Config) runStateDeleteCmd(cmd *cobra.Command, args []string) error { @@ -188,7 +203,7 @@ func (c *Config) runStateDumpCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - return c.marshal(c.Format, data) + return c.marshal(cmp.Or(c.state.dump.format.String(), c.Format), data) } func (c *Config) runStateGetCmd(cmd *cobra.Command, args []string) error { @@ -204,7 +219,7 @@ func (c *Config) runStateGetBucketCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - return c.marshal(c.Format, data) + return c.marshal(cmp.Or(c.state.getBucket.format.String(), c.Format), data) } func (c *Config) runStateResetCmd(cmd *cobra.Command, args []string) error { diff --git a/internal/cmd/testdata/scripts/completion.txtar b/internal/cmd/testdata/scripts/completion.txtar index affabeff533..fff3bfd8b41 100644 --- a/internal/cmd/testdata/scripts/completion.txtar +++ b/internal/cmd/testdata/scripts/completion.txtar @@ -18,6 +18,10 @@ stdout '#compdef chezmoi' exec chezmoi __complete --color t cmp stdout golden/auto-bool-t +# test that --config-format flags are completed +exec chezmoi __complete --config-format '' +cmp stdout golden/config-format + # test that --mode values are completed exec chezmoi __complete --mode '' cmp stdout golden/mode @@ -48,15 +52,15 @@ cmp stdout golden/archive-format # test that data --format values are completed exec chezmoi __complete data --format= -cmp stdout golden/output-format +cmp stdout golden/output-format-with-empty # test that dump --format values are completed exec chezmoi __complete dump --format= -cmp stdout golden/output-format +cmp stdout golden/output-format-with-empty # test that dump-config --format values are completed exec chezmoi __complete dump-config --format= -cmp stdout golden/output-format +cmp stdout golden/output-format-with-empty # test that managed path style values are completed exec chezmoi __complete managed --path-style= @@ -64,11 +68,11 @@ cmp stdout golden/path-style # test that state data --format values are completed exec chezmoi __complete state data --format= -cmp stdout golden/output-format +cmp stdout golden/output-format-with-empty # test that state dump --format values are completed exec chezmoi __complete state dump --format= -cmp stdout golden/output-format +cmp stdout golden/output-format-with-empty # test that status path style values are completed exec chezmoi __complete status --path-style= @@ -93,6 +97,12 @@ zip t true :4 +-- golden/config-format -- + +json +toml +yaml +:4 -- golden/entry-type-set -- all always @@ -120,6 +130,11 @@ file symlink :4 -- golden/output-format -- +json +yaml +:4 +-- golden/output-format-with-empty -- + json yaml :4 diff --git a/internal/cmd/testdata/scripts/edgecases.txtar b/internal/cmd/testdata/scripts/edgecases.txtar index dccdf603a29..6fc6e5967f7 100644 --- a/internal/cmd/testdata/scripts/edgecases.txtar +++ b/internal/cmd/testdata/scripts/edgecases.txtar @@ -22,11 +22,11 @@ stderr 'not allowed in \.chezmoitemplates directory' # test that chezmoi data returns an error if an unknown read format is specified ! exec chezmoi init --config-format=yml -stderr 'invalid or unsupported data format' +stderr 'flag: invalid value' # test that chezmoi data returns an error if an unknown write format is specified ! exec chezmoi data --format=yml -stderr 'invalid or unsupported data format' +stderr 'flag: invalid value' skip 'FIXME make the following test pass' From efa60764337e27b27687a8cfdac413ad828f4ba5 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Tue, 5 Nov 2024 18:04:49 +0000 Subject: [PATCH 4/5] chore: Use choiceFlag for archive format --- .../docs/reference/commands/archive.md | 5 -- internal/chezmoi/archive.go | 48 ++----------------- internal/cmd/archivecmd.go | 27 +++++++---- internal/cmd/config.go | 1 + .../cmd/testdata/scripts/completion.txtar | 6 +-- 5 files changed, 23 insertions(+), 64 deletions(-) diff --git a/assets/chezmoi.io/docs/reference/commands/archive.md b/assets/chezmoi.io/docs/reference/commands/archive.md index 586978b2e2b..3a8e52aaa7f 100644 --- a/assets/chezmoi.io/docs/reference/commands/archive.md +++ b/assets/chezmoi.io/docs/reference/commands/archive.md @@ -13,13 +13,8 @@ the extension, otherwise the default is `tar`. | Supported formats | | ----------------- | | `tar` | -| `tar.bz2` | | `tar.gz` | -| `tar.xz` | -| `tar.zst` | -| `tbz2` | | `tgz` | -| `txz` | | `zip` | ### `-z`, `--gzip` diff --git a/internal/chezmoi/archive.go b/internal/chezmoi/archive.go index efcc50f6f0e..4d8b740ebdf 100644 --- a/internal/chezmoi/archive.go +++ b/internal/chezmoi/archive.go @@ -32,52 +32,12 @@ const ( ArchiveFormatTarGz ArchiveFormat = "tar.gz" ArchiveFormatTarXz ArchiveFormat = "tar.xz" ArchiveFormatTarZst ArchiveFormat = "tar.zst" - ArchiveFormatTbz2 ArchiveFormat = "tbz2" - ArchiveFormatTgz ArchiveFormat = "tgz" - ArchiveFormatTxz ArchiveFormat = "txz" ArchiveFormatZip ArchiveFormat = "zip" ) -var ArchiveFormatFlagCompletionFunc = FlagCompletionFunc([]string{ - ArchiveFormatTar.String(), - ArchiveFormatTarBz2.String(), - ArchiveFormatTarGz.String(), - ArchiveFormatTarXz.String(), - ArchiveFormatTarZst.String(), - ArchiveFormatTbz2.String(), - ArchiveFormatTgz.String(), - ArchiveFormatTxz.String(), - ArchiveFormatZip.String(), -}) - -type UnknownArchiveFormatError string - -func (e UnknownArchiveFormatError) Error() string { - if e == UnknownArchiveFormatError(ArchiveFormatUnknown) { - return "unknown archive format" - } - return string(e) + ": unknown archive format" -} - // An WalkArchiveFunc is called once for each entry in an archive. type WalkArchiveFunc func(name string, info fs.FileInfo, r io.Reader, linkname string) error -// Set implements github.com/spf13/pflag.Value.Set. -func (f *ArchiveFormat) Set(s string) error { - *f = ArchiveFormat(s) - return nil -} - -// String implements github.com/spf13/pflag.Value.String. -func (f ArchiveFormat) String() string { - return string(f) -} - -// Type implements github.com/spf13/pflag.Value.Type. -func (f ArchiveFormat) Type() string { - return "format" -} - // GuessArchiveFormat guesses the archive format from the name and data. func GuessArchiveFormat(name string, data []byte) ArchiveFormat { switch nameLower := strings.ToLower(name); { @@ -123,17 +83,17 @@ func WalkArchive(data []byte, format ArchiveFormat, f WalkArchiveFunc) error { switch format { case ArchiveFormatTar: // Already in tar format, do nothing. - case ArchiveFormatTarBz2, ArchiveFormatTbz2: + case ArchiveFormatTarBz2: // Decompress with bzip2. r = bzip2.NewReader(r) - case ArchiveFormatTarGz, ArchiveFormatTgz: + case ArchiveFormatTarGz: // Decompress with gzip. var err error r, err = gzip.NewReader(r) if err != nil { return err } - case ArchiveFormatTarXz, ArchiveFormatTxz: + case ArchiveFormatTarXz: // Decompress with xz. var err error r, err = xz.NewReader(r) @@ -148,7 +108,7 @@ func WalkArchive(data []byte, format ArchiveFormat, f WalkArchiveFunc) error { return err } default: - return UnknownArchiveFormatError(format) + return fmt.Errorf("%s: unknown archive format", format) } return walkArchiveTar(r, f) } diff --git a/internal/cmd/archivecmd.go b/internal/cmd/archivecmd.go index dbe44e2531f..5372d5c289e 100644 --- a/internal/cmd/archivecmd.go +++ b/internal/cmd/archivecmd.go @@ -2,6 +2,7 @@ package cmd import ( "archive/tar" + "fmt" "os/user" "strconv" "strings" @@ -15,7 +16,7 @@ import ( type archiveCmdConfig struct { filter *chezmoi.EntryTypeFilter - format chezmoi.ArchiveFormat + format *choiceFlag gzip bool init bool parentDirs bool @@ -37,29 +38,37 @@ func (c *Config) newArchiveCmd() *cobra.Command { } archiveCmd.Flags().VarP(c.archive.filter.Exclude, "exclude", "x", "Exclude entry types") - archiveCmd.Flags().VarP(&c.archive.format, "format", "f", "Set archive format") + archiveCmd.Flags().VarP(c.archive.format, "format", "f", "Set archive format") + must(archiveCmd.RegisterFlagCompletionFunc("format", c.archive.format.FlagCompletionFunc())) archiveCmd.Flags().BoolVarP(&c.archive.gzip, "gzip", "z", c.archive.gzip, "Compress output with gzip") archiveCmd.Flags().VarP(c.archive.filter.Exclude, "include", "i", "Include entry types") archiveCmd.Flags().BoolVar(&c.archive.init, "init", c.archive.init, "Recreate config file from template") archiveCmd.Flags().BoolVarP(&c.archive.parentDirs, "parent-dirs", "P", c.archive.parentDirs, "Archive parent directories") archiveCmd.Flags().BoolVarP(&c.archive.recursive, "recursive", "r", c.archive.recursive, "Recurse into subdirectories") - must(archiveCmd.RegisterFlagCompletionFunc("format", chezmoi.ArchiveFormatFlagCompletionFunc)) - return archiveCmd } func (c *Config) runArchiveCmd(cmd *cobra.Command, args []string) error { - format := c.archive.format - if format == chezmoi.ArchiveFormatUnknown { + var format chezmoi.ArchiveFormat + switch formatStr := c.archive.format.String(); formatStr { + case "": format = chezmoi.GuessArchiveFormat(c.outputAbsPath.String(), nil) if format == chezmoi.ArchiveFormatUnknown { format = chezmoi.ArchiveFormatTar } + case "tar": + format = chezmoi.ArchiveFormatTar + case "tar.gz", "tgz": + format = chezmoi.ArchiveFormatTarGz + case "zip": + format = chezmoi.ArchiveFormatZip + default: + return fmt.Errorf("%s: invalid format", formatStr) } gzipOutput := c.archive.gzip - if format == chezmoi.ArchiveFormatTarGz || format == chezmoi.ArchiveFormatTgz { + if format == chezmoi.ArchiveFormatTarGz { gzipOutput = true } @@ -69,12 +78,10 @@ func (c *Config) runArchiveCmd(cmd *cobra.Command, args []string) error { Close() error } switch format { - case chezmoi.ArchiveFormatTar, chezmoi.ArchiveFormatTarGz, chezmoi.ArchiveFormatTgz: + case chezmoi.ArchiveFormatTar, chezmoi.ArchiveFormatTarGz: archiveSystem = chezmoi.NewTarWriterSystem(&output, tarHeaderTemplate()) case chezmoi.ArchiveFormatZip: archiveSystem = chezmoi.NewZIPWriterSystem(&output, time.Now().UTC()) - default: - return chezmoi.UnknownArchiveFormatError(format) } if err := c.applyArgs(cmd.Context(), archiveSystem, chezmoi.EmptyAbsPath, args, applyArgsOptions{ cmd: cmd, diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 031fe091bb9..90722435634 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -354,6 +354,7 @@ func newConfig(options ...configOption) (*Config, error) { }, archive: archiveCmdConfig{ filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), + format: newChoiceFlag("tar.gz", []string{"", "tar", "tar.gz", "tgz", "zip"}), recursive: true, }, data: dataCmdConfig{ diff --git a/internal/cmd/testdata/scripts/completion.txtar b/internal/cmd/testdata/scripts/completion.txtar index fff3bfd8b41..80146957a63 100644 --- a/internal/cmd/testdata/scripts/completion.txtar +++ b/internal/cmd/testdata/scripts/completion.txtar @@ -83,14 +83,10 @@ exec chezmoi __complete unmanaged --path-style= cmp stdout golden/unmanaged-path-style -- golden/archive-format -- + tar -tar.bz2 tar.gz -tar.xz -tar.zst -tbz2 tgz -txz zip :4 -- golden/auto-bool-t -- From 68fb67ea0bc6c90dc2171ecb0eb5bd68f95945c0 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Tue, 5 Nov 2024 19:37:39 +0000 Subject: [PATCH 5/5] chore: Use choiceFlag for path style --- internal/chezmoi/pathstyle.go | 88 ------------------- internal/cmd/config.go | 13 ++- internal/cmd/managedcmd.go | 15 ++-- internal/cmd/statuscmd.go | 16 ++-- .../cmd/testdata/scripts/completion.txtar | 6 +- internal/cmd/testdata/scripts/unmanaged.txtar | 2 +- internal/cmd/unmanagedcmd.go | 14 +-- 7 files changed, 35 insertions(+), 119 deletions(-) delete mode 100644 internal/chezmoi/pathstyle.go diff --git a/internal/chezmoi/pathstyle.go b/internal/chezmoi/pathstyle.go deleted file mode 100644 index 320de16e32c..00000000000 --- a/internal/chezmoi/pathstyle.go +++ /dev/null @@ -1,88 +0,0 @@ -package chezmoi - -import ( - "fmt" - "strings" -) - -type ( - PathStyle string - PathStyleSimple string -) - -const ( - PathStyleAbsolute PathStyle = "absolute" - PathStyleRelative PathStyle = "relative" - PathStyleSourceAbsolute PathStyle = "source-absolute" - PathStyleSourceRelative PathStyle = "source-relative" -) - -var ( - PathStyleStrings = []string{ - PathStyleAbsolute.String(), - PathStyleRelative.String(), - PathStyleSourceAbsolute.String(), - PathStyleSourceRelative.String(), - } - - PathStyleFlagCompletionFunc = FlagCompletionFunc(PathStyleStrings) - - PathStyleSimpleStrings = []string{ - PathStyleAbsolute.String(), - PathStyleRelative.String(), - } - - PathStyleSimpleFlagCompletionFunc = FlagCompletionFunc(PathStyleSimpleStrings) -) - -// Set implements github.com/spf13/pflag.Value.Set. -func (p *PathStyle) Set(s string) error { - uniqueAbbreviations := UniqueAbbreviations(PathStyleStrings) - pathStyleStr, ok := uniqueAbbreviations[s] - if !ok { - return fmt.Errorf("%s: unknown path style", s) - } - *p = PathStyle(pathStyleStr) - return nil -} - -func (p PathStyle) String() string { - return string(p) -} - -// Type implements github.com/spf13/pflag.Value.Type. -func (p PathStyle) Type() string { - return strings.Join(PathStyleStrings, "|") -} - -func (p PathStyle) Copy() *PathStyle { - return &p -} - -// Set implements github.com/spf13/pflag.Value.Set. -func (p *PathStyleSimple) Set(s string) error { - uniqueAbbreviations := UniqueAbbreviations(PathStyleSimpleStrings) - pathStyleStr, ok := uniqueAbbreviations[s] - if !ok { - return fmt.Errorf("%s: unknown path style", s) - } - *p = PathStyleSimple(pathStyleStr) - return nil -} - -func (p PathStyleSimple) String() string { - return string(p) -} - -// Type implements github.com/spf13/pflag.Value.Type. -func (p PathStyleSimple) Type() string { - return strings.Join(PathStyleSimpleStrings, "|") -} - -func (p PathStyleSimple) Copy() *PathStyleSimple { - return &p -} - -func (p PathStyleSimple) ToPathStyle() PathStyle { - return PathStyle(p) -} diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 90722435634..5179cec53a9 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -314,10 +314,9 @@ var ( whitespaceRx = regexp.MustCompile(`\s+`) commonFlagCompletionFuncs = map[string]func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective){ - "exclude": chezmoi.EntryTypeSetFlagCompletionFunc, - "include": chezmoi.EntryTypeSetFlagCompletionFunc, - "path-style": chezmoi.PathStyleFlagCompletionFunc, - "secrets": severityFlagCompletionFunc, + "exclude": chezmoi.EntryTypeSetFlagCompletionFunc, + "include": chezmoi.EntryTypeSetFlagCompletionFunc, + "secrets": severityFlagCompletionFunc, } ) @@ -383,7 +382,7 @@ func newConfig(options ...configOption) (*Config, error) { }, managed: managedCmdConfig{ filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), - pathStyle: chezmoi.PathStyleRelative, + pathStyle: newChoiceFlag("relative", []string{"absolute", "relative", "source-absolute", "source-relative"}), }, mergeAll: mergeAllCmdConfig{ recursive: true, @@ -404,7 +403,7 @@ func newConfig(options ...configOption) (*Config, error) { }, }, unmanaged: unmanagedCmdConfig{ - pathStyle: chezmoi.PathStyleSimple(chezmoi.PathStyleRelative), + pathStyle: newChoiceFlag("relative", []string{"absolute", "relative"}), }, // Configuration. @@ -2938,7 +2937,7 @@ func newConfigFile(bds *xdg.BaseDirectorySpecification) ConfigFile { }, Status: statusCmdConfig{ Exclude: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesNone), - PathStyle: chezmoi.PathStyleRelative.Copy(), + PathStyle: newChoiceFlag("relative", []string{"absolute", "relative"}), include: chezmoi.NewEntryTypeSet(chezmoi.EntryTypesAll), recursive: true, }, diff --git a/internal/cmd/managedcmd.go b/internal/cmd/managedcmd.go index fc662e3226e..6f7d710acb1 100644 --- a/internal/cmd/managedcmd.go +++ b/internal/cmd/managedcmd.go @@ -10,7 +10,7 @@ import ( type managedCmdConfig struct { filter *chezmoi.EntryTypeFilter - pathStyle chezmoi.PathStyle + pathStyle *choiceFlag tree bool } @@ -30,7 +30,8 @@ func (c *Config) newManagedCmd() *cobra.Command { managedCmd.Flags().VarP(c.managed.filter.Exclude, "exclude", "x", "Exclude entry types") managedCmd.Flags().VarP(c.managed.filter.Include, "include", "i", "Include entry types") - managedCmd.Flags().VarP(&c.managed.pathStyle, "path-style", "p", "Path style") + managedCmd.Flags().VarP(c.managed.pathStyle, "path-style", "p", "Path style") + must(managedCmd.RegisterFlagCompletionFunc("path-style", c.managed.pathStyle.FlagCompletionFunc())) managedCmd.Flags().BoolVarP(&c.managed.tree, "tree", "t", c.managed.tree, "Print paths as a tree") return managedCmd @@ -80,14 +81,14 @@ func (c *Config) runManagedCmd(cmd *cobra.Command, args []string, sourceState *c } var path fmt.Stringer - switch c.managed.pathStyle { - case chezmoi.PathStyleAbsolute: + switch c.managed.pathStyle.String() { + case "absolute": path = c.DestDirAbsPath.Join(targetRelPath) - case chezmoi.PathStyleRelative: + case "relative": path = targetRelPath - case chezmoi.PathStyleSourceAbsolute: + case "source-absolute": path = c.SourceDirAbsPath.Join(sourceStateEntry.SourceRelPath().RelPath()) - case chezmoi.PathStyleSourceRelative: + case "source-relative": path = sourceStateEntry.SourceRelPath().RelPath() } paths = append(paths, path) diff --git a/internal/cmd/statuscmd.go b/internal/cmd/statuscmd.go index 84363f5c3d5..326a1b3d5b7 100644 --- a/internal/cmd/statuscmd.go +++ b/internal/cmd/statuscmd.go @@ -1,7 +1,6 @@ package cmd import ( - "errors" "fmt" "io/fs" "log/slog" @@ -15,7 +14,7 @@ import ( type statusCmdConfig struct { Exclude *chezmoi.EntryTypeSet `json:"exclude" mapstructure:"exclude" yaml:"exclude"` - PathStyle *chezmoi.PathStyle `json:"pathStyle" mapstructure:"pathStyle" yaml:"pathStyle"` + PathStyle *choiceFlag `json:"pathStyle" mapstructure:"pathStyle" yaml:"pathStyle"` include *chezmoi.EntryTypeSet init bool parentDirs bool @@ -39,6 +38,7 @@ func (c *Config) newStatusCmd() *cobra.Command { statusCmd.Flags().VarP(c.Status.Exclude, "exclude", "x", "Exclude entry types") statusCmd.Flags().VarP(c.Status.PathStyle, "path-style", "p", "Path style") + must(statusCmd.RegisterFlagCompletionFunc("path-style", c.Status.PathStyle.FlagCompletionFunc())) statusCmd.Flags().VarP(c.Status.include, "include", "i", "Include entry types") statusCmd.Flags().BoolVar(&c.Status.init, "init", c.Status.init, "Recreate config file from template") statusCmd.Flags(). @@ -72,15 +72,13 @@ func (c *Config) runStatusCmd(cmd *cobra.Command, args []string) error { if x != ' ' || y != ' ' { var path string - switch *c.Status.PathStyle { - case chezmoi.PathStyleAbsolute: + switch pathStyle := c.Status.PathStyle.String(); pathStyle { + case "absolute": path = c.DestDirAbsPath.Join(targetRelPath).String() - case chezmoi.PathStyleRelative: + case "relative": path = targetRelPath.String() - case chezmoi.PathStyleSourceAbsolute: - return errors.New("source-absolute not supported for status") - case chezmoi.PathStyleSourceRelative: - return errors.New("source-relative not supported for status") + default: + return fmt.Errorf("%s: invalid path style", pathStyle) } fmt.Fprintf(&builder, "%c%c %s\n", x, y, path) diff --git a/internal/cmd/testdata/scripts/completion.txtar b/internal/cmd/testdata/scripts/completion.txtar index 80146957a63..a46a701911b 100644 --- a/internal/cmd/testdata/scripts/completion.txtar +++ b/internal/cmd/testdata/scripts/completion.txtar @@ -64,7 +64,7 @@ cmp stdout golden/output-format-with-empty # test that managed path style values are completed exec chezmoi __complete managed --path-style= -cmp stdout golden/path-style +cmp stdout golden/path-style-with-source # test that state data --format values are completed exec chezmoi __complete state data --format= @@ -137,6 +137,10 @@ yaml -- golden/path-style -- absolute relative +:4 +-- golden/path-style-with-source -- +absolute +relative source-absolute source-relative :4 diff --git a/internal/cmd/testdata/scripts/unmanaged.txtar b/internal/cmd/testdata/scripts/unmanaged.txtar index a197c4b2674..ffb53a7de81 100644 --- a/internal/cmd/testdata/scripts/unmanaged.txtar +++ b/internal/cmd/testdata/scripts/unmanaged.txtar @@ -33,7 +33,7 @@ cmp stdout golden/unmanaged-with-some-managed # test chezmoi unmanaged --path-style=source-absolute ! exec chezmoi unmanaged --path-style=source-absolute -stderr 'source-absolute: unknown path style' +stderr 'flag: invalid value' -- golden/unmanaged -- .local diff --git a/internal/cmd/unmanagedcmd.go b/internal/cmd/unmanagedcmd.go index b87616658cc..d332224f762 100644 --- a/internal/cmd/unmanagedcmd.go +++ b/internal/cmd/unmanagedcmd.go @@ -12,7 +12,7 @@ import ( ) type unmanagedCmdConfig struct { - pathStyle chezmoi.PathStyleSimple + pathStyle *choiceFlag tree bool } @@ -29,11 +29,10 @@ func (c *Config) newUnmanagedCmd() *cobra.Command { ), } - unmanagedCmd.Flags().VarP(&c.unmanaged.pathStyle, "path-style", "p", "Path style") + unmanagedCmd.Flags().VarP(c.unmanaged.pathStyle, "path-style", "p", "Path style") + must(unmanagedCmd.RegisterFlagCompletionFunc("path-style", c.unmanaged.pathStyle.FlagCompletionFunc())) unmanagedCmd.Flags().BoolVarP(&c.unmanaged.tree, "tree", "t", c.unmanaged.tree, "Print paths as a tree") - must(unmanagedCmd.RegisterFlagCompletionFunc("path-style", chezmoi.PathStyleSimpleFlagCompletionFunc)) - return unmanagedCmd } @@ -101,10 +100,13 @@ func (c *Config) runUnmanagedCmd(cmd *cobra.Command, args []string, sourceState paths := make([]fmt.Stringer, 0, len(unmanagedRelPaths.Elements())) for relPath := range unmanagedRelPaths { var path fmt.Stringer - if c.unmanaged.pathStyle.ToPathStyle() == chezmoi.PathStyleAbsolute { + switch pathStyle := c.unmanaged.pathStyle.String(); pathStyle { + case "absolute": path = c.DestDirAbsPath.Join(relPath) - } else { + case "relative": path = relPath + default: + return fmt.Errorf("%s: invalid path style", pathStyle) } paths = append(paths, path) }