Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ Supported config files:
Run `config help formats` for format-specific behavior.

```bash
config get path/to/config.toml server.port
config set path/to/config.toml server.port 3000
config array add path/to/config.toml sandbox_workspace_write.writable_roots '$HOME/.cache'
config unset path/to/config.toml server.password
config list path/to/config.toml server
config get -f path/to/config.toml server.port
config set -f path/to/config.toml server.port 3000
config array add -f path/to/config.toml sandbox_workspace_write.writable_roots '$HOME/.cache'
config unset -f path/to/config.toml server.password
config list -f path/to/config.toml server
```

For repeated edits, set `CONFIG_FILE` once:
Expand All @@ -74,20 +74,20 @@ config edit
Use `--diff` or `--diff --color` (`-dc`) to preview an edit:

```bash
config set config.yaml server.port 3000 -dc
config set -f config.yaml server.port 3000 -dc
```

Use `--string` when a value should remain text even if it looks like a typed
literal:

```bash
config set config.toml version 1.0 --string
config set -f config.toml version 1.0 --string
```

Use `--in` and `--on` to update records:

```bash
config set config.toml port 3000 --in servers --on name:api
config set -f config.toml port 3000 --in servers --on name:api
```

## Feature Specs
Expand Down
88 changes: 46 additions & 42 deletions cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,13 @@ func NewRootCommand(version string, stdout, stderr io.Writer) *cobra.Command {
func newSetCommand(stdout, stderr io.Writer) *cobra.Command {
var opts setOptions
cmd := &cobra.Command{
Use: "set [CONFIG_FILE] KEY VALUE [options]",
Use: "set KEY VALUE [options]",
Short: "Create or update config values",
Args: func(cmd *cobra.Command, args []string) error {
configFile, rest, err := parseCommandArgs(args, 2, 2, "usage: config set [CONFIG_FILE] KEY VALUE [options]")
rest, err := parseCommandArgs(args, 2, 2, "usage: config set KEY VALUE [options]")
if err != nil {
return err
}
opts.configFile = configFile
opts.key = rest[0]
opts.value = rest[1]
return nil
Expand All @@ -166,6 +165,7 @@ func newSetCommand(stdout, stderr io.Writer) *cobra.Command {
},
}
cmd.SetHelpFunc(helpPrinter("set"))
addFileFlag(cmd, &opts.configFile)
cmd.Flags().StringVar(&opts.in, "in", "", "Edit a record in COLLECTION")
cmd.Flags().StringVar(&opts.on, "on", "", "Select or create a record by FIELD:VALUE")
cmd.Flags().BoolVarP(&opts.string, "string", "s", false, "Store VALUE as a string")
Expand All @@ -179,14 +179,13 @@ func newSetCommand(stdout, stderr io.Writer) *cobra.Command {
func newGetCommand(stdout io.Writer) *cobra.Command {
var opts getOptions
cmd := &cobra.Command{
Use: "get [CONFIG_FILE] KEY",
Use: "get KEY",
Short: "Show a config value",
Args: func(cmd *cobra.Command, args []string) error {
configFile, rest, err := parseCommandArgs(args, 1, 1, "usage: config get [CONFIG_FILE] KEY")
rest, err := parseCommandArgs(args, 1, 1, "usage: config get KEY")
if err != nil {
return err
}
opts.configFile = configFile
opts.key = rest[0]
return nil
},
Expand All @@ -195,6 +194,7 @@ func newGetCommand(stdout io.Writer) *cobra.Command {
},
}
cmd.SetHelpFunc(helpPrinter("get"))
addFileFlag(cmd, &opts.configFile)
cmd.Flags().StringVar(&opts.in, "in", "", "Read a field from a record in COLLECTION")
cmd.Flags().StringArrayVar(&opts.on, "on", nil, "Select a record by FIELD:VALUE")
return cmd
Expand All @@ -203,14 +203,13 @@ func newGetCommand(stdout io.Writer) *cobra.Command {
func newUnsetCommand(stdout, stderr io.Writer) *cobra.Command {
var opts unsetOptions
cmd := &cobra.Command{
Use: "unset [CONFIG_FILE] KEY [options]",
Use: "unset KEY [options]",
Short: "Delete a config value",
Args: func(cmd *cobra.Command, args []string) error {
configFile, rest, err := parseCommandArgs(args, 1, 1, "usage: config unset [CONFIG_FILE] KEY [options]")
rest, err := parseCommandArgs(args, 1, 1, "usage: config unset KEY [options]")
if err != nil {
return err
}
opts.configFile = configFile
opts.key = rest[0]
return nil
},
Expand All @@ -223,6 +222,7 @@ func newUnsetCommand(stdout, stderr io.Writer) *cobra.Command {
},
}
cmd.SetHelpFunc(helpPrinter("unset"))
addFileFlag(cmd, &opts.configFile)
cmd.Flags().StringVar(&opts.in, "in", "", "Remove a field from a record in COLLECTION")
cmd.Flags().StringArrayVar(&opts.on, "on", nil, "Select a record by FIELD:VALUE")
cmd.Flags().StringVar(&opts.ifValue, "if", "", "Only unset when the current value matches VALUE")
Expand All @@ -237,15 +237,14 @@ func newUnsetCommand(stdout, stderr io.Writer) *cobra.Command {
func newDeleteCommand(stdout, stderr io.Writer) *cobra.Command {
var opts deleteOptions
cmd := &cobra.Command{
Use: "delete [CONFIG_FILE] KEY [options]",
Use: "delete KEY [options]",
Aliases: []string{"del"},
Short: "Delete a config container",
Args: func(cmd *cobra.Command, args []string) error {
configFile, rest, err := parseCommandArgs(args, 1, 1, "usage: config delete [CONFIG_FILE] KEY [options]")
rest, err := parseCommandArgs(args, 1, 1, "usage: config delete KEY [options]")
if err != nil {
return err
}
opts.configFile = configFile
opts.key = rest[0]
return nil
},
Expand All @@ -257,6 +256,7 @@ func newDeleteCommand(stdout, stderr io.Writer) *cobra.Command {
},
}
cmd.SetHelpFunc(helpPrinter("delete"))
addFileFlag(cmd, &opts.configFile)
cmd.Flags().StringArrayVar(&opts.on, "on", nil, "Select a record by FIELD:VALUE")
cmd.Flags().BoolVar(&opts.ifEmpty, "if-empty", false, "Only delete when the container has no values")
cmd.Flags().BoolVarP(&opts.dry, "dry", "n", false, "Print the updated config without modifying the file")
Expand All @@ -269,15 +269,14 @@ func newDeleteCommand(stdout, stderr io.Writer) *cobra.Command {
func newListCommand(stdout io.Writer) *cobra.Command {
var opts listOptions
cmd := &cobra.Command{
Use: "list [CONFIG_FILE] [KEY]",
Use: "list [KEY]",
Aliases: []string{"ls"},
Short: "Show config values",
Args: func(cmd *cobra.Command, args []string) error {
configFile, rest, err := parseCommandArgs(args, 0, 1, "usage: config list [CONFIG_FILE] [KEY]")
rest, err := parseCommandArgs(args, 0, 1, "usage: config list [KEY]")
if err != nil {
return err
}
opts.configFile = configFile
if len(rest) == 1 {
opts.key = rest[0]
}
Expand All @@ -288,21 +287,21 @@ func newListCommand(stdout io.Writer) *cobra.Command {
},
}
cmd.SetHelpFunc(helpPrinter("list"))
addFileFlag(cmd, &opts.configFile)
cmd.Flags().BoolVarP(&opts.color, "color", "c", false, "Colorize keys and separators")
return cmd
}

func newDumpCommand(stdout io.Writer) *cobra.Command {
var opts dumpOptions
cmd := &cobra.Command{
Use: "dump [CONFIG_FILE] [KEY]",
Use: "dump [KEY]",
Short: "Dump config data",
Args: func(cmd *cobra.Command, args []string) error {
configFile, rest, err := parseCommandArgs(args, 0, 1, "usage: config dump [CONFIG_FILE] [KEY]")
rest, err := parseCommandArgs(args, 0, 1, "usage: config dump [KEY]")
if err != nil {
return err
}
opts.configFile = configFile
if len(rest) == 1 {
opts.key = rest[0]
}
Expand All @@ -313,28 +312,29 @@ func newDumpCommand(stdout io.Writer) *cobra.Command {
},
}
cmd.SetHelpFunc(helpPrinter("dump"))
addFileFlag(cmd, &opts.configFile)
cmd.Flags().BoolVar(&opts.json, "json", false, "Dump as JSON")
return cmd
}

func newEditCommand(stdout, stderr io.Writer) *cobra.Command {
var opts editOptions
cmd := &cobra.Command{
Use: "edit [CONFIG_FILE]",
Use: "edit [options]",
Short: "Open the config file in an editor",
Args: func(cmd *cobra.Command, args []string) error {
configFile, _, err := parseCommandArgs(args, 0, 0, "usage: config edit [CONFIG_FILE]")
_, err := parseCommandArgs(args, 0, 0, "usage: config edit [options]")
if err != nil {
return err
}
opts.configFile = configFile
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return runOpenEditor(opts, cmd.InOrStdin(), stdout, stderr)
},
}
cmd.SetHelpFunc(helpPrinter("edit"))
addFileFlag(cmd, &opts.configFile)
return cmd
}

Expand Down Expand Up @@ -385,15 +385,14 @@ func newArrayCommand(stdout, stderr io.Writer) *cobra.Command {
func newArrayEditCommand(name, short string, stdout, stderr io.Writer, edit func(format.Document, string, arrayOptions) (string, error), aliases ...string) *cobra.Command {
var opts arrayOptions
cmd := &cobra.Command{
Use: name + " [CONFIG_FILE] KEY VALUE... [options]",
Use: name + " KEY VALUE... [options]",
Aliases: aliases,
Short: short,
Args: func(cmd *cobra.Command, args []string) error {
configFile, rest, err := parseCommandArgs(args, 2, -1, "usage: config array "+name+" [CONFIG_FILE] KEY VALUE... [options]")
rest, err := parseCommandArgs(args, 2, -1, "usage: config array "+name+" KEY VALUE... [options]")
if err != nil {
return err
}
opts.configFile = configFile
opts.key = rest[0]
opts.values = rest[1:]
return nil
Expand All @@ -406,6 +405,7 @@ func newArrayEditCommand(name, short string, stdout, stderr io.Writer, edit func
},
}
cmd.SetHelpFunc(helpPrinter("array-" + name))
addFileFlag(cmd, &opts.configFile)
cmd.Flags().BoolVarP(&opts.dry, "dry", "n", false, "Print the updated config without modifying the file")
cmd.Flags().BoolVarP(&opts.diff, "diff", "d", false, "Print a unified diff without modifying the file")
cmd.Flags().BoolVarP(&opts.color, "color", "c", false, "Colorize diff output")
Expand All @@ -419,7 +419,7 @@ func newHelpCommand(stdout io.Writer) *cobra.Command {
Short: "Show command help or topic help",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) > 2 {
return usageError{"usage: config help [COMMAND|TOPIC]"}
return usageError{"usage: " + helpUsage}
}
return nil
},
Expand Down Expand Up @@ -447,19 +447,27 @@ func newHelpCommand(stdout io.Writer) *cobra.Command {
return fmt.Errorf("unknown help topic %q", name)
},
}
cmd.SetHelpFunc(helpPrinter("help"))
cmd.SetHelpFunc(helpIndexPrinter(stdout))
return cmd
}

func helpIndexPrinter(stdout io.Writer) func(*cobra.Command, []string) {
return func(*cobra.Command, []string) {
fmt.Fprintln(stdout, helpIndex())
}
}

const helpUsage = "config help [COMMAND|TOPIC]"

func helpIndex() string {
lines := []string{
"Usage:",
" config help [TOPIC]",
" " + helpUsage,
"",
"Commands:",
}

for _, command := range []string{"set", "get", "unset", "delete", "array", "list", "dump", "edit", "completion"} {
for _, command := range []string{"set", "get", "unset", "delete", "array", "array set", "array add", "array delete", "list", "dump", "edit", "completion"} {
lines = append(lines, " "+command)
}

Expand Down Expand Up @@ -499,22 +507,15 @@ func commandHelpTopic(name string) (string, bool) {
}
}

func parseCommandArgs(args []string, minRest, maxRest int, usage string) (string, []string, error) {
configFile := ""
rest := args
if len(args) > 0 && format.TargetPath(args[0]) {
configFile = args[0]
rest = args[1:]
} else if env := os.Getenv("CONFIG_FILE"); env != "" {
configFile = env
} else {
return "", nil, errors.New("config file not specified")
}
func addFileFlag(cmd *cobra.Command, target *string) {
cmd.Flags().StringVarP(target, "file", "f", "", "Path to the config file")
}

if len(rest) < minRest || (maxRest >= 0 && len(rest) > maxRest) {
return "", nil, usageError{usage}
func parseCommandArgs(args []string, minRest, maxRest int, usage string) ([]string, error) {
if len(args) < minRest || (maxRest >= 0 && len(args) > maxRest) {
return nil, usageError{usage}
}
return configFile, rest, nil
return args, nil
}

func helpPrinter(name string) func(*cobra.Command, []string) {
Expand Down Expand Up @@ -840,6 +841,9 @@ func resolveConfigFile(explicit string) (string, error) {
if explicit != "" {
return explicit, nil
}
if env := os.Getenv("CONFIG_FILE"); env != "" {
return env, nil
}
return "", errors.New("config file not specified")
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/cli_edit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestEditOpensExplicitConfigFileInEditor(t *testing.T) {
t.Setenv("EDITOR", writeEditorScript(t))
t.Setenv("CONFIG_EDIT_LOG", logPath)

err := Execute([]string{"edit", path}, "1.2.3", &stdout, &stderr)
err := Execute([]string{"edit", "-f", path}, "1.2.3", &stdout, &stderr)

if err != nil {
t.Fatalf("Execute returned error: %v", err)
Expand Down Expand Up @@ -81,12 +81,12 @@ func TestEditRejectsExtraArguments(t *testing.T) {
var stdout, stderr bytes.Buffer
path := writeTempTOML(t, "name = \"demo\"\n")

err := Execute([]string{"edit", path, "extra"}, "1.2.3", &stdout, &stderr)
err := Execute([]string{"edit", "-f", path, "extra"}, "1.2.3", &stdout, &stderr)

if err == nil {
t.Fatal("expected error")
}
if err.Error() != "usage: config edit [CONFIG_FILE]" {
if err.Error() != "usage: config edit [options]" {
t.Fatalf("unexpected error: %v", err)
}
}
Loading
Loading