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