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