skilleter-modules 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skilleter_modules/__init__.py +0 -0
- skilleter_modules/colour.py +328 -0
- skilleter_modules/dc_curses.py +278 -0
- skilleter_modules/dc_defaults.py +221 -0
- skilleter_modules/dc_util.py +50 -0
- skilleter_modules/dircolors.py +310 -0
- skilleter_modules/files.py +154 -0
- skilleter_modules/git.py +1502 -0
- skilleter_modules/gitlab.py +193 -0
- skilleter_modules/path.py +153 -0
- skilleter_modules/popup.py +88 -0
- skilleter_modules/run.py +306 -0
- skilleter_modules/tfm_pane.py +593 -0
- skilleter_modules/tidy.py +177 -0
- skilleter_modules/venv_template.py +47 -0
- skilleter_modules-0.0.1.dist-info/METADATA +61 -0
- skilleter_modules-0.0.1.dist-info/RECORD +20 -0
- skilleter_modules-0.0.1.dist-info/WHEEL +5 -0
- skilleter_modules-0.0.1.dist-info/licenses/LICENSE +619 -0
- skilleter_modules-0.0.1.dist-info/top_level.txt +1 -0
skilleter_modules/run.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Code for running a subprocess, optionally capturing stderr and/or stdout and
|
|
5
|
+
optionally echoing either or both to the console in realtime and storing.
|
|
6
|
+
|
|
7
|
+
Uses threads to capture and output stderr and stdout since this seems to be
|
|
8
|
+
the only way to do it (Popen does not have the ability to output the process
|
|
9
|
+
stdout output to the stdout output).
|
|
10
|
+
|
|
11
|
+
Intended for more versatile replacement for the thingy process.run() function
|
|
12
|
+
which can handle all combinations of foreground/background console/return
|
|
13
|
+
stderr/stdout/both options. """
|
|
14
|
+
|
|
15
|
+
# TODO: This does not run on Python versions <3.5 (so Ubuntu 14.04 is a problem!)
|
|
16
|
+
################################################################################
|
|
17
|
+
|
|
18
|
+
################################################################################
|
|
19
|
+
# Imports
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
import subprocess
|
|
23
|
+
import threading
|
|
24
|
+
import shlex
|
|
25
|
+
|
|
26
|
+
import tidy
|
|
27
|
+
|
|
28
|
+
################################################################################
|
|
29
|
+
|
|
30
|
+
class RunError(Exception):
|
|
31
|
+
""" Run exception """
|
|
32
|
+
|
|
33
|
+
def __init__(self, msg, status=1):
|
|
34
|
+
super().__init__(msg)
|
|
35
|
+
self.msg = msg
|
|
36
|
+
self.status = status
|
|
37
|
+
|
|
38
|
+
################################################################################
|
|
39
|
+
|
|
40
|
+
# TODO: This is the _process() and run() replacement once additional parameters have been implemented
|
|
41
|
+
# as those functions are probably over-specified, so need to work out what functionality is ACTUALLY being used!
|
|
42
|
+
|
|
43
|
+
def command(cmd, show_stdout=False, show_stderr=False):
|
|
44
|
+
"""
|
|
45
|
+
Run an external command and optionally stream its stdout/stderr to the console
|
|
46
|
+
while capturing them.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cmd: Command to run (string or argv list). If a string, it will be split with shlex.split().
|
|
50
|
+
show_stdout: If True, echo stdout lines to the console as they arrive.
|
|
51
|
+
show_stderr: If True, echo stderr lines to the console as they arrive.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
(returncode, stdout_lines, stderr_lines)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def _pump(stream, sink, echo):
|
|
58
|
+
"""Thread to capture and optionally echo output from subprocess.Popen"""
|
|
59
|
+
|
|
60
|
+
# Read line-by-line until EOF
|
|
61
|
+
|
|
62
|
+
for line in iter(stream.readline, ""):
|
|
63
|
+
# Strip trailing newline when storing; keep original when echoing
|
|
64
|
+
sink.append(line.rstrip("\n"))
|
|
65
|
+
|
|
66
|
+
if echo:
|
|
67
|
+
print(tidy.convert_ansi(line), end="", flush=True)
|
|
68
|
+
|
|
69
|
+
stream.close()
|
|
70
|
+
|
|
71
|
+
# Normalize command to be a string
|
|
72
|
+
|
|
73
|
+
if isinstance(cmd, str):
|
|
74
|
+
cmd = shlex.split(cmd)
|
|
75
|
+
|
|
76
|
+
# Storage for stdout/stderr
|
|
77
|
+
|
|
78
|
+
stdout_lines = []
|
|
79
|
+
stderr_lines = []
|
|
80
|
+
|
|
81
|
+
# Start process with separate pipes; line-buffering for timely reads
|
|
82
|
+
|
|
83
|
+
proc = subprocess.Popen(
|
|
84
|
+
cmd,
|
|
85
|
+
stdout=subprocess.PIPE,
|
|
86
|
+
stderr=subprocess.PIPE,
|
|
87
|
+
text=True, # decode to str
|
|
88
|
+
bufsize=1, # line-buffered
|
|
89
|
+
universal_newlines=True, # compatibility alias
|
|
90
|
+
errors="replace" # avoid crashes on decoding issues
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Threads to read both streams concurrently (prevents deadlocks)
|
|
94
|
+
|
|
95
|
+
t_out = threading.Thread(target=_pump, args=(proc.stdout, stdout_lines, show_stdout))
|
|
96
|
+
t_err = threading.Thread(target=_pump, args=(proc.stderr, stderr_lines, show_stderr))
|
|
97
|
+
|
|
98
|
+
t_out.start()
|
|
99
|
+
t_err.start()
|
|
100
|
+
|
|
101
|
+
# Wait for process to complete and threads to drain
|
|
102
|
+
|
|
103
|
+
returncode = proc.wait()
|
|
104
|
+
t_out.join()
|
|
105
|
+
t_err.join()
|
|
106
|
+
|
|
107
|
+
# Return the status, stdout and stderr
|
|
108
|
+
|
|
109
|
+
return returncode, stdout_lines, stderr_lines
|
|
110
|
+
|
|
111
|
+
################################################################################
|
|
112
|
+
|
|
113
|
+
def capture_output(cmd, input_stream, output_streams):
|
|
114
|
+
""" Capture data from a stream (input_stream), optionally
|
|
115
|
+
outputting it (if output_streams is not None and optionally
|
|
116
|
+
saving it into a variable (data, if not None), terminating
|
|
117
|
+
when the specified command (cmd, which is presumed to be the process
|
|
118
|
+
outputting to the input stream) exits.
|
|
119
|
+
TODO: Use of convert_ansi should be controlled via a parameter (off/light/dark)
|
|
120
|
+
TODO: Another issue is that readline() only returns at EOF or EOL, so if you get a prompt "Continue?" with no newline you do not see it until after you respond to it.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
output = input_stream.readline()
|
|
125
|
+
|
|
126
|
+
if output:
|
|
127
|
+
if output_streams:
|
|
128
|
+
for stream in output_streams:
|
|
129
|
+
if isinstance(stream, list):
|
|
130
|
+
stream.append(output.rstrip())
|
|
131
|
+
else:
|
|
132
|
+
if stream in (sys.stdout, sys.stderr):
|
|
133
|
+
stream.write(tidy.convert_ansi(output))
|
|
134
|
+
else:
|
|
135
|
+
stream.write(output)
|
|
136
|
+
|
|
137
|
+
elif cmd.poll() is not None:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
################################################################################
|
|
141
|
+
|
|
142
|
+
def _process(command,
|
|
143
|
+
stdout=None, stderr=None,
|
|
144
|
+
output=None):
|
|
145
|
+
""" Run an external command.
|
|
146
|
+
|
|
147
|
+
stdout and stderr indicate whether stdout/err are output and/or sent to a file and/or stored in a variable.
|
|
148
|
+
They can be boolean (True: output to sys.stdout/err, False: Do nothing), a file handle or a variable, or an
|
|
149
|
+
array of any number of these (except booleans).
|
|
150
|
+
|
|
151
|
+
If output is True then stdout and stderr are both output as if stdout=True and stderr=True (in addition to
|
|
152
|
+
any other values passed in those parameters)
|
|
153
|
+
|
|
154
|
+
The return value is a tuple consisting of the status code, captured stdout (if any) and captured
|
|
155
|
+
stderr (if any).
|
|
156
|
+
|
|
157
|
+
Will raise OSError if the command could not be run and RunError if the command returned a non-zero status code.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
# If stdout/stderr are booleans then output to stdout/stderr if True, else discard output
|
|
161
|
+
|
|
162
|
+
if isinstance(stdout, bool):
|
|
163
|
+
stdout = sys.stdout if stdout else None
|
|
164
|
+
|
|
165
|
+
if isinstance(stderr, bool):
|
|
166
|
+
stderr = sys.stderr if stderr else None
|
|
167
|
+
|
|
168
|
+
# If stdout/stderr are not arrays then make them so
|
|
169
|
+
|
|
170
|
+
if not isinstance(stdout, list):
|
|
171
|
+
stdout = [stdout] if stdout else []
|
|
172
|
+
|
|
173
|
+
if not isinstance(stderr, list):
|
|
174
|
+
stderr = [stderr] if stderr else []
|
|
175
|
+
|
|
176
|
+
# If output is True then add stderr/out to the list of outputs
|
|
177
|
+
|
|
178
|
+
if output:
|
|
179
|
+
if sys.stdout not in stdout:
|
|
180
|
+
stdout.append(sys.stdout)
|
|
181
|
+
|
|
182
|
+
if sys.stderr not in stderr:
|
|
183
|
+
stderr.append(sys.stderr)
|
|
184
|
+
|
|
185
|
+
# Capture stdout/stderr to arrays
|
|
186
|
+
|
|
187
|
+
stdout_data = []
|
|
188
|
+
stderr_data = []
|
|
189
|
+
|
|
190
|
+
stdout.append(stdout_data)
|
|
191
|
+
stderr.append(stderr_data)
|
|
192
|
+
|
|
193
|
+
if isinstance(command, str):
|
|
194
|
+
command = shlex.split(command, comments=True)
|
|
195
|
+
|
|
196
|
+
# Use a pipe for stdout/stderr if are are capturing it
|
|
197
|
+
# and send it to /dev/null if we don't care about it at all.
|
|
198
|
+
|
|
199
|
+
if stdout == [sys.stdout] and not stderr:
|
|
200
|
+
stdout_stream = subprocess.STDOUT
|
|
201
|
+
stderr_stream = subprocess.DEVNULL
|
|
202
|
+
else:
|
|
203
|
+
stdout_stream = subprocess.PIPE if stdout else subprocess.DEVNULL
|
|
204
|
+
stderr_stream = subprocess.PIPE if stderr else subprocess.DEVNULL
|
|
205
|
+
|
|
206
|
+
# Run the command with no buffering and capturing output if we
|
|
207
|
+
# want it - this will raise OSError if there was a problem running
|
|
208
|
+
# the command.
|
|
209
|
+
|
|
210
|
+
cmd = subprocess.Popen(command,
|
|
211
|
+
bufsize=0,
|
|
212
|
+
stdout=stdout_stream,
|
|
213
|
+
stderr=stderr_stream,
|
|
214
|
+
text=True,
|
|
215
|
+
errors='ignore',
|
|
216
|
+
encoding='ascii')
|
|
217
|
+
|
|
218
|
+
# Create threads to capture stderr and/or stdout if necessary
|
|
219
|
+
|
|
220
|
+
if stdout_stream == subprocess.PIPE:
|
|
221
|
+
stdout_thread = threading.Thread(target=capture_output, args=(cmd, cmd.stdout, stdout), daemon=True)
|
|
222
|
+
stdout_thread.start()
|
|
223
|
+
else:
|
|
224
|
+
stdout_thread = None
|
|
225
|
+
|
|
226
|
+
if stderr_stream == subprocess.PIPE:
|
|
227
|
+
stderr_thread = threading.Thread(target=capture_output, args=(cmd, cmd.stderr, stderr), daemon=True)
|
|
228
|
+
stderr_thread.start()
|
|
229
|
+
else:
|
|
230
|
+
stderr_thread = None
|
|
231
|
+
|
|
232
|
+
# Wait until the command terminates (and set the returncode)
|
|
233
|
+
|
|
234
|
+
if stdout_thread:
|
|
235
|
+
stdout_thread.join()
|
|
236
|
+
|
|
237
|
+
if stderr_thread:
|
|
238
|
+
stderr_thread.join()
|
|
239
|
+
|
|
240
|
+
cmd.wait()
|
|
241
|
+
|
|
242
|
+
# If the command failed, raise an exception
|
|
243
|
+
|
|
244
|
+
if cmd.returncode:
|
|
245
|
+
raise RunError('\n'.join(stderr_data) if stderr_data else 'Error %d running "%s"' % (cmd.returncode, ' '.join(command)))
|
|
246
|
+
|
|
247
|
+
# Return status, stdout, stderr (the latter 2 may be empty if we did not capture data).
|
|
248
|
+
|
|
249
|
+
return {'status': cmd.returncode, 'stdout': stdout_data, 'stderr': stderr_data}
|
|
250
|
+
|
|
251
|
+
################################################################################
|
|
252
|
+
|
|
253
|
+
def run(command,
|
|
254
|
+
stdout=None,
|
|
255
|
+
stderr=None,
|
|
256
|
+
output=None):
|
|
257
|
+
""" Simple interface to the _process() function
|
|
258
|
+
Has the same parameters, with the same defaults.
|
|
259
|
+
The return value is either the data output to stdout, if any
|
|
260
|
+
or the data output to stderr otherwise.
|
|
261
|
+
The status code is not returned, but the function will raise an exception
|
|
262
|
+
if it is non-zero """
|
|
263
|
+
|
|
264
|
+
result = _process(command=command,
|
|
265
|
+
stdout=stdout,
|
|
266
|
+
stderr=stderr,
|
|
267
|
+
output=output)
|
|
268
|
+
|
|
269
|
+
return result['stdout'] if result['stdout'] else result['stderr']
|
|
270
|
+
|
|
271
|
+
################################################################################
|
|
272
|
+
|
|
273
|
+
if __name__ == '__main__':
|
|
274
|
+
def test_run(cmd,
|
|
275
|
+
stdout=None, stderr=None):
|
|
276
|
+
""" Test wrapper for the process() function. """
|
|
277
|
+
|
|
278
|
+
print('-' * 80)
|
|
279
|
+
print('Running: %s' % (cmd if isinstance(cmd, str) else ' '.join(cmd)))
|
|
280
|
+
|
|
281
|
+
result = _process(cmd, stdout=stdout, stderr=stderr)
|
|
282
|
+
|
|
283
|
+
print('Status: %d' % result['status'])
|
|
284
|
+
|
|
285
|
+
def test():
|
|
286
|
+
""" Test code """
|
|
287
|
+
|
|
288
|
+
test_run('echo nothing')
|
|
289
|
+
|
|
290
|
+
test_run(['ls', '-l', 'run_jed'])
|
|
291
|
+
test_run(['ls -l run_*'], stdout=True)
|
|
292
|
+
test_run('false')
|
|
293
|
+
test_run('true', stdout=sys.stdout)
|
|
294
|
+
test_run(['git', 'status'], stdout=sys.stdout, stderr=sys.stderr)
|
|
295
|
+
|
|
296
|
+
test_run(['make'], stderr=sys.stderr)
|
|
297
|
+
test_run(['make'], stdout=sys.stdout, stderr=[sys.stderr])
|
|
298
|
+
test_run(['make'], stdout=True)
|
|
299
|
+
test_run(['make'], stdout=sys.stdout)
|
|
300
|
+
test_run(['make'])
|
|
301
|
+
|
|
302
|
+
output = []
|
|
303
|
+
test_run('ls -l x*; sleep 1; echo "Bye!"', stderr=[sys.stderr, output], stdout=sys.stdout)
|
|
304
|
+
print('Output=%s' % output)
|
|
305
|
+
|
|
306
|
+
test()
|