Skip to content

Commit 842a28a

Browse files
Bash completion for the build examples helper script (project-chip#39915)
* Bash completion for the build examples helper script * Target completion support in build_examples script * Do not match path when setting up completion * Restyled by shfmt * Suggest modifiers only on demand * Unit test for target completion strings generator --------- Co-authored-by: Restyled.io <[email protected]>
1 parent 397ba02 commit 842a28a

File tree

4 files changed

+204
-12
lines changed

4 files changed

+204
-12
lines changed

scripts/build/build/target.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,15 @@ def AppendFixedTargets(self, parts: List[TargetPart]):
251251
"""
252252
self.fixed_targets.append(parts)
253253

254-
def AppendModifier(self, name: str, **kargs):
254+
def AppendModifier(self, name: str, **kwargs):
255255
"""Appends a specific modifier to a build target. For example:
256256
257257
target.AppendModifier(name='release', release=True)
258258
target.AppendModifier(name='clang', use_clang=True)
259259
target.AppendModifier(name='coverage', coverage=True).OnlyIfRe('-clang')
260260
261261
"""
262-
part = TargetPart(name, **kargs)
262+
part = TargetPart(name, **kwargs)
263263

264264
self.modifiers.append(part)
265265

@@ -285,8 +285,63 @@ def HumanString(self):
285285

286286
return result
287287

288+
def CompletionStrings(self, value: str) -> List[str]:
289+
"""Get a list of completion strings for this target."""
290+
291+
if self.name.startswith(value):
292+
return [self.name + "-"]
293+
if not value.startswith(self.name + "-"):
294+
return []
295+
296+
completions = []
297+
prefix = self.name
298+
suffix = value[len(self.name) + 1:]
299+
300+
for i, targets in enumerate(self.fixed_targets, 1):
301+
# If we are not processing the last fixed target,
302+
# append hyphen to the generated completions.
303+
hyphen = '-' if i != len(self.fixed_targets) else ''
304+
for target in targets:
305+
if suffix.startswith(target.name + hyphen):
306+
# Strip the prefix and continue processing.
307+
prefix += f"-{target.name}"
308+
suffix = suffix[len(target.name) + 1:]
309+
break
310+
else:
311+
for target in targets:
312+
# NOTE: We are not validating whether the target is acceptable
313+
# for the given value. Some validation rules require the
314+
# full target name to be known, so in case of completions
315+
# we just assume that the target is acceptable.
316+
completions.append(f"{prefix}-{target.name}{hyphen}")
317+
# Return validated completions.
318+
return [c for c in completions if c.startswith(value)]
319+
320+
modifiers = []
321+
while True:
322+
# Get modifiers which are already part of the value.
323+
for modifier in self.modifiers:
324+
if suffix.startswith(modifier.name):
325+
modifiers.append(modifier)
326+
# Strip the prefix and continue processing.
327+
prefix += f"-{modifier.name}"
328+
suffix = suffix[len(modifier.name) + 1:]
329+
break
330+
else:
331+
# No more modifiers in the suffix, break the loop.
332+
break
333+
334+
completions.append(prefix)
335+
# Return all valid modifiers that are not already part of the value.
336+
for modifier in self.modifiers:
337+
if modifier not in modifiers and modifier.Accept(prefix):
338+
completions.append(f"{prefix}-{modifier.name}")
339+
340+
# Return validated completions.
341+
return [c for c in completions if c.startswith(value)]
342+
288343
def ToDict(self):
289-
"""Outputs a parseable description of the available variants
344+
"""Outputs a parsable description of the available variants
290345
and modifiers:
291346
292347
like:

scripts/build/build/test_target.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def __init__(self, **kargs):
2626
self.kargs = kargs
2727

2828

29-
class TestGlobMatcher(unittest.TestCase):
29+
class TestBuildTarget(unittest.TestCase):
3030

3131
def test_one_fixed_target(self):
3232
t = BuildTarget('fake', FakeBuilder)
@@ -150,6 +150,51 @@ def test_modifiers(self):
150150
self.assertIsNone(t.StringIntoTargetParts('fake-bar-m1'))
151151
self.assertIsNone(t.StringIntoTargetParts('fake-foo-x1-y1'))
152152

153+
def test_completion_strings(self):
154+
t = BuildTarget('fake', FakeBuilder)
155+
156+
t.AppendFixedTargets([
157+
TargetPart('foo', foo=1),
158+
TargetPart('bar', bar=2),
159+
])
160+
161+
t.AppendFixedTargets([
162+
TargetPart('one', value=1),
163+
TargetPart('two', value=2),
164+
])
165+
166+
t.AppendModifier('m1', m=1).ExceptIfRe('-m2')
167+
t.AppendModifier('m2', m=2).ExceptIfRe('-m1')
168+
t.AppendModifier('extra', extra=1).OnlyIfRe('-foo-')
169+
170+
# Non-existing target should return an empty list.
171+
self.assertEqual(t.CompletionStrings('non-existing-'),
172+
[])
173+
174+
# Not the last fixed target - completions should be suffixed with "-".
175+
self.assertEqual(t.CompletionStrings(''),
176+
['fake-'])
177+
self.assertEqual(t.CompletionStrings('fake-'),
178+
['fake-foo-', 'fake-bar-'])
179+
self.assertEqual(t.CompletionStrings('fake-f'),
180+
['fake-foo-'])
181+
182+
# The last fixed target - completions should not be suffixed with "-".
183+
self.assertEqual(t.CompletionStrings('fake-foo-'),
184+
['fake-foo-one', 'fake-foo-two'])
185+
186+
# Modifiers should be added to the last fixed target.
187+
self.assertEqual(t.CompletionStrings('fake-foo-two'),
188+
['fake-foo-two', 'fake-foo-two-m1', 'fake-foo-two-m2', 'fake-foo-two-extra'])
189+
self.assertEqual(t.CompletionStrings('fake-foo-two-m'),
190+
['fake-foo-two-m1', 'fake-foo-two-m2'])
191+
192+
# After adding a modifier, completions should respect exclusion rules.
193+
self.assertEqual(t.CompletionStrings('fake-foo-one-m1'),
194+
['fake-foo-one-m1', 'fake-foo-one-m1-extra'])
195+
self.assertEqual(t.CompletionStrings('fake-bar-one-m1'),
196+
['fake-bar-one-m1'])
197+
153198

154199
if __name__ == '__main__':
155200
unittest.main()

scripts/build/build_examples.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,12 @@ def main(context, log_level, verbose, target, enable_link_map_file, repo,
169169
else:
170170
runner = ShellRunner(root=repo)
171171

172-
requested_targets = set([t.lower() for t in target])
173-
logging.info('Building targets: %s', CommaSeparate(requested_targets))
174-
175172
context.obj = build.Context(
176173
repository_path=repo, output_prefix=out_prefix, verbose=verbose,
177174
ninja_jobs=ninja_jobs, runner=runner
178175
)
176+
177+
requested_targets = set([t.lower() for t in target])
179178
context.obj.SetupBuilders(targets=requested_targets, options=BuilderOptions(
180179
enable_link_map_file=enable_link_map_file,
181180
enable_flashbundle=enable_flashbundle,
@@ -200,23 +199,31 @@ def cmd_generate(context):
200199
@click.option(
201200
'--format',
202201
default='summary',
203-
type=click.Choice(['summary', 'expanded', 'json'], case_sensitive=False),
202+
type=click.Choice(['summary', 'expanded', 'json', 'completion'], case_sensitive=False),
204203
help="""
205-
summary - list of shorthand strings summarzing the available targets;
204+
summary - list of shorthand strings summarizing the available targets;
206205
207206
expanded - list all possible targets rather than the shorthand string;
208207
209-
json - a JSON representation of the available targets
208+
json - a JSON representation of the available targets;
209+
210+
completion - a list of strings suitable for bash completion;
210211
""")
212+
@click.argument('COMPLETION-PREFIX', default='')
211213
@click.pass_context
212-
def cmd_targets(context, format):
214+
def cmd_targets(context, format, completion_prefix):
213215
if format == 'expanded':
216+
build.target.report_rejected_parts = False
214217
for target in build.targets.BUILD_TARGETS:
215-
build.target.report_rejected_parts = False
216218
for s in target.AllVariants():
217219
print(s)
218220
elif format == 'json':
219221
print(json.dumps([target.ToDict() for target in build.targets.BUILD_TARGETS], indent=4))
222+
elif format == 'completion':
223+
build.target.report_rejected_parts = False
224+
for target in build.targets.BUILD_TARGETS:
225+
for s in target.CompletionStrings(completion_prefix):
226+
print(s)
220227
else:
221228
for target in build.targets.BUILD_TARGETS:
222229
print(target.HumanString())

scripts/helpers/bash-completion.sh

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,88 @@
1616
# limitations under the License.
1717
#
1818

19+
_chip_build_example() {
20+
21+
local cur prev words cword split
22+
_init_completion -s || return
23+
24+
# The command names supported by the script.
25+
local commands="build gen targets"
26+
27+
local i
28+
local command command_comp_cword
29+
# Get the first non-option argument taking into account the options with their arguments.
30+
for ((i = 1; i <= COMP_CWORD; i++)); do
31+
case "${COMP_WORDS[i]}" in
32+
--log-level | --target | --repo | --out-prefix | --ninja-jobs | --pregen-dir | --dry-run-output | --pw-command-launcher)
33+
((i == COMP_CWORD)) && break
34+
((i == COMP_CWORD - 1)) && [[ "${COMP_WORDS[i + 1]}" = "=" ]] && break
35+
[[ "${COMP_WORDS[i + 1]}" = "=" ]] && ((i++))
36+
[[ "${COMP_WORDS[i + 1]}" ]] && ((i++))
37+
continue
38+
;;
39+
-* | =)
40+
continue
41+
;;
42+
esac
43+
command="${COMP_WORDS[i]}"
44+
command_comp_cword=$i
45+
break
46+
done
47+
48+
# Compete the global options if the command is not specified yet.
49+
if [[ -z "$command" ]]; then
50+
case "$prev" in
51+
--target)
52+
readarray -t COMPREPLY < <(compgen -W "$("$1" targets --format=completion "$cur")" -- "$cur")
53+
compopt -o nospace
54+
return
55+
;;
56+
--repo | --out-prefix | --pregen-dir)
57+
_filedir -d
58+
return
59+
;;
60+
--dry-run-output)
61+
_filedir
62+
return
63+
;;
64+
esac
65+
case "$cur" in
66+
-*)
67+
readarray -t COMPREPLY < <(compgen -W "$(_parse_help "$1")" -- "$cur")
68+
return
69+
;;
70+
esac
71+
fi
72+
73+
# Complete the command name.
74+
if [[ "$command_comp_cword" -eq "$COMP_CWORD" ]]; then
75+
readarray -t COMPREPLY < <(compgen -W "$commands" -- "$cur")
76+
return
77+
fi
78+
79+
# Check if the command is valid.
80+
[[ "$commands" =~ $command ]] || return
81+
82+
# Command-specific completion.
83+
case "$prev" in
84+
--format)
85+
readarray -t COMPREPLY < <(compgen -W "summary expanded json" -- "$cur")
86+
return
87+
;;
88+
--copy-artifacts-to | --create-archives)
89+
_filedir -d
90+
return
91+
;;
92+
esac
93+
case "$cur" in
94+
-*)
95+
readarray -t COMPREPLY < <(compgen -W "$("$1" "$command" --help | _parse_help -)" -- "$cur")
96+
;;
97+
esac
98+
99+
}
100+
19101
# Get the list of commands from the output of the chip-tool,
20102
# where each command is prefixed with the ' | * ' string.
21103
_chip_tool_get_commands() {
@@ -124,6 +206,9 @@ _chip_tool() {
124206

125207
}
126208

209+
complete -F _chip_build_example scripts/build/build_examples.py
210+
complete -F _chip_build_example build_examples.py
211+
127212
complete -F _chip_app chip-air-purifier-app
128213
complete -F _chip_app chip-all-clusters-app
129214
complete -F _chip_app chip-bridge-app

0 commit comments

Comments
 (0)