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