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