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