~abentley/bzrtools/bzrtools.dev

249 by Aaron Bentley
Got the shell basics working properly
1
# Copyright (C) 2004, 2005 Aaron Bentley
612 by Aaron Bentley
Update email address
2
# <aaron@aaronbentley.com>
249 by Aaron Bentley
Got the shell basics working properly
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
755.1.1 by Martin
Log tracebacks for errors during shell session
27
from bzrlib import osutils, trace
402 by Aaron Bentley
Clean up style
28
from bzrlib.branch import Branch
608 by Aaron Bentley
Shell now ensures the config directory works, and uses the bzrlib to find it
29
from bzrlib.config import config_dir, ensure_config_dir_exists
729 by Aaron Bentley
Fix deprecation warnings completing command names.
30
from bzrlib.commands import get_cmd_object, all_command_names, get_alias
249 by Aaron Bentley
Got the shell basics working properly
31
from bzrlib.errors import BzrError
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
32
from bzrlib.workingtree import WorkingTree
402 by Aaron Bentley
Clean up style
33
34
import terminal
35
249 by Aaron Bentley
Got the shell basics working properly
36
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
37
SHELL_BLACKLIST = set(['rm', 'ls'])
257 by Aaron Bentley
blacklisted 'shell' from completion
38
COMPLETION_BLACKLIST = set(['shell'])
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
39
402 by Aaron Bentley
Clean up style
40
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
41
class BlackListedCommand(BzrError):
42
    def __init__(self, command):
43
        BzrError.__init__(self, "The command %s is blacklisted for shell use" %
44
                          command)
45
402 by Aaron Bentley
Clean up style
46
262 by Aaron Bentley
Release 0.6
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):
283.1.1 by Aaron Bentley
Got completions working properly
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:
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
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)
283.1.1 by Aaron Bentley
Got completions working properly
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)
419 by Aaron Bentley
Nicer directory completion for bzr shell
84
                completions.extend(filter_completions(iter, self.text))
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
85
            return completions
262 by Aaron Bentley
Release 0.6
86
87
248 by Aaron Bentley
Initial import of Fai shell command
88
class PromptCmd(cmd.Cmd):
608 by Aaron Bentley
Shell now ensures the config directory works, and uses the bzrlib to find it
89
248 by Aaron Bentley
Initial import of Fai shell command
90
    def __init__(self):
91
        cmd.Cmd.__init__(self)
249 by Aaron Bentley
Got the shell basics working properly
92
        self.prompt = "bzr> "
248 by Aaron Bentley
Initial import of Fai shell command
93
        try:
313 by Aaron Bentley
Updated to match API changes
94
            self.tree = WorkingTree.open_containing('.')[0]
248 by Aaron Bentley
Initial import of Fai shell command
95
        except:
313 by Aaron Bentley
Updated to match API changes
96
            self.tree = None
248 by Aaron Bentley
Initial import of Fai shell command
97
        self.set_title()
98
        self.set_prompt()
99
        self.identchars += '-'
608 by Aaron Bentley
Shell now ensures the config directory works, and uses the bzrlib to find it
100
        ensure_config_dir_exists()
101
        self.history_file = osutils.pathjoin(config_dir(), 'shell-history')
723.1.1 by John Arbash Meinel
Possible fix for bug #431241
102
        whitespace = ''.join(c for c in string.whitespace if c < chr(127))
103
        readline.set_completer_delims(whitespace)
248 by Aaron Bentley
Initial import of Fai shell command
104
        if os.access(self.history_file, os.R_OK) and \
105
            os.path.isfile(self.history_file):
106
            readline.read_history_file(self.history_file)
107
        self.cwd = os.getcwd()
108
109
    def write_history(self):
110
        readline.write_history_file(self.history_file)
111
112
    def do_quit(self, args):
113
        self.write_history()
255 by Aaron Bentley
Fixed system.exit printing 0 bug
114
        raise StopIteration
248 by Aaron Bentley
Initial import of Fai shell command
115
116
    def do_exit(self, args):
117
        self.do_quit(args)
118
119
    def do_EOF(self, args):
120
        print
121
        self.do_quit(args)
122
123
    def postcmd(self, line, bar):
124
        self.set_title()
125
        self.set_prompt()
126
127
    def set_prompt(self):
313 by Aaron Bentley
Updated to match API changes
128
        if self.tree is not None:
248 by Aaron Bentley
Initial import of Fai shell command
129
            try:
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
130
                prompt_data = (self.tree.branch.nick, self.tree.branch.revno(),
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
131
                               self.tree.relpath('.'))
283.1.2 by Aaron Bentley
Got prompt and title working
132
                prompt = " %s:%d/%s" % prompt_data
248 by Aaron Bentley
Initial import of Fai shell command
133
            except:
134
                prompt = ""
135
        else:
136
            prompt = ""
249 by Aaron Bentley
Got the shell basics working properly
137
        self.prompt = "bzr%s> " % prompt
248 by Aaron Bentley
Initial import of Fai shell command
138
139
    def set_title(self, command=None):
140
        try:
283.1.2 by Aaron Bentley
Got prompt and title working
141
            b = Branch.open_containing('.')[0]
142
            version = "%s:%d" % (b.nick, b.revno())
248 by Aaron Bentley
Initial import of Fai shell command
143
        except:
144
            version = "[no version]"
145
        if command is None:
146
            command = ""
249 by Aaron Bentley
Got the shell basics working properly
147
        sys.stdout.write(terminal.term_title("bzr %s %s" % (command, version)))
248 by Aaron Bentley
Initial import of Fai shell command
148
149
    def do_cd(self, line):
150
        if line == "":
151
            line = "~"
152
        line = os.path.expanduser(line)
153
        if os.path.isabs(line):
154
            newcwd = line
155
        else:
156
            newcwd = self.cwd+'/'+line
157
        newcwd = os.path.normpath(newcwd)
158
        try:
159
            os.chdir(newcwd)
160
            self.cwd = newcwd
161
        except Exception, e:
162
            print e
163
        try:
313 by Aaron Bentley
Updated to match API changes
164
            self.tree = WorkingTree.open_containing(".")[0]
248 by Aaron Bentley
Initial import of Fai shell command
165
        except:
313 by Aaron Bentley
Updated to match API changes
166
            self.tree = None
248 by Aaron Bentley
Initial import of Fai shell command
167
168
    def do_help(self, line):
251 by Aaron Bentley
Got support for option completion
169
        self.default("help "+line)
248 by Aaron Bentley
Initial import of Fai shell command
170
171
    def default(self, line):
723.3.1 by Benoît Pierre
shell improvement: catch shlex.split ValueError exceptions
172
        try:
173
            args = shlex.split(line)
174
        except ValueError, e:
175
            print 'Parse error:', e
176
            return
177
325.1.1 by Aaron Bentley
Handle aliases in bzr shell
178
        alias_args = get_alias(args[0])
179
        if alias_args is not None:
180
            args[0] = alias_args.pop(0)
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
181
249 by Aaron Bentley
Got the shell basics working properly
182
        commandname = args.pop(0)
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
183
        for char in ('|', '<', '>'):
184
            commandname = commandname.split(char)[0]
185
        if commandname[-1] in ('|', '<', '>'):
186
            commandname = commandname[:-1]
249 by Aaron Bentley
Got the shell basics working properly
187
        try:
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
188
            if commandname in SHELL_BLACKLIST:
189
                raise BlackListedCommand(commandname)
249 by Aaron Bentley
Got the shell basics working properly
190
            cmd_obj = get_cmd_object(commandname)
253 by Aaron Bentley
Prevented bzr's rm and ls from being invoked in the shell
191
        except (BlackListedCommand, BzrError):
249 by Aaron Bentley
Got the shell basics working properly
192
            return os.system(line)
193
194
        try:
730.1.4 by Aaron Bentley
Handle more qbzr commands.
195
            is_qbzr = cmd_obj.__module__.startswith('bzrlib.plugins.qbzr.')
730.1.3 by Aaron Bentley
Use a subprocess for qbzr commands.
196
            if too_complicated(line) or is_qbzr:
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
197
                return os.system("bzr "+line)
198
            else:
325.1.1 by Aaron Bentley
Handle aliases in bzr shell
199
                return (cmd_obj.run_argv_aliases(args, alias_args) or 0)
249 by Aaron Bentley
Got the shell basics working properly
200
        except BzrError, e:
755.1.1 by Martin
Log tracebacks for errors during shell session
201
            trace.log_exception_quietly()
249 by Aaron Bentley
Got the shell basics working properly
202
            print e
203
        except KeyboardInterrupt, e:
204
            print "Interrupted"
205
        except Exception, e:
755.1.1 by Martin
Log tracebacks for errors during shell session
206
            trace.log_exception_quietly()
249 by Aaron Bentley
Got the shell basics working properly
207
            print "Unhandled error:\n%s" % (e)
208
248 by Aaron Bentley
Initial import of Fai shell command
209
210
    def completenames(self, text, line, begidx, endidx):
283.1.1 by Aaron Bentley
Got completions working properly
211
        return CompletionContext(text).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
212
213
    def completedefault(self, text, line, begidx, endidx):
214
        """Perform completion for native commands.
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
215
248 by Aaron Bentley
Initial import of Fai shell command
216
        :param text: The text to complete
217
        :type text: str
218
        :param line: The entire line to complete
219
        :type line: str
220
        :param begidx: The start of the text in the line
221
        :type begidx: int
222
        :param endidx: The end of the text in the line
223
        :type endidx: int
224
        """
251 by Aaron Bentley
Got support for option completion
225
        (cmd, args, foo) = self.parseline(line)
256 by Aaron Bentley
Enhanced shell completion
226
        if cmd == "bzr":
283.1.1 by Aaron Bentley
Got completions working properly
227
            cmd = None
228
        return CompletionContext(text, command=cmd).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
229
402 by Aaron Bentley
Clean up style
230
736.1.1 by Gordon Tyler
Added --directory option to shell command.
231
def run_shell(directory=None):
248 by Aaron Bentley
Initial import of Fai shell command
232
    try:
736.1.1 by Gordon Tyler
Added --directory option to shell command.
233
        if not directory is None:
234
            os.chdir(directory)
255 by Aaron Bentley
Fixed system.exit printing 0 bug
235
        prompt = PromptCmd()
723.2.1 by Benoît Pierre
shell improvement: handle KeyboardInterrupt like a regular shell
236
        while True:
237
            try:
238
                try:
239
                    prompt.cmdloop()
240
                except KeyboardInterrupt:
241
                    print
242
            finally:
243
                prompt.write_history()
255 by Aaron Bentley
Fixed system.exit printing 0 bug
244
    except StopIteration:
245
        pass
248 by Aaron Bentley
Initial import of Fai shell command
246
402 by Aaron Bentley
Clean up style
247
267 by Aaron Bentley
Added file completion when completing subcommands.
248
def iter_opt_completions(command_obj):
249
    for option_name, option in command_obj.options().items():
250
        yield "--" + option_name
495 by Aaron Bentley
Restore short_name stuff to match API
251
        short_name = option.short_name()
267 by Aaron Bentley
Added file completion when completing subcommands.
252
        if short_name:
253
            yield "-" + short_name
254
402 by Aaron Bentley
Clean up style
255
248 by Aaron Bentley
Initial import of Fai shell command
256
def iter_file_completions(arg, only_dirs = False):
257
    """Generate an iterator that iterates through filename completions.
258
259
    :param arg: The filename fragment to match
260
    :type arg: str
261
    :param only_dirs: If true, match only directories
262
    :type only_dirs: bool
263
    """
264
    cwd = os.getcwd()
265
    if cwd != "/":
266
        extras = [".", ".."]
267
    else:
268
        extras = []
269
    (dir, file) = os.path.split(arg)
270
    if dir != "":
271
        listingdir = os.path.expanduser(dir)
272
    else:
273
        listingdir = cwd
252 by Aaron Bentley
Fixed dirctory completion in shell
274
    for file in chain(os.listdir(listingdir), extras):
248 by Aaron Bentley
Initial import of Fai shell command
275
        if dir != "":
276
            userfile = dir+'/'+file
277
        else:
278
            userfile = file
279
        if userfile.startswith(arg):
280
            if os.path.isdir(listingdir+'/'+file):
281
                userfile+='/'
282
                yield userfile
283
            elif not only_dirs:
419 by Aaron Bentley
Nicer directory completion for bzr shell
284
                yield userfile + ' '
248 by Aaron Bentley
Initial import of Fai shell command
285
286
287
def iter_dir_completions(arg):
288
    """Generate an iterator that iterates through directory name completions.
289
290
    :param arg: The directory name fragment to match
291
    :type arg: str
292
    """
293
    return iter_file_completions(arg, True)
250 by Aaron Bentley
Got command completion working
294
402 by Aaron Bentley
Clean up style
295
250 by Aaron Bentley
Got command completion working
296
def iter_command_names(hidden=False):
729 by Aaron Bentley
Fix deprecation warnings completing command names.
297
    for real_cmd_name in all_command_names():
298
        cmd_obj = get_cmd_object(real_cmd_name)
299
        if not hidden and cmd_obj.hidden:
250 by Aaron Bentley
Got command completion working
300
            continue
729 by Aaron Bentley
Fix deprecation warnings completing command names.
301
        for name in [real_cmd_name] + cmd_obj.aliases:
256 by Aaron Bentley
Enhanced shell completion
302
            # Don't complete on aliases that are prefixes of the canonical name
303
            if name == real_cmd_name or not real_cmd_name.startswith(name):
304
                yield name
250 by Aaron Bentley
Got command completion working
305
402 by Aaron Bentley
Clean up style
306
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
307
def iter_executables(path):
308
    dirname, partial = os.path.split(path)
309
    for filename in os.listdir(dirname):
310
        if not filename.startswith(partial):
311
            continue
312
        fullpath = os.path.join(dirname, filename)
313
        mode=os.lstat(fullpath)[stat.ST_MODE]
314
        if stat.S_ISREG(mode) and 0111 & mode:
315
            yield fullpath + ' '
316
317
267.1.1 by Aaron Bentley
Improved completion in the middle of lines
318
def filter_completions(iter, arg):
319
    return (c for c in iter if c.startswith(arg))
320
402 by Aaron Bentley
Clean up style
321
250 by Aaron Bentley
Got command completion working
322
def iter_munged_completions(iter, arg, text):
323
    for completion in iter:
324
        completion = str(completion)
325
        if completion.startswith(arg):
256 by Aaron Bentley
Enhanced shell completion
326
            yield completion[len(arg)-len(text):]+" "
250 by Aaron Bentley
Got command completion working
327
402 by Aaron Bentley
Clean up style
328
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
329
def too_complicated(line):
403 by Aaron Bentley
Handle quoted strings without bailing to a subshell
330
    for char in '|<>*?':
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
331
        if char in line:
332
            return True
333
    return False