| # 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) |