|
@@
-1,275
+1,294
b''
|
|
1
|
1
|
#!/usr/bin/env python
|
|
2
|
2
|
"""Module for interactively running scripts.
|
|
3
|
3
|
|
|
4
|
4
|
This module implements classes for interactively running scripts written for
|
|
5
|
5
|
any system with a prompt which can be matched by a regexp suitable for
|
|
6
|
6
|
pexpect. It can be used to run as if they had been typed up interactively, an
|
|
7
|
7
|
arbitrary series of commands for the target system.
|
|
8
|
8
|
|
|
9
|
9
|
The module includes classes ready for IPython (with the default prompts),
|
|
10
|
10
|
plain Python and SAGE, but making a new one is trivial. To see how to use it,
|
|
11
|
11
|
simply run the module as a script:
|
|
12
|
12
|
|
|
13
|
13
|
./irunner.py --help
|
|
14
|
14
|
|
|
15
|
15
|
|
|
16
|
16
|
This is an extension of Ken Schutte <kschutte-AT-csail.mit.edu>'s script
|
|
17
|
17
|
contributed on the ipython-user list:
|
|
18
|
18
|
|
|
19
|
19
|
http://scipy.net/pipermail/ipython-user/2006-May/001705.html
|
|
20
|
20
|
|
|
21
|
21
|
|
|
22
|
22
|
NOTES:
|
|
23
|
23
|
|
|
24
|
24
|
- This module requires pexpect, available in most linux distros, or which can
|
|
25
|
25
|
be downloaded from
|
|
26
|
26
|
|
|
27
|
27
|
http://pexpect.sourceforge.net
|
|
28
|
28
|
|
|
29
|
29
|
- Because pexpect only works under Unix or Windows-Cygwin, this has the same
|
|
30
|
30
|
limitations. This means that it will NOT work under native windows Python.
|
|
31
|
31
|
"""
|
|
32
|
32
|
|
|
33
|
33
|
# Stdlib imports
|
|
34
|
34
|
import optparse
|
|
35
|
35
|
import sys
|
|
36
|
36
|
|
|
37
|
37
|
# Third-party modules.
|
|
38
|
38
|
import pexpect
|
|
39
|
39
|
|
|
40
|
40
|
# Global usage strings, to avoid indentation issues when typing it below.
|
|
41
|
41
|
USAGE = """
|
|
42
|
42
|
Interactive script runner, type: %s
|
|
43
|
43
|
|
|
44
|
44
|
runner [opts] script_name
|
|
45
|
45
|
"""
|
|
46
|
46
|
|
|
47
|
47
|
# The generic runner class
|
|
48
|
48
|
class InteractiveRunner(object):
|
|
49
|
49
|
"""Class to run a sequence of commands through an interactive program."""
|
|
50
|
50
|
|
|
51
|
51
|
def __init__(self,program,prompts,args=None):
|
|
52
|
52
|
"""Construct a runner.
|
|
53
|
53
|
|
|
54
|
54
|
Inputs:
|
|
55
|
55
|
|
|
56
|
56
|
- program: command to execute the given program.
|
|
57
|
57
|
|
|
58
|
58
|
- prompts: a list of patterns to match as valid prompts, in the
|
|
59
|
59
|
format used by pexpect. This basically means that it can be either
|
|
60
|
60
|
a string (to be compiled as a regular expression) or a list of such
|
|
61
|
61
|
(it must be a true list, as pexpect does type checks).
|
|
62
|
62
|
|
|
63
|
63
|
If more than one prompt is given, the first is treated as the main
|
|
64
|
64
|
program prompt and the others as 'continuation' prompts, like
|
|
65
|
65
|
python's. This means that blank lines in the input source are
|
|
66
|
66
|
ommitted when the first prompt is matched, but are NOT ommitted when
|
|
67
|
67
|
the continuation one matches, since this is how python signals the
|
|
68
|
68
|
end of multiline input interactively.
|
|
69
|
69
|
|
|
70
|
70
|
Optional inputs:
|
|
71
|
71
|
|
|
72
|
72
|
- args(None): optional list of strings to pass as arguments to the
|
|
73
|
73
|
child program.
|
|
|
74
|
|
|
|
75
|
Public members not parameterized in the constructor:
|
|
|
76
|
|
|
|
77
|
- delaybeforesend(0): Newer versions of pexpect have a delay before
|
|
|
78
|
sending each new input. For our purposes here, it's typically best
|
|
|
79
|
to just set this to zero, but if you encounter reliability problems
|
|
|
80
|
or want an interactive run to pause briefly at each prompt, just
|
|
|
81
|
increase this value (it is measured in seconds). Note that this
|
|
|
82
|
variable is not honored at all by older versions of pexpect.
|
|
74
|
83
|
"""
|
|
75
|
84
|
|
|
76
|
85
|
self.program = program
|
|
77
|
86
|
self.prompts = prompts
|
|
78
|
87
|
if args is None: args = []
|
|
79
|
88
|
self.args = args
|
|
|
89
|
# Other public members which we don't make as parameters, but which
|
|
|
90
|
# users may occasionally want to tweak
|
|
|
91
|
self.delaybeforesend = 0
|
|
80
|
92
|
|
|
81
|
93
|
def run_file(self,fname,interact=False):
|
|
82
|
94
|
"""Run the given file interactively.
|
|
83
|
95
|
|
|
84
|
96
|
Inputs:
|
|
85
|
97
|
|
|
86
|
98
|
-fname: name of the file to execute.
|
|
87
|
99
|
|
|
88
|
100
|
See the run_source docstring for the meaning of the optional
|
|
89
|
101
|
arguments."""
|
|
90
|
102
|
|
|
91
|
103
|
fobj = open(fname,'r')
|
|
92
|
104
|
try:
|
|
93
|
105
|
self.run_source(fobj,interact)
|
|
94
|
106
|
finally:
|
|
95
|
107
|
fobj.close()
|
|
96
|
108
|
|
|
97
|
109
|
def run_source(self,source,interact=False):
|
|
98
|
110
|
"""Run the given source code interactively.
|
|
99
|
111
|
|
|
100
|
112
|
Inputs:
|
|
101
|
113
|
|
|
102
|
114
|
- source: a string of code to be executed, or an open file object we
|
|
103
|
115
|
can iterate over.
|
|
104
|
116
|
|
|
105
|
117
|
Optional inputs:
|
|
106
|
118
|
|
|
107
|
119
|
- interact(False): if true, start to interact with the running
|
|
108
|
120
|
program at the end of the script. Otherwise, just exit.
|
|
109
|
121
|
"""
|
|
110
|
122
|
|
|
111
|
123
|
# if the source is a string, chop it up in lines so we can iterate
|
|
112
|
124
|
# over it just as if it were an open file.
|
|
113
|
125
|
if not isinstance(source,file):
|
|
114
|
126
|
source = source.splitlines(True)
|
|
115
|
127
|
|
|
116
|
128
|
# grab the true write method of stdout, in case anything later
|
|
117
|
129
|
# reassigns sys.stdout, so that we really are writing to the true
|
|
118
|
130
|
# stdout and not to something else.
|
|
119
|
131
|
write = sys.stdout.write
|
|
120
|
132
|
|
|
121
|
133
|
c = pexpect.spawn(self.program,self.args,timeout=None)
|
|
|
134
|
c.delaybeforesend = self.delaybeforesend
|
|
122
|
135
|
|
|
123
|
136
|
prompts = c.compile_pattern_list(self.prompts)
|
|
124
|
137
|
|
|
125
|
138
|
prompt_idx = c.expect_list(prompts)
|
|
126
|
139
|
# Flag whether the script ends normally or not, to know whether we can
|
|
127
|
140
|
# do anything further with the underlying process.
|
|
128
|
141
|
end_normal = True
|
|
129
|
142
|
for cmd in source:
|
|
130
|
143
|
# skip blank lines for all matches to the 'main' prompt, while the
|
|
131
|
144
|
# secondary prompts do not
|
|
132
|
145
|
if prompt_idx==0 and cmd.isspace():
|
|
133
|
146
|
continue
|
|
134
|
147
|
|
|
135
|
148
|
write(c.after)
|
|
136
|
149
|
c.send(cmd)
|
|
137
|
150
|
try:
|
|
138
|
151
|
prompt_idx = c.expect_list(prompts)
|
|
139
|
152
|
except pexpect.EOF:
|
|
140
|
153
|
# this will happen if the child dies unexpectedly
|
|
141
|
154
|
write(c.before)
|
|
142
|
155
|
end_normal = False
|
|
143
|
156
|
break
|
|
144
|
157
|
write(c.before)
|
|
145
|
158
|
|
|
146
|
159
|
if isinstance(source,file):
|
|
147
|
160
|
source.close()
|
|
148
|
161
|
|
|
149
|
162
|
if end_normal:
|
|
150
|
163
|
if interact:
|
|
151
|
164
|
c.send('\n')
|
|
152
|
165
|
print '<< Starting interactive mode >>',
|
|
153
|
166
|
try:
|
|
154
|
167
|
c.interact()
|
|
155
|
168
|
except OSError:
|
|
156
|
169
|
# This is what fires when the child stops. Simply print a
|
|
157
|
170
|
# newline so the system prompt is aligned. The extra
|
|
158
|
171
|
# space is there to make sure it gets printed, otherwise
|
|
159
|
172
|
# OS buffering sometimes just suppresses it.
|
|
160
|
173
|
write(' \n')
|
|
161
|
174
|
sys.stdout.flush()
|
|
162
|
175
|
else:
|
|
163
|
176
|
c.close()
|
|
164
|
177
|
else:
|
|
165
|
178
|
if interact:
|
|
166
|
179
|
e="Further interaction is not possible: child process is dead."
|
|
167
|
180
|
print >> sys.stderr, e
|
|
168
|
181
|
|
|
169
|
182
|
def main(self,argv=None):
|
|
170
|
183
|
"""Run as a command-line script."""
|
|
171
|
184
|
|
|
172
|
185
|
parser = optparse.OptionParser(usage=USAGE % self.__class__.__name__)
|
|
173
|
186
|
newopt = parser.add_option
|
|
174
|
187
|
newopt('-i','--interact',action='store_true',default=False,
|
|
175
|
188
|
help='Interact with the program after the script is run.')
|
|
176
|
189
|
|
|
177
|
190
|
opts,args = parser.parse_args(argv)
|
|
178
|
191
|
|
|
179
|
192
|
if len(args) != 1:
|
|
180
|
193
|
print >> sys.stderr,"You must supply exactly one file to run."
|
|
181
|
194
|
sys.exit(1)
|
|
182
|
195
|
|
|
183
|
196
|
self.run_file(args[0],opts.interact)
|
|
184
|
197
|
|
|
185
|
198
|
|
|
186
|
199
|
# Specific runners for particular programs
|
|
187
|
200
|
class IPythonRunner(InteractiveRunner):
|
|
188
|
201
|
"""Interactive IPython runner.
|
|
189
|
202
|
|
|
190
|
203
|
This initalizes IPython in 'nocolor' mode for simplicity. This lets us
|
|
191
|
204
|
avoid having to write a regexp that matches ANSI sequences, though pexpect
|
|
192
|
205
|
does support them. If anyone contributes patches for ANSI color support,
|
|
193
|
206
|
they will be welcome.
|
|
194
|
207
|
|
|
195
|
208
|
It also sets the prompts manually, since the prompt regexps for
|
|
196
|
209
|
pexpect need to be matched to the actual prompts, so user-customized
|
|
197
|
210
|
prompts would break this.
|
|
198
|
211
|
"""
|
|
199
|
212
|
|
|
200
|
213
|
def __init__(self,program = 'ipython',args=None):
|
|
201
|
214
|
"""New runner, optionally passing the ipython command to use."""
|
|
202
|
215
|
|
|
203
|
216
|
args0 = ['-colors','NoColor',
|
|
204
|
217
|
'-pi1','In [\\#]: ',
|
|
205
|
218
|
'-pi2',' .\\D.: ']
|
|
206
|
219
|
if args is None: args = args0
|
|
207
|
220
|
else: args = args0 + args
|
|
208
|
221
|
prompts = [r'In \[\d+\]: ',r' \.*: ']
|
|
209
|
222
|
InteractiveRunner.__init__(self,program,prompts,args)
|
|
210
|
223
|
|
|
211
|
224
|
|
|
212
|
225
|
class PythonRunner(InteractiveRunner):
|
|
213
|
226
|
"""Interactive Python runner."""
|
|
214
|
227
|
|
|
215
|
228
|
def __init__(self,program='python',args=None):
|
|
216
|
229
|
"""New runner, optionally passing the python command to use."""
|
|
217
|
230
|
|
|
218
|
231
|
prompts = [r'>>> ',r'\.\.\. ']
|
|
219
|
232
|
InteractiveRunner.__init__(self,program,prompts,args)
|
|
220
|
233
|
|
|
221
|
234
|
|
|
222
|
235
|
class SAGERunner(InteractiveRunner):
|
|
223
|
236
|
"""Interactive SAGE runner.
|
|
224
|
237
|
|
|
225
|
|
XXX - This class is currently untested, meant for feedback from the SAGE
|
|
226
|
|
team. """
|
|
|
238
|
WARNING: this runner only works if you manually configure your SAGE copy
|
|
|
239
|
to use 'colors NoColor' in the ipythonrc config file, since currently the
|
|
|
240
|
prompt matching regexp does not identify color sequences."""
|
|
227
|
241
|
|
|
228
|
242
|
def __init__(self,program='sage',args=None):
|
|
229
|
243
|
"""New runner, optionally passing the sage command to use."""
|
|
230
|
|
print 'XXX - This class is currently untested!!!'
|
|
231
|
|
print 'It is a placeholder, meant for feedback from the SAGE team.'
|
|
232
|
244
|
|
|
233
|
245
|
prompts = ['sage: ',r'\s*\.\.\. ']
|
|
234
|
246
|
InteractiveRunner.__init__(self,program,prompts,args)
|
|
235
|
247
|
|
|
236
|
248
|
# Global usage string, to avoid indentation issues if typed in a function def.
|
|
237
|
249
|
MAIN_USAGE = """
|
|
238
|
250
|
%prog [options] file_to_run
|
|
239
|
251
|
|
|
240
|
252
|
This is an interface to the various interactive runners available in this
|
|
241
|
253
|
module. If you want to pass specific options to one of the runners, you need
|
|
242
|
254
|
to first terminate the main options with a '--', and then provide the runner's
|
|
243
|
255
|
options. For example:
|
|
244
|
256
|
|
|
245
|
257
|
irunner.py --python -- --help
|
|
246
|
258
|
|
|
247
|
259
|
will pass --help to the python runner. Similarly,
|
|
248
|
260
|
|
|
249
|
|
irunner.py --ipython -- --log test.log script.ipy
|
|
|
261
|
irunner.py --ipython -- --interact script.ipy
|
|
|
262
|
|
|
|
263
|
will run the script.ipy file under the IPython runner, and then will start to
|
|
|
264
|
interact with IPython at the end of the script (instead of exiting).
|
|
|
265
|
|
|
|
266
|
The already implemented runners are listed below; adding one for a new program
|
|
|
267
|
is a trivial task, see the source for examples.
|
|
250
|
268
|
|
|
251
|
|
will run the script.ipy file under the IPython runner, logging all output into
|
|
252
|
|
the test.log file.
|
|
|
269
|
WARNING: the SAGE runner only works if you manually configure your SAGE copy
|
|
|
270
|
to use 'colors NoColor' in the ipythonrc config file, since currently the
|
|
|
271
|
prompt matching regexp does not identify color sequences.
|
|
253
|
272
|
"""
|
|
254
|
273
|
|
|
255
|
274
|
def main():
|
|
256
|
275
|
"""Run as a command-line script."""
|
|
257
|
276
|
|
|
258
|
277
|
parser = optparse.OptionParser(usage=MAIN_USAGE)
|
|
259
|
278
|
newopt = parser.add_option
|
|
260
|
279
|
parser.set_defaults(mode='ipython')
|
|
261
|
280
|
newopt('--ipython',action='store_const',dest='mode',const='ipython',
|
|
262
|
281
|
help='IPython interactive runner (default).')
|
|
263
|
282
|
newopt('--python',action='store_const',dest='mode',const='python',
|
|
264
|
283
|
help='Python interactive runner.')
|
|
265
|
284
|
newopt('--sage',action='store_const',dest='mode',const='sage',
|
|
266
|
|
help='SAGE interactive runner - UNTESTED.')
|
|
|
285
|
help='SAGE interactive runner.')
|
|
267
|
286
|
|
|
268
|
287
|
opts,args = parser.parse_args()
|
|
269
|
288
|
runners = dict(ipython=IPythonRunner,
|
|
270
|
289
|
python=PythonRunner,
|
|
271
|
290
|
sage=SAGERunner)
|
|
272
|
291
|
runners[opts.mode]().main(args)
|
|
273
|
292
|
|
|
274
|
293
|
if __name__ == '__main__':
|
|
275
|
294
|
main()
|