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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: multiCMD
3
- Version: 1.13
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
- optional arguments:
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 return the standard error output
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 return the standard error output
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.13'
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.name, self.command, self.returncode, self.stdout, self.stderr])
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 return the standard error output
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, dry_run, with_stdErr, return_code_only, return_object)[0]
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 return the standard error output
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
- commands = [command.split() if isinstance(command,str) else command for command in commands]
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 commands]
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(commands)
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
- for thread in threads:
216
- thread.join()
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
- # parser.add_argument('-p','--parse', action='store_true',help='parse ranged input')
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()
@@ -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,,