~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shell.py

  • Committer: Aaron Bentley
  • Date: 2012-03-20 02:40:57 UTC
  • Revision ID: aaron@aaronbentley.com-20120320024057-mqltf93xxs09r0ry
Compatibility fixes for bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005 Aaron Bentley
 
2
# <aaron@aaronbentley.com>
 
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
 
17
 
 
18
import cmd
 
19
from itertools import chain
 
20
import os
 
21
try:
 
22
    import readline
 
23
except ImportError:
 
24
    _has_readline = False
 
25
else:
 
26
    _has_readline = True
 
27
import shlex
 
28
import stat
 
29
import string
 
30
import sys
 
31
 
 
32
from bzrlib import osutils, trace
 
33
from bzrlib.branch import Branch
 
34
from bzrlib.config import config_dir, ensure_config_dir_exists
 
35
from bzrlib.commands import get_cmd_object, all_command_names, get_alias
 
36
from bzrlib.errors import BzrError
 
37
from bzrlib.workingtree import WorkingTree
 
38
 
 
39
import terminal
 
40
 
 
41
 
 
42
SHELL_BLACKLIST = set(['rm', 'ls'])
 
43
COMPLETION_BLACKLIST = set(['shell'])
 
44
 
 
45
 
 
46
class BlackListedCommand(BzrError):
 
47
    def __init__(self, command):
 
48
        BzrError.__init__(self, "The command %s is blacklisted for shell use" %
 
49
                          command)
 
50
 
 
51
 
 
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):
 
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:
 
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)
 
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)
 
89
                completions.extend(filter_completions(iter, self.text))
 
90
            return completions
 
91
 
 
92
 
 
93
class PromptCmd(cmd.Cmd):
 
94
 
 
95
    def __init__(self):
 
96
        cmd.Cmd.__init__(self)
 
97
        self.prompt = "bzr> "
 
98
        try:
 
99
            self.tree = WorkingTree.open_containing('.')[0]
 
100
        except:
 
101
            self.tree = None
 
102
        self.set_title()
 
103
        self.set_prompt()
 
104
        self.identchars += '-'
 
105
        ensure_config_dir_exists()
 
106
        self.history_file = osutils.pathjoin(config_dir(), 'shell-history')
 
107
        whitespace = ''.join(c for c in string.whitespace if c < chr(127))
 
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)
 
113
        self.cwd = os.getcwd()
 
114
 
 
115
    def write_history(self):
 
116
        if _has_readline:
 
117
            readline.write_history_file(self.history_file)
 
118
 
 
119
    def do_quit(self, args):
 
120
        self.write_history()
 
121
        raise StopIteration
 
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):
 
135
        if self.tree is not None:
 
136
            try:
 
137
                prompt_data = (self.tree.branch.nick, self.tree.branch.revno(),
 
138
                               self.tree.relpath('.'))
 
139
                prompt = " %s:%d/%s" % prompt_data
 
140
            except:
 
141
                prompt = ""
 
142
        else:
 
143
            prompt = ""
 
144
        self.prompt = "bzr%s> " % prompt
 
145
 
 
146
    def set_title(self, command=None):
 
147
        try:
 
148
            b = Branch.open_containing('.')[0]
 
149
            version = "%s:%d" % (b.nick, b.revno())
 
150
        except:
 
151
            version = "[no version]"
 
152
        if command is None:
 
153
            command = ""
 
154
        sys.stdout.write(terminal.term_title("bzr %s %s" % (command, version)))
 
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:
 
171
            self.tree = WorkingTree.open_containing(".")[0]
 
172
        except:
 
173
            self.tree = None
 
174
 
 
175
    def do_help(self, line):
 
176
        self.default("help "+line)
 
177
 
 
178
    def default(self, line):
 
179
        try:
 
180
            args = shlex.split(line)
 
181
        except ValueError, e:
 
182
            print 'Parse error:', e
 
183
            return
 
184
 
 
185
        alias_args = get_alias(args[0])
 
186
        if alias_args is not None:
 
187
            args[0] = alias_args.pop(0)
 
188
 
 
189
        commandname = args.pop(0)
 
190
        for char in ('|', '<', '>'):
 
191
            commandname = commandname.split(char)[0]
 
192
        if commandname[-1] in ('|', '<', '>'):
 
193
            commandname = commandname[:-1]
 
194
        try:
 
195
            if commandname in SHELL_BLACKLIST:
 
196
                raise BlackListedCommand(commandname)
 
197
            cmd_obj = get_cmd_object(commandname)
 
198
        except (BlackListedCommand, BzrError):
 
199
            return os.system(line)
 
200
 
 
201
        try:
 
202
            is_qbzr = cmd_obj.__module__.startswith('bzrlib.plugins.qbzr.')
 
203
            if too_complicated(line) or is_qbzr:
 
204
                return os.system("bzr "+line)
 
205
            else:
 
206
                return (cmd_obj.run_argv_aliases(args, alias_args) or 0)
 
207
        except BzrError, e:
 
208
            trace.log_exception_quietly()
 
209
            print e
 
210
        except KeyboardInterrupt, e:
 
211
            print "Interrupted"
 
212
        except Exception, e:
 
213
            trace.log_exception_quietly()
 
214
            print "Unhandled error:\n%s" % (e)
 
215
 
 
216
 
 
217
    def completenames(self, text, line, begidx, endidx):
 
218
        return CompletionContext(text).get_completions()
 
219
 
 
220
    def completedefault(self, text, line, begidx, endidx):
 
221
        """Perform completion for native commands.
 
222
 
 
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
        """
 
232
        (cmd, args, foo) = self.parseline(line)
 
233
        if cmd == "bzr":
 
234
            cmd = None
 
235
        return CompletionContext(text, command=cmd).get_completions()
 
236
 
 
237
 
 
238
def run_shell(directory=None):
 
239
    try:
 
240
        if not directory is None:
 
241
            os.chdir(directory)
 
242
        prompt = PromptCmd()
 
243
        while True:
 
244
            try:
 
245
                try:
 
246
                    prompt.cmdloop()
 
247
                except KeyboardInterrupt:
 
248
                    print
 
249
            finally:
 
250
                prompt.write_history()
 
251
    except StopIteration:
 
252
        pass
 
253
 
 
254
 
 
255
def iter_opt_completions(command_obj):
 
256
    for option_name, option in command_obj.options().items():
 
257
        yield "--" + option_name
 
258
        short_name = option.short_name()
 
259
        if short_name:
 
260
            yield "-" + short_name
 
261
 
 
262
 
 
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
 
281
    for file in chain(os.listdir(listingdir), extras):
 
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:
 
291
                yield userfile + ' '
 
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)
 
301
 
 
302
 
 
303
def iter_command_names(hidden=False):
 
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:
 
307
            continue
 
308
        for name in [real_cmd_name] + cmd_obj.aliases:
 
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
 
312
 
 
313
 
 
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
 
 
325
def filter_completions(iter, arg):
 
326
    return (c for c in iter if c.startswith(arg))
 
327
 
 
328
 
 
329
def iter_munged_completions(iter, arg, text):
 
330
    for completion in iter:
 
331
        completion = str(completion)
 
332
        if completion.startswith(arg):
 
333
            yield completion[len(arg)-len(text):]+" "
 
334
 
 
335
 
 
336
def too_complicated(line):
 
337
    for char in '|<>*?':
 
338
        if char in line:
 
339
            return True
 
340
    return False