Repository: kislyuk/argcomplete (language: python)

Issue:
zsh: Extra space is added during hierarchy completion (directory or custom)
Hello,
Please find example in attachment.
[test_comp.py.zip](https://github.com/kislyuk/argcomplete/files/12866822/test_comp.py.zip)


When I try to complete on zsh:
./test_comp.py --dir /ho\<tab\>
generates: "/home/ " with extra space. As a result - all child items are not available 

./test_comp.py --custom /pa\<tab\>
generates: "/parent/ " also with extra space

When I try to complete on bash - no issues are found.

Environment:
lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.3 LTS
Release:        22.04
Codename:       jammy

pip show argcomplete
Name: argcomplete
Version: 3.1.2
Summary: Bash tab completion for argparse
Home-page: https://github.com/kislyuk/argcomplete
Author: Andrey Kislyuk
Author-email: kislyuk@gmail.com
License: Apache Software License
Location: /home/dybala/.local/lib/python3.10/site-packages
Requires: 
Required-by: 

python --version
Python 3.10.12

zsh --version
zsh 5.8.1 (x86_64-ubuntu-linux-gnu)

bash --version
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)


Discussion / hints:
I found the proper solution: use DirectoriesCompleter

```py
parser.add_argument(
    'filepath',
    help='specify path to file',
).completer = DirectoriesCompleter
```
Hello, but I am more interesting in custom completer. (see example above).
It is working on bash, but have issues on zsh

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2644ff1a..cc36b5fb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,6 +21,9 @@ jobs:
       run: |
         [[ $(uname) == Linux ]] && sudo apt-get install --yes rpm tcsh fish zsh
         [[ $(uname) == Darwin ]] && brew install bash tcsh fish
+        # Some runners have python-argcomplete preinstalled
+        # as a dependency of pipx, which interferes with the tests.
+        [[ $(uname) == Darwin ]] && brew uninstall --ignore-dependencies python-argcomplete || true
         python -m pip install --quiet --upgrade codecov
     - run: make install
     - run: make lint
diff --git a/argcomplete/bash_completion.d/_python-argcomplete b/argcomplete/bash_completion.d/_python-argcomplete
index 1e4c66ce..cdb60a96 100644
--- a/argcomplete/bash_completion.d/_python-argcomplete
+++ b/argcomplete/bash_completion.d/_python-argcomplete
@@ -138,12 +138,6 @@ _python_argcomplete_global() {
         req_argv=( "" "${COMP_WORDS[@]:1}" )
         __python_argcomplete_expand_tilde_by_ref executable
     else
-        if [[ "$service" != "-default-" ]]; then
-            # TODO: this may not be sufficient - see https://zsh.sourceforge.io/Doc/Release/Completion-System.html
-            # May need to call _complete with avoid-completer=_python-argcomplete or something like that
-            _default
-            return
-        fi
         executable="${words[1]}"
         req_argv=( "${words[@]:1}" )
     fi
@@ -208,7 +202,15 @@ _python_argcomplete_global() {
                 _ARGCOMPLETE_SHELL="zsh" \
                 _ARGCOMPLETE_SUPPRESS_SPACE=1 \
                 __python_argcomplete_run "$executable" "${(@)req_argv[1, ${ARGCOMPLETE}-1]}"))
-            _describe "$executable" completions
+            local nosort=()
+            local nospace=()
+            if is-at-least 5.8; then
+                nosort=(-o nosort)
+            fi
+            if [[ "${completions-}" =~ ([^\\]): && "${BASH_REMATCH[2]}" =~ [=/:] ]]; then
+                nospace=(-S '')
+            fi
+            _describe "$executable" completions "${nosort[@]}" "${nospace[@]}"
         else
             COMPREPLY=($(IFS="$IFS" \
                 COMP_LINE="$COMP_LINE" \
@@ -234,5 +236,9 @@ _python_argcomplete_global() {
 if [[ -z "${ZSH_VERSION-}" ]]; then
     complete -o default -o bashdefault -D -F _python_argcomplete_global
 else
-    compdef _python_argcomplete_global -P '*'
+    autoload is-at-least
+    # Replace only the default completer (_default).
+    # There are many other special contexts we don't want to override.
+    # https://zsh.sourceforge.io/Doc/Release/Completion-System.html
+    compdef _python_argcomplete_global -default-
 fi
diff --git a/argcomplete/shell_integration.py b/argcomplete/shell_integration.py
index 53b8e182..73214bb6 100644
--- a/argcomplete/shell_integration.py
+++ b/argcomplete/shell_integration.py
@@ -42,7 +42,15 @@
             _ARGCOMPLETE_SHELL="zsh" \
             _ARGCOMPLETE_SUPPRESS_SPACE=1 \
             __python_argcomplete_run ${script:-${words[1]}}))
-        _describe "${words[1]}" completions -o nosort
+        local nosort=()
+        local nospace=()
+        if is-at-least 5.8; then
+            nosort=(-o nosort)
+        fi
+        if [[ "${completions-}" =~ ([^\\]): && "${match[1]}" =~ [=/:] ]]; then
+            nospace=(-S '')
+        fi
+        _describe "${words[1]}" completions "${nosort[@]}" "${nospace[@]}"
     else
         local SUPPRESS_SPACE=0
         if compopt +o nospace 2> /dev/null; then
@@ -67,6 +75,7 @@
 if [[ -z "${ZSH_VERSION-}" ]]; then
     complete %(complete_opts)s -F _python_argcomplete%(function_suffix)s %(executables)s
 else
+    autoload is-at-least
     compdef _python_argcomplete%(function_suffix)s %(executables)s
 fi
 """


Test patch:
diff --git a/test/test.py b/test/test.py
index d94e55ac..b741a585 100755
--- a/test/test.py
+++ b/test/test.py
@@ -77,6 +77,8 @@ def bash_repl(command="bash"):
 def zsh_repl(command="zsh"):
     sh = _repl_sh(command, ["--no-rcs", "-V"], non_printable_insert="%(!..)")
     sh.run_command("autoload compinit; compinit -u")
+    # Require two tabs to print all options (some tests rely on this).
+    sh.run_command("setopt BASH_AUTO_LIST")
     return sh
 
 
@@ -1256,9 +1258,6 @@ def setUp(self):
         path = ":".join([os.path.join(BASE_DIR, "scripts"), TEST_DIR, "$PATH"])
         sh.run_command("export PATH={0}".format(path))
         sh.run_command("export PYTHONPATH={0}".format(BASE_DIR))
-        if self.repl_provider == bash_repl:
-            # Disable the "python" module provided by bash-completion
-            sh.run_command("complete -r python python2 python3")
         output = sh.run_command(self.install_cmd)
         self.assertEqual(output, "")
         # Register a dummy completion with an external argcomplete script
@@ -1313,18 +1312,24 @@ class TestZsh(TestBashZshBase, unittest.TestCase):
         "test_parse_special_characters_dollar",
         "test_comp_point",  # FIXME
         "test_completion_environment",  # FIXME
-        "test_continuation",  # FIXME
-        "test_wordbreak_chars",  # FIXME
     ]
 
     def repl_provider(self):
         return zsh_repl()
 
 
-@unittest.skipIf(BASH_MAJOR_VERSION < 4, "complete -D not supported")
-class TestBashGlobal(TestBash):
+class TestBashZshGlobalBase(TestBashZshBase):
     install_cmd = 'eval "$(activate-global-python-argcomplete --dest=-)"'
 
+    def test_redirection_completion(self):
+        with TempDir(prefix="test_dir_py", dir="."):
+            self.sh.run_command("cd " + os.getcwd())
+            self.sh.run_command("echo failure > ./foo.txt")
+            self.sh.run_command("echo success > ./foo.\t")
+            with open("foo.txt") as f:
+                msg = f.read()
+            self.assertEqual(msg, "success\n")
+
     def test_python_completion(self):
         self.sh.run_command("cd " + TEST_DIR)
         self.assertEqual(self.sh.run_command("python3 ./prog basic f\t"), "foo\r\n")
@@ -1364,9 +1369,6 @@ def _test_console_script(self, package=False, wheel=False):
             command = "pip install {} --target .".format(test_package)
             if not wheel:
                 command += " --no-binary :all:"
-                if sys.platform == "darwin":
-                    # Work around https://stackoverflow.com/questions/24257803
-                    command += ' --install-option="--prefix="'
             install_output = self.sh.run_command(command)
             self.assertEqual(self.sh.run_command("echo $?"), "0\r\n", install_output)
             command = "test-module"
@@ -1375,27 +1377,32 @@ def _test_console_script(self, package=False, wheel=False):
             command += " a\t"
             self.assertEqual(self.sh.run_command(command), "arg\r\n")
 
-    @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS")
     def test_console_script_module(self):
         """Test completing a console_script for a module."""
         self._test_console_script()
 
-    @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS")
     def test_console_script_package(self):
         """Test completing a console_script for a package."""
         self._test_console_script(package=True)
 
-    @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS")
     def test_console_script_module_wheel(self):
         """Test completing a console_script for a module from a wheel."""
         self._test_console_script(wheel=True)
 
-    @unittest.skipIf(os.uname()[0] == "Darwin", "Skip test that fails on MacOS")
     def test_console_script_package_wheel(self):
         """Test completing a console_script for a package from a wheel."""
         self._test_console_script(package=True, wheel=True)
 
 
+@unittest.skipIf(BASH_MAJOR_VERSION < 4, "complete -D not supported")
+class TestBashGlobal(TestBash, TestBashZshGlobalBase):
+    pass
+
+
+class TestZshGlobal(TestZsh, TestBashZshGlobalBase):
+    pass
+
+
 class Shell:
     def __init__(self, shell):
         self.child = pexpect.spawn(shell, encoding="utf-8")