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