~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shell.py

  • Committer: Robert Collins
  • Date: 2005-09-13 15:11:39 UTC
  • mto: (147.2.6) (364.1.3 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: robertc@robertcollins.net-20050913151139-9ac920fc9d7bda31
TODOification

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