Repository: iterative/shtab

Issue:
[bug] shtab's --print-completion should stop completion
Actual behaviour:

```
  "--print-completion[print shell completion script]:print_completion:(bash zsh tcsh)"
```

Expected behaviour:

```
  "(- : *)--print-completion[print shell completion script]:print_completion:(bash zsh tcsh)"
```

Discussion:
sourcery-ai[bot]: # Sourcery Code Quality Report

✅ &nbsp;Merging this PR will **increase** code quality in the affected files by **1.47%**.

| Quality metrics | Before | After | Change |
| --- | :---: | :---: | :---: |
| [Complexity](https://docs.sourcery.ai/GitHub/Quality-Report/#complexity) | 46.94 ⛔ | 46.48 ⛔ | -0.46 👍 |
| [Method Length](https://docs.sourcery.ai/GitHub/Quality-Report/#method-length) | 137.00 😞 | 125.45 😞 | -11.55 👍 |
| [Working memory](https://docs.sourcery.ai/GitHub/Quality-Report/#working-memory) | 1.75 ⭐ | 1.69 ⭐ | -0.06 👍 |
| **[Quality](https://docs.sourcery.ai/GitHub/Quality-Report/#quality-score)** | **25.89%** 😞| **27.36%** 😞| **1.47%** 👍 |

| Other metrics | Before | After | Change |
| --- | :---: | :---: | :---: |
| Lines | 761 | 762 | 1 |

| Changed files | Quality Before | Quality After | Quality Change |
| --- | :---: | :---: | :---: |
| shtab/__init__.py | 25.89% 😞 | 27.36% 😞 | 1.47% 👍|

Here are some functions in these files that still need a tune-up:

| File | Function | Complexity | Length | Working Memory | Quality | Recommendation |
| --- | --- | :---: | :---: | :---: | :---: | --- |
| shtab/__init__.py | get\_bash\_commands | 78 ⛔ | 589 ⛔ |   | 1.22% ⛔ | Refactor to reduce nesting. Try splitting into smaller methods |
| shtab/__init__.py | complete\_zsh | 62 ⛔ | 900 ⛔ |   | 2.38% ⛔ | Refactor to reduce nesting. Try splitting into smaller methods |
| shtab/__init__.py | complete\_tcsh | 53 ⛔ | 470 ⛔ |   | 4.99% ⛔ | Refactor to reduce nesting. Try splitting into smaller methods |
| shtab/__init__.py | complete\_bash | 2 ⭐ | 106 🙂 | 11 😞 | 64.30% 🙂 | Extract out complex expressions |
| shtab/__init__.py | add\_argument\_to | 4 ⭐ | 89 🙂 | 11 😞 | 64.67% 🙂 | Extract out complex expressions |

## Legend and Explanation

The emojis denote the absolute quality of the code:

 - ⭐ excellent
 - 🙂 good
 - 😞 poor
 - ⛔ very poor


The 👍 and 👎 indicate whether the quality has improved or gotten worse with this pull request.

***

Please see our documentation [here](https://docs.sourcery.ai/GitHub/Quality-Report/) for details on how these metrics are calculated.

We are actively working on this report - lots more documentation and extra metrics to come!

Help us [improve](https://research.typeform.com/to/j06Spdfr?type=quality_report&github_login=Freed-Wu&base_repo=https%3A%2F%2Fgithub.com%2Fiterative%2Fshtab.git&base_remote_ref=refs%2Fpull%2F93%2Fhead&base_ref=master&base_sha=fa460a18c3ae13c62d7218ddde8509077123ab1e&head_repo=https%3A%2F%2Fgithub.com%2Fiterative%2Fshtab.git&head_ref=sourcery%2Fpull-93&base_pr_number=93&base_pr_state=open) this quality report!
casperdcl: @Freed-Wu Thanks! You wrote `fixes #91` in a commit message, just clarifying you meant `fixes #90`, right?
Freed-Wu: > @Freed-Wu Thanks! You wrote `fixes #91` in a commit message, just clarifying you meant `fixes #90`, right?

It also fix #91, which path completion failed in zsh. Because they are simple, I put them in one PR. If you like, I can split them to many PRs.

Patch:
diff --git a/shtab/__init__.py b/shtab/__init__.py
index 159c62f..81057cd 100644
--- a/shtab/__init__.py
+++ b/shtab/__init__.py
@@ -4,7 +4,10 @@ import logging
 import re
 import sys
 from argparse import (
+    ONE_OR_MORE,
+    REMAINDER,
     SUPPRESS,
+    ZERO_OR_MORE,
     Action,
     ArgumentParser,
     _AppendAction,
@@ -49,7 +52,13 @@ FLAG_OPTION = (
     _AppendConstAction,
     _CountAction,
 )
-OPTION_END = _HelpAction, _VersionAction
+
+
+class _ShtabPrintCompletionAction(Action):
+    pass
+
+
+OPTION_END = _HelpAction, _VersionAction, _ShtabPrintCompletionAction
 OPTION_MULTI = _AppendAction, _AppendConstAction, _CountAction
 
 
@@ -462,11 +471,16 @@ def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None):
     if choice_functions:
         choice_type2fn.update(choice_functions)
 
+    def is_opt_end(opt):
+        return isinstance(opt, OPTION_END) or opt.nargs == REMAINDER
+
+    def is_opt_multiline(opt):
+        return isinstance(opt, OPTION_MULTI)
+
     def format_optional(opt):
         return (('{nargs}{options}"[{help}]"' if isinstance(
             opt, FLAG_OPTION) else '{nargs}{options}"[{help}]:{dest}:{pattern}"').format(
-                nargs=('"(- :)"' if isinstance(opt, OPTION_END) else
-                       '"*"' if isinstance(opt, OPTION_MULTI) else ""),
+                nargs=('"(- : *)"' if is_opt_end(opt) else '"*"' if is_opt_multiline(opt) else ""),
                 options=("{{{}}}".format(",".join(opt.option_strings))
                          if len(opt.option_strings) > 1 else '"{}"'.format("".join(
                              opt.option_strings))),
@@ -480,7 +494,7 @@ def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None):
 
     def format_positional(opt):
         return '"{nargs}:{help}:{pattern}"'.format(
-            nargs={"+": "(*)", "*": "(*):"}.get(opt.nargs, ""),
+            nargs={ONE_OR_MORE: "(*)", ZERO_OR_MORE: "(*):", REMAINDER: "(-)*"}.get(opt.nargs, ""),
             help=escape_zsh((opt.help or opt.dest).strip().split("\n")[0]),
             pattern=complete2pattern(opt.complete, "zsh", choice_type2fn) if hasattr(
                 opt, "complete") else
@@ -492,10 +506,12 @@ def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None):
     all_commands = {
         root_prefix: {
             "cmd": prog, "arguments": [
-                format_optional(opt) for opt in parser._get_optional_actions()
-                if opt.help != SUPPRESS], "help": (parser.description
-                                                   or "").strip().split("\n")[0], "commands": [],
-            "paths": []}}
+                format_optional(opt)
+                for opt in parser._get_optional_actions() if opt.help != SUPPRESS] + [
+                    format_positional(opt) for opt in parser._get_positional_actions()
+                    if opt.help != SUPPRESS and opt.choices is None],
+            "help": (parser.description
+                     or "").strip().split("\n")[0], "commands": [], "paths": []}}
 
     def recurse(parser, prefix, paths=None):
         paths = paths or []
@@ -564,11 +580,12 @@ def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None):
 
         return """\
 {prefix}() {{
-  local context state line curcontext="$curcontext"
+  local context state line curcontext="$curcontext" one_or_more='(-)*' remainder='(*)'
 
-  _arguments -C ${prefix}_options \\
-    ': :{prefix}_commands' \\
-    '*::: :->{name}'
+  if ((${{{prefix}_options[(I)${{(q)one_or_more}}*]}} + ${{{prefix}_options[(I)${{(q)remainder}}*]}} == 0)); then  # noqa: E501
+    {prefix}_options+=(': :{prefix}_commands' '*::: :->{name}')
+  fi
+  _arguments -C ${prefix}_options
 
   case $state in
     {name})
@@ -766,7 +783,7 @@ def complete(parser: ArgumentParser, shell: str = "bash", root_prefix: Opt[str]
 
 
 def completion_action(parent=None, preamble=""):
-    class PrintCompletionAction(Action):
+    class PrintCompletionAction(_ShtabPrintCompletionAction):
         def __call__(self, parser, namespace, values, option_string=None):
             print(complete(parent or parser, values, preamble=preamble))
             parser.exit(0)


Test patch:
diff --git a/tests/test_shtab.py b/tests/test_shtab.py
index 87827d3..436f161 100644
--- a/tests/test_shtab.py
+++ b/tests/test_shtab.py
@@ -92,7 +92,7 @@ def test_prog_scripts(shell, caplog, capsys):
     elif shell == "zsh":
         assert script_py == [
             "#compdef script.py", "_describe 'script.py commands' _commands",
-            "'*::: :->script.py'", "script.py)"]
+            "_shtab_shtab_options+=(': :_shtab_shtab_commands' '*::: :->script.py')", "script.py)"]
     elif shell == "tcsh":
         assert script_py == ["complete script.py \\"]
     else: