/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bashcomp.py

Refactor to increase modularity.

The new structure has separate classes for data collection, internal data
representation, and shell code generation.  This should allow great
improvements to unit testing, as the parts can be tested independently.

https://code.launchpad.net/~gagern/bzr/bug560030-include-bash-completion-plugin/+merge/23912

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
import bzrlib
29
29
import re
30
30
 
31
 
head="""\
 
31
 
 
32
class BashCodeGen(object):
 
33
    """Generate a bash script for given completion data."""
 
34
 
 
35
    def __init__(self, data, function_name='_bzr', debug=False):
 
36
        self.data = data
 
37
        self.function_name = function_name
 
38
        self.debug = debug
 
39
 
 
40
    def script(self):
 
41
        return ("""\
32
42
# Programmable completion for the Bazaar-NG bzr command under bash.
33
43
# Known to work with bash 2.05a as well as bash 4.1.2, and probably
34
44
# all versions in between as well.
43
53
# Commands and options of bzr %(bzr_version)s
44
54
 
45
55
shopt -s progcomp
46
 
"""
47
 
fun="""\
 
56
%(function)s
 
57
complete -F %(function_name)s -o default bzr
 
58
"""     % {
 
59
            "function_name": self.function_name,
 
60
            "function": self.function(),
 
61
            "version": __version__,
 
62
            "bzr_version": self.bzr_version(),
 
63
        })
 
64
 
 
65
    def function(self):
 
66
        return ("""\
48
67
%(function_name)s ()
49
68
{
50
69
        local cur cmds cmdIdx cmd cmdOpts fixedWords i globalOpts
120
139
 
121
140
        return 0
122
141
}
123
 
"""
124
 
tail="""\
125
 
complete -F %(function_name)s -o default bzr
126
 
"""
127
 
debug_output=r"""
 
142
"""     % {
 
143
            "cmds": self.command_names(),
 
144
            "function_name": self.function_name,
 
145
            "cases": self.command_cases(),
 
146
            "global_options": self.global_options(),
 
147
            "debug": self.debug_output(),
 
148
        })
 
149
 
 
150
    def command_names(self):
 
151
        return " ".join(self.data.all_command_aliases())
 
152
 
 
153
    def debug_output(self):
 
154
        if not self.debug:
 
155
            return ''
 
156
        else:
 
157
            return (r"""
128
158
        # Debugging code enabled using the --debug command line switch.
129
159
        # Will dump some variables to the top portion of the terminal.
130
160
        echo -ne '\e[s\e[H'
135
165
                echo "\$${i}=\"${!i}\""$'\e[K'
136
166
        done
137
167
        echo -ne '---\e[K\e[u'
138
 
"""
139
 
 
140
 
def wrap_container(list, parser):
141
 
    def tweaked_add_option(*opts, **attrs):
142
 
        list.extend(opts)
143
 
    parser.add_option = tweaked_add_option
144
 
    return parser
145
 
 
146
 
def wrap_parser(list, parser):
147
 
    orig_add_option_group = parser.add_option_group
148
 
    def tweaked_add_option_group(*opts, **attrs):
149
 
        return wrap_container(list, orig_add_option_group(*opts, **attrs))
150
 
    parser.add_option_group = tweaked_add_option_group
151
 
    return wrap_container(list, parser)
152
 
 
153
 
def bash_completion_function(out, function_name="_bzr", function_only=False,
154
 
                             debug=False,
155
 
                             no_plugins=False, selected_plugins=None):
156
 
    cmds = []
157
 
    cases = ""
158
 
    reqarg = {}
159
 
    plugins = set()
160
 
    if selected_plugins:
161
 
        selected_plugins = set([x.replace('-', '_') for x in selected_plugins])
162
 
    else:
163
 
        selected_plugins = None
164
 
 
165
 
    re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s')
166
 
    help_text = help_topics.topic_registry.get_detail('global-options')
167
 
    global_options = set()
168
 
    for long, short in re_switch.findall(help_text):
169
 
        global_options.add(long)
170
 
        if short:
171
 
            global_options.add(short)
172
 
    global_options = " ".join(sorted(global_options))
173
 
 
174
 
    user_aliases = {} # dict from cmd name to set of user-defined alias names
175
 
    for alias, expansion in config.GlobalConfig().get_aliases().iteritems():
176
 
        for token in commands.shlex_split_unicode(expansion):
177
 
            if not token.startswith("-"):
178
 
                user_aliases.setdefault(token, set()).add(alias)
179
 
                break
180
 
 
181
 
    all_cmds = sorted(commands.all_command_names())
182
 
    for cmdname in all_cmds:
183
 
        cmd = commands.get_cmd_object(cmdname)
 
168
""")
 
169
 
 
170
    def bzr_version(self):
 
171
        bzr_version = bzrlib.version_string
 
172
        if not self.data.plugins:
 
173
            bzr_version += "."
 
174
        else:
 
175
            bzr_version += " and the following plugins:"
 
176
            for name, plugin in sorted(self.data.plugins.iteritems()):
 
177
                bzr_version += "\n# %s" % plugin
 
178
 
 
179
    def global_options(self):
 
180
        return " ".join(sorted(self.data.global_options))
 
181
 
 
182
    def command_cases(self):
 
183
        cases = ""
 
184
        for command in self.data.commands:
 
185
            cases += self.command_case(command)
 
186
        return cases
 
187
 
 
188
    def command_case(self, command):
 
189
        case = "\t%s)\n" % "|".join(command.aliases)
 
190
        if command.plugin:
 
191
            case += "\t\t# plugin \"%s\"\n" % command.plugin
 
192
        options = []
 
193
        enums = []
 
194
        for option in command.options:
 
195
            for message in option.error_messages:
 
196
                case += "\t\t# %s\n" % message
 
197
            if option.registry_keys:
 
198
                for key in option.registry_keys:
 
199
                    options.append("%s=%s" % (option, key))
 
200
                enums.append("%s) optEnums='%s' ;;" %
 
201
                             (option, ' '.join(option.registry_keys)))
 
202
            else:
 
203
                options.append(str(option))
 
204
        case += "\t\tcmdOpts='%s'\n" % " ".join(options)
 
205
        if command.fixed_words:
 
206
            fixed_words = command.fixed_words
 
207
            if isinstance(fixed_words, list):
 
208
                fixed_words = "'%s'" + ' '.join(fixed_words)
 
209
            case += "\t\tfixedWords=%s\n" % fixed_words
 
210
        if enums:
 
211
            case += "\t\tcase $curOpt in\n\t\t\t"
 
212
            case += "\n\t\t\t".join(enums)
 
213
            case += "\n\t\tesac\n"
 
214
        case += "\t\t;;\n"
 
215
        return case
 
216
 
 
217
 
 
218
class CompletionData(object):
 
219
 
 
220
    def __init__(self):
 
221
        self.plugins = {}
 
222
        self.global_options = set()
 
223
        self.commands = []
 
224
 
 
225
    def all_command_aliases(self):
 
226
        for c in self.commands:
 
227
            for a in c.aliases:
 
228
                yield a
 
229
 
 
230
 
 
231
class CommandData(object):
 
232
 
 
233
    def __init__(self, name):
 
234
        self.name = name
 
235
        self.aliases = [name]
 
236
        self.plugin = None
 
237
        self.options = []
 
238
        self.fixed_words = None
 
239
 
 
240
 
 
241
class PluginData(object):
 
242
 
 
243
    def __init__(self, name, version=None):
 
244
        if version is None:
 
245
            version = bzrlib.plugin.plugins()[name].__version__
 
246
        self.name = name
 
247
        self.version = version
 
248
 
 
249
    def __str__(self):
 
250
        if self.version == 'unknown':
 
251
            return self.name
 
252
        return '%s %s' % (self.name, self.version)
 
253
 
 
254
 
 
255
class OptionData(object):
 
256
 
 
257
    def __init__(self, name):
 
258
        self.name = name
 
259
        self.registry_keys = None
 
260
        self.error_messages = []
 
261
 
 
262
    def __str__(self):
 
263
        return self.name
 
264
 
 
265
    def __cmp__(self, other):
 
266
        return cmp(self.name, other.name)
 
267
 
 
268
 
 
269
class DataCollector(object):
 
270
 
 
271
    def __init__(self, no_plugins=False, selected_plugins=None):
 
272
        self.data = CompletionData()
 
273
        self.user_aliases = {}
 
274
        if no_plugins:
 
275
            self.selected_plugins = set()
 
276
        elif selected_plugins is None:
 
277
            self.selected_plugins = None
 
278
        else:
 
279
            self.selected_plugins = set([x.replace('-', '_')
 
280
                                         for x in selected_plugins])
 
281
 
 
282
    def collect(self):
 
283
        self.global_options()
 
284
        self.aliases()
 
285
        self.commands()
 
286
        return self.data
 
287
 
 
288
    def global_options(self):
 
289
        re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s')
 
290
        help_text = help_topics.topic_registry.get_detail('global-options')
 
291
        for long, short in re_switch.findall(help_text):
 
292
            self.data.global_options.add(long)
 
293
            if short:
 
294
                self.data.global_options.add(short)
 
295
 
 
296
    def aliases(self):
 
297
        for alias, expansion in config.GlobalConfig().get_aliases().iteritems():
 
298
            for token in commands.shlex_split_unicode(expansion):
 
299
                if not token.startswith("-"):
 
300
                    self.user_aliases.setdefault(token, set()).add(alias)
 
301
                    break
 
302
 
 
303
    def commands(self):
 
304
        for name in sorted(commands.all_command_names()):
 
305
            self.command(name)
 
306
 
 
307
    def command(self, name):
 
308
        cmd = commands.get_cmd_object(name)
 
309
        cmd_data = CommandData(name)
 
310
 
 
311
        plugin_name = cmd.plugin_name()
 
312
        if plugin_name is not None:
 
313
            if (self.selected_plugins is not None and
 
314
                plugin not in self.selected_plugins):
 
315
                return
 
316
            plugin_data = self.data.plugins.get(plugin_name)
 
317
            if plugin_data is None:
 
318
                plugin_data = PluginData(plugin_name)
 
319
                self.data.plugins[plugin_name] = plugin_data
 
320
            cmd_data.plugin = plugin_data
 
321
        self.data.commands.append(cmd_data)
184
322
 
185
323
        # Find all aliases to the command; both cmd-defined and user-defined.
186
324
        # We assume a user won't override one command with a different one,
187
325
        # but will choose completely new names or add options to existing
188
326
        # ones while maintaining the actual command name unchanged.
189
 
        aliases = [cmdname]
190
 
        aliases.extend(cmd.aliases)
191
 
        aliases.extend(sorted([alias
192
 
                               for name in aliases
193
 
                               if name in user_aliases
194
 
                               for alias in user_aliases[name]
195
 
                               if alias not in aliases]))
196
 
        cases += "\t%s)\n" % "|".join(aliases)
197
 
        cmds.extend(aliases)
198
 
        plugin = cmd.plugin_name()
199
 
        if plugin is not None:
200
 
            if selected_plugins is not None and plugin not in selected_plugins:
201
 
                continue
202
 
            plugins.add(plugin)
203
 
            cases += "\t\t# plugin \"%s\"\n" % plugin
 
327
        cmd_data.aliases.extend(cmd.aliases)
 
328
        cmd_data.aliases.extend(sorted([useralias
 
329
            for cmdalias in cmd_data.aliases
 
330
            if cmdalias in self.user_aliases
 
331
            for useralias in self.user_aliases[cmdalias]
 
332
            if useralias not in cmd_data.aliases]))
 
333
 
204
334
        opts = cmd.options()
205
 
        switches = []
206
 
        enums = []
207
 
        fixedWords = None
208
 
        for optname in sorted(cmd.options()):
209
 
            opt = opts[optname]
210
 
            optswitches = []
211
 
            parser = option.get_optparser({optname: opt})
212
 
            parser = wrap_parser(optswitches, parser)
213
 
            optswitches[:] = []
214
 
            opt.add_option(parser, opt.short_name())
215
 
            if isinstance(opt, option.RegistryOption) and opt.enum_switch:
216
 
                enum_switch = '--%s' % optname
 
335
        for optname, opt in sorted(opts.iteritems()):
 
336
            cmd_data.options.extend(self.option(opt))
 
337
 
 
338
        if 'help' == name or 'help' in cmd.aliases:
 
339
            cmd_data.fixed_words = ('"$cmds %s"' %
 
340
                " ".join(sorted(help_topics.topic_registry.keys())))
 
341
 
 
342
    def option(self, opt):
 
343
        optswitches = {}
 
344
        parser = option.get_optparser({opt.name: opt})
 
345
        parser = self.wrap_parser(optswitches, parser)
 
346
        optswitches.clear()
 
347
        opt.add_option(parser, opt.short_name())
 
348
        if isinstance(opt, option.RegistryOption) and opt.enum_switch:
 
349
            enum_switch = '--%s' % opt.name
 
350
            enum_data = optswitches.get(enum_switch)
 
351
            if enum_data:
217
352
                try:
218
 
                    keys = opt.registry.keys()
 
353
                    enum_data.registry_keys = opt.registry.keys()
219
354
                except ImportError, e:
220
 
                    cases += ("\t\t# ERROR getting registry keys for '--%s':"
221
 
                              + " %s\n") % (optname, str(e).split('\n')[0])
222
 
                else:
223
 
                    if enum_switch in optswitches and keys:
224
 
                        optswitches.remove(enum_switch)
225
 
                        for key in keys:
226
 
                            optswitches.append('%s=%s' % (enum_switch, key))
227
 
                            enums.append("%s) optEnums='%s' ;;"
228
 
                                         % (enum_switch, ' '.join(keys)))
229
 
            switches.extend(optswitches)
230
 
        if 'help' == cmdname or 'help' in cmd.aliases:
231
 
            fixedWords = " ".join(sorted(help_topics.topic_registry.keys()))
232
 
            fixedWords = '"$cmds %s"' % fixedWords
233
 
 
234
 
        cases += "\t\tcmdOpts='" + " ".join(switches) + "'\n"
235
 
        if fixedWords:
236
 
            if isinstance(fixedWords, list):
237
 
                fixedWords = "'" + join(fixedWords) + "'"
238
 
            cases += "\t\tfixedWords=" + fixedWords + "\n"
239
 
        if enums:
240
 
            cases += "\t\tcase $curOpt in\n\t\t\t"
241
 
            cases += "\n\t\t\t".join(enums)
242
 
            cases += "\n\t\tesac\n"
243
 
        cases += "\t\t;;\n"
244
 
 
245
 
    bzr_version = bzrlib.version_string
246
 
    if not plugins:
247
 
        bzr_version += "."
248
 
    else:
249
 
        bzr_version += " and the following plugins:"
250
 
        for plugin in sorted(plugins):
251
 
            pv = bzrlib.plugin.plugins()[plugin].__version__
252
 
            if pv == 'unknown':
253
 
                pv = ''
254
 
            else:
255
 
                pv = ' ' + pv
256
 
                bzr_version += "\n# %s%s" % (plugin, pv)
257
 
 
 
355
                    enum_data.error_messages.append(
 
356
                        "ERROR getting registry keys for '--%s': %s"
 
357
                        % (opt.name, str(e).split('\n')[0]))
 
358
        return sorted(optswitches.values())
 
359
 
 
360
    def wrap_container(self, optswitches, parser):
 
361
        def tweaked_add_option(*opts, **attrs):
 
362
            for name in opts:
 
363
                optswitches[name] = OptionData(name)
 
364
        parser.add_option = tweaked_add_option
 
365
        return parser
 
366
 
 
367
    def wrap_parser(self, optswitches, parser):
 
368
        orig_add_option_group = parser.add_option_group
 
369
        def tweaked_add_option_group(*opts, **attrs):
 
370
            return self.wrap_container(optswitches,
 
371
                orig_add_option_group(*opts, **attrs))
 
372
        parser.add_option_group = tweaked_add_option_group
 
373
        return self.wrap_container(optswitches, parser)
 
374
 
 
375
 
 
376
def bash_completion_function(out, function_name="_bzr", function_only=False,
 
377
                             debug=False,
 
378
                             no_plugins=False, selected_plugins=None):
 
379
    dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
 
380
    data = dc.collect()
 
381
    cg = BashCodeGen(data, function_name=function_name, debug=debug)
258
382
    if function_only:
259
 
        template = fun
260
 
    else:
261
 
        template = head + fun + tail
262
 
    if debug:
263
 
        perhaps_debug_output = debug_output
264
 
    else:
265
 
        perhaps_debug_output = ''
266
 
    out.write(template % {"cmds": " ".join(cmds),
267
 
                          "cases": cases,
268
 
                          "function_name": function_name,
269
 
                          "version": __version__,
270
 
                          "global_options": global_options,
271
 
                          "debug": perhaps_debug_output,
272
 
                          "bzr_version": bzr_version,
273
 
                          })
 
383
        res = cg.function()
 
384
    else:
 
385
        res = cg.script()
 
386
    out.write(res)
 
387
 
274
388
 
275
389
if __name__ == '__main__':
276
390