gitpro 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitpro/__init__.py +4 -0
- gitpro/__main__.py +5 -0
- gitpro/agents.py +1928 -0
- gitpro/approval_policy.py +151 -0
- gitpro/args.py +82 -0
- gitpro/audit.py +800 -0
- gitpro/change_contract.py +117 -0
- gitpro/changes.py +853 -0
- gitpro/cli.py +499 -0
- gitpro/clone.py +505 -0
- gitpro/completion.py +407 -0
- gitpro/config.py +517 -0
- gitpro/devcontainer.py +442 -0
- gitpro/discover.py +1579 -0
- gitpro/doctor.py +572 -0
- gitpro/envcmd.py +370 -0
- gitpro/example.toml +102 -0
- gitpro/files.py +139 -0
- gitpro/forge.py +1244 -0
- gitpro/guard.py +1121 -0
- gitpro/health.py +1241 -0
- gitpro/hot.py +38 -0
- gitpro/jsonfile.py +195 -0
- gitpro/jsonout.py +26 -0
- gitpro/keys.py +59 -0
- gitpro/lint.py +50 -0
- gitpro/lost.py +874 -0
- gitpro/mcp.py +1055 -0
- gitpro/operation_contract.py +178 -0
- gitpro/operation_io.py +256 -0
- gitpro/operation_reconcile.py +191 -0
- gitpro/operations.py +841 -0
- gitpro/pin.py +89 -0
- gitpro/plan.py +199 -0
- gitpro/policy.py +1808 -0
- gitpro/prompt.py +304 -0
- gitpro/render.py +474 -0
- gitpro/resolve.py +898 -0
- gitpro/scan.py +358 -0
- gitpro/scanroots.py +146 -0
- gitpro/setup.py +260 -0
- gitpro/trailers.py +218 -0
- gitpro/tui.py +178 -0
- gitpro/undo.py +63 -0
- gitpro/view.py +155 -0
- gitpro-0.3.1.dist-info/METADATA +103 -0
- gitpro-0.3.1.dist-info/RECORD +50 -0
- gitpro-0.3.1.dist-info/WHEEL +4 -0
- gitpro-0.3.1.dist-info/entry_points.txt +2 -0
- gitpro-0.3.1.dist-info/licenses/LICENSE +21 -0
gitpro/completion.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""`gitpro completion bash|zsh|fish`: tab completion generated at run time from the argparse tree.
|
|
2
|
+
|
|
3
|
+
generate(parser, shell) walks every subparser (including commands and flags that other packages add
|
|
4
|
+
through register()/extend()), so the script always matches the installed gitpro. Option values come
|
|
5
|
+
from argparse `choices`, or from `gitpro completion --values identities|kinds|hooks|shells` at
|
|
6
|
+
completion time for the options in DYNAMIC (or any action carrying a `gitpro_values` attribute:
|
|
7
|
+
`p.add_argument("--x").gitpro_values = "identities"`). Module level imports only the standard library.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import importlib
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
PROG = "gitpro"
|
|
19
|
+
SHELLS = ("bash", "zsh", "fish")
|
|
20
|
+
VALUE_KINDS = ("identities", "kinds", "hooks", "shells")
|
|
21
|
+
HOOK_NAMES = ("pre-commit", "pre-push", "commit-msg")
|
|
22
|
+
# (command path, option or positional dest) -> --values kind; "*" matches any command.
|
|
23
|
+
DYNAMIC: Dict[Tuple[str, str], str] = {
|
|
24
|
+
("*", "--identity"): "identities",
|
|
25
|
+
("lost", "--only"): "kinds",
|
|
26
|
+
("*", "hook"): "hooks",
|
|
27
|
+
("completion", "shell"): "shells",
|
|
28
|
+
}
|
|
29
|
+
# (command path, option) -> (module, attribute, extra words): static values read from the module that owns
|
|
30
|
+
# the option when the script is generated (R-067: env --only keys, env --shell shells).
|
|
31
|
+
STATIC: Dict[Tuple[str, str], Tuple[str, str, Tuple[str, ...]]] = {
|
|
32
|
+
("env", "--only"): ("gitpro.envcmd", "KEYS", ()),
|
|
33
|
+
("env", "--shell"): ("gitpro.envcmd", "SHELLS", ("bash", "zsh")),
|
|
34
|
+
}
|
|
35
|
+
INSTALL = {
|
|
36
|
+
"bash": '# add to ~/.bashrc: eval "$(gitpro completion bash)"',
|
|
37
|
+
"zsh": '# add to ~/.zshrc (after compinit): eval "$(gitpro completion zsh)"',
|
|
38
|
+
"fish": "# add to ~/.config/fish/config.fish: gitpro completion fish | source",
|
|
39
|
+
}
|
|
40
|
+
_WORD_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+@=,-]*$") # safe inside the generated scripts unquoted
|
|
41
|
+
_FLAG_RE = re.compile(r"^--?[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------- the tree
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Opt:
|
|
48
|
+
flags: List[str]
|
|
49
|
+
takes_value: bool
|
|
50
|
+
values: List[str] = field(default_factory=list) # static choices
|
|
51
|
+
dynamic: Optional[str] = None # a VALUE_KINDS entry
|
|
52
|
+
help: str = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Node:
|
|
57
|
+
path: List[str] # ["guard", "install"]; [] for gitpro itself
|
|
58
|
+
help: str = ""
|
|
59
|
+
opts: List[Opt] = field(default_factory=list)
|
|
60
|
+
subs: Dict[str, "Node"] = field(default_factory=dict)
|
|
61
|
+
pos_values: List[str] = field(default_factory=list)
|
|
62
|
+
pos_dynamic: Optional[str] = None
|
|
63
|
+
pos_files: bool = False # a positional without known values (a path)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def key(self) -> str:
|
|
67
|
+
return " ".join([PROG, *self.path])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _safe(words, pattern=_WORD_RE) -> List[str]:
|
|
71
|
+
"""The words that can go into a script unquoted; others are dropped (never completed)."""
|
|
72
|
+
return [str(w) for w in words if pattern.match(str(w))]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _dynamic(path: List[str], action: argparse.Action) -> Optional[str]:
|
|
76
|
+
kind = getattr(action, "gitpro_values", None)
|
|
77
|
+
if kind in VALUE_KINDS:
|
|
78
|
+
return kind
|
|
79
|
+
cmd = path[0] if path else ""
|
|
80
|
+
names = list(action.option_strings) or [action.dest]
|
|
81
|
+
for n in names:
|
|
82
|
+
for scope in (" ".join(path), cmd, "*"):
|
|
83
|
+
if (scope, n) in DYNAMIC:
|
|
84
|
+
return DYNAMIC[(scope, n)]
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _static(path: List[str], action: argparse.Action) -> List[str]:
|
|
89
|
+
"""Values for an option without argparse choices: an action's `gitpro_choices`, else STATIC."""
|
|
90
|
+
words = list(getattr(action, "gitpro_choices", None) or ())
|
|
91
|
+
for flag in action.option_strings:
|
|
92
|
+
spec = STATIC.get((" ".join(path), flag))
|
|
93
|
+
if spec and not words:
|
|
94
|
+
mod = sys.modules.get(spec[0])
|
|
95
|
+
if mod is None:
|
|
96
|
+
try:
|
|
97
|
+
mod = importlib.import_module(spec[0])
|
|
98
|
+
except ImportError:
|
|
99
|
+
continue
|
|
100
|
+
words = [*(getattr(mod, spec[1], None) or ()), *spec[2]]
|
|
101
|
+
return _safe(dict.fromkeys(words))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def tree(parser: argparse.ArgumentParser, path: Optional[List[str]] = None, help: str = "") -> Node:
|
|
105
|
+
"""The completion tree of parser: options, positionals and subcommands, recursively."""
|
|
106
|
+
node = Node(list(path or []), help)
|
|
107
|
+
for a in parser._actions:
|
|
108
|
+
if isinstance(a, argparse._SubParsersAction):
|
|
109
|
+
helps = {c.dest: c.help or "" for c in a._choices_actions}
|
|
110
|
+
for name, sp in a.choices.items():
|
|
111
|
+
if _WORD_RE.match(name):
|
|
112
|
+
node.subs[name] = tree(sp, node.path + [name], helps.get(name, ""))
|
|
113
|
+
continue
|
|
114
|
+
if a.help == argparse.SUPPRESS:
|
|
115
|
+
continue
|
|
116
|
+
dyn = _dynamic(node.path, a)
|
|
117
|
+
values = _safe(a.choices) if a.choices is not None else _static(node.path, a)
|
|
118
|
+
if a.option_strings:
|
|
119
|
+
node.opts.append(Opt(_safe(a.option_strings, _FLAG_RE), a.nargs != 0, values, dyn, a.help or ""))
|
|
120
|
+
elif values or dyn:
|
|
121
|
+
node.pos_values += values
|
|
122
|
+
node.pos_dynamic = node.pos_dynamic or dyn
|
|
123
|
+
else:
|
|
124
|
+
node.pos_files = True
|
|
125
|
+
return node
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _walk(node: Node):
|
|
129
|
+
yield node
|
|
130
|
+
for s in node.subs.values():
|
|
131
|
+
yield from _walk(s)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def values(kind: str, config_path=None) -> List[str]:
|
|
135
|
+
"""Completion values of one kind, only shell-safe words (bash's compgen -W expands $(...));
|
|
136
|
+
[] on any error (no config, bad config, package missing)."""
|
|
137
|
+
return _safe(_values(kind, config_path))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _values(kind: str, config_path=None) -> List[str]:
|
|
141
|
+
try:
|
|
142
|
+
if kind == "identities":
|
|
143
|
+
from pathlib import Path
|
|
144
|
+
|
|
145
|
+
from . import config
|
|
146
|
+
path, how = config.find(config_path, Path.home())
|
|
147
|
+
return [i.id for i in config.load_all(path, how).identities]
|
|
148
|
+
if kind == "kinds":
|
|
149
|
+
try:
|
|
150
|
+
import importlib
|
|
151
|
+
lost = importlib.import_module("gitpro.lost") # 03; honours sys.modules (unlike `from . import`)
|
|
152
|
+
except ImportError:
|
|
153
|
+
return []
|
|
154
|
+
kinds = getattr(lost, "KINDS", None) or getattr(lost, "ALL_KINDS", None) or ()
|
|
155
|
+
return [str(k) for k in kinds]
|
|
156
|
+
if kind == "hooks":
|
|
157
|
+
return list(HOOK_NAMES)
|
|
158
|
+
if kind == "shells":
|
|
159
|
+
return list(SHELLS)
|
|
160
|
+
except Exception:
|
|
161
|
+
return []
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------- bash (zsh reuses the helpers)
|
|
166
|
+
|
|
167
|
+
def _case(entries: List[Tuple[str, str]], default: str = "", subject: str = "$1") -> List[str]:
|
|
168
|
+
"""`case "SUBJECT" in "pattern") body ;; esac` lines; patterns are already shell-safe keys."""
|
|
169
|
+
out = [f' case "{subject}" in']
|
|
170
|
+
out += [f' "{pat}") {body} ;;' for pat, body in entries]
|
|
171
|
+
if default:
|
|
172
|
+
out.append(f" *) {default} ;;")
|
|
173
|
+
out.append(" esac")
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _dyn_cmd(kind: str) -> str:
|
|
178
|
+
return f"{PROG} completion --values {kind} 2>/dev/null"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _helpers(root: Node) -> List[str]:
|
|
182
|
+
"""Shell functions shared by bash and zsh (POSIX-ish syntax both accept). Each prints words or
|
|
183
|
+
returns a status; the keys are 'gitpro sub sub'."""
|
|
184
|
+
nodes = list(_walk(root))
|
|
185
|
+
lines = ["# _gitpro_child KEY WORD: the key of subcommand WORD under KEY, or fail",
|
|
186
|
+
"_gitpro_child() {"]
|
|
187
|
+
lines += _case([(f"{n.key} {s}", f"echo '{n.key} {s}'") for n in nodes for s in n.subs], "return 1", "$1 $2")
|
|
188
|
+
lines += ["}", "", "# _gitpro_subs KEY: subcommand names", "_gitpro_subs() {"]
|
|
189
|
+
lines += _case([(n.key, f"echo '{' '.join(n.subs)}'") for n in nodes if n.subs])
|
|
190
|
+
lines += ["}", "", "# _gitpro_opts KEY: option names", "_gitpro_opts() {"]
|
|
191
|
+
lines += _case([(n.key, f"echo '{' '.join(f for o in n.opts for f in o.flags)}'") for n in nodes if n.opts])
|
|
192
|
+
lines += ["}", "", "# _gitpro_takes KEY OPT: does OPT take a value?", "_gitpro_takes() {"]
|
|
193
|
+
takes = [(f"{n.key} {f}", "return 0") for n in nodes for o in n.opts if o.takes_value for f in o.flags]
|
|
194
|
+
lines += _case(takes, "return 1", "$1 $2")
|
|
195
|
+
lines += ["}", "",
|
|
196
|
+
"# _gitpro_optvals KEY OPT: values for OPT; status 1 means complete file names",
|
|
197
|
+
"_gitpro_optvals() {"]
|
|
198
|
+
vals = []
|
|
199
|
+
for n in nodes:
|
|
200
|
+
for o in n.opts:
|
|
201
|
+
body = _values_body(o.values, o.dynamic)
|
|
202
|
+
if body:
|
|
203
|
+
vals += [(f"{n.key} {f}", body) for f in o.flags]
|
|
204
|
+
lines += _case(vals, "return 1", "$1 $2")
|
|
205
|
+
lines += ["}", "", "# _gitpro_posvals KEY: values for positionals; status 1 means complete file names",
|
|
206
|
+
"_gitpro_posvals() {"]
|
|
207
|
+
pos = [(n.key, _values_body(n.pos_values, n.pos_dynamic)) for n in nodes if n.pos_values or n.pos_dynamic]
|
|
208
|
+
files_ok = [n.key for n in nodes if n.pos_files]
|
|
209
|
+
lines += _case(pos + [(k, "return 1") for k in files_ok if k not in dict(pos)], "return 2")
|
|
210
|
+
lines += ["}", "",
|
|
211
|
+
"# _gitpro_key WORDS...: walk the words before the cursor to the deepest subcommand key",
|
|
212
|
+
"_gitpro_key() {",
|
|
213
|
+
f" local key='{PROG}' skip=0 w next",
|
|
214
|
+
" for w in \"$@\"; do",
|
|
215
|
+
" if [ \"$skip\" = 1 ]; then skip=0; continue; fi",
|
|
216
|
+
" case \"$w\" in",
|
|
217
|
+
" --*=*) continue ;;",
|
|
218
|
+
" -*) _gitpro_takes \"$key\" \"$w\" && skip=1; continue ;;",
|
|
219
|
+
" esac",
|
|
220
|
+
" next=$(_gitpro_child \"$key\" \"$w\") && key=$next",
|
|
221
|
+
" done",
|
|
222
|
+
" echo \"$key\"",
|
|
223
|
+
"}"]
|
|
224
|
+
return lines
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _values_body(static: List[str], dynamic: Optional[str]) -> str:
|
|
228
|
+
parts = []
|
|
229
|
+
if static:
|
|
230
|
+
parts.append(f"echo '{' '.join(static)}'")
|
|
231
|
+
if dynamic:
|
|
232
|
+
parts.append(_dyn_cmd(dynamic))
|
|
233
|
+
return "; ".join(parts)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_BASH_MAIN = r"""
|
|
237
|
+
_gitpro_files() {
|
|
238
|
+
compopt -o filenames 2>/dev/null
|
|
239
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_gitpro() {
|
|
243
|
+
local cur="${COMP_WORDS[COMP_CWORD]}" prev="" key words pos rc
|
|
244
|
+
[ "$COMP_CWORD" -gt 1 ] && prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
245
|
+
key=$(_gitpro_key "${COMP_WORDS[@]:1:COMP_CWORD-1}")
|
|
246
|
+
COMPREPLY=()
|
|
247
|
+
if [[ $prev == -* ]] && _gitpro_takes "$key" "$prev"; then
|
|
248
|
+
if words=$(_gitpro_optvals "$key" "$prev"); then
|
|
249
|
+
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
250
|
+
else
|
|
251
|
+
_gitpro_files
|
|
252
|
+
fi
|
|
253
|
+
return 0
|
|
254
|
+
fi
|
|
255
|
+
if [[ $cur == -* ]]; then
|
|
256
|
+
COMPREPLY=($(compgen -W "$(_gitpro_opts "$key")" -- "$cur"))
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
259
|
+
words=$(_gitpro_subs "$key")
|
|
260
|
+
pos=$(_gitpro_posvals "$key"); rc=$?
|
|
261
|
+
[ $rc = 0 ] && words="$words $pos"
|
|
262
|
+
if [ -n "${words// /}" ]; then
|
|
263
|
+
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
264
|
+
elif [ $rc = 1 ]; then
|
|
265
|
+
_gitpro_files
|
|
266
|
+
fi
|
|
267
|
+
return 0
|
|
268
|
+
}
|
|
269
|
+
complete -F _gitpro gitpro
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _bash(root: Node) -> str:
|
|
274
|
+
head = f"# bash completion for {PROG} (generated by `{PROG} completion bash`; regenerate after upgrading)"
|
|
275
|
+
return "\n".join([head, *_helpers(root)]) + "\n" + _BASH_MAIN
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _zsh(root: Node) -> str:
|
|
279
|
+
lines = [f"#compdef {PROG}",
|
|
280
|
+
f"# zsh completion for {PROG} (generated by `{PROG} completion zsh`; regenerate after upgrading)",
|
|
281
|
+
*_helpers(root), "",
|
|
282
|
+
"_gitpro() {",
|
|
283
|
+
" local cur=${words[CURRENT]} prev=\"\" key vals pos rc",
|
|
284
|
+
" (( CURRENT > 2 )) && prev=${words[CURRENT-1]}",
|
|
285
|
+
" key=$(_gitpro_key \"${(@)words[2,CURRENT-1]}\")",
|
|
286
|
+
" if [[ $prev == -* ]] && _gitpro_takes \"$key\" \"$prev\"; then",
|
|
287
|
+
" if vals=$(_gitpro_optvals \"$key\" \"$prev\"); then",
|
|
288
|
+
" compadd -- ${=vals}",
|
|
289
|
+
" else",
|
|
290
|
+
" _files",
|
|
291
|
+
" fi",
|
|
292
|
+
" return 0",
|
|
293
|
+
" fi",
|
|
294
|
+
" if [[ $cur == -* ]]; then",
|
|
295
|
+
" compadd -- ${=$(_gitpro_opts \"$key\")}",
|
|
296
|
+
" return 0",
|
|
297
|
+
" fi",
|
|
298
|
+
" vals=$(_gitpro_subs \"$key\")",
|
|
299
|
+
" pos=$(_gitpro_posvals \"$key\"); rc=$?",
|
|
300
|
+
" (( rc == 0 )) && vals=\"$vals $pos\"",
|
|
301
|
+
" if [[ -n ${vals// /} ]]; then",
|
|
302
|
+
" compadd -- ${=vals}",
|
|
303
|
+
" elif (( rc == 1 )); then",
|
|
304
|
+
" _files",
|
|
305
|
+
" fi",
|
|
306
|
+
" return 0",
|
|
307
|
+
"}",
|
|
308
|
+
"",
|
|
309
|
+
"if [[ ${zsh_eval_context[-1]} == loadautofunc ]]; then",
|
|
310
|
+
" _gitpro \"$@\"",
|
|
311
|
+
"elif (( $+functions[compdef] )); then",
|
|
312
|
+
f" compdef _gitpro {PROG}",
|
|
313
|
+
"fi", ""]
|
|
314
|
+
return "\n".join(lines)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------- fish
|
|
318
|
+
|
|
319
|
+
def _fq(text: str) -> str:
|
|
320
|
+
"""A fish single-quoted string."""
|
|
321
|
+
return "'" + text.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _first_line(text: str) -> str:
|
|
325
|
+
return " ".join(text.split())[:80]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _fish(root: Node) -> str:
|
|
329
|
+
lines = [f"# fish completion for {PROG} (generated by `{PROG} completion fish`; regenerate after upgrading)",
|
|
330
|
+
f"complete -c {PROG} -e", ""]
|
|
331
|
+
for n in _walk(root):
|
|
332
|
+
if n.path:
|
|
333
|
+
cond = "__fish_seen_subcommand_from " + n.path[-1]
|
|
334
|
+
if len(n.path) > 1:
|
|
335
|
+
cond = " ; and ".join(f"__fish_seen_subcommand_from {p}" for p in n.path)
|
|
336
|
+
else:
|
|
337
|
+
cond = ""
|
|
338
|
+
if n.subs:
|
|
339
|
+
names = " ".join(n.subs)
|
|
340
|
+
here = (cond + " ; and " if cond else "") + f"not __fish_seen_subcommand_from {names}"
|
|
341
|
+
for name, s in n.subs.items():
|
|
342
|
+
d = f" -d {_fq(_first_line(s.help))}" if s.help else ""
|
|
343
|
+
lines.append(f"complete -c {PROG} -f -n {_fq(here)} -a {name}{d}")
|
|
344
|
+
for o in n.opts:
|
|
345
|
+
spec = []
|
|
346
|
+
for f in o.flags:
|
|
347
|
+
if f.startswith("--"):
|
|
348
|
+
spec.append(f"-l {f[2:]}")
|
|
349
|
+
elif len(f) == 2:
|
|
350
|
+
spec.append(f"-s {f[1]}")
|
|
351
|
+
else:
|
|
352
|
+
spec.append(f"-o {f[1:]}")
|
|
353
|
+
args = ""
|
|
354
|
+
if o.takes_value:
|
|
355
|
+
vals = " ".join(o.values)
|
|
356
|
+
if o.dynamic:
|
|
357
|
+
vals = (vals + " " if vals else "") + f"({PROG} completion --values {o.dynamic} 2>/dev/null)"
|
|
358
|
+
args = f" -x -a {_fq(vals)}" if vals else " -r"
|
|
359
|
+
n_ = f" -n {_fq(cond)}" if cond else ""
|
|
360
|
+
d = f" -d {_fq(_first_line(o.help))}" if o.help else ""
|
|
361
|
+
lines.append(f"complete -c {PROG}{n_} {' '.join(spec)}{args}{d}")
|
|
362
|
+
if n.path and (n.pos_values or n.pos_dynamic):
|
|
363
|
+
vals = " ".join(n.pos_values)
|
|
364
|
+
if n.pos_dynamic:
|
|
365
|
+
vals = (vals + " " if vals else "") + f"({PROG} completion --values {n.pos_dynamic} 2>/dev/null)"
|
|
366
|
+
lines.append(f"complete -c {PROG} -f -n {_fq(cond)} -a {_fq(vals)}")
|
|
367
|
+
return "\n".join(lines) + "\n"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def generate(parser: argparse.ArgumentParser, shell: str) -> str:
|
|
371
|
+
"""The completion script for shell ("bash", "zsh" or "fish") covering every command in parser."""
|
|
372
|
+
root = tree(parser)
|
|
373
|
+
if shell == "bash":
|
|
374
|
+
return _bash(root)
|
|
375
|
+
if shell == "zsh":
|
|
376
|
+
return _zsh(root)
|
|
377
|
+
if shell == "fish":
|
|
378
|
+
return _fish(root)
|
|
379
|
+
raise ValueError(f"unknown shell {shell!r} ({', '.join(SHELLS)})")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ---------------------------------------------------------------- command
|
|
383
|
+
|
|
384
|
+
def cmd_completion(args) -> int:
|
|
385
|
+
from .args import fail
|
|
386
|
+
if args.values:
|
|
387
|
+
for v in values(args.values, args.config):
|
|
388
|
+
print(v)
|
|
389
|
+
return 0
|
|
390
|
+
if not args.shell:
|
|
391
|
+
return fail(f"name a shell: {PROG} completion {'|'.join(SHELLS)}", args)
|
|
392
|
+
if args.shell not in SHELLS:
|
|
393
|
+
return fail(f"unknown shell {args.shell!r} ({', '.join(SHELLS)})", args)
|
|
394
|
+
from . import cli
|
|
395
|
+
parser, _subs = cli.build_parser()
|
|
396
|
+
sys.stdout.write(generate(parser, args.shell))
|
|
397
|
+
print(INSTALL[args.shell], file=sys.stderr)
|
|
398
|
+
return 0
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def register(sub, common, confirm) -> None:
|
|
402
|
+
from .args import capability
|
|
403
|
+
sp = sub.add_parser("completion", parents=[common], help="print a shell completion script (bash, zsh, fish)")
|
|
404
|
+
sp.add_argument("shell", nargs="?", metavar="SHELL", help="bash, zsh or fish")
|
|
405
|
+
sp.add_argument("--values", choices=VALUE_KINDS, help="print completion values, one per line (used by the scripts)")
|
|
406
|
+
sp.set_defaults(func=cmd_completion)
|
|
407
|
+
capability("completion")
|