import os
import subprocess
import sys
from pathlib import Path
TEMPLATE_VERSION = 2
def completion_dirs(env, home):
xdg_data = Path(env.get("XDG_DATA_HOME") or home / ".local" / "share")
xdg_config = Path(env.get("XDG_CONFIG_HOME") or home / ".config")
appdata = env.get("APPDATA")
nu = Path(appdata) / "nushell" / "autoload" if appdata else xdg_config / "nushell" / "autoload"
return {
"bash": (xdg_data / "bash-completion" / "completions", "{bin}"),
"zsh": (xdg_data / "zsh" / "site-functions", "_{bin}"),
"fish": (xdg_config / "fish" / "completions", "{bin}.fish"),
"elvish": (xdg_config / "elvish" / "lib", "{bin}.elv"),
"nushell": (nu, "50{bin}-completions.nu"),
"power-shell": (xdg_config / "powershell", "{bin}.ps1"),
}
def zsh_reads(directory):
try:
res = subprocess.run(
["zsh", "-i", "-c", "print -l $fpath"],
capture_output=True, text=True, timeout=20,
)
except (OSError, subprocess.SubprocessError):
return None
if res.returncode != 0:
return None
return str(directory) in res.stdout.splitlines()
def generate(binary, shell, out_path, repo_root, from_path=False):
if from_path:
cmd = [binary]
else:
candidates = [
repo_root / "target" / "release" / f"{binary}.exe",
repo_root / "target" / "release" / binary,
repo_root / "target" / "debug" / f"{binary}.exe",
repo_root / "target" / "debug" / binary,
]
exe = next((c for c in candidates if c.exists()), None)
cmd = [str(exe)] if exe else ["cargo", "run", "-q", "--bin", binary, "--"]
res = subprocess.run(cmd + ["--completions", shell], capture_output=True, text=True)
if res.returncode != 0:
raise RuntimeError(
f"generating {shell} completions for {binary} failed "
f"(exit {res.returncode}): {res.stderr.strip() or '<no stderr>'}"
)
if not res.stdout.strip():
raise RuntimeError(f"{binary} --completions {shell} produced no output")
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(res.stdout, encoding="utf-8")
def self_test():
home = Path("/home/u")
failures = []
def check(name, got, want):
if got != want:
failures.append(f" {name}\n expected: {want}\n got: {got}")
win = completion_dirs({"APPDATA": r"C:\Users\u\AppData\Roaming"}, home)
check("windows nushell dir",
win["nushell"][0], Path(r"C:\Users\u\AppData\Roaming") / "nushell" / "autoload")
unix = completion_dirs({}, home)
check("unix nushell dir", unix["nushell"][0], home / ".config" / "nushell" / "autoload")
for shell in ("bash", "zsh", "fish", "elvish", "power-shell"):
check(f"{shell} dir unaffected by APPDATA", win[shell][0], unix[shell][0])
over = completion_dirs({"XDG_DATA_HOME": "/x/data", "XDG_CONFIG_HOME": "/x/cfg"}, home)
check("XDG_DATA_HOME honoured", over["zsh"][0], Path("/x/data") / "zsh" / "site-functions")
check("XDG_CONFIG_HOME honoured", over["fish"][0], Path("/x/cfg") / "fish" / "completions")
empty = completion_dirs({"XDG_DATA_HOME": "", "XDG_CONFIG_HOME": ""}, home)
check("empty XDG_DATA_HOME falls back", empty["zsh"][0], unix["zsh"][0])
check("shell count", len(unix), 6)
if failures:
print(f"self-test FAILED (template v{TEMPLATE_VERSION}):", file=sys.stderr)
print("\n".join(failures), file=sys.stderr)
return 1
print(f"install_completions.py self-test passed (template v{TEMPLATE_VERSION})")
return 0
def main(argv):
if "--self-test" in argv:
return self_test()
from_path = "--from-path" in argv
binaries = [a for a in argv if not a.startswith("-")]
if not binaries:
print(
"usage: install_completions.py <binary> [binary...] [--from-path] | --self-test",
file=sys.stderr,
)
return 2
repo_root = Path(__file__).resolve().parent.parent
dirs = completion_dirs(os.environ, Path.home())
for binary in binaries:
for shell, (directory, pattern) in dirs.items():
out = directory / pattern.format(bin=binary)
generate(binary, shell, out, repo_root, from_path) src = "the installed binary" if from_path else "this checkout"
print(f"Installed completions for {binary} (from {src})")
print()
zsh_dir = dirs["zsh"][0]
state = zsh_reads(zsh_dir)
if state is True:
print(f" zsh auto-loaded from {zsh_dir}")
elif state is False:
print(f" zsh NOT ACTIVE -- {zsh_dir} is not on your $fpath.")
print(" The file is written but zsh will never read it. Add this to")
print(" ~/.zshrc BEFORE compinit runs, then restart the shell:")
print()
print(f" fpath+=({zsh_dir})")
print()
else:
print(" zsh not checked -- zsh is not installed, or could not be asked")
print(f" bash source {dirs['bash'][0]}/<cmd> (or restart shell)")
print(f" fish auto-loaded from {dirs['fish'][0]}")
print(f" elvish add to rc.elv: eval (slurp < {dirs['elvish'][0]}/<cmd>.elv)")
print(f" nushell auto-loaded from {dirs['nushell'][0]}")
if os.environ.get("APPDATA"):
print(" powershell NOT ACTIVE on Windows -- $PROFILE is under Documents\\PowerShell")
print(f" (OneDrive may move it) and does not source {dirs['power-shell'][0]}.")
print(" Dot-source the file from your $PROFILE to use it.")
else:
print(f" powershell add to $PROFILE: . {dirs['power-shell'][0]}/<cmd>.ps1")
print()
print(" Shell aliases do not inherit completions. For an alias, tell your shell they")
print(" are the same command: zsh compdef <alias>=<cmd>")
print(" fish complete -c <alias> -w <cmd>")
print(" bash complete -o default -F _<cmd> <alias>")
return 0
if __name__ == "__main__":
try:
sys.exit(main(sys.argv[1:]))
except RuntimeError as e:
print(f"error: {e}", file=sys.stderr)
sys.exit(1)