| # Copyright 2020 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. |
| """Generates a fish completion script for Bazel.""" |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import re |
| import subprocess |
| import tempfile |
| |
| from absl import app |
| from absl import flags |
| |
| flags.DEFINE_string('bazel', None, 'Path to the bazel binary') |
| flags.DEFINE_string('output', None, 'Where to put the generated fish script') |
| |
| flags.mark_flag_as_required('bazel') |
| flags.mark_flag_as_required('output') |
| |
| FLAGS = flags.FLAGS |
| _BAZEL = 'bazel' |
| _FISH_SEEN_SUBCOMMAND_FROM = '__fish_seen_subcommand_from' |
| _FISH_BAZEL_HEADER = """#!/usr/bin/env fish |
| # Copyright 2020 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. |
| |
| # Fish completion for Bazel commands and options. |
| # |
| # This script was generated from a specific Bazel build distribution. See |
| # https://github.com/bazelbuild/bazel/blob/master/scripts/generate_fish_completion.py |
| # for details and implementation. |
| """ |
| _FISH_BAZEL_COMMAND_LIST_VAR = 'BAZEL_COMMAND_LIST' |
| _FISH_BAZEL_SEEN_SUBCOMMAND = '__bazel_seen_subcommand' |
| _FISH_BAZEL_SEEN_SUBCOMMAND_DEF = """ |
| function {} -d "Checks whether the current command line contains a bazel subcommand." |
| {} ${} |
| end |
| """.format(_FISH_BAZEL_SEEN_SUBCOMMAND, _FISH_SEEN_SUBCOMMAND_FROM, |
| _FISH_BAZEL_COMMAND_LIST_VAR) |
| |
| |
| class BazelCompletionWriter(object): |
| """Constructs a Fish completion script for Bazel.""" |
| |
| def __init__(self, bazel, output_user_root): |
| """Initializes writer state. |
| |
| Args: |
| bazel: String containing a path the a bazel binary to run. |
| output_user_root: String path to user root directory used for |
| running bazel commands. |
| """ |
| self._bazel = bazel |
| self._output_user_root = output_user_root |
| self._startup_options = self._get_options_from_bazel( |
| ('help', 'startup_options')) |
| self._bazel_help_completion_text = self._get_bazel_output( |
| ('help', 'completion')) |
| self._param_types_by_subcommand = self._get_param_types() |
| self._subcommands = self._get_subcommands() |
| |
| def write_completion(self, output_file): |
| """Writes a Fish completion script for Bazel to an output file. |
| |
| Args: |
| output_file: File object opened in a writable mode. |
| """ |
| output_file.write(_FISH_BAZEL_HEADER) |
| output_file.write('set {} {}\n'.format( |
| _FISH_BAZEL_COMMAND_LIST_VAR, |
| ' '.join(c.name for c in self._subcommands))) |
| output_file.write(_FISH_BAZEL_SEEN_SUBCOMMAND_DEF) |
| for opt in self._startup_options: |
| opt.write_completion(output_file) |
| for sub in self._subcommands: |
| sub.write_completion(output_file) |
| |
| def _get_bazel_output(self, args): |
| return subprocess.check_output( |
| (self._bazel, '--output_user_root={}'.format(self._output_user_root)) + |
| tuple(args), |
| universal_newlines=True) |
| |
| def _get_options_from_bazel(self, bazel_args, **kwargs): |
| output = self._get_bazel_output(bazel_args) |
| return list( |
| Arg.generate_from_help( |
| r'^\s*--(\[no\])?(?P<name>\w+)\s+\((?P<desc>.*)\)$', output, |
| **kwargs)) |
| |
| def _get_param_types(self): |
| param_types = {} |
| for match in re.finditer( |
| r'^BAZEL_COMMAND_(?P<subcommand>.*)_ARGUMENT="(?P<type>.*)"$', |
| self._bazel_help_completion_text, re.MULTILINE): |
| sub = self._normalize_subcommand_name(match.group('subcommand')) |
| param_types[sub] = match.group('type') |
| return param_types |
| |
| def _get_subcommands(self): |
| """Runs `bazel help` and parses its output to derive Bazel commands. |
| |
| Returns: |
| (:obj:`list` of :obj:`Arg`): List of Bazel commands. |
| """ |
| subs = [] |
| output = self._get_bazel_output(('help',)) |
| block = re.search(r'Available commands:(.*\n\n)', output, re.DOTALL) |
| for sub in Arg.generate_from_help( |
| r'^\s*(?P<name>\S+)\s*(?P<desc>\S+.*\.)\s*$', |
| block.group(1), |
| is_subcommand=True): |
| sub.sub_opts = self._get_options_from_bazel(('help', sub.name), |
| expected_subcommand=sub.name) |
| sub.sub_params = self._get_params(sub.name) |
| subs.append(sub) |
| return subs |
| |
| _BAZEL_QUERY_BY_LABEL = { |
| 'label': r'//...', |
| 'label-bin': r'kind(".*_binary", //...)', |
| 'label-test': r'tests(//...)', |
| } |
| |
| def _get_params(self, subcommand): |
| """Produces a list of param completions for a given Bazel command. |
| |
| Uses a previously generated mapping of Bazel commands to parameter types |
| to determine how to complete params following a given command. For |
| example, `bazel build` expects `label` type params, whereas `bazel info` |
| expects an `info-key` type. The param type is finally translated into a |
| list of completion strings. |
| |
| Args: |
| subcommand: Bazel command string. |
| |
| Returns: |
| (:obj:`list` of :obj:`str`): List of completions based on the param |
| type for the given Bazel command. |
| """ |
| name = self._normalize_subcommand_name(subcommand) |
| if name not in self._param_types_by_subcommand: |
| return [] |
| params = [] |
| param_type = self._param_types_by_subcommand[name] |
| if param_type.startswith('label'): |
| query = self._BAZEL_QUERY_BY_LABEL[param_type] |
| params.append("({} query -k '{}' 2>/dev/null)".format(_BAZEL, query)) |
| elif param_type.startswith('command'): |
| match = re.match(r'command\|\{(?P<commands>.*)\}', param_type) |
| params.extend(match.group('commands').split(',')) |
| elif param_type == 'info-key': |
| match = re.search(r'BAZEL_INFO_KEYS="(?P<keys>[^"]*)"', |
| self._bazel_help_completion_text) |
| params.extend(match.group('keys').split()) |
| return params |
| |
| @staticmethod |
| def _normalize_subcommand_name(subcommand): |
| return subcommand.strip().lower().replace('_', '-') |
| |
| |
| class Arg(object): |
| """Represents a Bazel argument and its metadata. |
| |
| Attributes: |
| name: String containing the name of the argument. |
| desc: String describing the argument usage. |
| is_subcommand: True if this arg represents a Bazel subcommand. Defaults |
| to False, indicating that this arg is an option flag. |
| expected_subcommand: Nullable string containing a subcommand that this |
| option must follow. Defaults to None, indicating that this option or |
| subcommand must not follow another subcommand. |
| sub_opts: List of Args representing options of a subcommand. Used only |
| if is_subcommand is True. |
| sub_params: List of Args representing parameters of a subcommand. Used |
| only if is_subcommand is True. |
| """ |
| |
| def __init__(self, |
| name, |
| desc=None, |
| is_subcommand=False, |
| expected_subcommand=None): |
| self.name = name |
| self.desc = desc |
| self.is_subcommand = is_subcommand |
| self.expected_subcommand = expected_subcommand |
| self.sub_opts = [] |
| self.sub_params = [] |
| self._is_boolean = (self.desc and self.desc.startswith('a boolean')) |
| |
| @classmethod |
| def generate_from_help(cls, line_regex, text, **kwargs): |
| """Generates Arg objects using a line regex on a block of help text. |
| |
| Args: |
| line_regex: Regular expression string to match a line of text. |
| text: String of help text to parse. |
| **kwargs: Extra keywords to pass into the Arg constructor. |
| |
| Yields: |
| Arg objects parsed from the help text. |
| """ |
| for match in re.finditer(line_regex, text, re.MULTILINE): |
| kwargs.update(match.groupdict()) |
| yield cls(**kwargs) |
| |
| def write_completion(self, output_file, command=_BAZEL): |
| """Writes Fish completion commands to a file. |
| |
| Uses the metadata stored in this class to write Fish shell commands |
| that enable completion for this Bazel argument. |
| |
| Args: |
| output_file: File object to write completions into. Must be open in |
| a writable mode. |
| command: String containg the command name (i.e. "bazel"). |
| """ |
| args = self._get_complete_args_base( |
| command=command, subcommand=self.expected_subcommand) |
| |
| # Argument can be subcommand or option flag. |
| if self.is_subcommand: |
| args.append('-xa') # Exclusive subcommand argument. |
| else: |
| args.append('-l') # Long option. |
| args.append('"{}"'.format(self.name)) |
| name_index = len(args) - 1 |
| |
| if self.desc: |
| args.extend(('-d', '"{}"'.format(self._escape(self.desc)))) |
| |
| if not self._is_boolean: |
| args.append('-r') # Require a subsequent parameter. |
| |
| # Write completion commands to the file. |
| output_file.write(self._complete(args)) |
| if self._is_boolean: |
| # Include the "false" version of a boolean option. |
| args[name_index] = '"no{}"'.format(self.name) |
| output_file.write(self._complete(args)) |
| if self.is_subcommand: |
| for opt in self.sub_opts: |
| opt.write_completion(output_file, command=command) |
| self._write_params_completion(output_file, command=command) |
| output_file.write('\n') |
| |
| def _write_params_completion(self, output_file, command=_BAZEL): |
| args = self._get_complete_args_base(command, subcommand=self.name) |
| if self.sub_params: |
| args.extend( |
| ('-fa', '"{}"'.format(self._escape(' '.join(self.sub_params))))) |
| output_file.write(self._complete(args)) |
| |
| @staticmethod |
| def _get_complete_args_base(command, subcommand=None): |
| """Provides basic arguments for all fish `complete` invocations. |
| |
| Args: |
| command: Name of the Bazel executable (i.e. "bazel"). |
| subcommand: Optional Bazel command like "build". |
| |
| Returns: |
| (:obj:`list` of :obj:`str`): List of args for `complete`. |
| """ |
| args = ['-c', command] |
| |
| # Completion pre-condition. |
| args.append('-n') |
| if subcommand: |
| args.append('"{} {}"'.format(_FISH_SEEN_SUBCOMMAND_FROM, subcommand)) |
| else: |
| args.append('"not {}"'.format(_FISH_BAZEL_SEEN_SUBCOMMAND)) |
| |
| return args |
| |
| @staticmethod |
| def _complete(args): |
| return 'complete {}\n'.format(' '.join(args)) |
| |
| @staticmethod |
| def _escape(text): |
| return text.replace('"', r'\"') |
| |
| |
| def main(argv): |
| """Generates fish completion using provided flags.""" |
| del argv # Unused. |
| with tempfile.TemporaryDirectory() as output_user_root: |
| writer = BazelCompletionWriter(FLAGS.bazel, output_user_root) |
| with open(FLAGS.output, mode='w') as output: |
| writer.write_completion(output) |
| |
| |
| if __name__ == '__main__': |
| app.run(main) |