~abentley/bzrtools/bzrtools.dev

249 by Aaron Bentley
Got the shell basics working properly
1
# Copyright (C) 2004, 2005 Aaron Bentley
612 by Aaron Bentley
Update email address
2
# <aaron@aaronbentley.com>
249 by Aaron Bentley
Got the shell basics working properly
3
#
4
#    This program is free software; you can redistribute it and/or modify
5
#    it under the terms of the GNU General Public License as published by
6
#    the Free Software Foundation; either version 2 of the License, or
7
#    (at your option) any later version.
8
#
9
#    This program is distributed in the hope that it will be useful,
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
#    GNU General Public License for more details.
13
#
14
#    You should have received a copy of the GNU General Public License
15
#    along with this program; if not, write to the Free Software
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
402 by Aaron Bentley
Clean up style
17
249 by Aaron Bentley
Got the shell basics working properly
18
import cmd
402 by Aaron Bentley
Clean up style
19
from itertools import chain
249 by Aaron Bentley
Got the shell basics working properly
20
import os
755.2.1 by Martin
Make shell readline features conditional on import succeeding
21
try:
22
    import readline
23
except ImportError:
24
    _has_readline = False
25
else:
26
    _has_readline = True
755.2.4 by Martin
Address review comments from Aaron Bentley
27
import shlex
28
import stat
29
import string
30
import sys
755.2.1 by Martin
Make shell readline features conditional on import succeeding
31
755.1.1 by Martin
Log tracebacks for errors during shell session
32
from bzrlib import osutils, trace
402 by Aaron Bentley
Clean up style
33
from bzrlib.branch import Branch
608 by Aaron Bentley
Shell now ensures the config directory works, and uses the bzrlib to find it
34
from bzrlib.config import config_dir, ensure_config_dir_exists
729 by Aaron Bentley
Fix deprecation warnings completing command names.
35
from bzrlib.commands import get_cmd_object, all_command_names, get_alias
249 by Aaron Bentley
Got the shell basics working properly
36
from bzrlib.errors import BzrError
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
37
from bzrlib.workingtree import WorkingTree
402 by Aaron Bentley
Clean up style
38
39
import terminal
40
249 by Aaron Bentley
Got the shell basics working properly
41
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
42
SHELL_BLACKLIST = set(['rm', 'ls'])
257 by Aaron Bentley
blacklisted 'shell' from completion
43
COMPLETION_BLACKLIST = set(['shell'])
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
44
402 by Aaron Bentley
Clean up style
45
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
46
class BlackListedCommand(BzrError):
47
    def __init__(self, command):
48
        BzrError.__init__(self, "The command %s is blacklisted for shell use" %
49
                          command)
50
402 by Aaron Bentley
Clean up style
51
262 by Aaron Bentley
Release 0.6
52
class CompletionContext(object):
53
    def __init__(self, text, command=None, prev_opt=None, arg_pos=None):
54
        self.text = text
55
        self.command = command
56
        self.prev_opt = prev_opt
57
        self.arg_pos = None
58
59
    def get_completions(self):
283.1.1 by Aaron Bentley
Got completions working properly
60
        try:
61
            return self.get_completions_or_raise()
62
        except Exception, e:
63
            print e, type(e)
64
            return []
65
66
    def get_option_completions(self):
67
        try:
68
            command_obj = get_cmd_object(self.command)
69
        except BzrError:
70
            return []
71
        opts = [o+" " for o in iter_opt_completions(command_obj)]
72
        return list(filter_completions(opts, self.text))
73
74
    def get_completions_or_raise(self):
75
        if self.command is None:
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
76
            if '/' in self.text:
77
                iter = iter_executables(self.text)
78
            else:
79
                iter = (c+" " for c in iter_command_names() if
80
                        c not in COMPLETION_BLACKLIST)
283.1.1 by Aaron Bentley
Got completions working properly
81
            return list(filter_completions(iter, self.text))
82
        if self.prev_opt is None:
83
            completions = self.get_option_completions()
84
            if self.command == "cd":
85
                iter = iter_dir_completions(self.text)
86
                completions.extend(list(filter_completions(iter, self.text)))
87
            else:
88
                iter = iter_file_completions(self.text)
419 by Aaron Bentley
Nicer directory completion for bzr shell
89
                completions.extend(filter_completions(iter, self.text))
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
90
            return completions
262 by Aaron Bentley
Release 0.6
91
92
248 by Aaron Bentley
Initial import of Fai shell command
93
class PromptCmd(cmd.Cmd):
608 by Aaron Bentley
Shell now ensures the config directory works, and uses the bzrlib to find it
94
248 by Aaron Bentley
Initial import of Fai shell command
95
    def __init__(self):
96
        cmd.Cmd.__init__(self)
249 by Aaron Bentley
Got the shell basics working properly
97
        self.prompt = "bzr> "
248 by Aaron Bentley
Initial import of Fai shell command
98
        try:
313 by Aaron Bentley
Updated to match API changes
99
            self.tree = WorkingTree.open_containing('.')[0]
248 by Aaron Bentley
Initial import of Fai shell command
100
        except:
313 by Aaron Bentley
Updated to match API changes
101
            self.tree = None
248 by Aaron Bentley
Initial import of Fai shell command
102
        self.set_title()
103
        self.set_prompt()
104
        self.identchars += '-'
608 by Aaron Bentley
Shell now ensures the config directory works, and uses the bzrlib to find it
105
        ensure_config_dir_exists()
106
        self.history_file = osutils.pathjoin(config_dir(), 'shell-history')
723.1.1 by John Arbash Meinel
Possible fix for bug #431241
107
        whitespace = ''.join(c for c in string.whitespace if c < chr(127))
755.2.1 by Martin
Make shell readline features conditional on import succeeding
108
        if _has_readline:
109
            readline.set_completer_delims(whitespace)
110
            if os.access(self.history_file, os.R_OK) and \
111
                os.path.isfile(self.history_file):
112
                readline.read_history_file(self.history_file)
248 by Aaron Bentley
Initial import of Fai shell command
113
        self.cwd = os.getcwd()
114
115
    def write_history(self):
755.2.1 by Martin
Make shell readline features conditional on import succeeding
116
        if _has_readline:
117
            readline.write_history_file(self.history_file)
248 by Aaron Bentley
Initial import of Fai shell command
118
119
    def do_quit(self, args):
120
        self.write_history()
255 by Aaron Bentley
Fixed system.exit printing 0 bug
121
        raise StopIteration
248 by Aaron Bentley
Initial import of Fai shell command
122
123
    def do_exit(self, args):
124
        self.do_quit(args)
125
126
    def do_EOF(self, args):
127
        print
128
        self.do_quit(args)
129
130
    def postcmd(self, line, bar):
131
        self.set_title()
132
        self.set_prompt()
133
134
    def set_prompt(self):
313 by Aaron Bentley
Updated to match API changes
135
        if self.tree is not None:
248 by Aaron Bentley
Initial import of Fai shell command
136
            try:
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
137
                prompt_data = (self.tree.branch.nick, self.tree.branch.revno(),
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
138
                               self.tree.relpath('.'))
283.1.2 by Aaron Bentley
Got prompt and title working
139
                prompt = " %s:%d/%s" % prompt_data
248 by Aaron Bentley
Initial import of Fai shell command
140
            except:
141
                prompt = ""
142
        else:
143
            prompt = ""
249 by Aaron Bentley
Got the shell basics working properly
144
        self.prompt = "bzr%s> " % prompt
248 by Aaron Bentley
Initial import of Fai shell command
145
146
    def set_title(self, command=None):
147
        try:
283.1.2 by Aaron Bentley
Got prompt and title working
148
            b = Branch.open_containing('.')[0]
149
            version = "%s:%d" % (b.nick, b.revno())
248 by Aaron Bentley
Initial import of Fai shell command
150
        except:
151
            version = "[no version]"
152
        if command is None:
153
            command = ""
249 by Aaron Bentley
Got the shell basics working properly
154
        sys.stdout.write(terminal.term_title("bzr %s %s" % (command, version)))
248 by Aaron Bentley
Initial import of Fai shell command
155
156
    def do_cd(self, line):
157
        if line == "":
158
            line = "~"
159
        line = os.path.expanduser(line)
160
        if os.path.isabs(line):
161
            newcwd = line
162
        else:
163
            newcwd = self.cwd+'/'+line
164
        newcwd = os.path.normpath(newcwd)
165
        try:
166
            os.chdir(newcwd)
167
            self.cwd = newcwd
168
        except Exception, e:
169
            print e
170
        try:
313 by Aaron Bentley
Updated to match API changes
171
            self.tree = WorkingTree.open_containing(".")[0]
248 by Aaron Bentley
Initial import of Fai shell command
172
        except:
313 by Aaron Bentley
Updated to match API changes
173
            self.tree = None
248 by Aaron Bentley
Initial import of Fai shell command
174
175
    def do_help(self, line):
251 by Aaron Bentley
Got support for option completion
176
        self.default("help "+line)
248 by Aaron Bentley
Initial import of Fai shell command
177
178
    def default(self, line):
723.3.1 by Benoît Pierre
shell improvement: catch shlex.split ValueError exceptions
179
        try:
180
            args = shlex.split(line)
181
        except ValueError, e:
182
            print 'Parse error:', e
183
            return
184
325.1.1 by Aaron Bentley
Handle aliases in bzr shell
185
        alias_args = get_alias(args[0])
186
        if alias_args is not None:
187
            args[0] = alias_args.pop(0)
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
188
249 by Aaron Bentley
Got the shell basics working properly
189
        commandname = args.pop(0)
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
190
        for char in ('|', '<', '>'):
191
            commandname = commandname.split(char)[0]
192
        if commandname[-1] in ('|', '<', '>'):
193
            commandname = commandname[:-1]
249 by Aaron Bentley
Got the shell basics working properly
194
        try:
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
195
            if commandname in SHELL_BLACKLIST:
196
                raise BlackListedCommand(commandname)
249 by Aaron Bentley
Got the shell basics working properly
197
            cmd_obj = get_cmd_object(commandname)
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
198
        except (BlackListedCommand, BzrError):
249 by Aaron Bentley
Got the shell basics working properly
199
            return os.system(line)
200
201
        try:
730.1.4 by Aaron Bentley
Handle more qbzr commands.
202
            is_qbzr = cmd_obj.__module__.startswith('bzrlib.plugins.qbzr.')
730.1.3 by Aaron Bentley
Use a subprocess for qbzr commands.
203
            if too_complicated(line) or is_qbzr:
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
204
                return os.system("bzr "+line)
205
            else:
325.1.1 by Aaron Bentley
Handle aliases in bzr shell
206
                return (cmd_obj.run_argv_aliases(args, alias_args) or 0)
249 by Aaron Bentley
Got the shell basics working properly
207
        except BzrError, e:
755.1.1 by Martin
Log tracebacks for errors during shell session
208
            trace.log_exception_quietly()
249 by Aaron Bentley
Got the shell basics working properly
209
            print e
210
        except KeyboardInterrupt, e:
211
            print "Interrupted"
212
        except Exception, e:
755.1.1 by Martin
Log tracebacks for errors during shell session
213
            trace.log_exception_quietly()
249 by Aaron Bentley
Got the shell basics working properly
214
            print "Unhandled error:\n%s" % (e)
215
248 by Aaron Bentley
Initial import of Fai shell command
216
217
    def completenames(self, text, line, begidx, endidx):
283.1.1 by Aaron Bentley
Got completions working properly
218
        return CompletionContext(text).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
219
220
    def completedefault(self, text, line, begidx, endidx):
221
        """Perform completion for native commands.
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
222
248 by Aaron Bentley
Initial import of Fai shell command
223
        :param text: The text to complete
224
        :type text: str
225
        :param line: The entire line to complete
226
        :type line: str
227
        :param begidx: The start of the text in the line
228
        :type begidx: int
229
        :param endidx: The end of the text in the line
230
        :type endidx: int
231
        """
251 by Aaron Bentley
Got support for option completion
232
        (cmd, args, foo) = self.parseline(line)
256 by Aaron Bentley
Enhanced shell completion
233
        if cmd == "bzr":
283.1.1 by Aaron Bentley
Got completions working properly
234
            cmd = None
235
        return CompletionContext(text, command=cmd).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
236
402 by Aaron Bentley
Clean up style
237
736.1.1 by Gordon Tyler
Added --directory option to shell command.
238
def run_shell(directory=None):
248 by Aaron Bentley
Initial import of Fai shell command
239
    try:
736.1.1 by Gordon Tyler
Added --directory option to shell command.
240
        if not directory is None:
241
            os.chdir(directory)
255 by Aaron Bentley
Fixed system.exit printing 0 bug
242
        prompt = PromptCmd()
723.2.1 by Benoît Pierre
shell improvement: handle KeyboardInterrupt like a regular shell
243
        while True:
244
            try:
245
                try:
246
                    prompt.cmdloop()
247
                except KeyboardInterrupt:
248
                    print
249
            finally:
250
                prompt.write_history()
255 by Aaron Bentley
Fixed system.exit printing 0 bug
251
    except StopIteration:
252
        pass
248 by Aaron Bentley
Initial import of Fai shell command
253
402 by Aaron Bentley
Clean up style
254
267 by Aaron Bentley
Added file completion when completing subcommands.
255
def iter_opt_completions(command_obj):
256
    for option_name, option in command_obj.options().items():
257
        yield "--" + option_name
495 by Aaron Bentley
Restore short_name stuff to match API
258
        short_name = option.short_name()
267 by Aaron Bentley
Added file completion when completing subcommands.
259
        if short_name:
260
            yield "-" + short_name
261
402 by Aaron Bentley
Clean up style
262
248 by Aaron Bentley
Initial import of Fai shell command
263
def iter_file_completions(arg, only_dirs = False):
264
    """Generate an iterator that iterates through filename completions.
265
266
    :param arg: The filename fragment to match
267
    :type arg: str
268
    :param only_dirs: If true, match only directories
269
    :type only_dirs: bool
270
    """
271
    cwd = os.getcwd()
272
    if cwd != "/":
273
        extras = [".", ".."]
274
    else:
275
        extras = []
276
    (dir, file) = os.path.split(arg)
277
    if dir != "":
278
        listingdir = os.path.expanduser(dir)
279
    else:
280
        listingdir = cwd
252 by Aaron Bentley
Fixed dirctory completion in shell
281
    for file in chain(os.listdir(listingdir), extras):
248 by Aaron Bentley
Initial import of Fai shell command
282
        if dir != "":
283
            userfile = dir+'/'+file
284
        else:
285
            userfile = file
286
        if userfile.startswith(arg):
287
            if os.path.isdir(listingdir+'/'+file):
288
                userfile+='/'
289
                yield userfile
290
            elif not only_dirs:
419 by Aaron Bentley
Nicer directory completion for bzr shell
291
                yield userfile + ' '
248 by Aaron Bentley
Initial import of Fai shell command
292
293
294
def iter_dir_completions(arg):
295
    """Generate an iterator that iterates through directory name completions.
296
297
    :param arg: The directory name fragment to match
298
    :type arg: str
299
    """
300
    return iter_file_completions(arg, True)
250 by Aaron Bentley
Got command completion working
301
402 by Aaron Bentley
Clean up style
302
250 by Aaron Bentley
Got command completion working
303
def iter_command_names(hidden=False):
729 by Aaron Bentley
Fix deprecation warnings completing command names.
304
    for real_cmd_name in all_command_names():
305
        cmd_obj = get_cmd_object(real_cmd_name)
306
        if not hidden and cmd_obj.hidden:
250 by Aaron Bentley
Got command completion working
307
            continue
729 by Aaron Bentley
Fix deprecation warnings completing command names.
308
        for name in [real_cmd_name] + cmd_obj.aliases:
256 by Aaron Bentley
Enhanced shell completion
309
            # Don't complete on aliases that are prefixes of the canonical name
310
            if name == real_cmd_name or not real_cmd_name.startswith(name):
311
                yield name
250 by Aaron Bentley
Got command completion working
312
402 by Aaron Bentley
Clean up style
313
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
314
def iter_executables(path):
315
    dirname, partial = os.path.split(path)
316
    for filename in os.listdir(dirname):
317
        if not filename.startswith(partial):
318
            continue
319
        fullpath = os.path.join(dirname, filename)
320
        mode=os.lstat(fullpath)[stat.ST_MODE]
321
        if stat.S_ISREG(mode) and 0111 & mode:
322
            yield fullpath + ' '
323
324
267.1.1 by Aaron Bentley
Improved completion in the middle of lines
325
def filter_completions(iter, arg):
326
    return (c for c in iter if c.startswith(arg))
327
402 by Aaron Bentley
Clean up style
328
250 by Aaron Bentley
Got command completion working
329
def iter_munged_completions(iter, arg, text):
330
    for completion in iter:
331
        completion = str(completion)
332
        if completion.startswith(arg):
256 by Aaron Bentley
Enhanced shell completion
333
            yield completion[len(arg)-len(text):]+" "
250 by Aaron Bentley
Got command completion working
334
402 by Aaron Bentley
Clean up style
335
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
336
def too_complicated(line):
403 by Aaron Bentley
Handle quoted strings without bailing to a subshell
337
    for char in '|<>*?':
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
338
        if char in line:
339
            return True
340
    return False