~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to shell.py

  • Committer: Aaron Bentley
  • Date: 2008-04-11 00:03:51 UTC
  • Revision ID: aaron@aaronbentley.com-20080411000351-dmbvgmanygnphzgp
Add escaping to HTML output

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
 
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, get_all_cmds, 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
        readline.set_completer_delims(string.whitespace)
 
103
        if os.access(self.history_file, os.R_OK) and \
 
104
            os.path.isfile(self.history_file):
 
105
            readline.read_history_file(self.history_file)
 
106
        self.cwd = os.getcwd()
 
107
 
 
108
    def write_history(self):
 
109
        readline.write_history_file(self.history_file)
 
110
 
 
111
    def do_quit(self, args):
 
112
        self.write_history()
 
113
        raise StopIteration
 
114
 
 
115
    def do_exit(self, args):
 
116
        self.do_quit(args)
 
117
 
 
118
    def do_EOF(self, args):
 
119
        print
 
120
        self.do_quit(args)
 
121
 
 
122
    def postcmd(self, line, bar):
 
123
        self.set_title()
 
124
        self.set_prompt()
 
125
 
 
126
    def set_prompt(self):
 
127
        if self.tree is not None:
 
128
            try:
 
129
                prompt_data = (self.tree.branch.nick, self.tree.branch.revno(),
 
130
                               self.tree.relpath('.'))
 
131
                prompt = " %s:%d/%s" % prompt_data
 
132
            except:
 
133
                prompt = ""
 
134
        else:
 
135
            prompt = ""
 
136
        self.prompt = "bzr%s> " % prompt
 
137
 
 
138
    def set_title(self, command=None):
 
139
        try:
 
140
            b = Branch.open_containing('.')[0]
 
141
            version = "%s:%d" % (b.nick, b.revno())
 
142
        except:
 
143
            version = "[no version]"
 
144
        if command is None:
 
145
            command = ""
 
146
        sys.stdout.write(terminal.term_title("bzr %s %s" % (command, version)))
 
147
 
 
148
    def do_cd(self, line):
 
149
        if line == "":
 
150
            line = "~"
 
151
        line = os.path.expanduser(line)
 
152
        if os.path.isabs(line):
 
153
            newcwd = line
 
154
        else:
 
155
            newcwd = self.cwd+'/'+line
 
156
        newcwd = os.path.normpath(newcwd)
 
157
        try:
 
158
            os.chdir(newcwd)
 
159
            self.cwd = newcwd
 
160
        except Exception, e:
 
161
            print e
 
162
        try:
 
163
            self.tree = WorkingTree.open_containing(".")[0]
 
164
        except:
 
165
            self.tree = None
 
166
 
 
167
    def do_help(self, line):
 
168
        self.default("help "+line)
 
169
 
 
170
    def default(self, line):
 
171
        args = shlex.split(line)
 
172
        alias_args = get_alias(args[0])
 
173
        if alias_args is not None:
 
174
            args[0] = alias_args.pop(0)
 
175
 
 
176
        commandname = args.pop(0)
 
177
        for char in ('|', '<', '>'):
 
178
            commandname = commandname.split(char)[0]
 
179
        if commandname[-1] in ('|', '<', '>'):
 
180
            commandname = commandname[:-1]
 
181
        try:
 
182
            if commandname in SHELL_BLACKLIST:
 
183
                raise BlackListedCommand(commandname)
 
184
            cmd_obj = get_cmd_object(commandname)
 
185
        except (BlackListedCommand, BzrError):
 
186
            return os.system(line)
 
187
 
 
188
        try:
 
189
            if too_complicated(line):
 
190
                return os.system("bzr "+line)
 
191
            else:
 
192
                return (cmd_obj.run_argv_aliases(args, alias_args) or 0)
 
193
        except BzrError, e:
 
194
            print e
 
195
        except KeyboardInterrupt, e:
 
196
            print "Interrupted"
 
197
        except Exception, e:
 
198
#            print "Unhandled error:\n%s" % errors.exception_str(e)
 
199
            print "Unhandled error:\n%s" % (e)
 
200
 
 
201
 
 
202
    def completenames(self, text, line, begidx, endidx):
 
203
        return CompletionContext(text).get_completions()
 
204
 
 
205
    def completedefault(self, text, line, begidx, endidx):
 
206
        """Perform completion for native commands.
 
207
 
 
208
        :param text: The text to complete
 
209
        :type text: str
 
210
        :param line: The entire line to complete
 
211
        :type line: str
 
212
        :param begidx: The start of the text in the line
 
213
        :type begidx: int
 
214
        :param endidx: The end of the text in the line
 
215
        :type endidx: int
 
216
        """
 
217
        (cmd, args, foo) = self.parseline(line)
 
218
        if cmd == "bzr":
 
219
            cmd = None
 
220
        return CompletionContext(text, command=cmd).get_completions()
 
221
 
 
222
 
 
223
def run_shell():
 
224
    try:
 
225
        prompt = PromptCmd()
 
226
        try:
 
227
            prompt.cmdloop()
 
228
        finally:
 
229
            prompt.write_history()
 
230
    except StopIteration:
 
231
        pass
 
232
 
 
233
 
 
234
def iter_opt_completions(command_obj):
 
235
    for option_name, option in command_obj.options().items():
 
236
        yield "--" + option_name
 
237
        short_name = option.short_name()
 
238
        if short_name:
 
239
            yield "-" + short_name
 
240
 
 
241
 
 
242
def iter_file_completions(arg, only_dirs = False):
 
243
    """Generate an iterator that iterates through filename completions.
 
244
 
 
245
    :param arg: The filename fragment to match
 
246
    :type arg: str
 
247
    :param only_dirs: If true, match only directories
 
248
    :type only_dirs: bool
 
249
    """
 
250
    cwd = os.getcwd()
 
251
    if cwd != "/":
 
252
        extras = [".", ".."]
 
253
    else:
 
254
        extras = []
 
255
    (dir, file) = os.path.split(arg)
 
256
    if dir != "":
 
257
        listingdir = os.path.expanduser(dir)
 
258
    else:
 
259
        listingdir = cwd
 
260
    for file in chain(os.listdir(listingdir), extras):
 
261
        if dir != "":
 
262
            userfile = dir+'/'+file
 
263
        else:
 
264
            userfile = file
 
265
        if userfile.startswith(arg):
 
266
            if os.path.isdir(listingdir+'/'+file):
 
267
                userfile+='/'
 
268
                yield userfile
 
269
            elif not only_dirs:
 
270
                yield userfile + ' '
 
271
 
 
272
 
 
273
def iter_dir_completions(arg):
 
274
    """Generate an iterator that iterates through directory name completions.
 
275
 
 
276
    :param arg: The directory name fragment to match
 
277
    :type arg: str
 
278
    """
 
279
    return iter_file_completions(arg, True)
 
280
 
 
281
 
 
282
def iter_command_names(hidden=False):
 
283
    for real_cmd_name, cmd_class in get_all_cmds():
 
284
        if not hidden and cmd_class.hidden:
 
285
            continue
 
286
        for name in [real_cmd_name] + cmd_class.aliases:
 
287
            # Don't complete on aliases that are prefixes of the canonical name
 
288
            if name == real_cmd_name or not real_cmd_name.startswith(name):
 
289
                yield name
 
290
 
 
291
 
 
292
def iter_executables(path):
 
293
    dirname, partial = os.path.split(path)
 
294
    for filename in os.listdir(dirname):
 
295
        if not filename.startswith(partial):
 
296
            continue
 
297
        fullpath = os.path.join(dirname, filename)
 
298
        mode=os.lstat(fullpath)[stat.ST_MODE]
 
299
        if stat.S_ISREG(mode) and 0111 & mode:
 
300
            yield fullpath + ' '
 
301
 
 
302
 
 
303
def filter_completions(iter, arg):
 
304
    return (c for c in iter if c.startswith(arg))
 
305
 
 
306
 
 
307
def iter_munged_completions(iter, arg, text):
 
308
    for completion in iter:
 
309
        completion = str(completion)
 
310
        if completion.startswith(arg):
 
311
            yield completion[len(arg)-len(text):]+" "
 
312
 
 
313
 
 
314
def too_complicated(line):
 
315
    for char in '|<>*?':
 
316
        if char in line:
 
317
            return True
 
318
    return False