Repository: iterative/dvc

Issue:
Machine readable format of DVC status
Hi guys, can we have machine readable output format option for `dvc status`, for example:

```
dvc status --json

{
    "stages": [
        {
            "name":    "path/to/a.txt.dvc",
            "deps_changed": ["path/to/b.txt.dvc"],
            "outs_changed": [],
            "callback": False,
            "checksum_changed": True
        },
        {
            "name":    "path/to/b.txt.dvc",
            "locked": True
        }
    ]
}
```

I don't think it will be needed for other commands, at least for now - we can show dvc console output for user actions such as `dvc repro`.


Discussion:

Patch:
diff --git a/dvc/command/data_sync.py b/dvc/command/data_sync.py
index baee9343a..bf384befa 100644
--- a/dvc/command/data_sync.py
+++ b/dvc/command/data_sync.py
@@ -363,5 +363,11 @@ def add_parser(subparsers, _parent_parser):
         default=False,
         help="Show status of all stages in the specified directory.",
     )
+    status_parser.add_argument(
+        "--show-json",
+        action="store_true",
+        default=False,
+        help="Show status in JSON format.",
+    )
 
     status_parser.set_defaults(func=CmdDataStatus)
diff --git a/dvc/command/status.py b/dvc/command/status.py
index 08d921bd6..97e51513f 100644
--- a/dvc/command/status.py
+++ b/dvc/command/status.py
@@ -50,11 +50,16 @@ class CmdDataStatus(CmdDataBase):
                 with_deps=self.args.with_deps,
                 recursive=self.args.recursive,
             )
-            if st:
-                if self.args.quiet:
-                    return 1
-                else:
-                    self._show(st, indent)
+
+            if self.args.quiet:
+                return bool(st)
+
+            if self.args.show_json:
+                import json
+
+                logger.info(json.dumps(st))
+            elif st:
+                self._show(st, indent)
             else:
                 logger.info(self.UP_TO_DATE_MSG)
 


Test patch:
diff --git a/tests/unit/command/test_status.py b/tests/unit/command/test_status.py
index 6ac7c6970..f2e1c5746 100644
--- a/tests/unit/command/test_status.py
+++ b/tests/unit/command/test_status.py
@@ -1,3 +1,7 @@
+import json
+
+import pytest
+
 from dvc.cli import parse_args
 from dvc.command.status import CmdDataStatus
 
@@ -38,3 +42,34 @@ def test_cloud_status(mocker):
         with_deps=True,
         recursive=True,
     )
+
+
+@pytest.mark.parametrize("status", [{}, {"a": "b", "c": [1, 2, 3]}, [1, 2, 3]])
+def test_status_show_json(mocker, caplog, status):
+    cli_args = parse_args(["status", "--show-json"])
+    assert cli_args.func == CmdDataStatus
+
+    cmd = cli_args.func(cli_args)
+
+    mocker.patch.object(cmd.repo, "status", autospec=True, return_value=status)
+    caplog.clear()
+    assert cmd.run() == 0
+    assert caplog.messages == [json.dumps(status)]
+
+
+@pytest.mark.parametrize(
+    "status, ret", [({}, 0), ({"a": "b", "c": [1, 2, 3]}, 1), ([1, 2, 3], 1)]
+)
+def test_status_quiet(mocker, caplog, capsys, status, ret):
+    cli_args = parse_args(["status", "-q"])
+    assert cli_args.func == CmdDataStatus
+
+    cmd = cli_args.func(cli_args)
+
+    mocker.patch.object(cmd.repo, "status", autospec=True, return_value=status)
+    caplog.clear()
+    assert cmd.run() == ret
+    assert not caplog.messages
+    captured = capsys.readouterr()
+    assert not captured.err
+    assert not captured.out