Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save e3krisztian/6ef30c4fe40c59d80261a76444b0172c to your computer and use it in GitHub Desktop.

Select an option

Save e3krisztian/6ef30c4fe40c59d80261a76444b0172c to your computer and use it in GitHub Desktop.
argparse/argcomplete `cmd [arg1] [arg2]` -> `cmd [arg1 [arg2]]`

Excellent — yes, that’s exactly the right approach for taming argcomplete’s ambiguity: wrap your completer in a small factory that inspects the parse state (parsed_args) and only delegates to the inner completer if it’s actually “safe” to do so.

Let’s go step-by-step and build that.


🧩 The core idea

You can absolutely write something like:

parser.add_argument("arg1", nargs="?").completer = arg1_completer
parser.add_argument("arg2", nargs="?").completer = require("arg1", arg2_completer)

where require("arg1", arg2_completer) means:

“Only call arg2_completer if arg1 has already been assigned a non-default value.”

Otherwise, return [] so argcomplete has nothing to merge from that positional.


🧱 Implementation: the require() factory

def require(required_attr, inner_completer):
    """Return a completer that runs only if parsed_args.<required_attr> is set."""
    def wrapper(prefix, parsed_args, **kwargs):
        # getattr defaults to None if arg not present
        value = getattr(parsed_args, required_attr, None)
        # You can also check for the default specifically if needed
        if value in (None, ''):
            return []  # don't yield anything
        # delegate to inner completer
        return inner_completer(prefix, parsed_args, **kwargs)
    return wrapper

🧠 Example usage

import argparse, argcomplete

def arg1_completer(prefix, **_):
    return ["one", "two", "three"]

def arg2_completer(prefix, **_):
    return ["alpha", "beta", "gamma"]

def require(required_attr, inner_completer):
    def wrapper(prefix, parsed_args, **kwargs):
        if getattr(parsed_args, required_attr, None) in (None, ''):
            return []
        return inner_completer(prefix, parsed_args, **kwargs)
    return wrapper

parser = argparse.ArgumentParser()
parser.add_argument("arg1", nargs="?", default=None).completer = arg1_completer
parser.add_argument("arg2", nargs="?", default=None).completer = require("arg1", arg2_completer)

argcomplete.autocomplete(parser)
args = parser.parse_args()
print(args)

Behavior:

Command line Completion results
cmd <TAB> only options + arg1 completions
cmd one <TAB> arg2 completions
cmd one beta <TAB> none

That fixes the “merged completions” problem elegantly.


🧩 Advanced: generic conditional_completer

If you have many cases, you can generalize this:

def conditional_completer(condition, inner_completer):
    def wrapper(prefix, parsed_args, **kwargs):
        if not condition(parsed_args):
            return []
        return inner_completer(prefix, parsed_args, **kwargs)
    return wrapper

Then you can express:

parser.add_argument("arg2", nargs="?").completer = conditional_completer(
    lambda args: args.arg1 is not None and args.arg1 != '',
    arg2_completer,
)

🧠 Notes

  • parsed_args is exactly what argparse.parse_known_args() produced for the partial command line at completion time — so if the user hasn’t typed anything for arg1 yet, it will still be None or its default.
  • Returning an empty list tells argcomplete “this completer yields nothing,” which effectively suppresses the merged results.
  • You can combine this with more complex logic — e.g. check that arg1’s value matches a particular prefix before enabling arg2 completions.

In summary

Yes — the “require” completer factory pattern works beautifully for this case:

def require(attr, inner):
    def wrapper(prefix, parsed_args, **kw):
        if getattr(parsed_args, attr, None) in (None, ''):
            return []
        return inner(prefix, parsed_args, **kw)
    return wrapper

It gives you fine-grained control over when completions for later optional positionals appear, fixing the “merged completions” problem while keeping both args optional in the actual CLI.


Would you like me to show a variant where require() also checks that the previous arg’s value is not the default (i.e. parsed_args.arg1 != parser.get_default("arg1"))? That’s sometimes useful when your optional args have defaults that shouldn’t count as “filled.”

Perfect 👍 — let’s extend the require() factory so that it also checks whether the “required” argument has been explicitly provided (not just filled with its default value).

That way, completions for the next positional (e.g. arg2) only trigger after the user has typed a real value for arg1.


🧱 Improved require() implementation

def require(required_attr, inner_completer, parser=None):
    """
    Only run `inner_completer` if parsed_args.<required_attr> is set
    AND differs from its default (if parser provided).
    """
    def wrapper(prefix, parsed_args, **kwargs):
        val = getattr(parsed_args, required_attr, None)

        # Check against parser's default (if parser is available)
        if parser is not None:
            default = parser.get_default(required_attr)
        else:
            default = None

        # Skip if not set or only default
        if val in (None, '') or val == default:
            return []
        return inner_completer(prefix, parsed_args, **kwargs)
    return wrapper

🧠 Example usage

import argparse, argcomplete

def arg1_completer(prefix, **_):
    return ["one", "two", "three"]

def arg2_completer(prefix, **_):
    return ["alpha", "beta", "gamma"]

parser = argparse.ArgumentParser()
parser.add_argument("arg1", nargs="?", default="foo").completer = arg1_completer
parser.add_argument("arg2", nargs="?", default="bar").completer = require("arg1", arg2_completer, parser)

argcomplete.autocomplete(parser)
args = parser.parse_args()
print(args)

🔍 Behavior

Command line parsed_args.arg1 Completions shown Why
cmd <TAB> "foo" (default) only options + arg1 completions require() sees value = default → skip arg2
cmd one <TAB> "one" arg2 completions (alpha, beta, gamma) explicit non-default value
cmd one beta <TAB> "one", "beta" none all filled

⚙️ Optional: automatically infer parser

If you like syntactic sugar, you can even wrap parser registration:

def add_arg_with_require(parser, name, *a, require_arg=None, completer=None, **kw):
    arg = parser.add_argument(name, *a, **kw)
    if completer:
        if require_arg:
            arg.completer = require(require_arg, completer, parser)
        else:
            arg.completer = completer
    return arg

Usage:

add_arg_with_require(parser, "arg1", nargs="?", default="foo", completer=arg1_completer)
add_arg_with_require(parser, "arg2", nargs="?", default="bar", require_arg="arg1", completer=arg2_completer)

Summary

Your improved require() pattern now:

  • suppresses premature completions for ambiguous optional positionals,

  • only activates the dependent completer when the prior argument:

    • is present and
    • differs from its default,
  • works seamlessly with argcomplete.


Would you like me to show how to combine this with dynamic completions (e.g. where arg2’s completions depend on the value of arg1)?

Perfect — let’s extend your setup one step further: now arg2’s completions will depend on the actual value of arg1, and won’t activate until arg1 is explicitly set (not just the default).

This pattern is super common for CLIs like:

mytool project <project> <environment>

where the environments available depend on the chosen project.


🧩 Full working example (argcomplete + dynamic dependency)

import argparse
import argcomplete

# --- Example dynamic data source ---
# Let's pretend each project has its own environments
PROJECTS = {
    "alpha": ["dev", "staging", "prod"],
    "beta": ["test", "prod"],
    "gamma": ["sandbox", "prod"],
}


# --- Base completers ---

def project_completer(prefix, **_):
    """Completes the first positional argument."""
    return [p for p in PROJECTS if p.startswith(prefix)]


def environment_completer(prefix, parsed_args, **_):
    """Completes the second positional based on selected project."""
    project = getattr(parsed_args, "project", None)
    if not project or project not in PROJECTS:
        return []
    return [env for env in PROJECTS[project] if env.startswith(prefix)]


# --- The require() factory (improved) ---

def require(required_attr, inner_completer, parser=None):
    """
    Only run `inner_completer` if parsed_args.<required_attr> is set
    and not equal to its default value (if parser given).
    """
    def wrapper(prefix, parsed_args, **kwargs):
        val = getattr(parsed_args, required_attr, None)
        default = parser.get_default(required_attr) if parser else None

        if val in (None, '') or val == default:
            return []  # suppress completions until requirement satisfied
        return inner_completer(prefix, parsed_args, **kwargs)
    return wrapper


# --- CLI definition ---

parser = argparse.ArgumentParser(prog="mytool")
parser.add_argument("project", nargs="?", default=None).completer = project_completer
parser.add_argument("environment", nargs="?", default=None).completer = require(
    "project", environment_completer, parser
)

# Enable argcomplete
argcomplete.autocomplete(parser)

# Regular program logic
args = parser.parse_args()
print(f"Selected project: {args.project}, environment: {args.environment}")

🔍 How it behaves in Bash or Zsh

Command Completion result Reason
mytool <TAB> alpha beta gamma Completes project names
mytool a<TAB> alpha Partial prefix
mytool alpha <TAB> dev staging prod require() sees project set ⇒ runs environment_completer
mytool beta <TAB> test prod Dynamically filtered by project
mytool <TAB> (no arg1) no environments require() suppresses premature completions

⚙️ Notes

  • You can chain this pattern for deeper hierarchies:

    arg3.completer = require("arg2", arg3_completer, parser)
  • Works equally for optional positionals (nargs="?") or flags (--something).

  • require() prevents the “merged” completion problem and enables clean dependencies.


✅ Why this pattern rocks

  • Avoids noise: completions appear only when logically possible.
  • Dynamically adapts based on previous argument values.
  • Pure Python — no shell logic needed.
  • Keeps your parser definition declarative.

Would you like me to show the minimal bash setup (how to enable and test argcomplete completions for this script interactively)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment