~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shell.py

  • Committer: Aaron Bentley
  • Date: 2011-04-15 02:33:43 UTC
  • mfrom: (749.2.3 2.3)
  • Revision ID: aaron@aaronbentley.com-20110415023343-6xsksva3m7muhikp
Merged 2.3 into bzrtools.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
import readline
 
22
import shlex
 
23
import stat
 
24
import string
 
25
import sys
 
26
 
 
27
from bzrlib import osutils, trace
 
28
from bzrlib.branch import Branch
 
29
from bzrlib.config import config_dir, ensure_config_dir_exists
 
30
from bzrlib.commands import get_cmd_object, all_command_names, get_alias
 
31
from bzrlib.errors import BzrError
 
32
from bzrlib.workingtree import WorkingTree
 
33
 
 
34
import terminal
 
35
 
 
36
 
 
37
SHELL_BLACKLIST = set(['rm', 'ls'])
 
38
COMPLETION_BLACKLIST = set(['shell'])
 
39
 
 
40
 
 
41
class BlackListedCommand(BzrError):
 
42
    def __init__(self, command):
 
43
        BzrError.__init__(self, "The command %s is blacklisted for shell use" %
 
44
                          command)
 
45
 
 
46
 
 
47
class CompletionContext(object):
 
48
    def __init__(self, text, command=None, prev_opt=None, arg_pos=None):
 
49
        self.text = text
 
50
        self.command = command
 
51
        self.prev_opt = prev_opt
 
52
        self.arg_pos = None
 
53
 
 
54
    def get_completions(self):
 
55
        try:
 
56
            return self.get_completions_or_raise()
 
57
        except Exception, e:
 
58
            print e, type(e)
 
59
            return []
 
60
 
 
61
    def get_option_completions(self):
 
62
        try:
 
63
            command_obj = get_cmd_object(self.command)
 
64
        except BzrError:
 
65
            return []
 
66
        opts = [o+" " for o in iter_opt_completions(command_obj)]
 
67
        return list(filter_completions(opts, self.text))
 
68
 
 
69
    def get_completions_or_raise(self):
 
70
        if self.command is None:
 
71
            if '/' in self.text:
 
72
                iter = iter_executables(self.text)
 
73
            else:
 
74
                iter = (c+" " for c in iter_command_names() if
 
75
                        c not in COMPLETION_BLACKLIST)
 
76
            return list(filter_completions(iter, self.text))
 
77
        if self.prev_opt is None:
 
78
            completions = self.get_option_completions()
 
79
            if self.command == "cd":
 
80
                iter = iter_dir_completions(self.text)
 
81
                completions.extend(list(filter_completions(iter, self.text)))
 
82
            else:
 
83
                iter = iter_file_completions(self.text)
 
84
                completions.extend(filter_completions(iter, self.text))
 
85
            return completions
 
86
 
 
87
 
 
88
class PromptCmd(cmd.Cmd):
 
89
 
 
90
    def __init__(self):
 
91
        cmd.Cmd.__init__(self)
 
92
        self.prompt = "bzr> "
 
93
        try:
 
94
            self.tree = WorkingTree.open_containing('.')[0]
 
95
        except:
 
96
            self.tree = None
 
97
        self.set_title()
 
98
        self.set_prompt()
 
99
        self.identchars += '-'
 
100
        ensure_config_dir_exists()
 
101
        self.history_file = osutils.pathjoin(config_dir(), 'shell-history')
 
102
        whitespace = ''.join(c for c in string.whitespace if c < chr(127))
 
103
        readline.set_completer_delims(whitespace)
 
104
        if os.access(self.history_file, os.R_OK) and \
 
105
            os.path.isfile(self.history_file):
 
106
            readline.read_history_file(self.history_file)
 
107
        self.cwd = os.getcwd()
 
108
 
 
109
    def write_history(self):
 
110
        readline.write_history_file(self.history_file)
 
111
 
 
112
    def do_quit(self, args):
 
113
        self.write_history()
 
114
        raise StopIteration
 
115
 
 
116
    def do_exit(self, args):
 
117
        self.do_quit(args)
 
118
 
 
119
    def do_EOF(self, args):
 
120
        print
 
121
        self.do_quit(args)
 
122
 
 
123
    def postcmd(self, line, bar):
 
124
        self.set_title()
 
125
        self.set_prompt()
 
126
 
 
127
    def set_prompt(self):
 
128
        if self.tree is not None:
 
129
            try:
 
130
                prompt_data = (self.tree.branch.nick, self.tree.branch.revno(),
 
131
                               self.tree.relpath('.'))
 
132
                prompt = " %s:%d/%s" % prompt_data
 
133
            except:
 
134
                prompt = ""
 
135
        else:
 
136
            prompt = ""
 
137
        self.prompt = "bzr%s> " % prompt
 
138
 
 
139
    def set_title(self, command=None):
 
140
        try:
 
141
            b = Branch.open_containing('.')[0]
 
142
            version = "%s:%d" % (b.nick, b.revno())
 
143
        except:
 
144
            version = "[no version]"
 
145
        if command is None:
 
146
            command = ""
 
147
        sys.stdout.write(terminal.term_title("bzr %s %s" % (command, version)))
 
148
 
 
149
    def do_cd(self, line):
 
150
        if line == "":
 
151
            line = "~"
 
152
        line = os.path.expanduser(line)
 
153
        if os.path.isabs(line):
 
154
            newcwd = line
 
155
        else:
 
156
            newcwd = self.cwd+'/'+line
 
157
        newcwd = os.path.normpath(newcwd)
 
158
        try:
 
159
            os.chdir(newcwd)
 
160
            self.cwd = newcwd
 
161
        except Exception, e:
 
162
            print e
 
163
        try:
 
164
            self.tree = WorkingTree.open_containing(".")[0]
 
165
        except:
 
166
            self.tree = None
 
167
 
 
168
    def do_help(self, line):
 
169
        self.default("help "+line)
 
170
 
 
171
    def default(self, line):
 
172
        try:
 
173
            args = shlex.split(line)
 
174
        except ValueError, e:
 
175
            print 'Parse error:', e
 
176
            return
 
177
 
 
178
        alias_args = get_alias(args[0])
 
179
        if alias_args is not None:
 
180
            args[0] = alias_args.pop(0)
 
181
 
 
182
        commandname = args.pop(0)
 
183
        for char in ('|', '<', '>'):
 
184
            commandname = commandname.split(char)[0]
 
185
        if commandname[-1] in ('|', '<', '>'):
 
186
            commandname = commandname[:-1]
 
187
        try:
 
188
            if commandname in SHELL_BLACKLIST:
 
189
                raise BlackListedCommand(commandname)
 
190
            cmd_obj = get_cmd_object(commandname)
 
191
        except (BlackListedCommand, BzrError):
 
192
            return os.system(line)
 
193
 
 
194
        try:
 
195
            is_qbzr = cmd_obj.__module__.startswith('bzrlib.plugins.qbzr.')
 
196
            if too_complicated(line) or is_qbzr:
 
197
                return os.system("bzr "+line)
 
198
            else:
 
199
                return (cmd_obj.run_argv_aliases(args, alias_args) or 0)
 
200
        except BzrError, e:
 
201
            trace.log_exception_quietly()
 
202
            print e
 
203
        except KeyboardInterrupt, e:
 
204
            print "Interrupted"
 
205
        except Exception, e:
 
206
            trace.log_exception_quietly()
 
207
            print "Unhandled error:\n%s" % (e)
 
208
 
 
209
 
 
210
    def completenames(self, text, line, begidx, endidx):
 
211
        return CompletionContext(text).get_completions()
 
212
 
 
213
    def completedefault(self, text, line, begidx, endidx):
 
214
        """Perform completion for native commands.
 
215
 
 
216
        :param text: The text to complete
 
217
        :type text: str
 
218
        :param line: The entire line to complete
 
219
        :type line: str
 
220
        :param begidx: The start of the text in the line
 
221
        :type begidx: int
 
222
        :param endidx: The end of the text in the line
 
223
        :type endidx: int
 
224
        """
 
225
        (cmd, args, foo) = self.parseline(line)
 
226
        if cmd == "bzr":
 
227
            cmd = None
 
228
        return CompletionContext(text, command=cmd).get_completions()
 
229
 
 
230
 
 
231
def run_shell(directory=None):
 
232
    try:
 
233
        if not directory is None:
 
234
            os.chdir(directory)
 
235
        prompt = PromptCmd()
 
236
        while True:
 
237
            try:
 
238
                try:
 
239
                    prompt.cmdloop()
 
240
                except KeyboardInterrupt:
 
241
                    print
 
242
            finally:
 
243
                prompt.write_history()
 
244
    except StopIteration:
 
245
        pass
 
246
 
 
247
 
 
248
def iter_opt_completions(command_obj):
 
249
    for option_name, option in command_obj.options().items():
 
250
        yield "--" + option_name
 
251
        short_name = option.short_name()
 
252
        if short_name:
 
253
            yield "-" + short_name
 
254
 
 
255
 
 
256
def iter_file_completions(arg, only_dirs = False):
 
257
    """Generate an iterator that iterates through filename completions.
 
258
 
 
259
    :param arg: The filename fragment to match
 
260
    :type arg: str
 
261
    :param only_dirs: If true, match only directories
 
262
    :type only_dirs: bool
 
263
    """
 
264
    cwd = os.getcwd()
 
265
    if cwd != "/":
 
266
        extras = [".", ".."]
 
267
    else:
 
268
        extras = []
 
269
    (dir, file) = os.path.split(arg)
 
270
    if dir != "":
 
271
        listingdir = os.path.expanduser(dir)
 
272
    else:
 
273
        listingdir = cwd
 
274
    for file in chain(os.listdir(listingdir), extras):
 
275
        if dir != "":
 
276
            userfile = dir+'/'+file
 
277
        else:
 
278
            userfile = file
 
279
        if userfile.startswith(arg):
 
280
            if os.path.isdir(listingdir+'/'+file):
 
281
                userfile+='/'
 
282
                yield userfile
 
283
            elif not only_dirs:
 
284
                yield userfile + ' '
 
285
 
 
286
 
 
287
def iter_dir_completions(arg):
 
288
    """Generate an iterator that iterates through directory name completions.
 
289
 
 
290
    :param arg: The directory name fragment to match
 
291
    :type arg: str
 
292
    """
 
293
    return iter_file_completions(arg, True)
 
294
 
 
295
 
 
296
def iter_command_names(hidden=False):
 
297
    for real_cmd_name in all_command_names():
 
298
        cmd_obj = get_cmd_object(real_cmd_name)
 
299
        if not hidden and cmd_obj.hidden:
 
300
            continue
 
301
        for name in [real_cmd_name] + cmd_obj.aliases:
 
302
            # Don't complete on aliases that are prefixes of the canonical name
 
303
            if name == real_cmd_name or not real_cmd_name.startswith(name):
 
304
                yield name
 
305
 
 
306
 
 
307
def iter_executables(path):
 
308
    dirname, partial = os.path.split(path)
 
309
    for filename in os.listdir(dirname):
 
310
        if not filename.startswith(partial):
 
311
            continue
 
312
        fullpath = os.path.join(dirname, filename)
 
313
        mode=os.lstat(fullpath)[stat.ST_MODE]
 
314
        if stat.S_ISREG(mode) and 0111 & mode:
 
315
            yield fullpath + ' '
 
316
 
 
317
 
 
318
def filter_completions(iter, arg):
 
319
    return (c for c in iter if c.startswith(arg))
 
320
 
 
321
 
 
322
def iter_munged_completions(iter, arg, text):
 
323
    for completion in iter:
 
324
        completion = str(completion)
 
325
        if completion.startswith(arg):
 
326
            yield completion[len(arg)-len(text):]+" "
 
327
 
 
328
 
 
329
def too_complicated(line):
 
330
    for char in '|<>*?':
 
331
        if char in line:
 
332
            return True
 
333
    return False