multiCMD 1.13__py3-none-any.whl → 1.16__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.
- {multiCMD-1.13.dist-info → multiCMD-1.16.dist-info}/METADATA +34 -8
- multiCMD-1.16.dist-info/RECORD +7 -0
- multiCMD.py +141 -20
- multiCMD-1.13.dist-info/RECORD +0 -7
- {multiCMD-1.13.dist-info → multiCMD-1.16.dist-info}/LICENSE +0 -0
- {multiCMD-1.13.dist-info → multiCMD-1.16.dist-info}/WHEEL +0 -0
- {multiCMD-1.13.dist-info → multiCMD-1.16.dist-info}/entry_points.txt +0 -0
- {multiCMD-1.13.dist-info → multiCMD-1.16.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: multiCMD
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.16
|
4
4
|
Summary: Run commands simultaneously
|
5
5
|
Home-page: https://github.com/yufei-pan/multiCMD
|
6
6
|
Author: Yufei Pan
|
@@ -8,6 +8,7 @@ Author-email: pan@zopyr.us
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
10
10
|
Classifier: Operating System :: POSIX :: Linux
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
11
12
|
Requires-Python: >=3.6
|
12
13
|
Description-Content-Type: text/markdown
|
13
14
|
License-File: LICENSE
|
@@ -31,6 +32,10 @@ Can be used in bash scripts for automation actions.
|
|
31
32
|
|
32
33
|
Also able to be imported and act as a wrapper for subprocess.
|
33
34
|
|
35
|
+
Use return_object=True with run_commands or run_command to get the Task Object (definition below)
|
36
|
+
|
37
|
+
Use quiet=True and wait_for_return=False to create a daemon thread that async updates the return list / objects when return comes
|
38
|
+
|
34
39
|
For each process, it will initialize a thread if using -m/--max_threads > 1
|
35
40
|
|
36
41
|
For each thread, it will use subprocess lib to open a process for the command task
|
@@ -68,8 +73,9 @@ Run multiple commands in parallel
|
|
68
73
|
positional arguments:
|
69
74
|
command commands to run
|
70
75
|
|
71
|
-
|
76
|
+
options:
|
72
77
|
-h, --help show this help message and exit
|
78
|
+
-p, --parse Parse ranged input and expand them into multiple commands
|
73
79
|
-t timeout, --timeout timeout
|
74
80
|
timeout for each command
|
75
81
|
-m max_threads, --max_threads max_threads
|
@@ -80,25 +86,27 @@ optional arguments:
|
|
80
86
|
|
81
87
|
```python
|
82
88
|
def run_commands(commands, timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,
|
83
|
-
return_code_only=False,return_object=False):
|
89
|
+
return_code_only=False,return_object=False, parse = False, wait_for_return = True):
|
84
90
|
'''
|
85
91
|
Run multiple commands in parallel
|
86
92
|
|
87
93
|
@params:
|
88
|
-
commands: A list of commands to run
|
94
|
+
commands: A list of commands to run ( list[str] | list[list[str]] )
|
89
95
|
timeout: The timeout for each command
|
90
96
|
max_threads: The maximum number of threads to use
|
91
97
|
quiet: Whether to suppress output
|
92
98
|
dry_run: Whether to simulate running the commands
|
93
|
-
with_stdErr: Whether to
|
99
|
+
with_stdErr: Whether to append the standard error output to the standard output
|
94
100
|
return_code_only: Whether to return only the return code
|
95
101
|
return_object: Whether to return the Task object
|
102
|
+
parse: Whether to parse ranged input
|
103
|
+
wait_for_return: Whether to wait for the return of the commands
|
96
104
|
|
97
105
|
@returns:
|
98
106
|
list: The output of the commands ( list[None] | list[int] | list[list[str]] | list[Task] )
|
99
|
-
|
107
|
+
'''
|
100
108
|
def run_command(command, timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,
|
101
|
-
return_code_only=False,return_object=False):
|
109
|
+
return_code_only=False,return_object=False,wait_for_return=True):
|
102
110
|
'''
|
103
111
|
Run a command
|
104
112
|
|
@@ -108,13 +116,25 @@ def run_command(command, timeout=0,max_threads=1,quiet=False,dry_run=False,with_
|
|
108
116
|
max_threads: The maximum number of threads to use
|
109
117
|
quiet: Whether to suppress output
|
110
118
|
dry_run: Whether to simulate running the command
|
111
|
-
with_stdErr: Whether to
|
119
|
+
with_stdErr: Whether to append the standard error output to the standard output
|
112
120
|
return_code_only: Whether to return only the return code
|
113
121
|
return_object: Whether to return the Task object
|
122
|
+
wait_for_return: Whether to wait for the return of the command
|
114
123
|
|
115
124
|
@returns:
|
116
125
|
None | int | list[str] | Task: The output of the command
|
117
126
|
'''
|
127
|
+
def join_threads(threads=__running_threads,timeout=None):
|
128
|
+
'''
|
129
|
+
Join threads
|
130
|
+
|
131
|
+
@params:
|
132
|
+
threads: The threads to join
|
133
|
+
timeout: The timeout
|
134
|
+
|
135
|
+
@returns:
|
136
|
+
None
|
137
|
+
'''
|
118
138
|
def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
|
119
139
|
"""
|
120
140
|
Read an input from the user with a timeout and a countdown.
|
@@ -160,4 +180,10 @@ def int_to_color(n, brightness_threshold=500):
|
|
160
180
|
@returns:
|
161
181
|
(int,int,int): The RGB color
|
162
182
|
'''
|
183
|
+
class Task:
|
184
|
+
def __init__(self, command):
|
185
|
+
self.command = command
|
186
|
+
self.returncode = None
|
187
|
+
self.stdout = []
|
188
|
+
self.stderr = []
|
163
189
|
```
|
@@ -0,0 +1,7 @@
|
|
1
|
+
multiCMD.py,sha256=4sGtNKYAdZihHWtXbKHLaqLqJrnPVlEbhu8hbT2lxmw,16130
|
2
|
+
multiCMD-1.16.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
3
|
+
multiCMD-1.16.dist-info/METADATA,sha256=ZeWIRiZ6OhbiFwoVNxzgxxnxH6Q1HASXJSzOJqfI2sg,5758
|
4
|
+
multiCMD-1.16.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
5
|
+
multiCMD-1.16.dist-info/entry_points.txt,sha256=nSLBkYrcUCQxt1w3LIJkvgOhpRYEC0xAPqNG7u4OYs8,89
|
6
|
+
multiCMD-1.16.dist-info/top_level.txt,sha256=DSqgftD40G09F3qEjpHRCUNUsGUvGZZG69Sm3YEUiWI,9
|
7
|
+
multiCMD-1.16.dist-info/RECORD,,
|
multiCMD.py
CHANGED
@@ -7,10 +7,14 @@ import sys
|
|
7
7
|
import subprocess
|
8
8
|
import select
|
9
9
|
import os
|
10
|
+
import string
|
11
|
+
import re
|
12
|
+
import itertools
|
10
13
|
|
11
|
-
version = '1.
|
14
|
+
version = '1.16'
|
12
15
|
__version__ = version
|
13
16
|
|
17
|
+
__running_threads = []
|
14
18
|
class Task:
|
15
19
|
def __init__(self, command):
|
16
20
|
self.command = command
|
@@ -18,7 +22,65 @@ class Task:
|
|
18
22
|
self.stdout = []
|
19
23
|
self.stderr = []
|
20
24
|
def __iter__(self):
|
21
|
-
return zip(['command', 'returncode', 'stdout', 'stderr'], [self.
|
25
|
+
return zip(['command', 'returncode', 'stdout', 'stderr'], [self.command, self.returncode, self.stdout, self.stderr])
|
26
|
+
def __repr__(self):
|
27
|
+
return f'Task(command={self.command}, returncode={self.returncode}, stdout={self.stdout}, stderr={self.stderr})'
|
28
|
+
def __str__(self):
|
29
|
+
return str(dict(self))
|
30
|
+
|
31
|
+
def _expand_ranges(inStr):
|
32
|
+
'''
|
33
|
+
Expand ranges in a string
|
34
|
+
|
35
|
+
@params:
|
36
|
+
inStr: The string to expand
|
37
|
+
|
38
|
+
@returns:
|
39
|
+
list[str]: The expanded string
|
40
|
+
'''
|
41
|
+
expandingStr = [inStr]
|
42
|
+
expandedList = []
|
43
|
+
# all valid alphanumeric characters
|
44
|
+
alphanumeric = string.digits + string.ascii_letters
|
45
|
+
while len(expandingStr) > 0:
|
46
|
+
currentStr = expandingStr.pop()
|
47
|
+
match = re.search(r'\[(.*?)]', currentStr)
|
48
|
+
if not match:
|
49
|
+
expandedList.append(currentStr)
|
50
|
+
continue
|
51
|
+
group = match.group(1)
|
52
|
+
parts = group.split(',')
|
53
|
+
for part in parts:
|
54
|
+
part = part.strip()
|
55
|
+
if '-' in part:
|
56
|
+
try:
|
57
|
+
range_start,_, range_end = part.partition('-')
|
58
|
+
except ValueError:
|
59
|
+
expandedList.append(currentStr)
|
60
|
+
continue
|
61
|
+
range_start = range_start.strip()
|
62
|
+
range_end = range_end.strip()
|
63
|
+
if range_start.isdigit() and range_end.isdigit():
|
64
|
+
padding_length = min(len(range_start), len(range_end))
|
65
|
+
format_str = "{:0" + str(padding_length) + "d}"
|
66
|
+
for i in range(int(range_start), int(range_end) + 1):
|
67
|
+
formatted_i = format_str.format(i)
|
68
|
+
expandingStr.append(currentStr.replace(match.group(0), formatted_i, 1))
|
69
|
+
elif all(c in string.hexdigits for c in range_start + range_end):
|
70
|
+
for i in range(int(range_start, 16), int(range_end, 16) + 1):
|
71
|
+
expandingStr.append(currentStr.replace(match.group(0), format(i, 'x'), 1))
|
72
|
+
else:
|
73
|
+
try:
|
74
|
+
start_index = alphanumeric.index(range_start)
|
75
|
+
end_index = alphanumeric.index(range_end)
|
76
|
+
for i in range(start_index, end_index + 1):
|
77
|
+
expandingStr.append(currentStr.replace(match.group(0), alphanumeric[i], 1))
|
78
|
+
except ValueError:
|
79
|
+
expandedList.append(currentStr)
|
80
|
+
else:
|
81
|
+
expandingStr.append(currentStr.replace(match.group(0), part, 1))
|
82
|
+
expandedList.reverse()
|
83
|
+
return expandedList
|
22
84
|
|
23
85
|
def __handle_stream(stream,target,pre='',post='',quiet=False):
|
24
86
|
'''
|
@@ -120,11 +182,11 @@ def __run_command(task,sem, timeout=60, quiet=False,dry_run=False,with_stdErr=Fa
|
|
120
182
|
#host.stdout = []
|
121
183
|
proc = subprocess.Popen(task.command,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE)
|
122
184
|
# create a thread to handle stdout
|
123
|
-
stdout_thread = threading.Thread(target=__handle_stream, args=(proc.stdout,task.stdout,pre,post,quiet))
|
185
|
+
stdout_thread = threading.Thread(target=__handle_stream, args=(proc.stdout,task.stdout,pre,post,quiet),daemon=True)
|
124
186
|
stdout_thread.start()
|
125
187
|
# create a thread to handle stderr
|
126
188
|
#host.stderr = []
|
127
|
-
stderr_thread = threading.Thread(target=__handle_stream, args=(proc.stderr,task.stderr,pre,post,quiet))
|
189
|
+
stderr_thread = threading.Thread(target=__handle_stream, args=(proc.stderr,task.stderr,pre,post,quiet),daemon=True)
|
128
190
|
stderr_thread.start()
|
129
191
|
# Monitor the subprocess and terminate it after the timeout
|
130
192
|
start_time = time.time()
|
@@ -163,7 +225,7 @@ def __run_command(task,sem, timeout=60, quiet=False,dry_run=False,with_stdErr=Fa
|
|
163
225
|
return task.stdout
|
164
226
|
|
165
227
|
def run_command(command, timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,
|
166
|
-
return_code_only=False,return_object=False):
|
228
|
+
return_code_only=False,return_object=False,wait_for_return=True):
|
167
229
|
'''
|
168
230
|
Run a command
|
169
231
|
|
@@ -173,47 +235,93 @@ def run_command(command, timeout=0,max_threads=1,quiet=False,dry_run=False,with_
|
|
173
235
|
max_threads: The maximum number of threads to use
|
174
236
|
quiet: Whether to suppress output
|
175
237
|
dry_run: Whether to simulate running the command
|
176
|
-
with_stdErr: Whether to
|
238
|
+
with_stdErr: Whether to append the standard error output to the standard output
|
177
239
|
return_code_only: Whether to return only the return code
|
178
240
|
return_object: Whether to return the Task object
|
241
|
+
wait_for_return: Whether to wait for the return of the command
|
179
242
|
|
180
243
|
@returns:
|
181
244
|
None | int | list[str] | Task: The output of the command
|
182
245
|
'''
|
183
|
-
return run_commands([command], timeout, max_threads, quiet,
|
246
|
+
return run_commands(commands=[command], timeout=timeout, max_threads=max_threads, quiet=quiet,
|
247
|
+
dry_run=dry_run, with_stdErr=with_stdErr, return_code_only=return_code_only,
|
248
|
+
return_object=return_object,parse=False,wait_for_return=wait_for_return)[0]
|
249
|
+
|
250
|
+
def __format_command(command,expand = False):
|
251
|
+
'''
|
252
|
+
Format a command
|
253
|
+
|
254
|
+
@params:
|
255
|
+
command: The command to format
|
256
|
+
expand: Whether to expand ranges
|
257
|
+
|
258
|
+
@returns:
|
259
|
+
list[list[str]]: The formatted commands sequence
|
260
|
+
'''
|
261
|
+
if isinstance(command,str):
|
262
|
+
if expand:
|
263
|
+
commands = _expand_ranges(command)
|
264
|
+
else:
|
265
|
+
commands = [command]
|
266
|
+
return [command.split() for command in commands]
|
267
|
+
# elif it is a iterable
|
268
|
+
elif hasattr(command,'__iter__'):
|
269
|
+
sanitized_command = []
|
270
|
+
for field in command:
|
271
|
+
if isinstance(field,str):
|
272
|
+
sanitized_command.append(field)
|
273
|
+
else:
|
274
|
+
sanitized_command.append(repr(field))
|
275
|
+
if not expand:
|
276
|
+
return [sanitized_command]
|
277
|
+
sanitized_expanded_command = [_expand_ranges(field) for field in sanitized_command]
|
278
|
+
# now the command had been expanded to list of list of fields with each field expanded to all possible options
|
279
|
+
# we need to generate all possible combinations of the fields
|
280
|
+
# we will use the cartesian product to do this
|
281
|
+
commands = list(itertools.product(*sanitized_expanded_command))
|
282
|
+
return [list(command) for command in commands]
|
283
|
+
else:
|
284
|
+
return __format_command(str(command),expand=expand)
|
184
285
|
|
185
286
|
def run_commands(commands, timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,
|
186
|
-
return_code_only=False,return_object=False):
|
287
|
+
return_code_only=False,return_object=False, parse = False, wait_for_return = True):
|
187
288
|
'''
|
188
289
|
Run multiple commands in parallel
|
189
290
|
|
190
291
|
@params:
|
191
|
-
commands: A list of commands to run
|
292
|
+
commands: A list of commands to run ( list[str] | list[list[str]] )
|
192
293
|
timeout: The timeout for each command
|
193
294
|
max_threads: The maximum number of threads to use
|
194
295
|
quiet: Whether to suppress output
|
195
296
|
dry_run: Whether to simulate running the commands
|
196
|
-
with_stdErr: Whether to
|
297
|
+
with_stdErr: Whether to append the standard error output to the standard output
|
197
298
|
return_code_only: Whether to return only the return code
|
198
299
|
return_object: Whether to return the Task object
|
300
|
+
parse: Whether to parse ranged input
|
301
|
+
wait_for_return: Whether to wait for the return of the commands
|
199
302
|
|
200
303
|
@returns:
|
201
304
|
list: The output of the commands ( list[None] | list[int] | list[list[str]] | list[Task] )
|
202
305
|
'''
|
203
306
|
# split the commands in commands if it is a string
|
204
|
-
|
307
|
+
formatedCommands = []
|
308
|
+
for command in commands:
|
309
|
+
formatedCommands.extend(__format_command(command,expand=parse))
|
205
310
|
# initialize the tasks
|
206
|
-
tasks = [Task(command) for command in
|
311
|
+
tasks = [Task(command) for command in formatedCommands]
|
207
312
|
# run the tasks with max_threads. if max_threads is 0, use the number of commands
|
208
313
|
if max_threads < 1:
|
209
|
-
max_threads = len(
|
210
|
-
if max_threads > 1:
|
314
|
+
max_threads = len(formatedCommands)
|
315
|
+
if max_threads > 1 or not wait_for_return:
|
211
316
|
sem = threading.Semaphore(max_threads) # Limit concurrent sessions
|
212
|
-
threads = [threading.Thread(target=__run_command, args=(task,sem,timeout,quiet,dry_run,...)) for task in tasks]
|
317
|
+
threads = [threading.Thread(target=__run_command, args=(task,sem,timeout,quiet,dry_run,...),daemon=True) for task in tasks]
|
213
318
|
for thread in threads:
|
214
319
|
thread.start()
|
215
|
-
|
216
|
-
thread
|
320
|
+
if wait_for_return:
|
321
|
+
for thread in threads:
|
322
|
+
thread.join()
|
323
|
+
else:
|
324
|
+
__running_threads.extend(threads)
|
217
325
|
else:
|
218
326
|
# just process the commands sequentially
|
219
327
|
sem = threading.Semaphore(1)
|
@@ -229,6 +337,20 @@ def run_commands(commands, timeout=0,max_threads=1,quiet=False,dry_run=False,wit
|
|
229
337
|
else:
|
230
338
|
return [task.stdout for task in tasks]
|
231
339
|
|
340
|
+
def join_threads(threads=__running_threads,timeout=None):
|
341
|
+
'''
|
342
|
+
Join threads
|
343
|
+
|
344
|
+
@params:
|
345
|
+
threads: The threads to join
|
346
|
+
timeout: The timeout
|
347
|
+
|
348
|
+
@returns:
|
349
|
+
None
|
350
|
+
'''
|
351
|
+
for thread in threads:
|
352
|
+
thread.join(timeout=timeout)
|
353
|
+
|
232
354
|
def input_with_timeout_and_countdown(timeout, prompt='Please enter your selection'):
|
233
355
|
"""
|
234
356
|
Read an input from the user with a timeout and a countdown.
|
@@ -361,17 +483,16 @@ def print_progress_bar(iteration, total, prefix='', suffix=''):
|
|
361
483
|
if iteration % 5 == 0:
|
362
484
|
print(_genrate_progress_bar(iteration, total, prefix, suffix))
|
363
485
|
|
364
|
-
|
365
486
|
def main():
|
366
487
|
parser = argparse.ArgumentParser(description='Run multiple commands in parallel')
|
367
488
|
parser.add_argument('commands', metavar='command', type=str, nargs='+',help='commands to run')
|
368
|
-
|
489
|
+
parser.add_argument('-p','--parse', action='store_true',help='Parse ranged input and expand them into multiple commands')
|
369
490
|
parser.add_argument('-t','--timeout', metavar='timeout', type=int, default=60,help='timeout for each command')
|
370
491
|
parser.add_argument('-m','--max_threads', metavar='max_threads', type=int, default=1,help='maximum number of threads to use')
|
371
492
|
parser.add_argument('-q','--quiet', action='store_true',help='quiet mode')
|
372
493
|
parser.add_argument('-V','--version', action='version', version=f'%(prog)s {version} by pan@zopyr.us')
|
373
494
|
args = parser.parse_args()
|
374
|
-
run_commands(args.commands, args.timeout, args.max_threads, args.quiet)
|
495
|
+
run_commands(args.commands, args.timeout, args.max_threads, args.quiet,parse = args.parse)
|
375
496
|
|
376
497
|
if __name__ == '__main__':
|
377
498
|
main()
|
multiCMD-1.13.dist-info/RECORD
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
multiCMD.py,sha256=_agL0neMTFIiApwMJ-Pg0Sygc267k1qbaGku0ROP34E,12082
|
2
|
-
multiCMD-1.13.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
3
|
-
multiCMD-1.13.dist-info/METADATA,sha256=RraYvntxzCblSCs8ccYpsR6ISfnjn71V6ik3XO3ih4M,4772
|
4
|
-
multiCMD-1.13.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
5
|
-
multiCMD-1.13.dist-info/entry_points.txt,sha256=nSLBkYrcUCQxt1w3LIJkvgOhpRYEC0xAPqNG7u4OYs8,89
|
6
|
-
multiCMD-1.13.dist-info/top_level.txt,sha256=DSqgftD40G09F3qEjpHRCUNUsGUvGZZG69Sm3YEUiWI,9
|
7
|
-
multiCMD-1.13.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|