~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
250 by Aaron Bentley
Got command completion working
25
from bzrlib.commands import get_cmd_object, get_all_cmds
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:
283.1.2 by Aaron Bentley
Got prompt and title working
80
            self.branch = Branch.open_containing('.')[0]
248 by Aaron Bentley
Initial import of Fai shell command
81
        except:
283.1.2 by Aaron Bentley
Got prompt and title working
82
            self.branch = 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):
283.1.2 by Aaron Bentley
Got prompt and title working
112
        if self.branch is not None:
248 by Aaron Bentley
Initial import of Fai shell command
113
            try:
283.1.2 by Aaron Bentley
Got prompt and title working
114
                prompt_data = (self.branch.nick, self.branch.revno(), 
115
                               self.branch.working_tree().relpath('.'))
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:
283.1.2 by Aaron Bentley
Got prompt and title working
148
            self.branch = Branch.open_containing(".")[0]
248 by Aaron Bentley
Initial import of Fai shell command
149
        except:
283.1.2 by Aaron Bentley
Got prompt and title working
150
            self.branch = 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()
249 by Aaron Bentley
Got the shell basics working properly
157
        commandname = args.pop(0)
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
158
        for char in ('|', '<', '>'):
159
            commandname = commandname.split(char)[0]
160
        if commandname[-1] in ('|', '<', '>'):
161
            commandname = commandname[:-1]
249 by Aaron Bentley
Got the shell basics working properly
162
        try:
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
163
            if commandname in SHELL_BLACKLIST:
164
                raise BlackListedCommand(commandname)
249 by Aaron Bentley
Got the shell basics working properly
165
            cmd_obj = get_cmd_object(commandname)
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
166
        except (BlackListedCommand, BzrError):
249 by Aaron Bentley
Got the shell basics working properly
167
            return os.system(line)
168
169
        try:
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
170
            if too_complicated(line):
171
                return os.system("bzr "+line)
172
            else:
173
                return (cmd_obj.run_argv(args) or 0)
249 by Aaron Bentley
Got the shell basics working properly
174
        except BzrError, e:
175
            print e
176
        except KeyboardInterrupt, e:
177
            print "Interrupted"
178
        except Exception, e:
179
#            print "Unhandled error:\n%s" % errors.exception_str(e)
180
            print "Unhandled error:\n%s" % (e)
181
248 by Aaron Bentley
Initial import of Fai shell command
182
183
    def completenames(self, text, line, begidx, endidx):
283.1.1 by Aaron Bentley
Got completions working properly
184
        return CompletionContext(text).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
185
186
    def completedefault(self, text, line, begidx, endidx):
187
        """Perform completion for native commands.
188
        
189
        :param text: The text to complete
190
        :type text: str
191
        :param line: The entire line to complete
192
        :type line: str
193
        :param begidx: The start of the text in the line
194
        :type begidx: int
195
        :param endidx: The end of the text in the line
196
        :type endidx: int
197
        """
251 by Aaron Bentley
Got support for option completion
198
        (cmd, args, foo) = self.parseline(line)
256 by Aaron Bentley
Enhanced shell completion
199
        if cmd == "bzr":
283.1.1 by Aaron Bentley
Got completions working properly
200
            cmd = None
201
        return CompletionContext(text, command=cmd).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
202
203
def run_shell():
204
    try:
255 by Aaron Bentley
Fixed system.exit printing 0 bug
205
        prompt = PromptCmd()
206
        try:
207
            prompt.cmdloop()
208
        finally:
209
            prompt.write_history()
210
    except StopIteration:
211
        pass
248 by Aaron Bentley
Initial import of Fai shell command
212
267 by Aaron Bentley
Added file completion when completing subcommands.
213
def iter_opt_completions(command_obj):
214
    for option_name, option in command_obj.options().items():
215
        yield "--" + option_name
216
        short_name = option.short_name()
217
        if short_name:
218
            yield "-" + short_name
219
248 by Aaron Bentley
Initial import of Fai shell command
220
def iter_file_completions(arg, only_dirs = False):
221
    """Generate an iterator that iterates through filename completions.
222
223
    :param arg: The filename fragment to match
224
    :type arg: str
225
    :param only_dirs: If true, match only directories
226
    :type only_dirs: bool
227
    """
228
    cwd = os.getcwd()
229
    if cwd != "/":
230
        extras = [".", ".."]
231
    else:
232
        extras = []
233
    (dir, file) = os.path.split(arg)
234
    if dir != "":
235
        listingdir = os.path.expanduser(dir)
236
    else:
237
        listingdir = cwd
252 by Aaron Bentley
Fixed dirctory completion in shell
238
    for file in chain(os.listdir(listingdir), extras):
248 by Aaron Bentley
Initial import of Fai shell command
239
        if dir != "":
240
            userfile = dir+'/'+file
241
        else:
242
            userfile = file
243
        if userfile.startswith(arg):
244
            if os.path.isdir(listingdir+'/'+file):
245
                userfile+='/'
246
                yield userfile
247
            elif not only_dirs:
248
                yield userfile
249
250
251
def iter_dir_completions(arg):
252
    """Generate an iterator that iterates through directory name completions.
253
254
    :param arg: The directory name fragment to match
255
    :type arg: str
256
    """
257
    return iter_file_completions(arg, True)
250 by Aaron Bentley
Got command completion working
258
259
def iter_command_names(hidden=False):
260
    for real_cmd_name, cmd_class in get_all_cmds():
261
        if not hidden and cmd_class.hidden:
262
            continue
263
        for name in [real_cmd_name] + cmd_class.aliases:
256 by Aaron Bentley
Enhanced shell completion
264
            # Don't complete on aliases that are prefixes of the canonical name
265
            if name == real_cmd_name or not real_cmd_name.startswith(name):
266
                yield name
250 by Aaron Bentley
Got command completion working
267
267.1.1 by Aaron Bentley
Improved completion in the middle of lines
268
def filter_completions(iter, arg):
269
    return (c for c in iter if c.startswith(arg))
270
250 by Aaron Bentley
Got command completion working
271
def iter_munged_completions(iter, arg, text):
272
    for completion in iter:
273
        completion = str(completion)
274
        if completion.startswith(arg):
256 by Aaron Bentley
Enhanced shell completion
275
            yield completion[len(arg)-len(text):]+" "
250 by Aaron Bentley
Got command completion working
276
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
277
def too_complicated(line):
261 by Aaron Bentley
Added *? to the list of tricky characters.
278
    for char in '|<>"\"*?':
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
279
        if char in line:
280
            return True
281
    return False