Repository: pex-tool/pex

Issue:
Errors when using PEX_TOOLS=1
Hi, this is solely a UX issue, there doesn't appear to be anything fundamentally broken but it might be confusing to the user.

Running the following commands works:
```
PEX_TOOLS=1 pex --help
PEX_TOOLS=1 pex repository --help
```
Dropping `--help` results in:
```
Traceback (most recent call last):
  File "/home/florian/.pex/unzipped_pexes/01a8d22400b26b1d132a9849fb6e28962f1ecf9e/.bootstrap/pex/pex.py", line 473, in execute
    exit_value = tools.main(pex=PEX(sys.argv[0]))
  File "/home/florian/.pex/unzipped_pexes/01a8d22400b26b1d132a9849fb6e28962f1ecf9e/.bootstrap/pex/tools/main.py", line 78, in main
    with pex_tools.parsed_command() as pex_command, TRACER.timed(
  File "/usr/lib64/python3.9/contextlib.py", line 119, in __enter__
    return next(self.gen)
  File "/home/florian/.pex/unzipped_pexes/01a8d22400b26b1d132a9849fb6e28962f1ecf9e/.bootstrap/pex/commands/command.py", line 488, in parsed_command
    with global_environment(options):
  File "/usr/lib64/python3.9/contextlib.py", line 119, in __enter__
    return next(self.gen)
  File "/home/florian/.pex/unzipped_pexes/01a8d22400b26b1d132a9849fb6e28962f1ecf9e/.bootstrap/pex/commands/command.py", line 372, in global_environment
    with _configured_env(options):
  File "/usr/lib64/python3.9/contextlib.py", line 119, in __enter__
    return next(self.gen)
  File "/home/florian/.pex/unzipped_pexes/01a8d22400b26b1d132a9849fb6e28962f1ecf9e/.bootstrap/pex/commands/command.py", line 353, in _configured_env
    if options.rc_file or not ENV.PEX_IGNORE_RCFILES:
AttributeError: 'Namespace' object has no attribute 'rc_file'
```
in both cases. It would be great if instead if could maybe also show the help (so one can discover subcommands easily)


Discussion:

Resolving patch:
diff --git a/pex/commands/command.py b/pex/commands/command.py
--- a/pex/commands/command.py
+++ b/pex/commands/command.py
@@ -37,6 +37,7 @@
         IO,
         NoReturn,
         Optional,
+        Sequence,
         Type,
         TypeVar,
         Union,
@@ -450,8 +451,8 @@ def add_arguments(self, parser):
         pass
 
     @contextmanager
-    def parsed_command(self):
-        # type: () -> Iterator[_C]
+    def parsed_command(self, args=None):
+        # type: (Optional[Sequence[str]]) -> Iterator[_C]
         logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)
 
         # By default, let argparse derive prog from sys.argv[0].
@@ -468,6 +469,7 @@ def parsed_command(self):
         )
         parser.add_argument("-V", "--version", action="version", version=__version__)
         parser.set_defaults(command_type=functools.partial(Command.show_help, parser))
+        register_global_arguments(parser)
         self.add_arguments(parser)
         if self._command_types:
             subparsers = parser.add_subparsers(description=self._subparsers_description)
@@ -484,7 +486,7 @@ def parsed_command(self):
                 command_type.add_arguments(command_parser)
                 command_parser.set_defaults(command_type=command_type)
 
-        options = parser.parse_args()
+        options = parser.parse_args(args=args)
         with global_environment(options):
             command_type = cast("Type[_C]", options.command_type)
             yield command_type(options)


Test patch:
diff --git a/tests/commands/test_command.py b/tests/commands/test_command.py
--- a/tests/commands/test_command.py
+++ b/tests/commands/test_command.py
@@ -3,15 +3,15 @@
 
 import pytest
 
-from pex.commands.command import Error, ResultError, catch, try_
+from pex.commands.command import Command, Error, Main, ResultError, catch, try_
 from pex.typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from typing import Any, Union
+    from typing import Any, Optional, Union
 
 
-def test():
-    # type: () -> Any
+def test_try_catch():
+    # type: () -> None
 
     def classify(number):
         # type: (Union[float, int]) -> Union[str, Error]
@@ -32,3 +32,41 @@ def classify(number):
     assert ResultError(Error("Insignificant.")) == exc_info.value
 
     assert Error("Insignificant.") == catch(try_, classify(1 / 137))
+
+
+def test_main(capsys):
+    # type: (Any) -> None
+
+    class TestCommand(Command):
+        pass
+
+    class Command1(TestCommand):
+        pass
+
+    class Command2(TestCommand):
+        pass
+
+    main = Main[TestCommand](prog="test_main", command_types=(Command1, Command2))
+
+    def assert_output(
+        expected_stdout=None,  # type: Optional[str]
+        expected_stderr=None,  # type: Optional[str]
+    ):
+        # type: (...) -> None
+        captured = capsys.readouterr()
+        assert expected_stdout in captured.out if expected_stdout else "" == captured.out
+        assert expected_stderr in captured.err if expected_stderr else "" == captured.err
+
+    with main.parsed_command(["command1"]) as command:
+        assert isinstance(command, Command1)
+    assert_output()
+
+    with main.parsed_command(["command2"]) as command:
+        assert isinstance(command, Command2)
+    assert_output()
+
+    cm = main.parsed_command([])
+    with pytest.raises(SystemExit) as exc_info:
+        cm.__enter__()
+    assert 2 == exc_info.value.code
+    assert_output(expected_stderr="test_main [-h] [-V] [-v] [--emit-warnings] ")
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
--- a/tests/test_packaging.py
+++ b/tests/test_packaging.py
@@ -44,8 +44,10 @@ def test_pex_script():
 def test_pex_tools_script():
     # type: () -> None
     command_names = ",".join([command_type.name() for command_type in all_commands()])
-    expected_first_line = "usage: pex-tools [-h] [-V] PATH {{{command_names}}} ...".format(
-        command_names=command_names
+    expected_first_line = (
+        "usage: pex-tools [-h] [-V] [-v] [--emit-warnings] [--pex-root PEX_ROOT] [--disable-cache] "
+        "[--cache-dir CACHE_DIR] [--tmpdir TMPDIR] [--rcfile RC_FILE] PATH "
+        "{{{command_names}}} ...".format(command_names=command_names)
     )
 
     # Make sure we don't word-wrap for simplicity of testing.