~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
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:
201
            print e
202
        except KeyboardInterrupt, e:
203
            print "Interrupted"
204
        except Exception, e:
205
#            print "Unhandled error:\n%s" % errors.exception_str(e)
206
            print "Unhandled error:\n%s" % (e)
207
248 by Aaron Bentley
Initial import of Fai shell command
208
209
    def completenames(self, text, line, begidx, endidx):
283.1.1 by Aaron Bentley
Got completions working properly
210
        return CompletionContext(text).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
211
212
    def completedefault(self, text, line, begidx, endidx):
213
        """Perform completion for native commands.
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
214
248 by Aaron Bentley
Initial import of Fai shell command
215
        :param text: The text to complete
216
        :type text: str
217
        :param line: The entire line to complete
218
        :type line: str
219
        :param begidx: The start of the text in the line
220
        :type begidx: int
221
        :param endidx: The end of the text in the line
222
        :type endidx: int
223
        """
251 by Aaron Bentley
Got support for option completion
224
        (cmd, args, foo) = self.parseline(line)
256 by Aaron Bentley
Enhanced shell completion
225
        if cmd == "bzr":
283.1.1 by Aaron Bentley
Got completions working properly
226
            cmd = None
227
        return CompletionContext(text, command=cmd).get_completions()
248 by Aaron Bentley
Initial import of Fai shell command
228
402 by Aaron Bentley
Clean up style
229
736.1.1 by Gordon Tyler
Added --directory option to shell command.
230
def run_shell(directory=None):
248 by Aaron Bentley
Initial import of Fai shell command
231
    try:
736.1.1 by Gordon Tyler
Added --directory option to shell command.
232
        if not directory is None:
233
            os.chdir(directory)
255 by Aaron Bentley
Fixed system.exit printing 0 bug
234
        prompt = PromptCmd()
723.2.1 by Benoît Pierre
shell improvement: handle KeyboardInterrupt like a regular shell
235
        while True:
236
            try:
237
                try:
238
                    prompt.cmdloop()
239
                except KeyboardInterrupt:
240
                    print
241
            finally:
242
                prompt.write_history()
255 by Aaron Bentley
Fixed system.exit printing 0 bug
243
    except StopIteration:
244
        pass
248 by Aaron Bentley
Initial import of Fai shell command
245
402 by Aaron Bentley
Clean up style
246
267 by Aaron Bentley
Added file completion when completing subcommands.
247
def iter_opt_completions(command_obj):
248
    for option_name, option in command_obj.options().items():
249
        yield "--" + option_name
495 by Aaron Bentley
Restore short_name stuff to match API
250
        short_name = option.short_name()
267 by Aaron Bentley
Added file completion when completing subcommands.
251
        if short_name:
252
            yield "-" + short_name
253
402 by Aaron Bentley
Clean up style
254
248 by Aaron Bentley
Initial import of Fai shell command
255
def iter_file_completions(arg, only_dirs = False):
256
    """Generate an iterator that iterates through filename completions.
257
258
    :param arg: The filename fragment to match
259
    :type arg: str
260
    :param only_dirs: If true, match only directories
261
    :type only_dirs: bool
262
    """
263
    cwd = os.getcwd()
264
    if cwd != "/":
265
        extras = [".", ".."]
266
    else:
267
        extras = []
268
    (dir, file) = os.path.split(arg)
269
    if dir != "":
270
        listingdir = os.path.expanduser(dir)
271
    else:
272
        listingdir = cwd
252 by Aaron Bentley
Fixed dirctory completion in shell
273
    for file in chain(os.listdir(listingdir), extras):
248 by Aaron Bentley
Initial import of Fai shell command
274
        if dir != "":
275
            userfile = dir+'/'+file
276
        else:
277
            userfile = file
278
        if userfile.startswith(arg):
279
            if os.path.isdir(listingdir+'/'+file):
280
                userfile+='/'
281
                yield userfile
282
            elif not only_dirs:
419 by Aaron Bentley
Nicer directory completion for bzr shell
283
                yield userfile + ' '
248 by Aaron Bentley
Initial import of Fai shell command
284
285
286
def iter_dir_completions(arg):
287
    """Generate an iterator that iterates through directory name completions.
288
289
    :param arg: The directory name fragment to match
290
    :type arg: str
291
    """
292
    return iter_file_completions(arg, True)
250 by Aaron Bentley
Got command completion working
293
402 by Aaron Bentley
Clean up style
294
250 by Aaron Bentley
Got command completion working
295
def iter_command_names(hidden=False):
729 by Aaron Bentley
Fix deprecation warnings completing command names.
296
    for real_cmd_name in all_command_names():
297
        cmd_obj = get_cmd_object(real_cmd_name)
298
        if not hidden and cmd_obj.hidden:
250 by Aaron Bentley
Got command completion working
299
            continue
729 by Aaron Bentley
Fix deprecation warnings completing command names.
300
        for name in [real_cmd_name] + cmd_obj.aliases:
256 by Aaron Bentley
Enhanced shell completion
301
            # Don't complete on aliases that are prefixes of the canonical name
302
            if name == real_cmd_name or not real_cmd_name.startswith(name):
303
                yield name
250 by Aaron Bentley
Got command completion working
304
402 by Aaron Bentley
Clean up style
305
420 by Aaron Bentley
Allow completion on executables, fix broken prompt code
306
def iter_executables(path):
307
    dirname, partial = os.path.split(path)
308
    for filename in os.listdir(dirname):
309
        if not filename.startswith(partial):
310
            continue
311
        fullpath = os.path.join(dirname, filename)
312
        mode=os.lstat(fullpath)[stat.ST_MODE]
313
        if stat.S_ISREG(mode) and 0111 & mode:
314
            yield fullpath + ' '
315
316
267.1.1 by Aaron Bentley
Improved completion in the middle of lines
317
def filter_completions(iter, arg):
318
    return (c for c in iter if c.startswith(arg))
319
402 by Aaron Bentley
Clean up style
320
250 by Aaron Bentley
Got command completion working
321
def iter_munged_completions(iter, arg, text):
322
    for completion in iter:
323
        completion = str(completion)
324
        if completion.startswith(arg):
256 by Aaron Bentley
Enhanced shell completion
325
            yield completion[len(arg)-len(text):]+" "
250 by Aaron Bentley
Got command completion working
326
402 by Aaron Bentley
Clean up style
327
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
328
def too_complicated(line):
403 by Aaron Bentley
Handle quoted strings without bailing to a subshell
329
    for char in '|<>*?':
254 by Aaron Bentley
Added fallback to shell for lines with quotes and IO redirection
330
        if char in line:
331
            return True
332
    return False