Repository: iterative/dvc

Issue:
Cli parameter error handling
General rule - DVC should never output Python stack to console since it is not Python only tool.

The issue might be partially related to the last command line parser redesign.

```
$ dvc sync
Traceback (most recent call last):
  File "/Users/dmitry/src/dvc/dvc.py", line 4, in <module>
    main()
  File "/Users/dmitry/src/dvc/dvc/main.py", line 11, in main
    sys.exit(instance.run())
  File "/Users/dmitry/src/dvc/dvc/command/data_sync.py", line 26, in run
    raise DataSyncError('Sync target is not specified')
dvc.command.data_sync.DataSyncError: Data sync error: Sync target is not specified
```

```
$ dvc run
Traceback (most recent call last):
  File "/Users/dmitry/src/dvc/dvc.py", line 4, in <module>
    main()
  File "/Users/dmitry/src/dvc/dvc/main.py", line 11, in main
    sys.exit(instance.run())
  File "/Users/dmitry/src/dvc/dvc/command/run.py", line 47, in run
    self.parsed_args.shell)
  File "/Users/dmitry/src/dvc/dvc/command/run.py", line 69, in run_and_commit_if_needed
    lock)
  File "/Users/dmitry/src/dvc/dvc/command/run.py", line 100, in run_command
    repo_change = RepositoryChange(cmd_args, self.settings, stdout, stderr, shell=shell)
  File "/Users/dmitry/src/dvc/dvc/repository_change.py", line 30, in __init__
    Executor.exec_cmd_only_success(cmd_args, stdout, stderr, shell=shell)
  File "/Users/dmitry/src/dvc/dvc/executor.py", line 54, in exec_cmd_only_success
    raise ExecutorError('Git command error ({}):\n{}'.format(' '.join(cmd), err))
dvc.executor.ExecutorError: Git command error ():
list index out of range
```


Discussion:

Resolving patch:
diff --git a/dvc/cli.py b/dvc/cli.py
--- a/dvc/cli.py
+++ b/dvc/cli.py
@@ -110,8 +110,11 @@ def parse_args(argv=None):
                         help='Lock data item - disable reproduction.')
     run_parser.add_argument(
                         'command',
-                        nargs=argparse.REMAINDER,
                         help='Command to execute')
+    run_parser.add_argument(
+                        'args',
+                        nargs=argparse.REMAINDER,
+                        help='Arguments of a command')
     run_parser.set_defaults(func=CmdRun)
 
     # Sync
@@ -121,8 +124,7 @@ def parse_args(argv=None):
                         help='Synchronize data file with cloud (cloud settings already setup.')
     sync_parser.add_argument(
                         'targets',
-                        metavar='',
-                        nargs='*',
+                        nargs='+',
                         help='File or directory to sync.')
     sync_parser.add_argument('-j',
                         '--jobs',
@@ -138,7 +140,6 @@ def parse_args(argv=None):
                         help='Reproduce data')
     repro_parser.add_argument(
                         'target',
-                        metavar='',
                         nargs='*',
                         help='Data items to reproduce.')
     repro_parser.add_argument('-f',
@@ -151,7 +152,7 @@ def parse_args(argv=None):
                         action='store_true',
                         default=False,
                         help='Reproduce only single data item without recursive dependencies check.')
-    repro_parser.set_defaults(func=CmdRun)
+    repro_parser.set_defaults(func=CmdRepro)
 
     # Remove
     remove_parser = subparsers.add_parser(
@@ -159,7 +160,6 @@ def parse_args(argv=None):
                         parents=[parent_parser],
                         help='Remove data item from data directory.')
     remove_parser.add_argument('target',
-                        metavar='',
                         nargs='*',
                         help='Target to remove - file or directory.')
     remove_parser.add_argument('-l',
@@ -219,7 +219,6 @@ def parse_args(argv=None):
                         help='Unlock data item - enable reproduction.')
     lock_parser.add_argument(
                         'files',
-                        metavar='',
                         nargs='*',
                         help='Data items to lock or unlock.')
     lock_parser.set_defaults(func=CmdLock)
@@ -230,7 +229,6 @@ def parse_args(argv=None):
                         parents=[parent_parser],
                         help='Collect garbage')
     gc_parser.add_argument('target',
-                        metavar='',
                         nargs='*',
                         help='Target to remove - file or directory.')
     gc_parser.add_argument('-l',
@@ -255,7 +253,6 @@ def parse_args(argv=None):
                         parents=[parent_parser],
                         help='Set default target')
     target_parser.add_argument('target_file',
-                        metavar='',
                         nargs='?',
                         help='Target data item.')
     target_parser.add_argument('-u',
diff --git a/dvc/command/run.py b/dvc/command/run.py
--- a/dvc/command/run.py
+++ b/dvc/command/run.py
@@ -38,8 +38,9 @@ def declaration_output_data_items(self):
 
     def run(self):
         with DvcLock(self.is_locker, self.git):
-            data_items_from_args, not_data_items_from_args = self.argv_files_by_type(self.parsed_args.command)
-            return self.run_and_commit_if_needed(self.parsed_args.command,
+            cmd = [self.parsed_args.command] + self.parsed_args.args
+            data_items_from_args, not_data_items_from_args = self.argv_files_by_type(cmd)
+            return self.run_and_commit_if_needed(cmd,
                                                  data_items_from_args,
                                                  not_data_items_from_args,
                                                  self.parsed_args.stdout,


Test patch:
diff --git a/tests/test_cmd_run.py b/tests/test_cmd_run.py
--- a/tests/test_cmd_run.py
+++ b/tests/test_cmd_run.py
@@ -31,7 +31,7 @@ def setUp(self):
         self.config = ConfigI('data', 'cache', 'state')
         self.path_factory = PathFactory(self.git, self.config)
 
-        self.settings = Settings(['run'], self.git, self.config)
+        self.settings = Settings('run cmd', self.git, self.config)
         pass
 
     def init_git_repo(self):
@@ -68,7 +68,7 @@ def setUp(self):
         self.input_param_file = os.path.join('data', 'extra_input')
         self.extra_output_file = os.path.join('data', 'extra_output')
 
-        self.settings.parse_args('run --input {} --output {}'.format(self.input_param_file, self.extra_output_file))
+        self.settings.parse_args('run --input {} --output {} cmd'.format(self.input_param_file, self.extra_output_file))
         self.settings._args = []
         cmd_run = CmdRun(self.settings)