From 175277e12a6b900091ae90fa81a0f7175b843563 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 19 Nov 2020 11:39:53 +0100 Subject: [PATCH] Add new commands to edit config files --- cli/config/add.go | 63 ++++++++ cli/config/config.go | 4 + cli/config/delete.go | 70 ++++++++ cli/config/init.go | 4 +- cli/config/remove.go | 72 +++++++++ cli/config/set.go | 79 +++++++++ cli/config/validate.go | 44 +++++ test/test_config.py | 357 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 691 insertions(+), 2 deletions(-) create mode 100644 cli/config/add.go create mode 100644 cli/config/delete.go create mode 100644 cli/config/remove.go create mode 100644 cli/config/set.go create mode 100644 cli/config/validate.go diff --git a/cli/config/add.go b/cli/config/add.go new file mode 100644 index 00000000000..ca62731be37 --- /dev/null +++ b/cli/config/add.go @@ -0,0 +1,63 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package config + +import ( + "os" + "reflect" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cli/configuration" + "github.com/spf13/cobra" +) + +func initAddCommand() *cobra.Command { + addCommand := &cobra.Command{ + Use: "add", + Short: "Adds one or more values to a setting.", + Long: "Adds one or more values to a setting.", + Example: "" + + " " + os.Args[0] + " config add board_manager.additional_urls https://example.com/package_example_index.json\n" + + " " + os.Args[0] + " config add board_manager.additional_urls https://example.com/package_example_index.json https://another-url.com/package_another_index.json\n", + Args: cobra.MinimumNArgs(2), + Run: runAddCommand, + } + return addCommand +} + +func runAddCommand(cmd *cobra.Command, args []string) { + key := args[0] + kind, err := typeOf(key) + if err != nil { + feedback.Error(err) + os.Exit(errorcodes.ErrGeneric) + } + + if kind != reflect.Slice { + feedback.Errorf("The key '%v' is not a list of items, can't add to it.\nMaybe use 'config set'?", key) + os.Exit(errorcodes.ErrGeneric) + } + + v := configuration.Settings.GetStringSlice(key) + v = append(v, args[1:]...) + configuration.Settings.Set(key, v) + + if err := configuration.Settings.WriteConfig(); err != nil { + feedback.Errorf("Can't write config file: %v", err) + os.Exit(errorcodes.ErrGeneric) + } +} diff --git a/cli/config/config.go b/cli/config/config.go index c382532df8f..c4f69ab6863 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -29,8 +29,12 @@ func NewCommand() *cobra.Command { Example: " " + os.Args[0] + " config init", } + configCommand.AddCommand(initAddCommand()) + configCommand.AddCommand(initDeleteCommand()) configCommand.AddCommand(initDumpCmd()) configCommand.AddCommand(initInitCommand()) + configCommand.AddCommand(initRemoveCommand()) + configCommand.AddCommand(initSetCommand()) return configCommand } diff --git a/cli/config/delete.go b/cli/config/delete.go new file mode 100644 index 00000000000..a45d7651c77 --- /dev/null +++ b/cli/config/delete.go @@ -0,0 +1,70 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package config + +import ( + "os" + "strings" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cli/configuration" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func initDeleteCommand() *cobra.Command { + addCommand := &cobra.Command{ + Use: "delete", + Short: "Deletes a settings key and all its sub keys.", + Long: "Deletes a settings key and all its sub keys.", + Example: "" + + " " + os.Args[0] + " config delete board_manager\n" + + " " + os.Args[0] + " config delete board_manager.additional_urls", + Args: cobra.ExactArgs(1), + Run: runDeleteCommand, + } + return addCommand +} + +func runDeleteCommand(cmd *cobra.Command, args []string) { + toDelete := args[0] + + keys := []string{} + exists := false + for _, v := range configuration.Settings.AllKeys() { + if !strings.HasPrefix(v, toDelete) { + keys = append(keys, v) + continue + } + exists = true + } + + if !exists { + feedback.Errorf("Settings key doesn't exist") + os.Exit(errorcodes.ErrGeneric) + } + + updatedSettings := viper.New() + for _, k := range keys { + updatedSettings.Set(k, configuration.Settings.Get(k)) + } + + if err := updatedSettings.WriteConfigAs(configuration.Settings.ConfigFileUsed()); err != nil { + feedback.Errorf("Can't write config file: %v", err) + os.Exit(errorcodes.ErrGeneric) + } +} diff --git a/cli/config/init.go b/cli/config/init.go index e9cdf28cb95..a242e7e3fd6 100644 --- a/cli/config/init.go +++ b/cli/config/init.go @@ -42,8 +42,8 @@ func initInitCommand() *cobra.Command { Long: "Creates or updates the configuration file in the data directory or custom directory with the current configuration settings.", Example: "" + " # Writes current configuration to the configuration file in the data directory.\n" + - " " + os.Args[0] + " config init" + - " " + os.Args[0] + " config init --dest-dir /home/user/MyDirectory" + + " " + os.Args[0] + " config init\n" + + " " + os.Args[0] + " config init --dest-dir /home/user/MyDirectory\n" + " " + os.Args[0] + " config init --dest-file /home/user/MyDirectory/my_settings.yaml", Args: cobra.NoArgs, Run: runInitCommand, diff --git a/cli/config/remove.go b/cli/config/remove.go new file mode 100644 index 00000000000..f5101a03bab --- /dev/null +++ b/cli/config/remove.go @@ -0,0 +1,72 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package config + +import ( + "os" + "reflect" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cli/configuration" + "github.com/spf13/cobra" +) + +func initRemoveCommand() *cobra.Command { + addCommand := &cobra.Command{ + Use: "remove", + Short: "Removes one or more values from a setting.", + Long: "Removes one or more values from a setting.", + Example: "" + + " " + os.Args[0] + " config remove board_manager.additional_urls https://example.com/package_example_index.json\n" + + " " + os.Args[0] + " config remove board_manager.additional_urls https://example.com/package_example_index.json https://another-url.com/package_another_index.json\n", + Args: cobra.MinimumNArgs(2), + Run: runRemoveCommand, + } + return addCommand +} + +func runRemoveCommand(cmd *cobra.Command, args []string) { + key := args[0] + kind, err := typeOf(key) + if err != nil { + feedback.Error(err) + os.Exit(errorcodes.ErrGeneric) + } + + if kind != reflect.Slice { + feedback.Errorf("The key '%v' is not a list of items, can't remove from it.\nMaybe use 'config delete'?", key) + os.Exit(errorcodes.ErrGeneric) + } + + mappedValues := map[string]bool{} + for _, v := range configuration.Settings.GetStringSlice(key) { + mappedValues[v] = true + } + for _, arg := range args[1:] { + delete(mappedValues, arg) + } + values := []string{} + for k := range mappedValues { + values = append(values, k) + } + configuration.Settings.Set(key, values) + + if err := configuration.Settings.WriteConfig(); err != nil { + feedback.Errorf("Can't write config file: %v", err) + os.Exit(errorcodes.ErrGeneric) + } +} diff --git a/cli/config/set.go b/cli/config/set.go new file mode 100644 index 00000000000..6d397c2b69d --- /dev/null +++ b/cli/config/set.go @@ -0,0 +1,79 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package config + +import ( + "os" + "reflect" + "strconv" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cli/configuration" + "github.com/spf13/cobra" +) + +func initSetCommand() *cobra.Command { + addCommand := &cobra.Command{ + Use: "set", + Short: "Sets a setting value.", + Long: "Sets a setting value.", + Example: "" + + " " + os.Args[0] + " config set logging.level trace\n" + + " " + os.Args[0] + " config set logging.file my-log.txt\n" + + " " + os.Args[0] + " config set sketch.always_export_binaries true\n" + + " " + os.Args[0] + " config set board_manager.additional_urls https://example.com/package_example_index.json https://another-url.com/package_another_index.json", + Args: cobra.MinimumNArgs(2), + Run: runSetCommand, + } + return addCommand +} + +func runSetCommand(cmd *cobra.Command, args []string) { + key := args[0] + kind, err := typeOf(key) + if err != nil { + feedback.Error(err) + os.Exit(errorcodes.ErrGeneric) + } + + if kind != reflect.Slice && len(args) > 2 { + feedback.Errorf("Can't set multiple values in key %v", key) + os.Exit(errorcodes.ErrGeneric) + } + + var value interface{} + switch kind { + case reflect.Slice: + value = args[1:] + case reflect.String: + value = args[1] + case reflect.Bool: + var err error + value, err = strconv.ParseBool(args[1]) + if err != nil { + feedback.Errorf("error parsing value: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + } + + configuration.Settings.Set(key, value) + + if err := configuration.Settings.WriteConfig(); err != nil { + feedback.Errorf("Writing config file: %v", err) + os.Exit(errorcodes.ErrGeneric) + } +} diff --git a/cli/config/validate.go b/cli/config/validate.go new file mode 100644 index 00000000000..d448ed32955 --- /dev/null +++ b/cli/config/validate.go @@ -0,0 +1,44 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package config + +import ( + "fmt" + "reflect" +) + +var validMap = map[string]reflect.Kind{ + "board_manager.additional_urls": reflect.Slice, + "daemon.port": reflect.String, + "directories.data": reflect.String, + "directories.downloads": reflect.String, + "directories.user": reflect.String, + "library.enable_unsafe_install": reflect.Bool, + "logging.file": reflect.String, + "logging.format": reflect.String, + "logging.level": reflect.String, + "sketch.always_export_binaries": reflect.Bool, + "telemetry.addr": reflect.String, + "telemetry.enabled": reflect.Bool, +} + +func typeOf(key string) (reflect.Kind, error) { + t, ok := validMap[key] + if !ok { + return reflect.Invalid, fmt.Errorf("Settings key doesn't exist") + } + return t, nil +} diff --git a/test/test_config.py b/test/test_config.py index 4f51d809d12..13bace79030 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -226,3 +226,360 @@ def test_dump_with_config_file_flag(run_command, working_dir): assert result.ok settings_json = json.loads(result.stdout) assert ["https://another-url.com"] == settings_json["board_manager"]["additional_urls"] + + +def test_add_remove_set_delete_on_unexisting_key(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + res = run_command("config add some.key some_value") + assert res.failed + assert "Settings key doesn't exist" in res.stderr + + res = run_command("config remove some.key some_value") + assert res.failed + assert "Settings key doesn't exist" in res.stderr + + res = run_command("config set some.key some_value") + assert res.failed + assert "Settings key doesn't exist" in res.stderr + + res = run_command("config delete some.key") + assert res.failed + assert "Settings key doesn't exist" in res.stderr + + +def test_add_single_argument(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies no additional urls are present + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [] == settings_json["board_manager"]["additional_urls"] + + # Adds one URL + url = "https://example.com" + assert run_command(f"config add board_manager.additional_urls {url}") + + # Verifies URL has been saved + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert ["https://example.com"] == settings_json["board_manager"]["additional_urls"] + + +def test_add_multiple_arguments(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies no additional urls are present + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [] == settings_json["board_manager"]["additional_urls"] + + # Adds multiple URLs at the same time + urls = [ + "https://example.com/package_example_index.json", + "https://example.com/yet_another_package_example_index.json", + ] + assert run_command(f"config add board_manager.additional_urls {' '.join(urls)}") + + # Verifies URL has been saved + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert 2 == len(settings_json["board_manager"]["additional_urls"]) + assert urls[0] in settings_json["board_manager"]["additional_urls"] + assert urls[1] in settings_json["board_manager"]["additional_urls"] + + +def test_add_on_unsupported_key(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default value + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert "50051" == settings_json["daemon"]["port"] + + # Tries and fails to add a new item + result = run_command("config add daemon.port 50000") + assert result.failed + assert "The key 'daemon.port' is not a list of items, can't add to it.\nMaybe use 'config set'?" in result.stderr + + # Verifies value is not changed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert "50051" == settings_json["daemon"]["port"] + + +def test_remove_single_argument(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Adds URLs + urls = [ + "https://example.com/package_example_index.json", + "https://example.com/yet_another_package_example_index.json", + ] + assert run_command(f"config add board_manager.additional_urls {' '.join(urls)}") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert 2 == len(settings_json["board_manager"]["additional_urls"]) + assert urls[0] in settings_json["board_manager"]["additional_urls"] + assert urls[1] in settings_json["board_manager"]["additional_urls"] + + # Remove first URL + assert run_command(f"config remove board_manager.additional_urls {urls[0]}") + + # Verifies URLs has been removed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert ["https://example.com/yet_another_package_example_index.json"] == settings_json["board_manager"][ + "additional_urls" + ] + + +def test_remove_multiple_arguments(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Adds URLs + urls = [ + "https://example.com/package_example_index.json", + "https://example.com/yet_another_package_example_index.json", + ] + assert run_command(f"config add board_manager.additional_urls {' '.join(urls)}") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert 2 == len(settings_json["board_manager"]["additional_urls"]) + assert urls[0] in settings_json["board_manager"]["additional_urls"] + assert urls[1] in settings_json["board_manager"]["additional_urls"] + + # Remove all URLs + assert run_command(f"config remove board_manager.additional_urls {' '.join(urls)}") + + # Verifies all URLs have been removed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [] == settings_json["board_manager"]["additional_urls"] + + +def test_remove_on_unsupported_key(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default value + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert "50051" == settings_json["daemon"]["port"] + + # Tries and fails to add a new item + result = run_command("config remove daemon.port 50051") + assert result.failed + assert ( + "The key 'daemon.port' is not a list of items, can't remove from it.\nMaybe use 'config delete'?" + in result.stderr + ) + + # Verifies value is not changed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert "50051" == settings_json["daemon"]["port"] + + +def test_set_slice_with_single_argument(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [] == settings_json["board_manager"]["additional_urls"] + + # Set an URL in the list + url = "https://example.com/package_example_index.json" + assert run_command(f"config set board_manager.additional_urls {url}") + + # Verifies value is changed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [url] == settings_json["board_manager"]["additional_urls"] + + # Sets another URL + url = "https://example.com/yet_another_package_example_index.json" + assert run_command(f"config set board_manager.additional_urls {url}") + + # Verifies previous value is overwritten + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [url] == settings_json["board_manager"]["additional_urls"] + + +def test_set_slice_with_multiple_arguments(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [] == settings_json["board_manager"]["additional_urls"] + + # Set some URLs in the list + urls = [ + "https://example.com/first_package_index.json", + "https://example.com/second_package_index.json", + ] + assert run_command(f"config set board_manager.additional_urls {' '.join(urls)}") + + # Verifies value is changed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert 2 == len(settings_json["board_manager"]["additional_urls"]) + assert urls[0] in settings_json["board_manager"]["additional_urls"] + assert urls[1] in settings_json["board_manager"]["additional_urls"] + + # Sets another set of URL + urls = [ + "https://example.com/third_package_index.json", + "https://example.com/fourth_package_index.json", + ] + assert run_command(f"config set board_manager.additional_urls {' '.join(urls)}") + + # Verifies previous value is overwritten + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert 2 == len(settings_json["board_manager"]["additional_urls"]) + assert urls[0] in settings_json["board_manager"]["additional_urls"] + assert urls[1] in settings_json["board_manager"]["additional_urls"] + + +def test_set_string_with_single_argument(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert "info" == settings_json["logging"]["level"] + + # Changes value + assert run_command("" "config set logging.level trace") + + # Verifies value is changed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert "trace" == settings_json["logging"]["level"] + + +def test_set_string_with_multiple_arguments(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert "info" == settings_json["logging"]["level"] + + # Tries to change value + res = run_command("config set logging.level trace debug") + assert res.failed + assert "Can't set multiple values in key logging.level" in res.stderr + + +def test_set_bool_with_single_argument(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert not settings_json["library"]["enable_unsafe_install"] + + # Changes value + assert run_command("config set library.enable_unsafe_install true") + + # Verifies value is changed + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert settings_json["library"]["enable_unsafe_install"] + + +def test_set_bool_with_multiple_arguments(run_command): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert not settings_json["library"]["enable_unsafe_install"] + + # Changes value' + res = run_command("config set library.enable_unsafe_install true foo") + assert res.failed + assert "Can't set multiple values in key library.enable_unsafe_install" in res.stderr + + +def test_delete(run_command, working_dir): + # Create a config file + assert run_command("config init --dest-dir .") + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert not settings_json["library"]["enable_unsafe_install"] + + # Delete config key + assert run_command("config delete library.enable_unsafe_install") + + # Verifies value is not found, we read directly from file instead of using + # the dump command since that would still print the deleted value if it has + # a default + config_file = Path(working_dir, "arduino-cli.yaml") + config_lines = config_file.open().readlines() + assert "enable_unsafe_install" not in config_lines + + # Verifies default state + result = run_command("config dump --format json") + assert result.ok + settings_json = json.loads(result.stdout) + assert [] == settings_json["board_manager"]["additional_urls"] + + # Delete config key and sub keys + assert run_command("config delete board_manager") + + # Verifies value is not found, we read directly from file instead of using + # the dump command since that would still print the deleted value if it has + # a default + config_file = Path(working_dir, "arduino-cli.yaml") + config_lines = config_file.open().readlines() + assert "additional_urls" not in config_lines + assert "board_manager" not in config_lines