Skip to content

Commit a4c3d89

Browse files
fmeumcopybara-github
authored andcommitted
Add bash completion for external targets
Completion for both repository names and targets in (fetched) external repositories is provided based on `bazel mod dump_repo_mapping`. RELNOTES: Bazel's Bash completion can now complete external repository labels when using `--enable_bzlmod`. Closes #20693. PiperOrigin-RevId: 602923007 Change-Id: I7c7dfe3d90a2921f473c2d2fedbc054da67932ac
1 parent 8164990 commit a4c3d89

5 files changed

Lines changed: 435 additions & 5 deletions

File tree

scripts/BUILD

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ genrule(
2222
":generate_bash_completion.sh",
2323
"//src:bazel",
2424
],
25-
visibility = ["//scripts/packages:__subpackages__"],
25+
visibility = [
26+
"//scripts/packages:__subpackages__",
27+
"//src/test/py/bazel:__pkg__",
28+
],
2629
)
2730

2831
sh_test(

scripts/bazel-complete-template.bash

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ _bazel__get_workspace_path() {
9898
echo $workspace
9999
}
100100

101-
102101
# Find the current piece of the line to complete, but only do word breaks at
103-
# certain characters. In particular, ignore these: "':=
102+
# certain characters. In particular, ignore these: "':=@
104103
# This method also takes into account the current cursor position.
105104
#
106105
# Works with both bash 3 and 4! Bash 3 and 4 perform different word breaks when
@@ -109,13 +108,14 @@ _bazel__get_workspace_path() {
109108
_bazel__get_cword() {
110109
local cur=${COMP_LINE:0:$COMP_POINT}
111110
# This expression finds the last word break character, as defined in the
112-
# COMP_WORDBREAKS variable, but without '=' or ':', which is not preceeded by
113-
# a slash. Quote characters are also excluded.
111+
# COMP_WORDBREAKS variable, but without '@', '=' or ':', which is not
112+
# preceded by a slash. Quote characters are also excluded.
114113
local wordbreaks="$COMP_WORDBREAKS"
115114
wordbreaks="${wordbreaks//\'/}"
116115
wordbreaks="${wordbreaks//\"/}"
117116
wordbreaks="${wordbreaks//:/}"
118117
wordbreaks="${wordbreaks//=/}"
118+
wordbreaks="${wordbreaks//@/}"
119119
local word_start=$(expr "$cur" : '.*[^\]['"${wordbreaks}"']')
120120
echo "${cur:$word_start}"
121121
}
@@ -281,6 +281,111 @@ _bazel__expand_package_name() {
281281
done
282282
}
283283

284+
# Usage: _bazel__filter_repo_mapping <filter> <field>
285+
#
286+
# Returns all entries of the main repo's repository mapping whose apparent repo
287+
# name, followed by a double quote, matches the given filter. To return the
288+
# matching apparent names, set field to 2. To return the matching canonical
289+
# names, set field to 4.
290+
# Note: Instead of returning an empty canonical name for the main repository,
291+
# this function returns the string "_main" so that this case can be
292+
# distinguished from that of no match.
293+
_bazel__filter_repo_mapping() {
294+
local filter=$1 field=$2
295+
# 1. dump_repo_mapping '' returns a single line consisting of a minified JSON
296+
# object.
297+
# 2. Transform JSON to have lines of the form "apparent_name":"canonical_name".
298+
# 3. Filter by apparent repo name.
299+
# 4. Replace an empty canonical name with "_main".
300+
# 5. Cut out either the apparent or canonical name.
301+
${BAZEL} mod dump_repo_mapping '' --noshow_progress 2>/dev/null |
302+
tr '{},' '\n' |
303+
"grep" "^\"${filter}" |
304+
sed 's|:""$|:"_main"|' |
305+
cut -d'"' -f${field}
306+
}
307+
308+
# Usage: _bazel__expand_repo_name <current>
309+
#
310+
# Returns completions for apparent repository names. Each line is of the form
311+
# @apparent_name or @apparent_name//, where apparent_name starts with current.
312+
_bazel__expand_repo_name() {
313+
local current=$1
314+
# If current exactly matches a repo name, also provide the @current//
315+
# completion so that users can tab through to package completion, but also
316+
# complete just the shorthand for "@repo_name//:repo_name".
317+
_bazel__filter_repo_mapping "${current#@}" 2 |
318+
sed 's|^|@|' |
319+
sed "s|^${current}\$|${current} ${current}//|"
320+
}
321+
322+
# Usage: _bazel__repo_root <workspace> <repo>
323+
#
324+
# Returns the absolute path to the root of the repository identified by the
325+
# repository part <repo> of a label. <repo> can be either of the form
326+
# "@apparent_name" or "@@canonical_name" and may also refer to the main
327+
# repository.
328+
_bazel__repo_root() {
329+
local workspace=$1 repo=$2
330+
local canonical_repo
331+
if [[ "$repo" == @@ ]]; then
332+
# Match the sentinel value for the main repository used by
333+
# _bazel__filter_repo_mapping.
334+
canonical_repo=_main
335+
elif [[ "$repo" =~ ^@@ ]]; then
336+
# Canonical repo names should not go through repo mapping.
337+
canonical_repo=${repo#@@}
338+
else
339+
canonical_repo=$(_bazel__filter_repo_mapping "${repo#@}\"" 4)
340+
fi
341+
if [ -z "$canonical_repo" ]; then
342+
return
343+
fi
344+
if [ "$canonical_repo" == "_main" ]; then
345+
echo "$workspace"
346+
return
347+
fi
348+
local output_base="$(${BAZEL} info output_base --noshow_progress 2>/dev/null)"
349+
if [ -z "$output_base" ]; then
350+
return
351+
fi
352+
local repo_root="$output_base/external/$canonical_repo"
353+
echo "$repo_root"
354+
}
355+
356+
# Usage: _bazel__expand_package_name <workspace> <current> <label-type>
357+
#
358+
# Expands packages under the potentially external repository pointed to by
359+
# <current>, which is expected to start with "@repo//".
360+
_bazel__expand_external_package_name() {
361+
local workspace=$1 current=$2 label_syntax=$3
362+
local repo=$(echo "$current" | cut -f1 -d/)
363+
local package=$(echo "$current" | cut -f3- -d/)
364+
local repo_root=$(_bazel__repo_root "$workspace" "$repo")
365+
if [ -z "$repo_root" ]; then
366+
return
367+
fi
368+
_bazel__expand_package_name "$repo_root" "" "$package" "$label_syntax" |
369+
sed "s|^|${repo}//|"
370+
}
371+
372+
# Usage: _bazel__expand_rules_in_external_package <workspace> <current>
373+
# <label-type>
374+
#
375+
# Expands rule names in the potentially external package pointed to by
376+
# <current>, which is expected to start with "@repo//some/pkg:".
377+
_bazel__expand_rules_in_external_package() {
378+
local workspace=$1 current=$2 label_syntax=$3
379+
local repo=$(echo "$current" | cut -f1 -d/)
380+
local package=$(echo "$current" | cut -f3- -d/ | cut -f1 -d:)
381+
local name=$(echo "$current" | cut -f2 -d:)
382+
local repo_root=$(_bazel__repo_root "$workspace" "$repo")
383+
if [ -z "$repo_root" ]; then
384+
return
385+
fi
386+
_bazel__expand_rules_in_package "$repo_root" "" "//$package:$name" "$label_syntax"
387+
}
388+
284389
# Usage: _bazel__expand_target_pattern <workspace> <displacement>
285390
# <word> <label-syntax>
286391
#
@@ -290,6 +395,26 @@ _bazel__expand_package_name() {
290395
_bazel__expand_target_pattern() {
291396
local workspace=$1 displacement=$2 current=$3 label_syntax=$4
292397
case "$current" in
398+
@*//*:*) # Expand rule names within external repository.
399+
_bazel__expand_rules_in_external_package "$workspace" "$current" "$label_syntax"
400+
;;
401+
@*/*) # Expand package names within external repository.
402+
# Append a second slash after the repo name before performing completion
403+
# if there is no second slash already.
404+
if [[ "$current" =~ ^@[^/]*/$ ]]; then
405+
current="$current/"
406+
fi
407+
_bazel__expand_external_package_name "$workspace" "$current" "$label_syntax"
408+
;;
409+
@*) # Expand external repository names.
410+
# Do not expand canonical repository names: Users are not expected to
411+
# compose them manually and completing them based on the contents of the
412+
# external directory has a high risk of returning stale results.
413+
if [[ "$current" =~ ^@@ ]]; then
414+
return
415+
fi
416+
_bazel__expand_repo_name "$current"
417+
;;
293418
//*:*) # Expand rule names within package, no displacement.
294419
if [ "${label_syntax}" = "label-package" ]; then
295420
compgen -S " " -W "BUILD" "$(echo current | cut -f ':' -d2)"

src/test/py/bazel/BUILD

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,19 @@ py_test(
410410
":test_base",
411411
],
412412
)
413+
414+
py_test(
415+
name = "external_repo_completion_test",
416+
size = "large",
417+
srcs = ["bzlmod/external_repo_completion_test.py"],
418+
data = ["//scripts:bash_completion"],
419+
tags = [
420+
"no_windows", # //scripts:bash_completion does not build on Windows
421+
"requires-network",
422+
],
423+
deps = [
424+
":bzlmod_test_utils",
425+
":test_base",
426+
requirement("bazel-runfiles"),
427+
],
428+
)

0 commit comments

Comments
 (0)