blob: 531e3296d85e6c083a820bbecfc570038ef0343c [file] [log] [blame]
# -*- sh -*- (Bash only)
#
# Copyright 2015 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# Bash completion of Bazel commands.
#
# The template is expanded at build time using tables of commands/options
# derived from the bazel executable built in the same client; the expansion is
# written to bazel-complete.bash.
#
# Provides command-completion for:
# - bazel prefix options (e.g. --host_jvm_args)
# - blaze command-set (e.g. build, test)
# - blaze command-specific options (e.g. --copts)
# - values for enum-valued options
# - package-names, exploring all package-path roots.
# - targets within packages.
# The package path used by the completion routines. Unfortunately
# this isn't necessarily the same as the actual package path used by
# Bazel, but that's ok. (It's impossible for us to reliably know what
# the relevant package-path, so this is just a good guess. Users can
# override it if they want.)
#
# Don't use it directly. Generate the final script with
# bazel build //scripts:bash_completion instead.
#
: ${BAZEL_COMPLETION_PACKAGE_PATH:=%workspace%}
# If true, Bazel query is used for autocompletion. This is more
# accurate than the heuristic grep, especially for strangely-formatted
# BUILD files. But it can be slower, especially if the Bazel server
# is busy, and more brittle, if the BUILD file contains serious
# errors. This is an experimental feature.
: ${BAZEL_COMPLETION_USE_QUERY:=false}
# If true, Bazel run allows autocompletion for test targets. This is convenient
# for users who run a lot of tests/benchmarks locally with blaze run.
: ${BAZEL_COMPLETION_ALLOW_TESTS_FOR_RUN:=false}
# Some commands might interfer with the important one, so don't complete them
: ${BAZEL_IGNORED_COMMAND_REGEX:="__none__"}
# bazel & ibazel commands
: ${BAZEL:=bazel}
: ${IBAZEL:=ibazel}
# Pattern to match for looking for a target
# BAZEL_BUILD_MATCH_PATTERN__* give the pattern for label-*
# when looking in the the build file.
# BAZEL_QUERY_MATCH_PATTERN__* give the pattern for label-*
# when using 'bazel query'.
# _RUNTEST are special case when BAZEL_COMPLETION_ALLOW_TESTS_FOR_RUN
# is on.
: ${BAZEL_BUILD_MATCH_PATTERN__test:='(.*_test|test_suite)'}
: ${BAZEL_QUERY_MATCH_PATTERN__test:='(test|test_suite)'}
: ${BAZEL_BUILD_MATCH_PATTERN__bin:='.*_binary'}
: ${BAZEL_QUERY_MATCH_PATTERN__bin:='(binary)'}
: ${BAZEL_BUILD_MATCH_PATTERN_RUNTEST__bin:='(.*_(binary|test)|test_suite)'}
: ${BAZEL_QUERY_MATCH_PATTERN_RUNTEST__bin:='(binary|test)'}
: ${BAZEL_BUILD_MATCH_PATTERN__:='.*'}
: ${BAZEL_QUERY_MATCH_PATTERN__:=''}
# Usage: _bazel__get_rule_match_pattern <command>
# Determine what kind of rules to match, based on command.
_bazel__get_rule_match_pattern() {
local var_name pattern
if _bazel__is_true "$BAZEL_COMPLETION_USE_QUERY"; then
var_name="BAZEL_QUERY_MATCH_PATTERN"
else
var_name="BAZEL_BUILD_MATCH_PATTERN"
fi
if [[ "$1" =~ ^label-?([a-z]*)$ ]]; then
pattern=${BASH_REMATCH[1]:-}
if _bazel__is_true "$BAZEL_COMPLETION_ALLOW_TESTS_FOR_RUN"; then
eval "echo \"\${${var_name}_RUNTEST__${pattern}:-\$${var_name}__${pattern}}\""
else
eval "echo \"\$${var_name}__${pattern}\""
fi
fi
}
# Compute workspace directory. Search for the innermost
# enclosing directory with a WORKSPACE file.
_bazel__get_workspace_path() {
local workspace=$PWD
while true; do
if [ -f "${workspace}/WORKSPACE" ]; then
break
elif [ -z "$workspace" -o "$workspace" = "/" ]; then
workspace=$PWD
break;
fi
workspace=${workspace%/*}
done
echo $workspace
}
# Find the current piece of the line to complete, but only do word breaks at
# certain characters. In particular, ignore these: "':=
# This method also takes into account the current cursor position.
#
# Works with both bash 3 and 4! Bash 3 and 4 perform different word breaks when
# computing the COMP_WORDS array. We need this here because Bazel options are of
# the form --a=b, and labels of the form //some/label:target.
_bazel__get_cword() {
local cur=${COMP_LINE:0:$COMP_POINT}
# This expression finds the last word break character, as defined in the
# COMP_WORDBREAKS variable, but without '=' or ':', which is not preceeded by
# a slash. Quote characters are also excluded.
local wordbreaks="$COMP_WORDBREAKS"
wordbreaks="${wordbreaks//\'/}"
wordbreaks="${wordbreaks//\"/}"
wordbreaks="${wordbreaks//:/}"
wordbreaks="${wordbreaks//=/}"
local word_start=$(expr "$cur" : '.*[^\]['"${wordbreaks}"']')
echo "${cur:$word_start}"
}
# Usage: _bazel__package_path <workspace> <displacement>
#
# Prints a list of package-path root directories, displaced using the
# current displacement from the workspace. All elements have a
# trailing slash.
_bazel__package_path() {
local workspace=$1 displacement=$2 root
IFS=:
for root in ${BAZEL_COMPLETION_PACKAGE_PATH//\%workspace\%/$workspace}; do
unset IFS
echo "$root/$displacement"
done
}
# Usage: _bazel__options_for <command>
#
# Prints the set of options for a given Bazel command, e.g. "build".
_bazel__options_for() {
local options
if [[ "${BAZEL_COMMAND_LIST}" =~ ^(.* )?$1( .*)?$ ]]; then
# assumes option names only use ASCII characters
local option_name=$(echo $1 | tr a-z A-Z | tr "-" "_")
eval "echo \${BAZEL_COMMAND_${option_name}_FLAGS}" | tr " " "\n"
fi
}
# Usage: _bazel__expansion_for <command>
#
# Prints the completion pattern for a given Bazel command, e.g. "build".
_bazel__expansion_for() {
local options
if [[ "${BAZEL_COMMAND_LIST}" =~ ^(.* )?$1( .*)?$ ]]; then
# assumes option names only use ASCII characters
local option_name=$(echo $1 | tr a-z A-Z | tr "-" "_")
eval "echo \${BAZEL_COMMAND_${option_name}_ARGUMENT}"
fi
}
# Usage: _bazel__matching_targets <kind> <prefix>
#
# Prints target names of kind <kind> and starting with <prefix> in the BUILD
# file given as standard input. <kind> is a basic regex (BRE) used to match the
# bazel rule kind and <prefix> is the prefix of the target name.
_bazel__matching_targets() {
local kind_pattern="$1"
local target_prefix="$2"
# The following commands do respectively:
# Remove BUILD file comments
# Replace \n by spaces to have the BUILD file in a single line
# Extract all rule types and target names
# Grep the kind pattern and the target prefix
# Returns the target name
sed 's/#.*$//' \
| tr "\n" " " \
| sed 's/\([a-zA-Z0-9_]*\) *(\([^)]* \)\{0,1\}name *= *['\''"]\([a-zA-Z0-9_/.+=,@~-]*\)['\''"][^)]*)/\
type:\1 name:\3\
/g' \
| "grep" -E "^type:$kind_pattern name:$target_prefix" \
| cut -d ':' -f 3
}
# Usage: _bazel__is_true <string>
#
# Returns true or false based on the input string. The following are
# valid true values (the rest are false): "1", "true".
_bazel__is_true() {
local str="$1"
[[ "$str" == "1" || "$str" == "true" ]]
}
# Usage: _bazel__expand_rules_in_package <workspace> <displacement>
# <current> <label-type>
#
# Expands rules in specified packages, exploring all roots of
# $BAZEL_COMPLETION_PACKAGE_PATH, not just $(pwd). Only rules
# appropriate to the command are printed. Sets $COMPREPLY array to
# result.
#
# If $BAZEL_COMPLETION_USE_QUERY is true, 'bazel query' is used
# instead, with the actual Bazel package path;
# $BAZEL_COMPLETION_PACKAGE_PATH is ignored in this case, since the
# actual Bazel value is likely to be more accurate.
_bazel__expand_rules_in_package() {
local workspace=$1 displacement=$2 current=$3 label_type=$4
local package_name=$(echo "$current" | cut -f1 -d:)
local rule_prefix=$(echo "$current" | cut -f2 -d:)
local root buildfile rule_pattern r result
result=
pattern=$(_bazel__get_rule_match_pattern "$label_type")
if _bazel__is_true "$BAZEL_COMPLETION_USE_QUERY"; then
package_name=$(echo "$package_name" | tr -d "'\"") # remove quotes
result=$(${BAZEL} --output_base=/tmp/${BAZEL}-completion-$USER query \
--keep_going --noshow_progress \
"kind('$pattern rule', '$package_name:*')" 2>/dev/null |
cut -f2 -d: | "grep" "^$rule_prefix")
else
for root in $(_bazel__package_path "$workspace" "$displacement"); do
buildfile="$root/$package_name/BUILD.bazel"
if [ ! -f "$buildfile" ]; then
buildfile="$root/$package_name/BUILD"
fi
if [ -f "$buildfile" ]; then
result=$(_bazel__matching_targets \
"$pattern" "$rule_prefix" <"$buildfile")
break
fi
done
fi
index=$(echo $result | wc -w)
if [ -n "$result" ]; then
echo "$result" | tr " " "\n" | sed 's|$| |'
fi
# Include ":all" wildcard if there was no unique match. (The zero
# case is tricky: we need to include "all" in that case since
# otherwise we won't expand "a" to "all" in the absence of rules
# starting with "a".)
if [ $index -ne 1 ] && expr all : "\\($rule_prefix\\)" >/dev/null; then
echo "all "
fi
}
# Usage: _bazel__expand_package_name <workspace> <displacement> <current-word>
# <label-type>
#
# Expands directories, but explores all roots of
# BAZEL_COMPLETION_PACKAGE_PATH, not just $(pwd). When a directory is
# a bazel package, the completion offers "pkg:" so you can expand
# inside the package.
# Sets $COMPREPLY array to result.
_bazel__expand_package_name() {
local workspace=$1 displacement=$2 current=$3 type=${4:-} root dir index
for root in $(_bazel__package_path "$workspace" "$displacement"); do
found=0
for dir in $(compgen -d $root$current); do
[ -L "$dir" ] && continue # skip symlinks (e.g. bazel-bin)
[[ "$dir" =~ ^(.*/)?\.[^/]*$ ]] && continue # skip dotted dir (e.g. .git)
found=1
echo "${dir#$root}/"
if [ -f $dir/BUILD.bazel -o -f $dir/BUILD ]; then
if [ "${type}" = "label-package" ]; then
echo "${dir#$root} "
else
echo "${dir#$root}:"
fi
fi
done
[ $found -gt 0 ] && break # Stop searching package path upon first match.
done
}
# Usage: _bazel__expand_target_pattern <workspace> <displacement>
# <word> <label-syntax>
#
# Expands "word" to match target patterns, using the current workspace
# and displacement from it. "command" is used to filter rules.
# Sets $COMPREPLY array to result.
_bazel__expand_target_pattern() {
local workspace=$1 displacement=$2 current=$3 label_syntax=$4
case "$current" in
//*:*) # Expand rule names within package, no displacement.
if [ "${label_syntax}" = "label-package" ]; then
compgen -S " " -W "BUILD" "$(echo current | cut -f ':' -d2)"
else
_bazel__expand_rules_in_package "$workspace" "" "$current" "$label_syntax"
fi
;;
*:*) # Expand rule names within package, displaced.
if [ "${label_syntax}" = "label-package" ]; then
compgen -S " " -W "BUILD" "$(echo current | cut -f ':' -d2)"
else
_bazel__expand_rules_in_package \
"$workspace" "$displacement" "$current" "$label_syntax"
fi
;;
//*) # Expand filenames using package-path, no displacement
_bazel__expand_package_name "$workspace" "" "$current" "$label_syntax"
;;
*) # Expand filenames using package-path, displaced.
if [ -n "$current" ]; then
_bazel__expand_package_name "$workspace" "$displacement" "$current" "$label_syntax"
fi
;;
esac
}
_bazel__get_command() {
for word in "${COMP_WORDS[@]:1:COMP_CWORD-1}"; do
if echo "$BAZEL_COMMAND_LIST" | "grep" -wsq -e "$word"; then
echo $word
break
fi
done
}
# Returns the displacement to the workspace given in $1
_bazel__get_displacement() {
if [[ "$PWD" =~ ^$1/.*$ ]]; then
echo ${PWD##$1/}/
fi
}
# Usage: _bazel__complete_pattern <workspace> <displacement> <current>
# <type>
#
# Expand a word according to a type. The currently supported types are:
# - {a,b,c}: an enum that can take value a, b or c
# - label: a label of any kind
# - label-bin: a label to a runnable rule (basically to a _binary rule)
# - label-test: a label to a test rule
# - info-key: an info key as listed by `bazel help info-keys`
# - command: the name of a command
# - path: a file path
# - combinaison of previous type using | as separator
_bazel__complete_pattern() {
local workspace=$1 displacement=$2 current=$3 types=$4
for type in $(echo $types | tr "|" "\n"); do
case "$type" in
label*)
_bazel__expand_target_pattern "$workspace" "$displacement" \
"$current" "$type"
;;
info-key)
compgen -S " " -W "${BAZEL_INFO_KEYS}" -- "$current"
;;
"command")
local commands=$(echo "${BAZEL_COMMAND_LIST}" \
| tr " " "\n" | "grep" -v "^${BAZEL_IGNORED_COMMAND_REGEX}$")
compgen -S " " -W "${commands}" -- "$current"
;;
path)
compgen -f -- "$current"
;;
*)
compgen -S " " -W "$type" -- "$current"
;;
esac
done
}
# Usage: _bazel__expand_options <workspace> <displacement> <current-word>
# <options>
#
# Expands options, making sure that if current-word contains an equals sign,
# it is handled appropriately.
_bazel__expand_options() {
local workspace="$1" displacement="$2" cur="$3" options="$4"
if [[ $cur =~ = ]]; then
# also expands special labels
current=$(echo "$cur" | cut -f2 -d=)
_bazel__complete_pattern "$workspace" "$displacement" "$current" \
"$(compgen -W "$options" -- "$cur" | cut -f2 -d=)" \
| sort -u
else
compgen -W "$(echo "$options" | sed 's|=.*$|=|')" -- "$cur" \
| sed 's|\([^=]\)$|\1 |'
fi
}
_bazel__complete_stdout() {
local cur=$(_bazel__get_cword) word command displacement workspace
# Determine command: "" (startup-options) or one of $BAZEL_COMMAND_LIST.
command="$(_bazel__get_command)"
workspace="$(_bazel__get_workspace_path)"
displacement="$(_bazel__get_displacement ${workspace})"
case "$command" in
"") # Expand startup-options or commands
local commands=$(echo "${BAZEL_COMMAND_LIST}" \
| tr " " "\n" | "grep" -v "^${BAZEL_IGNORED_COMMAND_REGEX}$")
_bazel__expand_options "$workspace" "$displacement" "$cur" \
"${commands}\
${BAZEL_STARTUP_OPTIONS}"
;;
*)
case "$cur" in
-*) # Expand options:
_bazel__expand_options "$workspace" "$displacement" "$cur" \
"$(_bazel__options_for $command)"
;;
*) # Expand target pattern
expansion_pattern="$(_bazel__expansion_for $command)"
NON_QUOTE_REGEX="^[\"']"
if [[ $command = query && $cur =~ $NON_QUOTE_REGEX ]]; then
: # Ideally we would expand query expressions---it's not
# that hard, conceptually---but readline is just too
# damn complex when it comes to quotation. Instead,
# for query, we just expand target patterns, unless
# the first char is a quote.
elif [ -n "$expansion_pattern" ]; then
_bazel__complete_pattern \
"$workspace" "$displacement" "$cur" "$expansion_pattern"
fi
;;
esac
;;
esac
}
_bazel__to_compreply() {
local replies="$1"
COMPREPLY=()
# Trick to preserve whitespaces
while IFS="" read -r reply; do
COMPREPLY+=("${reply}")
done < <(echo "${replies}")
}
_bazel__complete() {
_bazel__to_compreply "$(_bazel__complete_stdout)"
}
# Some users have aliases such as bt="bazel test" or bb="bazel build", this
# completion function allows them to have auto-completion for these aliases.
_bazel__complete_target_stdout() {
local cur=$(_bazel__get_cword) word command displacement workspace
# Determine command: "" (startup-options) or one of $BAZEL_COMMAND_LIST.
command="$1"
workspace="$(_bazel__get_workspace_path)"
displacement="$(_bazel__get_displacement ${workspace})"
_bazel__to_compreply "$(_bazel__expand_target_pattern "$workspace" "$displacement" \
"$cur" "$(_bazel__expansion_for $command)")"
}
# default completion for bazel
complete -F _bazel__complete -o nospace "${BAZEL}"
complete -F _bazel__complete -o nospace "${IBAZEL}"