Blob Blame History Raw
#! /usr/libexec/platform-python
# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, print_function, unicode_literals

import os, sys, shutil, argparse, subprocess, unittest
import pexpect, pexpect.replwrap
from tempfile import TemporaryFile, mkdtemp

TEST_DIR = os.path.abspath(os.path.dirname(__file__))
BASE_DIR = os.path.dirname(TEST_DIR)
sys.path.insert(0, BASE_DIR)

from argparse import ArgumentParser, SUPPRESS
from argcomplete import (
    autocomplete,
    CompletionFinder,
    split_line,
    ExclusiveCompletionFinder,
    _check_module
)
from argcomplete.completers import FilesCompleter, DirectoriesCompleter, SuppressCompleter
from argcomplete.compat import USING_PYTHON2, str, sys_encoding, ensure_str, ensure_bytes

IFS = "\013"
COMP_WORDBREAKS = " \t\n\"'><=;&(:"

BASH_VERSION = subprocess.check_output(['bash', '-c', 'echo $BASH_VERSION']).decode()
BASH_MAJOR_VERSION = int(BASH_VERSION.split('.')[0])


class TempDir(object):
    """
    Temporary directory for testing FilesCompletion

    Usage:

        with TempDir(prefix="temp_fc") as t:
            print("tempdir", t)
            # you are not chdir-ed to the temporary directory
            # everything created here will be deleted
    """
    def __init__(self, suffix="", prefix="tmpdir", dir=None):
        self.tmp_dir = mkdtemp(suffix=suffix, prefix=prefix, dir=dir)
        self.old_dir = os.getcwd()

    def __enter__(self):
        os.chdir(self.tmp_dir)
        return self.tmp_dir

    def __exit__(self, *err):
        os.chdir(self.old_dir)
        shutil.rmtree(self.tmp_dir)


class TestArgcomplete(unittest.TestCase):
    def setUp(self):
        self._os_environ = os.environ
        os.environ = os.environ.copy()
        os.environ["_ARGCOMPLETE"] = "1"
        os.environ["_ARC_DEBUG"] = "yes"
        os.environ["IFS"] = IFS
        os.environ["_ARGCOMPLETE_COMP_WORDBREAKS"] = COMP_WORDBREAKS

    def tearDown(self):
        os.environ = self._os_environ

    def run_completer(self, parser, command, point=None, completer=autocomplete, **kwargs):
        command = ensure_str(command)

        if point is None:
            point = str(len(command))
        with TemporaryFile() as t:
            os.environ["COMP_LINE"] = ensure_bytes(command) if USING_PYTHON2 else command
            os.environ["COMP_POINT"] = point
            self.assertRaises(SystemExit, completer, parser, output_stream=t,
                              exit_method=sys.exit, **kwargs)
            t.seek(0)
            return t.read().decode(sys_encoding).split(IFS)

    def test_basic_completion(self):
        p = ArgumentParser()
        p.add_argument("--foo")
        p.add_argument("--bar")

        completions = self.run_completer(p, "prog ")
        self.assertEqual(set(completions), set(["-h", "--help", "--foo", "--bar"]))

        completions = self.run_completer(p, "prog -")
        self.assertEqual(set(completions), set(["-h", "--help", "--foo", "--bar"]))

        completions = self.run_completer(p, "prog ", always_complete_options=False)
        self.assertEqual(set(completions), set([""]))

        completions = self.run_completer(p, "prog -", always_complete_options=False)
        self.assertEqual(set(completions), set(["-h", "--help", "--foo", "--bar"]))

    def test_choices(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--ship", choices=["submarine", b"speedboat"])
            return parser

        expected_outputs = (
            ("prog ", ["--ship", "-h", "--help"]),
            ("prog --shi", ["--ship "]),
            ("prog --ship ", ["submarine", "speedboat"]),
            ("prog --ship s", ["submarine", "speedboat"]),
            ("prog --ship su", ["submarine "])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_non_str_choices(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("x", type=int, choices=[4, 8, 15, 16, 23, 42])
            return parser

        expected_outputs = (
            ("prog ", ["4", "8", "15", "16", "23", "42", "-h", "--help"]),
            ("prog 1", ["15", "16"]),
            ("prog 2", ["23 "]),
            ("prog 4", ["4", "42"]),
            ("prog 4 ", ["-h", "--help"])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_suppress_args(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--foo")
            parser.add_argument("--bar", help=SUPPRESS)
            return parser

        expected_outputs = (
            ("prog ", ["--foo", "-h", "--help"]),
            ("prog --b", [""])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

        expected_outputs = (
            ("prog ", ["--foo", "--bar", "-h", "--help"]),
            ("prog --b", ["--bar "])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd, print_suppressed=True)), set(output))

    def test_suppress_completer(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--foo")
            arg = parser.add_argument("--bar")
            arg.completer = SuppressCompleter()
            return parser

        expected_outputs = (
            ("prog ", ["--foo", "-h", "--help"]),
            ("prog --b", [""])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

        expected_outputs = (
            ("prog ", ["--foo", "--bar", "-h", "--help"]),
            ("prog --b", ["--bar "])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd, print_suppressed=True)), set(output))

    def test_action_activation(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("var", choices=["bus", "car"])
            parser.add_argument("value", choices=["orange", "apple"])
            return parser

        expected_outputs = (
            ("prog ", ["bus", "car", "-h", "--help"]),
            ("prog bu", ["bus "]),
            ("prog bus ", ["apple", "orange", "-h", "--help"]),
            ("prog bus appl", ["apple "]),
            ("prog bus apple ", ["-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_action_activation_with_subparser(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("name", nargs=2, choices=["a", "b", "c"])
            subparsers = parser.add_subparsers(title="subcommands", metavar="subcommand")
            subparser_build = subparsers.add_parser("build")
            subparser_build.add_argument("var", choices=["bus", "car"])
            subparser_build.add_argument("--profile", nargs=1)
            return parser

        expected_outputs = (
            ("prog ", ["a", "b", "c", "-h", "--help"]),
            ("prog b", ["b "]),
            ("prog b ", ["a", "b", "c", "-h", "--help"]),
            ("prog c b ", ["build", "-h", "--help"]),
            ("prog c b bu", ["build "]),
            ("prog c b build ", ["bus", "car", "--profile", "-h", "--help"]),
            ("prog c b build ca", ["car "]),
            ("prog c b build car ", ["--profile", "-h", "--help"]),
            ("prog build car ", ["-h", "--help"]),
            ("prog a build car ", ["-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_completers(self):
        def c_url(prefix, parsed_args, **kwargs):
            return ["http://url1", "http://url2"]

        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--url").completer = c_url
            parser.add_argument("--email", nargs=3, choices=["a@b.c", "a@b.d", "ab@c.d", "bcd@e.f", "bce@f.g"])
            return parser

        expected_outputs = (
            ("prog --url ", ["http://url1", "http://url2"]),
            ("prog --url \"", ['http://url1', 'http://url2']),
            ("prog --url \"http://url1\" --email ", ["a@b.c", "a@b.d", "ab@c.d", "bcd@e.f", "bce@f.g"]),
            ("prog --url \"http://url1\" --email a", ["a@b.c", "a@b.d", "ab@c.d"]),
            ("prog --url \"http://url1\" --email \"a@", ['a@b.c', 'a@b.d']),
            ("prog --url \"http://url1\" --email \"a@b.c\" \"a@b.d\" \"a@", ['a@b.c', 'a@b.d']),
            ("prog --url \"http://url1\" --email \"a@b.c\" \"a@b.d\" \"ab@c.d\" ",
             ["--url", "--email", "-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_subparser_completers(self):
        def c_depends_on_positional_arg1(prefix, parsed_args, **kwargs):
            return [parsed_args.arg1]

        def c_depends_on_optional_arg5(prefix, parsed_args, **kwargs):
            return [parsed_args.arg5]

        def make_parser():
            parser = ArgumentParser()
            subparsers = parser.add_subparsers()
            subparser = subparsers.add_parser('subcommand')
            subparser.add_argument('arg1')
            subparser.add_argument('arg2').completer = c_depends_on_positional_arg1
            subparser.add_argument('arg3').completer = c_depends_on_optional_arg5
            subparser.add_argument('--arg4').completer = c_depends_on_optional_arg5
            subparser.add_argument('--arg5')
            return parser

        expected_outputs = (
            ("prog subcommand val1 ", ["val1", "--arg4", "--arg5", "-h", "--help"]),
            ("prog subcommand val1 val2 --arg5 val5 ", ["val5", "--arg4", "--arg5", "-h", "--help"]),
            ("prog subcommand val1 val2 --arg5 val6 --arg4 v", ["val6 "]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_file_completion(self):
        # setup and teardown should probably be in class
        with TempDir(prefix="test_dir_fc", dir="."):
            fc = FilesCompleter()
            os.makedirs(os.path.join("abcdefж", "klm"))
            self.assertEqual(fc("a"), ["abcdefж/"])
            os.makedirs(os.path.join("abcaha", "klm"))
            with open("abcxyz", "w") as fp:
                fp.write("test")
            self.assertEqual(set(fc("a")), set(["abcdefж/", "abcaha/", "abcxyz"]))

    def test_filescompleter_filetype_integration(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument('--r', type=argparse.FileType('r'))
            parser.add_argument('--w', type=argparse.FileType('w'))
            return parser

        with TempDir(prefix="test_dir_fc2", dir="."):
            os.makedirs(os.path.join("abcdefж", "klm"))
            os.makedirs(os.path.join("abcaha", "klm"))
            with open("abcxyz", "w") as fh, open("abcdefж/klm/test", "w") as fh2:
                fh.write("test")
                fh2.write("test")

            expected_outputs = (
                ("prog subcommand --r ", ["abcxyz", "abcdefж/", "abcaha/"]),
                ("prog subcommand --w abcdefж/klm/t", ["abcdefж/klm/test "]),
            )

            for cmd, output in expected_outputs:
                self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_directory_completion(self):
        completer = DirectoriesCompleter()

        def c(prefix):
            return set(completer(prefix))
        with TempDir(prefix="test_dir", dir="."):
            # Create some temporary dirs and files (files must be ignored)
            os.makedirs(os.path.join("abc", "baz"))
            os.makedirs(os.path.join("abb", "baz"))
            os.makedirs(os.path.join("abc", "faz"))
            os.makedirs(os.path.join("def", "baz"))
            with open("abc1", "w") as fp1:
                with open("def1", "w") as fp2:
                    fp1.write("A test")
                    fp2.write("Another test")
            # Test completions
            self.assertEqual(c("a"), set(["abb/", "abc/"]))
            self.assertEqual(c("ab"), set(["abc/", "abb/"]))
            self.assertEqual(c("abc"), set(["abc/"]))
            self.assertEqual(c("abc/"), set(["abc/baz/", "abc/faz/"]))
            self.assertEqual(c("d"), set(["def/"]))
            self.assertEqual(c("def/"), set(["def/baz/"]))
            self.assertEqual(c("e"), set([]))
            self.assertEqual(c("def/k"), set([]))
        return

    def test_default_completer(self):
        def make_parser():
            parser = ArgumentParser(add_help=False)
            parser.add_argument("--one")
            parser.add_argument("--many", nargs="+")
            return parser

        with TempDir(prefix="test_dir_dc", dir="."):
            os.mkdir("test")

            expected_outputs = (
                ("prog --one ", ["test/"]),
                ("prog --many ", ["test/"]),
                ("prog --many test/ ", ["test/", "--one", "--many"]),
            )

            for cmd, output in expected_outputs:
                self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_subparsers(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--age", type=int)
            sub = parser.add_subparsers()
            eggs = sub.add_parser("eggs")
            eggs.add_argument("type", choices=["on a boat", "with a goat", "in the rain", "on a train"])
            spam = sub.add_parser("spam")
            spam.add_argument("type", choices=["ham", "iberico"])
            return parser

        expected_outputs = (
            ("prog ", ["--help", "eggs", "-h", "spam", "--age"]),
            ("prog --age 1 eggs", ["eggs "]),
            ("prog --age 2 eggs ",
             [r"on\ a\ train", r"with\ a\ goat", r"on\ a\ boat", r"in\ the\ rain", "--help", "-h"]),
            ("prog eggs ", [r"on\ a\ train", r"with\ a\ goat", r"on\ a\ boat", r"in\ the\ rain", "--help", "-h"]),
            ("prog eggs \"on a", ['on a train', 'on a boat']),
            ("prog eggs on\\ a", [r"on\ a\ train", "on\ a\ boat"]),
            ("prog spam ", ["iberico", "ham", "--help", "-h"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))
            self.assertEqual(set(self.run_completer(make_parser(), cmd, exclude=["-h"])), set(output) - set(["-h"]))
            self.assertEqual(set(self.run_completer(make_parser(), cmd, exclude=["-h", "--help"])),
                             set(output) - set(["-h", "--help"]))

    def test_non_ascii(self):
        def make_parser():
            _str = ensure_bytes if USING_PYTHON2 else str
            parser = ArgumentParser()
            # Python 2 argparse only works with byte strings or ascii unicode strings.
            # Python 3 argparse only works with unicode strings.
            parser.add_argument(_str("--книга"), choices=[
                _str("Трудно быть богом"),
                _str("Парень из преисподней"),
                _str("Понедельник начинается в субботу"),
            ])
            return parser

        expected_outputs = (
            ("prog ", ["--книга", "-h", "--help"]),
            ("prog --книга ",
             [r"Трудно\ быть\ богом", r"Парень\ из\ преисподней", r"Понедельник\ начинается\ в\ субботу"]),
            ("prog --книга П", [r"Парень\ из\ преисподней", "Понедельник\ начинается\ в\ субботу"]),
            ("prog --книга Пу", [""]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)),
                             set(output))

    def test_custom_validator(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("var", choices=["bus", "car"])
            parser.add_argument("value", choices=["orange", "apple"])
            return parser

        expected_outputs = (
            ("prog ", ["-h", "--help"]),
            ("prog bu", [""]),
            ("prog bus ", ["-h", "--help"]),
            ("prog bus appl", [""]),
            ("prog bus apple ", ["-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(
                set(self.run_completer(make_parser(), cmd, validator=lambda x, y: False)),
                set(output)
            )

    def test_different_validators(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("var", choices=["bus", "car"])
            parser.add_argument("value", choices=["orange", "apple"])
            return parser

        validators = (
            lambda x, y: False,
            lambda x, y: True,
            lambda x, y: x.startswith(y),
        )

        expected_outputs = (
            ("prog ", ["-h", "--help"], validators[0]),
            ("prog ", ["bus", "car", "-h", "--help"], validators[1]),
            ("prog bu", ["bus", "car"], validators[1]),
            ("prog bus ", ["apple", "orange", "-h", "--help"], validators[1]),
            ("prog bus appl", ["apple "], validators[2]),
            ("prog bus cappl", [""], validators[2]),
            ("prog bus pple ", ["-h", "--help"], validators[2]),
        )

        for cmd, output, validator in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd, validator=validator)), set(output))

    def test_readline_entry_point(self):
        def get_readline_completions(completer, text):
            completions = []
            for i in range(9999):
                completion = completer.rl_complete(text, i)
                if completion is None:
                    break
                completions.append(completion)
            return completions

        parser = ArgumentParser()
        parser.add_argument("rover", choices=["sojourner", "spirit", "opportunity", "curiosity"])
        parser.add_argument("antenna", choices=["low gain", "high gain"])
        completer = CompletionFinder(parser)
        self.assertEqual(get_readline_completions(completer, ""),
                         ["-h", "--help", "sojourner", "spirit", "opportunity", "curiosity"])
        self.assertEqual(get_readline_completions(completer, "s"), ["sojourner", "spirit"])
        self.assertEqual(get_readline_completions(completer, "x"), [])

    def test_display_completions(self):
        parser = ArgumentParser()
        parser.add_argument("rover",
                            choices=["sojourner", "spirit", "opportunity", "curiosity"],
                            help="help for rover ")
        parser.add_argument("antenna", choices=["low gain", "high gain"], help="help for antenna")
        sub = parser.add_subparsers()
        p = sub.add_parser("list")
        p.add_argument("-o", "--oh", help="ttt")
        p.add_argument("-c", "--ch", help="ccc")
        sub2 = p.add_subparsers()
        sub2.add_parser("cat", help="list cat")
        sub2.add_parser("dog", help="list dog")

        completer = CompletionFinder(parser)

        completer.rl_complete("", 0)
        disp = completer.get_display_completions()
        self.assertEqual("help for rover ", disp.get("spirit", ""))
        self.assertEqual("help for rover ", disp.get("sojourner", ""))
        self.assertEqual("", disp.get("low gain", ""))

        completer.rl_complete('opportunity "low gain" list ', 0)
        disp = completer.get_display_completions()
        self.assertEqual("ttt", disp.get("-o --oh", ""))
        self.assertEqual("list cat", disp.get("cat", ""))

        completer.rl_complete("opportunity low\\ gain list --", 0)
        disp = completer.get_display_completions()
        self.assertEqual("ttt", disp.get("--oh", ""))
        self.assertEqual("ccc", disp.get("--ch", ""))

    def test_nargs_one_or_more(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("h1", choices=["c", "d"])
            parser.add_argument("var", choices=["bus", "car"], nargs="+")
            parser.add_argument("value", choices=["orange", "apple"])
            parser.add_argument("end", choices=["end"])
            return parser

        expected_outputs = (
            ("prog ", ["c", "d", "-h", "--help"]),
            ("prog c ", ["bus", "car", "-h", "--help"]),
            ("prog c bu", ["bus "]),
            ("prog c bus ", ["bus", "car", "apple", "orange", "-h", "--help"]),
            ("prog c bus car ", ["bus", "car", "apple", "orange", "-h", "--help"]),
            ("prog c bus appl", ["apple "]),
            # No way to know which completers to run past this point.
            ("prog c bus apple ", ["bus", "car", "apple", "orange", "end", "-h", "--help"]),
            ("prog c bus car apple ", ["bus", "car", "apple", "orange", "end", "-h", "--help"]),
            ("prog c bus car apple end ", ["bus", "car", "apple", "orange", "end", "-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_nargs_zero_or_more(self):
        def make_parser():
            parser = ArgumentParser()
            # default="foo" is necessary to stop argparse trying to validate []
            parser.add_argument("foo", choices=["foo"], nargs="*", default="foo")
            parser.add_argument("bar", choices=["bar"])
            return parser

        expected_outputs = (
            ("prog ", ["foo", "bar", "-h", "--help"]),
            ("prog foo ", ["foo", "bar", "-h", "--help"]),
            ("prog foo bar ", ["foo", "bar", "-h", "--help"]),
            ("prog foo foo bar ", ["foo", "bar", "-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_nargs_optional(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("foo", choices=["foo"], nargs="?")
            parser.add_argument("bar", choices=["bar"])
            return parser

        expected_outputs = (
            ("prog ", ["foo", "bar", "-h", "--help"]),
            ("prog foo ", ["foo", "bar", "-h", "--help"]),
            ("prog foo bar ", ["-h", "--help"]),
            ("prog bar ", ["foo", "bar", "-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_optional_nargs(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--foo", choices=["foo1", "foo2"], nargs=2)
            parser.add_argument("--bar", choices=["bar1", "bar2"], nargs="?")
            parser.add_argument("--baz", choices=["baz1", "baz2"], nargs="*")
            parser.add_argument("--qux", choices=["qux1", "qux2"], nargs="+")
            parser.add_argument("--foobar", choices=["pos", "--opt"], nargs=argparse.REMAINDER)
            return parser

        options = ["--foo", "--bar", "--baz", "--qux", "--foobar", "-h", "--help"]

        expected_outputs = (
            ("prog ", options),

            ("prog --foo ", ["foo1", "foo2"]),
            ("prog --foo foo1 ", ["foo1", "foo2"]),
            ("prog --foo foo1 foo2 ", options),

            ("prog --bar ", ["bar1", "bar2"] + options),
            ("prog --bar bar1 ", options),

            ("prog --baz ", ["baz1", "baz2"] + options),
            ("prog --baz baz1 ", ["baz1", "baz2"] + options),

            ("prog --qux ", ["qux1", "qux2"]),
            ("prog --qux qux1 ", ["qux1", "qux2"] + options),

            ("prog --foobar ", ["pos", "--opt"]),
            ("prog --foobar pos ", ["pos", "--opt"]),
            ("prog --foobar --", ["--opt "]),
            ("prog --foobar --opt ", ["pos", "--opt"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_positional_remainder(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--foo", choices=["foo1", "foo2"])
            parser.add_argument("remainder", choices=["pos", "--opt"], nargs=argparse.REMAINDER)
            return parser

        options = ["--foo", "-h", "--help"]

        expected_outputs = (
            ("prog ", ["pos", "--opt"] + options),
            ("prog --foo foo1 ", ["pos", "--opt"] + options),
            ("prog pos ", ["pos", "--opt"]),
            ("prog -- ", ["pos", "--opt"]),
            ("prog -- --opt ", ["pos", "--opt"])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_skipped_completer(self):
        parser = ArgumentParser(add_help=False)
        parser.add_argument("--foo", choices=["--bar"])
        self.assertEqual(self.run_completer(parser, "prog --foo --"), ["--foo "])

    def test_optional_long_short_filtering(self):
        def make_parser():
            parser = ArgumentParser()
            parser.add_argument("--foo")
            parser.add_argument("-b", "--bar")
            parser.add_argument("--baz", "--xyz")
            parser.add_argument("-t")
            parser.add_argument("-z", "--zzz")
            parser.add_argument("-x")
            return parser

        long_opts = "--foo --bar --baz --xyz --zzz --help -x -t".split()
        short_opts = "-b -t -x -z -h --foo --baz --xyz".split()
        expected_outputs = (
            ("prog ", {"long": long_opts, "short": short_opts, True: long_opts + short_opts, False: [""]}),
            ("prog --foo", {"long": ["--foo "], "short": ["--foo "], True: ["--foo "], False: ["--foo "]}),
            ("prog --b", {"long": ["--bar", "--baz"],
                          "short": ["--bar", "--baz"],
                          True: ["--bar", "--baz"],
                          False: ["--bar", "--baz"]}),
            ("prog -z -x", {"long": ["-x "], "short": ["-x "], True: ["-x "], False: ["-x "]}),
        )
        for cmd, outputs in expected_outputs:
            for always_complete_options, output in outputs.items():
                result = self.run_completer(make_parser(), cmd, always_complete_options=always_complete_options)
                self.assertEqual(set(result), set(output))

    def test_exclusive(self):
        def make_parser():
            parser = ArgumentParser(add_help=False)
            parser.add_argument("--foo", action="store_true")
            group = parser.add_mutually_exclusive_group()
            group.add_argument("--bar", action="store_true")
            group.add_argument("--no-bar", action="store_true")
            return parser

        expected_outputs = (
            ("prog ", ["--foo", "--bar", "--no-bar"]),
            ("prog --foo ", ["--foo", "--bar", "--no-bar"]),
            ("prog --bar ", ["--foo", "--bar"]),
            ("prog --foo --no-bar ", ["--foo", "--no-bar"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_mixed_optional_positional(self):
        def make_parser():
            parser = ArgumentParser(add_help=False)
            parser.add_argument("name", choices=["name1", "name2"])
            group = parser.add_mutually_exclusive_group()
            group.add_argument("--get", action="store_true")
            group.add_argument("--set", action="store_true")
            return parser

        expected_outputs = (
            ("prog ", ["--get", "--set", "name1", "name2"]),
            ("prog --", ["--get", "--set"]),
            ("prog --get ", ["--get", "name1", "name2"]),
            ("prog --get name1 ", ["--get "])
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(make_parser(), cmd)), set(output))

    def test_append_space(self):
        def make_parser():
            parser = ArgumentParser(add_help=False)
            parser.add_argument("foo", choices=["bar"])
            return parser

        self.assertEqual(self.run_completer(make_parser(), "prog "), ["bar "])
        self.assertEqual(self.run_completer(make_parser(), "prog ", append_space=False), ["bar"])

    def test_exclusive_class(self):
        parser = ArgumentParser(add_help=False)
        parser.add_argument("--foo", dest="types", action="append_const", const=str)
        parser.add_argument("--bar", dest="types", action="append", choices=["bar1", "bar2"])
        parser.add_argument("--baz", choices=["baz1", "baz2"])
        parser.add_argument("--no-bar", action="store_true")

        completer = ExclusiveCompletionFinder(parser, always_complete_options=True)

        expected_outputs = (
            ("prog ", ["--foo", "--bar", "--baz", "--no-bar"]),
            ("prog --baz ", ["baz1", "baz2"]),
            ("prog --baz baz1 ", ["--foo", "--bar", "--no-bar"]),
            ("prog --foo --no-bar ", ["--foo", "--bar", "--baz"]),
            ("prog --foo --bar bar1 ", ["--foo", "--bar", "--baz", "--no-bar"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(parser, cmd, completer=completer)), set(output))

    def test_escape_special_chars(self):
        def make_parser():
            parser = ArgumentParser(add_help=False)
            parser.add_argument("-1", choices=["bar<$>baz"])
            parser.add_argument("-2", choices=[r"\* "])
            parser.add_argument("-3", choices=["\"'"])
            return parser

        self.assertEqual(set(self.run_completer(make_parser(), "prog -1 ")), {r"bar\<\$\>baz "})
        self.assertEqual(set(self.run_completer(make_parser(), "prog -2 ")), {r"\\\*\  "})
        self.assertEqual(set(self.run_completer(make_parser(), "prog -3 ")), {r"\"\' "})
        self.assertEqual(set(self.run_completer(make_parser(), 'prog -3 "')), {r"\"'"})
        self.assertEqual(set(self.run_completer(make_parser(), "prog -3 '")), {"\"'\\''"})
        os.environ["_ARGCOMPLETE_SHELL"] = "tcsh"
        self.assertEqual(set(self.run_completer(make_parser(), "prog -1 ")), {"bar<$>baz "})
        # The trailing space won't actually work correctly in tcsh.
        self.assertEqual(set(self.run_completer(make_parser(), "prog -2 ")), {r"\*  "})
        self.assertEqual(set(self.run_completer(make_parser(), "prog -3 ")), {"\"' "})
        self.assertEqual(set(self.run_completer(make_parser(), 'prog -3 "')), {"\"'"})
        self.assertEqual(set(self.run_completer(make_parser(), "prog -3 '")), {"\"'"})


class TestArgcompleteREPL(unittest.TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def run_completer(self, parser, completer, command, point=None, **kwargs):
        cword_prequote, cword_prefix, cword_suffix, comp_words, first_colon_pos = split_line(command)

        completions = completer._get_completions(
            comp_words, cword_prefix, cword_prequote, first_colon_pos)

        return completions

    def test_repl_multiple_complete(self):
        p = ArgumentParser()
        p.add_argument("--foo")
        p.add_argument("--bar")

        c = CompletionFinder(p, always_complete_options=True)

        completions = self.run_completer(p, c, "prog ")
        assert(set(completions) == set(["-h", "--help", "--foo", "--bar"]))

        completions = self.run_completer(p, c, "prog --")
        assert(set(completions) == set(["--help", "--foo", "--bar"]))

    def test_repl_parse_after_complete(self):
        p = ArgumentParser()
        p.add_argument("--foo", required=True)
        p.add_argument("bar", choices=["bar"])

        c = CompletionFinder(p, always_complete_options=True)

        completions = self.run_completer(p, c, "prog ")
        assert(set(completions) == set(["-h", "--help", "--foo", "bar"]))

        args = p.parse_args(["--foo", "spam", "bar"])
        assert(args.foo == "spam")
        assert(args.bar == "bar")

        # Both options are required - check the parser still enforces this.
        with self.assertRaises(SystemExit):
            p.parse_args(["--foo", "spam"])
        with self.assertRaises(SystemExit):
            p.parse_args(["bar"])

    def test_repl_subparser_parse_after_complete(self):
        p = ArgumentParser()
        sp = p.add_subparsers().add_parser("foo")
        sp.add_argument("bar", choices=["bar"])

        c = CompletionFinder(p, always_complete_options=True)

        completions = self.run_completer(p, c, "prog foo ")
        assert(set(completions) == set(["-h", "--help", "bar"]))

        args = p.parse_args(["foo", "bar"])
        assert(args.bar == "bar")

        # "bar" is required - check the parser still enforces this.
        with self.assertRaises(SystemExit):
            p.parse_args(["foo"])

    def test_repl_subcommand(self):
        p = ArgumentParser()
        p.add_argument("--foo")
        p.add_argument("--bar")

        s = p.add_subparsers()
        s.add_parser("list")
        s.add_parser("set")
        show = s.add_parser("show")

        def abc():
            pass

        show.add_argument("--test")
        ss = show.add_subparsers()
        de = ss.add_parser("depth")
        de.set_defaults(func=abc)

        c = CompletionFinder(p, always_complete_options=True)

        expected_outputs = (
            ("prog ", ["-h", "--help", "--foo", "--bar", "list", "show", "set"]),
            ("prog li", ["list "]),
            ("prog s", ["show", "set"]),
            ("prog show ", ["--test", "depth", "-h", "--help"]),
            ("prog show d", ["depth "]),
            ("prog show depth ", ["-h", "--help"]),
        )

        for cmd, output in expected_outputs:
            self.assertEqual(set(self.run_completer(p, c, cmd)), set(output))

    def test_repl_reuse_parser_with_positional(self):
        p = ArgumentParser()
        p.add_argument("foo", choices=["aa", "bb", "cc"])
        p.add_argument("bar", choices=["d", "e"])

        c = CompletionFinder(p, always_complete_options=True)

        self.assertEqual(set(self.run_completer(p, c, "prog ")),
                         set(["-h", "--help", "aa", "bb", "cc"]))

        self.assertEqual(set(self.run_completer(p, c, "prog aa ")),
                         set(["-h", "--help", "d", "e"]))

        self.assertEqual(set(self.run_completer(p, c, "prog ")),
                         set(["-h", "--help", "aa", "bb", "cc"]))


class TestSplitLine(unittest.TestCase):
    def setUp(self):
        self._os_environ = os.environ
        os.environ = os.environ.copy()
        os.environ['_ARGCOMPLETE_COMP_WORDBREAKS'] = COMP_WORDBREAKS

    def tearDown(self):
        os.environ = self._os_environ

    def prefix(self, line):
        return split_line(line)[1]

    def wordbreak(self, line):
        return split_line(line)[4]

    def test_simple(self):
        self.assertEqual(self.prefix('a b c'), 'c')

    def test_escaped_special(self):
        self.assertEqual(self.prefix('a\$b'), 'a$b')
        self.assertEqual(self.prefix('a\`b'), 'a`b')

    def test_unescaped_special(self):
        self.assertEqual(self.prefix('a$b'), 'a$b')
        self.assertEqual(self.prefix('a`b'), 'a`b')

    @unittest.expectedFailure
    def test_escaped_special_in_double_quotes(self):
        self.assertEqual(self.prefix('"a\$b'), 'a$b')
        self.assertEqual(self.prefix('"a\`b'), 'a`b')

    def test_punctuation(self):
        self.assertEqual(self.prefix('a,'), 'a,')

    def test_last_wordbreak_pos(self):
        self.assertEqual(self.wordbreak('a'), None)
        self.assertEqual(self.wordbreak('a b:c'), 1)
        self.assertEqual(self.wordbreak('a b:c=d'), 3)
        self.assertEqual(self.wordbreak('a b:c=d '), None)
        self.assertEqual(self.wordbreak('a b:c=d e'), None)
        self.assertEqual(self.wordbreak('"b:c'), None)
        self.assertEqual(self.wordbreak('"b:c=d'), None)
        self.assertEqual(self.wordbreak('"b:c=d"'), None)
        self.assertEqual(self.wordbreak('"b:c=d" '), None)


class TestCheckModule(unittest.TestCase):
    def setUp(self):
        self.dir = TempDir(prefix="test_dir_module", dir=".")
        self.dir.__enter__()
        # There is some odd bug that seems to only come up in Python 3.4 where
        # using "." in sys.path sometimes won't find modules, so we'll use the
        # full path each time.
        sys.path.insert(0, os.getcwd())

    def tearDown(self):
        sys.path.pop(0)
        self.dir.__exit__()

    def test_module(self):
        self._mkfile('module.py')
        path = _check_module.find('module')
        self.assertEqual(path, os.path.abspath('module.py'))
        self.assertNotIn('module', sys.modules)

    def test_package(self):
        os.mkdir('package')
        self._mkfile('package/__init__.py')
        self._mkfile('package/module.py')
        path = _check_module.find('package.module')
        self.assertEqual(path, os.path.abspath('package/module.py'))
        self.assertNotIn('package', sys.modules)
        self.assertNotIn('package.module', sys.modules)

    def test_subpackage(self):
        os.mkdir('package')
        self._mkfile('package/__init__.py')
        os.mkdir('package/subpackage')
        self._mkfile('package/subpackage/__init__.py')
        self._mkfile('package/subpackage/module.py')
        path = _check_module.find('package.subpackage.module')
        self.assertEqual(path, os.path.abspath('package/subpackage/module.py'))
        self.assertNotIn('package', sys.modules)
        self.assertNotIn('package.subpackage', sys.modules)
        self.assertNotIn('package.subpackage.module', sys.modules)

    def test_package_main(self):
        os.mkdir('package')
        self._mkfile('package/__init__.py')
        self._mkfile('package/__main__.py')
        path = _check_module.find('package')
        self.assertEqual(path, os.path.abspath('package/__main__.py'))
        self.assertNotIn('package', sys.modules)

    def test_not_package(self):
        self._mkfile('module.py')
        with self.assertRaisesRegexp(Exception, 'module is not a package'):
            _check_module.find('module.bad')
        self.assertNotIn('module', sys.modules)

    def _mkfile(self, path):
        open(path, 'w').close()


class _TestSh(object):
    """
    Contains tests which should work in any shell using argcomplete.

    Tests use the test program in this directory named ``prog``.
    All commands are expected to input one of the valid choices
    which is then printed and collected by the shell wrapper.

    Any tabs in the input line simulate the user pressing tab.
    For example, ``self.sh.run_command('prog basic "b\tr\t')`` will
    simulate having the user:

    1. Type ``prog basic "b``
    2. Push tab, which returns ``['bar', 'baz']``, filling in ``a``
    3. Type ``r``
    4. Push tab, which returns ``['bar']``, filling in ``" ``
    5. Push enter, submitting ``prog basic "bar" ``

    The end result should be ``bar`` being printed to the screen.
    """
    sh = None
    expected_failures = []

    @classmethod
    def setUpClass(cls, *args, **kwargs):
        for name in cls.expected_failures:
            test = getattr(cls, name)

            @unittest.expectedFailure
            def wrapped(self, test=test):
                test(self)
            setattr(cls, name, wrapped)
        super(_TestSh, cls).setUpClass(*args, **kwargs)

    def setUp(self):
        raise NotImplementedError

    def tearDown(self):
        with self.assertRaises(pexpect.EOF):
            self.sh.run_command('exit')

    def test_simple_completion(self):
        self.assertEqual(self.sh.run_command('prog basic f\t'), 'foo\r\n')

    def test_partial_completion(self):
        self.assertEqual(self.sh.run_command('prog basic b\tr'), 'bar\r\n')

    def test_single_quoted_completion(self):
        self.assertEqual(self.sh.run_command("prog basic 'f\t"), 'foo\r\n')

    def test_double_quoted_completion(self):
        self.assertEqual(self.sh.run_command('prog basic "f\t'), 'foo\r\n')

    def test_unquoted_space(self):
        self.assertEqual(self.sh.run_command('prog space f\t'), 'foo bar\r\n')

    def test_quoted_space(self):
        self.assertEqual(self.sh.run_command('prog space "f\t'), 'foo bar\r\n')

    def test_continuation(self):
        # This produces 'prog basic foo --', and '--' is ignored.
        self.assertEqual(self.sh.run_command('prog basic f\t--'), 'foo\r\n')
        # These do not insert a space, so the '--' is part of the token.
        self.assertEqual(self.sh.run_command('prog cont f\t--'), 'foo=--\r\n')
        self.assertEqual(self.sh.run_command('prog cont bar\t--'), 'bar/--\r\n')
        self.assertEqual(self.sh.run_command('prog cont baz\t--'), 'baz:--\r\n')

    def test_quoted_exact(self):
        self.assertEqual(self.sh.run_command('prog basic "f\t--'), 'foo\r\n')

    def test_special_characters(self):
        self.assertEqual(self.sh.run_command('prog spec d\tf'), 'd$e$f\r\n')
        self.assertEqual(self.sh.run_command('prog spec x\t'), 'x!x\r\n')
        self.assertEqual(self.sh.run_command('prog spec y\t'), 'y\\y\r\n')

    def test_special_characters_single_quoted(self):
        self.assertEqual(self.sh.run_command("prog spec 'd\tf'"), 'd$e$f\r\n')

    def test_special_characters_double_quoted(self):
        self.assertEqual(self.sh.run_command('prog spec "d\tf"'), 'd$e$f\r\n')

    def test_parse_special_characters(self):
        self.assertEqual(self.sh.run_command('prog spec d$e$\tf'), 'd$e$f\r\n')
        self.assertEqual(self.sh.run_command('prog spec d$e\tf'), 'd$e$f\r\n')
        self.assertEqual(self.sh.run_command("prog spec 'd$e\tf\t"), 'd$e$f\r\n')

    def test_parse_special_characters_dollar(self):
        # First tab expands to 'd\$e\$'; completion works with 'd$' but not 'd\$'.
        self.assertEqual(self.sh.run_command('prog spec "d$e\tf\t'), 'd$e$f\r\n')

    def test_exclamation_in_double_quotes(self):
        # Exclamation marks cannot be properly escaped within double quotes.
        # 'a!b' == "a"\!"b"
        self.assertEqual(self.sh.run_command('prog spec "x\t'), 'x!x\r\n')

    def test_quotes(self):
        self.assertEqual(self.sh.run_command('prog quote 1\t'), "1'1\r\n")
        self.assertEqual(self.sh.run_command('prog quote 2\t'), '2"2\r\n')

    def test_single_quotes_in_double_quotes(self):
        self.assertEqual(self.sh.run_command('prog quote "1\t'), "1'1\r\n")

    def test_single_quotes_in_single_quotes(self):
        # Single quotes cannot be escaped within single quotes.
        # "a'b" == 'a'\''b'
        self.assertEqual(self.sh.run_command("prog quote '1\t"), "1'1\r\n")

    def test_wordbreak_chars(self):
        self.assertEqual(self.sh.run_command('prog break a\tc'), 'a:b:c\r\n')
        self.assertEqual(self.sh.run_command('prog break a:b:\tc'), 'a:b:c\r\n')
        self.assertEqual(self.sh.run_command('prog break a:b\tc'), 'a:b:c\r\n')
        self.assertEqual(self.sh.run_command("prog break 'a\tc'"), 'a:b:c\r\n')
        self.assertEqual(self.sh.run_command("prog break 'a:b\tc\t"), 'a:b:c\r\n')
        self.assertEqual(self.sh.run_command('prog break "a\tc"'), 'a:b:c\r\n')
        self.assertEqual(self.sh.run_command('prog break "a:b\tc\t'), 'a:b:c\r\n')

    def test_completion_environment(self):
        self.assertEqual(self.sh.run_command('prog env o\t'), 'ok\r\n')

    def test_comp_point(self):
        # Use environment variable to change how prog behaves
        self.assertEqual(self.sh.run_command('export POINT=1'), '')
        self.assertEqual(self.sh.run_command('prog point hi\t'), '13\r\n')
        self.assertEqual(self.sh.run_command('prog point hi \t'), '14\r\n')
        self.assertEqual(self.sh.run_command('prog point 你好嘚瑟\t'), '15\r\n')
        self.assertEqual(self.sh.run_command('prog point 你好嘚瑟 \t'), '16\r\n')


class TestBash(_TestSh, unittest.TestCase):
    expected_failures = [
        'test_parse_special_characters_dollar',
        'test_exclamation_in_double_quotes',
    ]
    if BASH_MAJOR_VERSION < 4:
        # This requires compopt which is not available in 3.x.
        expected_failures.append('test_quoted_exact')

    install_cmd = 'eval "$(register-python-argcomplete prog)"'

    def setUp(self):
        sh = pexpect.replwrap.bash()
        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))
        # 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, '')
        self.sh = sh

    def test_one_space_after_exact(self):
        """Test exactly one space is appended after an exact match."""
        # Actual command run is 'echo "prog basic foo "'.
        result = self.sh.run_command('prog basic f\t"\1echo "')
        self.assertEqual(result, 'prog basic foo \r\n')

    def test_debug_output(self):
        self.assertEqual(self.sh.run_command('prog debug f\t'), 'foo\r\n')
        self.sh.run_command('export _ARC_DEBUG=1')
        output = self.sh.run_command('prog debug f\t')
        self.assertIn('PYTHON_ARGCOMPLETE_STDOUT\r\n', output)
        self.assertIn('PYTHON_ARGCOMPLETE_STDERR\r\n', output)
        self.assertTrue(output.endswith('foo\r\n'))


@unittest.skipIf(BASH_MAJOR_VERSION < 4, 'complete -D not supported')
class TestBashGlobal(TestBash):
    install_cmd = 'eval "$(activate-global-python-argcomplete --dest=-)"'

    def test_python_completion(self):
        self.sh.run_command('cd ' + TEST_DIR)
        self.assertEqual(self.sh.run_command('python ./prog basic f\t'), 'foo\r\n')

    def test_python_filename_completion(self):
        self.sh.run_command('cd ' + TEST_DIR)
        self.assertEqual(self.sh.run_command('python ./pro\tbasic f\t'), 'foo\r\n')

    def test_python_not_executable(self):
        """Test completing a script that cannot be run directly."""
        prog = os.path.join(TEST_DIR, 'prog')
        with TempDir(prefix='test_dir_py', dir='.'):
            shutil.copy(prog, '.')
            self.sh.run_command('cd ' + os.getcwd())
            self.sh.run_command('chmod -x ./prog')
            # Ensure prog is no longer able to be run as "./prog".
            self.assertIn('<<126>>', self.sh.run_command('./prog; echo "<<$?>>"'))
            # Ensure completion still functions when run via python.
            self.assertEqual(self.sh.run_command('python ./prog basic f\t'), 'foo\r\n')

    def test_python_module(self):
        """Test completing a module run with python -m."""
        prog = os.path.join(TEST_DIR, 'prog')
        with TempDir(prefix='test_dir_py', dir='.'):
            os.mkdir('package')
            open('package/__init__.py', 'w').close()
            shutil.copy(prog, 'package/prog.py')
            self.sh.run_command('cd ' + os.getcwd())
            self.assertEqual(self.sh.run_command('python -m package.prog basic f\t'), 'foo\r\n')


class TestTcsh(_TestSh, unittest.TestCase):
    expected_failures = [
        'test_unquoted_space',
        'test_quoted_space',
        'test_continuation',
        'test_parse_special_characters',
        'test_parse_special_characters_dollar',
        # Test case doesn't work under tcsh, could be fixed.
        'test_comp_point',
    ]

    def setUp(self):
        sh = Shell('tcsh')
        path = ' '.join([os.path.join(BASE_DIR, 'scripts'), TEST_DIR, '$path'])
        sh.run_command('set path = ({0})'.format(path))
        sh.run_command('setenv PYTHONPATH {0}'.format(BASE_DIR))
        output = sh.run_command('eval `register-python-argcomplete --shell tcsh prog`')
        self.assertEqual(output, '')
        self.sh = sh

    def tearDown(self):
        # The shell wrapper is fragile; exactly which exception is raised
        # differs depending on environment.
        with self.assertRaises((pexpect.EOF, OSError)):
            self.sh.run_command('exit')
            self.sh.run_command('')


class Shell(object):
    def __init__(self, shell):
        self.child = pexpect.spawn(shell, encoding='utf-8')

    def run_command(self, command):
        try:
            self.child.sendline(r"echo -n \#\#\#; {0}; echo -n \#\#\#".format(command))
            self.child.expect_exact('###', timeout=5)
            self.child.expect_exact('###', timeout=5)
            return self.child.before
        finally:
            # Send Ctrl+C in case we get stuck.
            self.child.sendline('\x03')


if __name__ == "__main__":
    unittest.main()