blob: 962abb55d5d8837db85ef944bf5739cda85dc492 [file] [log] [blame]
Akira Baruah20febac2020-10-16 02:10:37 -07001# Copyright 2020 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
Googlerbd7a6b92022-02-24 07:38:58 -08007# http://www.apache.org/licenses/LICENSE-2.0
Akira Baruah20febac2020-10-16 02:10:37 -07008#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Generates a fish completion script for Bazel."""
15
16from __future__ import absolute_import
17from __future__ import division
18from __future__ import print_function
19
20import re
21import subprocess
22import tempfile
23
24from absl import app
25from absl import flags
26
27flags.DEFINE_string('bazel', None, 'Path to the bazel binary')
28flags.DEFINE_string('output', None, 'Where to put the generated fish script')
29
30flags.mark_flag_as_required('bazel')
31flags.mark_flag_as_required('output')
32
33FLAGS = flags.FLAGS
34_BAZEL = 'bazel'
35_FISH_SEEN_SUBCOMMAND_FROM = '__fish_seen_subcommand_from'
36_FISH_BAZEL_HEADER = """#!/usr/bin/env fish
37# Copyright 2020 The Bazel Authors. All rights reserved.
38#
39# Licensed under the Apache License, Version 2.0 (the "License");
40# you may not use this file except in compliance with the License.
41# You may obtain a copy of the License at
42#
43# http://www.apache.org/licenses/LICENSE-2.0
44#
45# Unless required by applicable law or agreed to in writing, software
46# distributed under the License is distributed on an "AS IS" BASIS,
47# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
48# See the License for the specific language governing permissions and
49# limitations under the License.
50
51# Fish completion for Bazel commands and options.
52#
53# This script was generated from a specific Bazel build distribution. See
54# https://github.com/bazelbuild/bazel/blob/master/scripts/generate_fish_completion.py
55# for details and implementation.
56"""
57_FISH_BAZEL_COMMAND_LIST_VAR = 'BAZEL_COMMAND_LIST'
58_FISH_BAZEL_SEEN_SUBCOMMAND = '__bazel_seen_subcommand'
59_FISH_BAZEL_SEEN_SUBCOMMAND_DEF = """
60function {} -d "Checks whether the current command line contains a bazel subcommand."
61 {} ${}
62end
63""".format(_FISH_BAZEL_SEEN_SUBCOMMAND, _FISH_SEEN_SUBCOMMAND_FROM,
64 _FISH_BAZEL_COMMAND_LIST_VAR)
65
66
67class BazelCompletionWriter(object):
68 """Constructs a Fish completion script for Bazel."""
69
70 def __init__(self, bazel, output_user_root):
71 """Initializes writer state.
72
73 Args:
74 bazel: String containing a path the a bazel binary to run.
75 output_user_root: String path to user root directory used for
76 running bazel commands.
77 """
78 self._bazel = bazel
79 self._output_user_root = output_user_root
80 self._startup_options = self._get_options_from_bazel(
81 ('help', 'startup_options'))
82 self._bazel_help_completion_text = self._get_bazel_output(
83 ('help', 'completion'))
84 self._param_types_by_subcommand = self._get_param_types()
85 self._subcommands = self._get_subcommands()
86
87 def write_completion(self, output_file):
88 """Writes a Fish completion script for Bazel to an output file.
89
90 Args:
91 output_file: File object opened in a writable mode.
92 """
93 output_file.write(_FISH_BAZEL_HEADER)
94 output_file.write('set {} {}\n'.format(
95 _FISH_BAZEL_COMMAND_LIST_VAR,
96 ' '.join(c.name for c in self._subcommands)))
97 output_file.write(_FISH_BAZEL_SEEN_SUBCOMMAND_DEF)
98 for opt in self._startup_options:
99 opt.write_completion(output_file)
100 for sub in self._subcommands:
101 sub.write_completion(output_file)
102
103 def _get_bazel_output(self, args):
104 return subprocess.check_output(
105 (self._bazel, '--output_user_root={}'.format(self._output_user_root)) +
106 tuple(args),
107 universal_newlines=True)
108
109 def _get_options_from_bazel(self, bazel_args, **kwargs):
110 output = self._get_bazel_output(bazel_args)
111 return list(
112 Arg.generate_from_help(
113 r'^\s*--(\[no\])?(?P<name>\w+)\s+\((?P<desc>.*)\)$', output,
114 **kwargs))
115
116 def _get_param_types(self):
117 param_types = {}
118 for match in re.finditer(
119 r'^BAZEL_COMMAND_(?P<subcommand>.*)_ARGUMENT="(?P<type>.*)"$',
120 self._bazel_help_completion_text, re.MULTILINE):
121 sub = self._normalize_subcommand_name(match.group('subcommand'))
122 param_types[sub] = match.group('type')
123 return param_types
124
125 def _get_subcommands(self):
126 """Runs `bazel help` and parses its output to derive Bazel commands.
127
128 Returns:
129 (:obj:`list` of :obj:`Arg`): List of Bazel commands.
130 """
131 subs = []
132 output = self._get_bazel_output(('help',))
133 block = re.search(r'Available commands:(.*\n\n)', output, re.DOTALL)
134 for sub in Arg.generate_from_help(
135 r'^\s*(?P<name>\S+)\s*(?P<desc>\S+.*\.)\s*$',
136 block.group(1),
137 is_subcommand=True):
138 sub.sub_opts = self._get_options_from_bazel(('help', sub.name),
139 expected_subcommand=sub.name)
140 sub.sub_params = self._get_params(sub.name)
141 subs.append(sub)
142 return subs
143
144 _BAZEL_QUERY_BY_LABEL = {
145 'label': r'//...',
146 'label-bin': r'kind(".*_binary", //...)',
147 'label-test': r'tests(//...)',
148 }
149
150 def _get_params(self, subcommand):
151 """Produces a list of param completions for a given Bazel command.
152
153 Uses a previously generated mapping of Bazel commands to parameter types
154 to determine how to complete params following a given command. For
155 example, `bazel build` expects `label` type params, whereas `bazel info`
156 expects an `info-key` type. The param type is finally translated into a
157 list of completion strings.
158
159 Args:
160 subcommand: Bazel command string.
161
162 Returns:
163 (:obj:`list` of :obj:`str`): List of completions based on the param
164 type for the given Bazel command.
165 """
166 name = self._normalize_subcommand_name(subcommand)
167 if name not in self._param_types_by_subcommand:
168 return []
169 params = []
170 param_type = self._param_types_by_subcommand[name]
171 if param_type.startswith('label'):
172 query = self._BAZEL_QUERY_BY_LABEL[param_type]
173 params.append("({} query -k '{}' 2>/dev/null)".format(_BAZEL, query))
174 elif param_type.startswith('command'):
175 match = re.match(r'command\|\{(?P<commands>.*)\}', param_type)
176 params.extend(match.group('commands').split(','))
177 elif param_type == 'info-key':
178 match = re.search(r'BAZEL_INFO_KEYS="(?P<keys>[^"]*)"',
179 self._bazel_help_completion_text)
180 params.extend(match.group('keys').split())
181 return params
182
183 @staticmethod
184 def _normalize_subcommand_name(subcommand):
185 return subcommand.strip().lower().replace('_', '-')
186
187
188class Arg(object):
189 """Represents a Bazel argument and its metadata.
190
191 Attributes:
192 name: String containing the name of the argument.
193 desc: String describing the argument usage.
194 is_subcommand: True if this arg represents a Bazel subcommand. Defaults
195 to False, indicating that this arg is an option flag.
196 expected_subcommand: Nullable string containing a subcommand that this
197 option must follow. Defaults to None, indicating that this option or
198 subcommand must not follow another subcommand.
199 sub_opts: List of Args representing options of a subcommand. Used only
200 if is_subcommand is True.
201 sub_params: List of Args representing parameters of a subcommand. Used
202 only if is_subcommand is True.
203 """
204
205 def __init__(self,
206 name,
207 desc=None,
208 is_subcommand=False,
209 expected_subcommand=None):
210 self.name = name
211 self.desc = desc
212 self.is_subcommand = is_subcommand
213 self.expected_subcommand = expected_subcommand
214 self.sub_opts = []
215 self.sub_params = []
216 self._is_boolean = (self.desc and self.desc.startswith('a boolean'))
217
218 @classmethod
219 def generate_from_help(cls, line_regex, text, **kwargs):
220 """Generates Arg objects using a line regex on a block of help text.
221
222 Args:
223 line_regex: Regular expression string to match a line of text.
224 text: String of help text to parse.
225 **kwargs: Extra keywords to pass into the Arg constructor.
226
227 Yields:
228 Arg objects parsed from the help text.
229 """
230 for match in re.finditer(line_regex, text, re.MULTILINE):
231 kwargs.update(match.groupdict())
232 yield cls(**kwargs)
233
234 def write_completion(self, output_file, command=_BAZEL):
235 """Writes Fish completion commands to a file.
236
237 Uses the metadata stored in this class to write Fish shell commands
238 that enable completion for this Bazel argument.
239
240 Args:
241 output_file: File object to write completions into. Must be open in
242 a writable mode.
243 command: String containg the command name (i.e. "bazel").
244 """
245 args = self._get_complete_args_base(
246 command=command, subcommand=self.expected_subcommand)
247
248 # Argument can be subcommand or option flag.
249 if self.is_subcommand:
250 args.append('-xa') # Exclusive subcommand argument.
251 else:
252 args.append('-l') # Long option.
253 args.append('"{}"'.format(self.name))
254 name_index = len(args) - 1
255
256 if self.desc:
257 args.extend(('-d', '"{}"'.format(self._escape(self.desc))))
258
259 if not self._is_boolean:
260 args.append('-r') # Require a subsequent parameter.
261
262 # Write completion commands to the file.
263 output_file.write(self._complete(args))
264 if self._is_boolean:
265 # Include the "false" version of a boolean option.
266 args[name_index] = '"no{}"'.format(self.name)
267 output_file.write(self._complete(args))
268 if self.is_subcommand:
269 for opt in self.sub_opts:
270 opt.write_completion(output_file, command=command)
271 self._write_params_completion(output_file, command=command)
272 output_file.write('\n')
273
274 def _write_params_completion(self, output_file, command=_BAZEL):
275 args = self._get_complete_args_base(command, subcommand=self.name)
276 if self.sub_params:
277 args.extend(
278 ('-fa', '"{}"'.format(self._escape(' '.join(self.sub_params)))))
279 output_file.write(self._complete(args))
280
281 @staticmethod
282 def _get_complete_args_base(command, subcommand=None):
283 """Provides basic arguments for all fish `complete` invocations.
284
285 Args:
286 command: Name of the Bazel executable (i.e. "bazel").
287 subcommand: Optional Bazel command like "build".
288
289 Returns:
290 (:obj:`list` of :obj:`str`): List of args for `complete`.
291 """
292 args = ['-c', command]
293
294 # Completion pre-condition.
295 args.append('-n')
296 if subcommand:
297 args.append('"{} {}"'.format(_FISH_SEEN_SUBCOMMAND_FROM, subcommand))
298 else:
299 args.append('"not {}"'.format(_FISH_BAZEL_SEEN_SUBCOMMAND))
300
301 return args
302
303 @staticmethod
304 def _complete(args):
305 return 'complete {}\n'.format(' '.join(args))
306
307 @staticmethod
308 def _escape(text):
309 return text.replace('"', r'\"')
310
311
312def main(argv):
313 """Generates fish completion using provided flags."""
314 del argv # Unused.
315 with tempfile.TemporaryDirectory() as output_user_root:
316 writer = BazelCompletionWriter(FLAGS.bazel, output_user_root)
317 with open(FLAGS.output, mode='w') as output:
318 writer.write_completion(output)
319
320
321if __name__ == '__main__':
322 app.run(main)