Repository: iterative/dvc

Issue:
config: add --list to list all variables and their values
Something that would be handy for us during debugging and for users. Same as `git config --list`. Useful to avoid confusion such as in https://github.com/iterative/dvc/issues/4970


Discussion:

Patch:
diff --git a/dvc/command/config.py b/dvc/command/config.py
--- a/dvc/command/config.py
+++ b/dvc/command/config.py
@@ -3,6 +3,7 @@
 
 from dvc.command.base import CmdBaseNoRepo, append_doc_link
 from dvc.config import Config, ConfigError
+from dvc.utils.flatten import flatten
 
 logger = logging.getLogger(__name__)
 
@@ -14,6 +15,22 @@ def __init__(self, args):
         self.config = Config(validate=False)
 
     def run(self):
+        if self.args.list:
+            if any((self.args.name, self.args.value, self.args.unset)):
+                logger.error(
+                    "-l/--list can't be used together with any of these "
+                    "options: -u/--unset, name, value"
+                )
+                return 1
+
+            conf = self.config.load_one(self.args.level)
+            logger.info("\n".join(self._format_config(conf)))
+            return 0
+
+        if self.args.name is None:
+            logger.error("name argument is required")
+            return 1
+
         section, opt = self.args.name.lower().strip().split(".", 1)
 
         if self.args.value is None and not self.args.unset:
@@ -48,6 +65,11 @@ def _check(self, conf, section, opt=None):
             msg = "option {} doesn't exist"
             raise ConfigError(msg.format(self.args.name))
 
+    @staticmethod
+    def _format_config(config):
+        for key, value in flatten(config).items():
+            yield f"{key}={value}"
+
 
 parent_config_parser = argparse.ArgumentParser(add_help=False)
 level_group = parent_config_parser.add_mutually_exclusive_group()
@@ -92,6 +114,13 @@ def add_parser(subparsers, parent_parser):
         action="store_true",
         help="Unset option.",
     )
-    config_parser.add_argument("name", help="Option name.")
+    config_parser.add_argument("name", nargs="?", help="Option name.")
     config_parser.add_argument("value", nargs="?", help="Option value.")
+    config_parser.add_argument(
+        "-l",
+        "--list",
+        default=False,
+        action="store_true",
+        help="list all defined config values",
+    )
     config_parser.set_defaults(func=CmdConfig)


Test patch:
diff --git a/tests/func/test_cli.py b/tests/func/test_cli.py
--- a/tests/func/test_cli.py
+++ b/tests/func/test_cli.py
@@ -151,6 +151,13 @@ def test(self):
         self.assertEqual(args.name, name)
         self.assertEqual(args.value, value)
 
+    def test_config_list(self):
+        args = parse_args(["config", "--list"])
+
+        self.assertTrue(args.list)
+        self.assertIsNone(args.name)
+        self.assertIsNone(args.value)
+
 
 class TestCheckout(TestDvc):
     def test(self):
diff --git a/tests/func/test_config.py b/tests/func/test_config.py
--- a/tests/func/test_config.py
+++ b/tests/func/test_config.py
@@ -60,6 +60,9 @@ def _do_test(self, local=False):
         self.assertEqual(ret, 0)
         self.assertFalse(self._contains(section, field, value, local))
 
+        ret = main(base + ["--list"])
+        self.assertEqual(ret, 0)
+
     def test(self):
         self._do_test(False)
 
@@ -85,6 +88,16 @@ def test_non_existing(self):
         ret = main(["config", "core.non_existing_field", "-u"])
         self.assertEqual(ret, 251)
 
+    def test_invalid_config_list(self):
+        ret = main(["config"])
+        self.assertEqual(ret, 1)
+
+        ret = main(["config", "--list", "core.analytics"])
+        self.assertEqual(ret, 1)
+
+        ret = main(["config", "--list", "-u"])
+        self.assertEqual(ret, 1)
+
 
 def test_set_invalid_key(dvc):
     with pytest.raises(ConfigError, match=r"extra keys not allowed"):
diff --git a/tests/unit/command/test_config.py b/tests/unit/command/test_config.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/command/test_config.py
@@ -0,0 +1,20 @@
+from dvc.command.config import CmdConfig
+
+
+def test_config_formatter():
+    example_config = {
+        "section_foo": {"option_bar": True, "option_baz": False},
+        "section_foo2": {
+            "option_bar2": {"option_baz2": True},
+            "option_baz3": {"option_baz4": False},
+        },
+        "section_foo3": {},
+    }
+
+    config_lines = tuple(CmdConfig._format_config(example_config))
+    assert config_lines == (
+        "section_foo.option_bar=True",
+        "section_foo.option_baz=False",
+        "section_foo2.option_bar2.option_baz2=True",
+        "section_foo2.option_baz3.option_baz4=False",
+    )