1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
#!/usr/bin/env python3
# Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only
"""Generate completions/firefly.bash from the live command registry.
python scripts/gen_completion.py > completions/firefly.bash
No drift: groups, leaves, and per-leaf flags are read straight off the
argparse subparsers the registry builds.
"""
import argparse
import os
import sys
from collections import defaultdict
# Run from the repo, not any installed copy: prepend the repo root so
# `python scripts/gen_completion.py` imports this tree, not a stale install.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from firefly_cli import registry
import firefly_cli.commands # noqa: F401 triggers registration
GROUP_ORDER = ["auth", "account", "category", "tag", "tx"]
# Static value suggestions for flags whose choices are a fixed enum, keyed on
# "<group leaf>" then flag. Account/transaction types are fixed in Firefly, so
# no API call is needed (a completion must not hit the network).
FLAG_VALUES = {
"account list": {"--type": "asset expense revenue liability"},
"account create": {"--type": "asset expense revenue"},
"tx add": {"--type": "withdrawal deposit transfer"},
"tx edit": {"--type": "withdrawal deposit transfer"},
}
def collect():
groups = defaultdict(dict) # group -> {leaf: [--flag, ...]}
for c in registry.all_commands():
g, _, leaf = c.name.partition(" ")
p = argparse.ArgumentParser()
if c.args:
c.args(p)
opts = []
for a in p._actions:
opts += [s for s in a.option_strings
if s.startswith("--") and s != "--help"]
groups[g][leaf] = sorted(opts)
return groups
def render(groups):
order = [g for g in GROUP_ORDER if g in groups]
order += [g for g in sorted(groups) if g not in GROUP_ORDER]
leaf_cases, group_cases = [], []
for g in order:
leaves = " ".join(sorted(groups[g]))
group_cases.append(f' {g}) leaves="{leaves}";;')
for leaf in sorted(groups[g]):
flags = " ".join(groups[g][leaf])
if flags:
leaf_cases.append(
f' "{g} {leaf}")'.ljust(28) + f'leaf_opts="{flags}";;')
# Per-command flag-value cases: "<group leaf> <flag>") vals="..."
value_cases = []
for cmd in sorted(FLAG_VALUES):
for flag in sorted(FLAG_VALUES[cmd]):
vals = FLAG_VALUES[cmd][flag]
value_cases.append(f' "{cmd} {flag}")'.ljust(34)
+ f'vals="{vals}";;')
return TEMPLATE.format(
groups=" ".join(order),
leaf_cases="\n".join(leaf_cases),
group_cases="\n".join(group_cases),
value_cases="\n".join(value_cases),
)
TEMPLATE = '''# bash completion for firefly (firefly-cli)
# Install: source this file, or drop it in /etc/bash_completion.d/ or
# /usr/share/bash-completion/completions/firefly
#
# Generated by scripts/gen_completion.py -- do not edit by hand.
# Regenerate when commands change (see CLAUDE.md):
# python scripts/gen_completion.py > completions/firefly.bash
_firefly() {{
local cur prev words cword
_init_completion 2>/dev/null || {{
cur="${{COMP_WORDS[COMP_CWORD]}}"
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
words=("${{COMP_WORDS[@]}}")
cword=$COMP_CWORD
}}
local global_opts="--human --url --token -h --help"
local groups="{groups}"
# Find the group and leaf among the words (skip the program name at 0).
local group="" leaf="" i
for ((i=1; i < cword; i++)); do
local w="${{words[i]}}"
case "$w" in
-*) ;; # an option, skip
--url|--token) ((i++));; # option that takes a value
*)
if [[ -z $group ]]; then group="$w"
elif [[ -z $leaf ]]; then leaf="$w"
fi
;;
esac
done
# Leaf-specific options.
local leaf_opts=""
case "$group $leaf" in
{leaf_cases}
esac
# Leaves per group.
local leaves=""
case "$group" in
{group_cases}
esac
# Flag values: when the previous word is a flag with a fixed value set for
# this command, suggest those values instead of more flags.
local vals=""
case "$group $leaf $prev" in
{value_cases}
esac
if [[ -n $vals ]]; then
COMPREPLY=($(compgen -W "$vals" -- "$cur"))
return
fi
if [[ -z $group ]]; then
COMPREPLY=($(compgen -W "$groups $global_opts" -- "$cur"))
elif [[ -z $leaf ]]; then
COMPREPLY=($(compgen -W "$leaves" -- "$cur"))
else
COMPREPLY=($(compgen -W "$leaf_opts --help" -- "$cur"))
fi
}}
complete -F _firefly firefly
'''
if __name__ == "__main__":
print(render(collect()), end="")
|