~abentley/bzrtools/bzrtools.dev

249 by Aaron Bentley
Got the shell basics working properly
1
# Copyright (C) 2004, 2005 Aaron Bentley
2
# <aaron.bentley@utoronto.ca>
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
import cmd
18
import sys
19
import os
20
import terminal
21
import readline
22
import string
252 by Aaron Bentley
Fixed dirctory completion in shell
23
from itertools import chain
249 by Aaron Bentley
Got the shell basics working properly
24
from bzrlib.errors import BzrError
325.1.1 by Aaron Bentley
Handle aliases in bzr shell
25
from bzrlib.commands import get_cmd_object, get_all_cmds, get_alias
283.1.2 by Aaron Bentley
Got prompt and title working
26
from bzrlib.branch import Branch
249 by Aaron Bentley
Got the shell basics working properly
27
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
28
SHELL_BLACKLIST = set(['rm', 'ls'])
257 by Aaron Bentley
blacklisted 'shell' from completion
29
COMPLETION_BLACKLIST = set(['shell'])
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
30
31
class BlackListedCommand(BzrError):
32
    def __init__(self, command):
33
        BzrError.__init__(self, "The command %s is blacklisted for shell use" %
34
                          command)
35
262 by Aaron Bentley
Release 0.6
36
class CompletionContext(object):
37
    def __init__(self, text, command=None, prev_opt=None, arg_pos=None):
38
        self.text = text
39
        self.command = command
40
        self.prev_opt = prev_opt
41
        self.arg_pos = None
42
43
    def get_completions(self):
283.1.1 by Aaron Bentley
Got completions working properly
44
        try:
45
            return self.get_completions_or_raise()
46
        except Exception, e:
47
            print e, type(e)
48
            return []
49
50
    def get_option_completions(self):
51
        try:
52
            command_obj = get_cmd_object(self.command)
53
        except BzrError:
54
            return []
55
        opts = [o+" " for o in iter_opt_completions(command_obj)]
56
        return list(filter_completions(opts, self.text))
57
58
    def get_completions_or_raise(self):
59
        if self.command is None:
60
            iter = (c+" " for c in iter_command_names() if
262 by Aaron Bentley
Release 0.6
61
                    c not in COMPLETION_BLACKLIST)
283.1.1 by Aaron Bentley
Got completions working properly
62
            return list(filter_completions(iter, self.text))
63
        if self.prev_opt is None:
64
            completions = self.get_option_completions()
65
            if self.command == "cd":
66
                iter = iter_dir_completions(self.text)
67
                completions.extend(list(filter_completions(iter, self.text)))
68
            else:
69
                iter = iter_file_completions(self.text)
70
                completions.extend([f+" " for f in 
71
                                    filter_completions(iter, self.text)])
72
            return completions 
262 by Aaron Bentley
Release 0.6
73
74
248 by Aaron Bentley
Initial import of Fai shell command
75
class PromptCmd(cmd.Cmd):
76
    def __init__(self):
77
        cmd.Cmd.__init__(self)
249 by Aaron Bentley
Got the shell basics working properly
78
        self.prompt = "bzr> "
248 by Aaron Bentley
Initial import of Fai shell command
79
        try:
313 by Aaron Bentley
Updated to match API changes
80
            self.tree = WorkingTree.open_containing('.')[0]
248 by Aaron Bentley
Initial import of Fai shell command
81
        except:
313 by Aaron Bentley
Updated to match API changes
82
            self.tree = None
248 by Aaron Bentley
Initial import of Fai shell command
83
        self.set_title()
84
        self.set_prompt()
85
        self.identchars += '-'
249 by Aaron Bentley
Got the shell basics working properly
86
        self.history_file = os.path.expanduser("~/.bazaar/shell-history")
248 by Aaron Bentley
Initial import of Fai shell command
87
        readline.set_completer_delims(string.whitespace)
88
        if os.access(self.history_file, os.R_OK) and \
89
            os.path.isfile(self.history_file):
90
            readline.read_history_file(self.history_file)
91
        self.cwd = os.getcwd()
92
93
    def write_history(self):
94
        readline.write_history_file(self.history_file)
95
96
    def do_quit(self, args):
97
        self.write_history()
255 by Aaron Bentley
Fixed system.exit printing 0 bug
98
        raise StopIteration
248 by Aaron Bentley
Initial import of Fai shell command
99
100
    def do_exit(self, args):
101
        self.do_quit(args)
102
103
    def do_EOF(self, args):
104
        print
105
        self.do_quit(args)
106
107
    def postcmd(self, line, bar):
108
        self.set_title()
109
        self.set_prompt()
110
111
    def set_prompt(self):
313 by Aaron Bentley
Updated to match API changes
112
        if self.tree is not None:
248 by Aaron Bentley
Initial import of Fai shell command
113
            try:
313 by Aaron Bentley
Updated to match API changes
114
                prompt_data = (self.tree.branch.nick, self.tree.branch.revno(), 
115
                               self.tree.branch.relpath('.'))
283.1.2 by Aaron Bentley
Got prompt and title working
116
                prompt = " %s:%d/%s" % prompt_data
248 by Aaron Bentley
Initial import of Fai shell command
117
            except:
118
                prompt = ""
119
        else:
120
            prompt = ""
249 by Aaron Bentley
Got the shell basics working properly
121
        self.prompt = "bzr%s> " % prompt
248 by Aaron Bentley
Initial import of Fai shell command
122
123
    def set_title(self, command=None):
124
        try:
283.1.2 by Aaron Bentley
Got prompt and title working
125
            b = Branch.open_containing('.')[0]
126
            version = "%s:%d" % (b.nick, b.revno())
248 by Aaron Bentley
Initial import of Fai shell command
127
        except:
128
            version = "[no version]"
129
        if command is None:
130
            command = ""
249 by Aaron Bentley
Got the shell basics working properly
131
        sys.stdout.write(terminal.term_title("bzr %s %s" % (command, version)))
248 by Aaron Bentley
Initial import of Fai shell command
132
133
    def do_cd(self, line):
134
        if line == "":
135
            line = "~"
136
        line = os.path.expanduser(line)
137
        if os.path.isabs(line):
138
            newcwd = line
139
        else:
140
            newcwd = self.cwd+'/'+line
141
        newcwd = os.path.normpath(newcwd)
142
        try:
143
            os.chdir(newcwd)
144
            self.cwd = newcwd
145
        except Exception, e:
146
            print e
147
        try:
313 by Aaron Bentley
Updated to match API changes
148
            self.tree = WorkingTree.open_containing(".")[0]
248 by Aaron Bentley
Initial import of Fai shell command
149
        except:
313 by Aaron Bentley
Updated to match API changes
150
            self.tree = None
248 by Aaron Bentley
Initial import of Fai shell command
151
152
    def do_help(self, line):
251 by Aaron Bentley
Got support for option completion
153
        self.default("help "+line)
248 by Aaron Bentley
Initial import of Fai shell command
154
155
    def default(self, line):
156
        args = line.split()
325.1.1 by Aaron Bentley
Handle aliases in bzr shell
157
        alias_args = get_alias(args[0])
158
        if alias_args is not None:
159
            args[0] = alias_args.pop(0)
160
            
249 by Aaron Bentley
Got the shell basics working properly
161
        commandname = args.pop(0)
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
162
        for char in ('|', '<', '>'):
163
            commandname = commandname.split(char)[0]
164
        if commandname[-1] in ('|', '<', '>'):
165
            commandname = commandname[:-1]
249 by Aaron Bentley
Got the shell basics working properly
166
        try:
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
167
            if commandname in SHELL_BLACKLIST:
168
                raise BlackListedCommand(commandname)
249 by Aaron Bentley
Got the shell basics working properly
169
            cmd_obj = get_cmd_object(commandname)
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
170
        except (BlackListedCommand, BzrError):
249 by Aaron Bentley
Got the shell basics working properly
171
            return os.system(line)
172
173
        try:
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
174
            if too_complicated(line):
175
                return os.system("bzr "+line)
176
            else:
325.1.1 by Aaron Bentley
Handle aliases in bzr shell
177
                return (cmd_obj.run_argv_aliases(args, alias_args) or 0)
249 by Aaron Bentley
Got the shell basics working properly
178
        except BzrError, e:
179
            print e
180
        except KeyboardInterrupt, e:
181
            print "Interrupted"
182
        except Exception, e:
183
#            print "Unhandled error:\n%s" % errors.exception_str(e)
184
            print "Unhandled error:\n%s" % (e)
185
248 by Aaron Bentley
Initial import of Fai shell command
186
187
    def completenames(self, text, line, begidx, endidx):
283.1.1 by Aaron Bentley
Got completions working properly
188
        return CompletionContext(text).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
189
190
    def completedefault(self, text, line, begidx, endidx):
191
        """Perform completion for native commands.
192
        
193
        :param text: The text to complete
194
        :type text: str
195
        :param line: The entire line to complete
196
        :type line: str
197
        :param begidx: The start of the text in the line
198
        :type begidx: int
199
        :param endidx: The end of the text in the line
200
        :type endidx: int
201
        """
251 by Aaron Bentley
Got support for option completion
202
        (cmd, args, foo) = self.parseline(line)
256 by Aaron Bentley
Enhanced shell completion
203
        if cmd == "bzr":
283.1.1 by Aaron Bentley
Got completions working properly
204
            cmd = None
205
        return CompletionContext(text, command=cmd).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
206
207
def run_shell():
208
    try:
255 by Aaron Bentley
Fixed system.exit printing 0 bug
209
        prompt = PromptCmd()
210
        try:
211
            prompt.cmdloop()
212
        finally:
213
            prompt.write_history()
214
    except StopIteration:
215
        pass
248 by Aaron Bentley
Initial import of Fai shell command
216
267 by Aaron Bentley
Added file completion when completing subcommands.
217
def iter_opt_completions(command_obj):
218
    for option_name, option in command_obj.options().items():
219
        yield "--" + option_name
220
        short_name = option.short_name()
221
        if short_name:
222
            yield "-" + short_name
223
248 by Aaron Bentley
Initial import of Fai shell command
224
def iter_file_completions(arg, only_dirs = False):
225
    """Generate an iterator that iterates through filename completions.
226
227
    :param arg: The filename fragment to match
228
    :type arg: str
229
    :param only_dirs: If true, match only directories
230
    :type only_dirs: bool
231
    """
232
    cwd = os.getcwd()
233
    if cwd != "/":
234
        extras = [".", ".."]
235
    else:
236
        extras = []
237
    (dir, file) = os.path.split(arg)
238
    if dir != "":
239
        listingdir = os.path.expanduser(dir)
240
    else:
241
        listingdir = cwd
252 by Aaron Bentley
Fixed dirctory completion in shell
242
    for file in chain(os.listdir(listingdir), extras):
248 by Aaron Bentley
Initial import of Fai shell command
243
        if dir != "":
244
            userfile = dir+'/'+file
245
        else:
246
            userfile = file
247
        if userfile.startswith(arg):
248
            if os.path.isdir(listingdir+'/'+file):
249
                userfile+='/'
250
                yield userfile
251
            elif not only_dirs:
252
                yield userfile
253
254
255
def iter_dir_completions(arg):
256
    """Generate an iterator that iterates through directory name completions.
257
258
    :param arg: The directory name fragment to match
259
    :type arg: str
260
    """
261
    return iter_file_completions(arg, True)
250 by Aaron Bentley
Got command completion working
262
263
def iter_command_names(hidden=False):
264
    for real_cmd_name, cmd_class in get_all_cmds():
265
        if not hidden and cmd_class.hidden:
266
            continue
267
        for name in [real_cmd_name] + cmd_class.aliases:
256 by Aaron Bentley
Enhanced shell completion
268
            # Don't complete on aliases that are prefixes of the canonical name
269
            if name == real_cmd_name or not real_cmd_name.startswith(name):
270
                yield name
250 by Aaron Bentley
Got command completion working
271
267.1.1 by Aaron Bentley
Improved completion in the middle of lines
272
def filter_completions(iter, arg):
273
    return (c for c in iter if c.startswith(arg))
274
250 by Aaron Bentley
Got command completion working
275
def iter_munged_completions(iter, arg, text):
276
    for completion in iter:
277
        completion = str(completion)
278
        if completion.startswith(arg):
256 by Aaron Bentley
Enhanced shell completion
279
            yield completion[len(arg)-len(text):]+" "
250 by Aaron Bentley
Got command completion working
280
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
281
def too_complicated(line):
261 by Aaron Bentley
Added *? to the list of tricky characters.
282
    for char in '|<>"\"*?':
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
283
        if char in line:
284
            return True
285
    return False