Repository: globus/globus-cli

Issue:
Argument parsing issue with --batch
Hi there,

I've hit an issue running `globus transfer` - there seems to be an issue with the argument parsing when `--batch` is provided. For example, running:
```
globus transfer --batch=$FILE_DIR/files.txt $SRC_UUID:$SRC_DIR/ $DST_UUID:$DST_DIR/
```
Gives the following error
```
Usage: globus [OPTIONS] SOURCE_PATH DEST_PATH
Try 'globus --help' for help.

Error: Missing argument 'DEST_PATH'.
```
Thanks for your help with this

Discussion:

Patch:
diff --git a/changelog.d/20230606_233129_sirosen_fix_handling_bad_batch.md b/changelog.d/20230606_233129_sirosen_fix_handling_bad_batch.md
new file mode 100644
index 00000000..182da84d
--- /dev/null
+++ b/changelog.d/20230606_233129_sirosen_fix_handling_bad_batch.md
@@ -0,0 +1,4 @@
+### Enhancements
+
+* Error reporting for parsing errors during `--batch` processing has been
+  improved to better indicate the source of the error
diff --git a/src/globus_cli/commands/delete.py b/src/globus_cli/commands/delete.py
index b1c15740..a9b443a4 100644
--- a/src/globus_cli/commands/delete.py
+++ b/src/globus_cli/commands/delete.py
@@ -159,7 +159,7 @@ def delete_command(
             """
             delete_data.add_item(str(path))
 
-        utils.shlex_process_stream(process_batch_line, batch)
+        utils.shlex_process_stream(process_batch_line, batch, "--batch")
     else:
         if not star_silent and enable_globs and path.endswith("*"):
             # not intuitive, but `click.confirm(abort=True)` prints to stdout
diff --git a/src/globus_cli/services/transfer/data.py b/src/globus_cli/services/transfer/data.py
index 8cdf9a2f..341051da 100644
--- a/src/globus_cli/services/transfer/data.py
+++ b/src/globus_cli/services/transfer/data.py
@@ -37,7 +37,7 @@ def add_batch_to_transfer_data(
             recursive=recursive,
         )
 
-    shlex_process_stream(process_batch_line, batch)
+    shlex_process_stream(process_batch_line, batch, "--batch")
 
 
 def display_name_or_cname(ep_doc: dict | globus_sdk.GlobusHTTPResponse) -> str:
diff --git a/src/globus_cli/utils.py b/src/globus_cli/utils.py
index dfaf02a5..b036ffcd 100644
--- a/src/globus_cli/utils.py
+++ b/src/globus_cli/utils.py
@@ -147,7 +147,9 @@ class PagingWrapper:
         return converter
 
 
-def shlex_process_stream(process_command: click.Command, stream: t.TextIO) -> None:
+def shlex_process_stream(
+    process_command: click.Command, stream: t.TextIO, name: str
+) -> None:
     """
     Use shlex to process stdin line-by-line.
     Also prints help text.
@@ -161,14 +163,22 @@ def shlex_process_stream(process_command: click.Command, stream: t.TextIO) -> No
     # use readlines() rather than implicit file read line looping to force
     # python to properly capture EOF (otherwise, EOF acts as a flush and
     # things get weird)
-    for line in stream.readlines():
+    for lineno, line in enumerate(stream.readlines()):
         # get the argument vector:
         # do a shlex split to handle quoted paths with spaces in them
         # also lets us have comments with #
         argv = shlex.split(line, comments=True)
         if argv:
             try:
-                process_command.main(args=argv)
-            except SystemExit as e:
-                if e.code != 0:
-                    raise
+                with process_command.make_context(f"<process {name}>", argv) as ctx:
+                    process_command.invoke(ctx)
+            except click.ClickException as error:
+                click.echo(
+                    f"error encountered processing '{name}' in "
+                    f"{stream.name} at line {lineno}:",
+                    err=True,
+                )
+                click.echo(
+                    click.style(f"  {error.format_message()}", fg="yellow"), err=True
+                )
+                click.get_current_context().exit(2)


Test patch:
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 2f947c84..a6978abe 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -1,8 +1,12 @@
+import unittest.mock
+
+import click
 import pytest
 
 from globus_cli.utils import (
     format_list_of_words,
     format_plural_str,
+    shlex_process_stream,
     unquote_cmdprompt_single_quotes,
 )
 
@@ -38,3 +42,55 @@ def test_format_plural_str():
 )
 def test_unquote_cmdprompt_squote(arg, expect):
     assert unquote_cmdprompt_single_quotes(arg) == expect
+
+
+def test_shlex_process_stream_success():
+    @click.command()
+    def outer_main():
+        pass
+
+    values = []
+
+    @click.command()
+    @click.argument("bar")
+    def foo(bar):
+        values.append(bar)
+
+    text_like = unittest.mock.Mock()
+    text_like.readlines.return_value = ["alpha\n", "beta  # gamma\n"]
+    text_like.name = "alphabet.txt"
+
+    with outer_main.make_context("main", []):
+        shlex_process_stream(foo, text_like, "data")
+    assert values == ["alpha", "beta"]
+
+
+def test_shlex_process_stream_error_handling(capsys):
+    @click.command()
+    def outer_main():
+        pass
+
+    values = []
+
+    @click.command()
+    @click.argument("bar")
+    def foo(bar):
+        values.append(bar)
+
+    text_like = unittest.mock.Mock()
+    text_like.readlines.return_value = ["alpha beta\n"]
+    text_like.name = "alphabet.txt"
+
+    with pytest.raises(click.exceptions.Exit) as excinfo:
+        with outer_main.make_context("main", []):
+            shlex_process_stream(foo, text_like, "data")
+
+    assert excinfo.value.exit_code == 2
+    captured = capsys.readouterr()
+    assert (
+        """\
+error encountered processing 'data' in alphabet.txt at line 0:
+  Got unexpected extra argument (beta)
+"""
+        in captured.err
+    )