multiCMD 1.51__tar.gz → 1.52__tar.gz

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.
Files changed (38) hide show
  1. multicmd-1.52/PKG-INFO +436 -0
  2. multicmd-1.52/README.md +412 -0
  3. multicmd-1.52/multiCMD.egg-info/PKG-INFO +436 -0
  4. {multicmd-1.51 → multicmd-1.52}/multiCMD.egg-info/SOURCES.txt +12 -2
  5. multicmd-1.52/multiCMD.py +2341 -0
  6. {multicmd-1.51 → multicmd-1.52}/setup.py +0 -3
  7. multicmd-1.52/tests/test_api.py +53 -0
  8. {multicmd-1.51 → multicmd-1.52}/tests/test_async_executor.py +67 -0
  9. multicmd-1.52/tests/test_cli.py +171 -0
  10. multicmd-1.52/tests/test_cli_input.py +109 -0
  11. multicmd-1.52/tests/test_cli_summary.py +103 -0
  12. multicmd-1.52/tests/test_display.py +115 -0
  13. multicmd-1.52/tests/test_expand_ranges.py +84 -0
  14. multicmd-1.52/tests/test_parsing.py +59 -0
  15. multicmd-1.52/tests/test_pool.py +70 -0
  16. multicmd-1.52/tests/test_process_groups.py +155 -0
  17. multicmd-1.52/tests/test_pty.py +74 -0
  18. multicmd-1.52/tests/test_reader.py +131 -0
  19. multicmd-1.52/tests/test_supervision.py +207 -0
  20. multicmd-1.52/tests/test_windows.py +116 -0
  21. multicmd-1.51/PKG-INFO +0 -288
  22. multicmd-1.51/README.md +0 -262
  23. multicmd-1.51/multiCMD.egg-info/PKG-INFO +0 -288
  24. multicmd-1.51/multiCMD.egg-info/requires.txt +0 -1
  25. multicmd-1.51/multiCMD.py +0 -1394
  26. multicmd-1.51/tests/test_cli.py +0 -45
  27. multicmd-1.51/tests/test_expand_ranges.py +0 -48
  28. {multicmd-1.51 → multicmd-1.52}/multiCMD.egg-info/dependency_links.txt +0 -0
  29. {multicmd-1.51 → multicmd-1.52}/multiCMD.egg-info/entry_points.txt +0 -0
  30. {multicmd-1.51 → multicmd-1.52}/multiCMD.egg-info/top_level.txt +0 -0
  31. {multicmd-1.51 → multicmd-1.52}/setup.cfg +0 -0
  32. {multicmd-1.51 → multicmd-1.52}/tests/test_helpers.py +0 -0
  33. {multicmd-1.51 → multicmd-1.52}/tests/test_ping.py +0 -0
  34. {multicmd-1.51 → multicmd-1.52}/tests/test_regressions.py +0 -0
  35. {multicmd-1.51 → multicmd-1.52}/tests/test_run_command.py +0 -0
  36. {multicmd-1.51 → multicmd-1.52}/tests/test_run_commands.py +0 -0
  37. {multicmd-1.51 → multicmd-1.52}/tests/test_sudo.py +0 -0
  38. {multicmd-1.51 → multicmd-1.52}/tests/test_task.py +0 -0
multicmd-1.52/PKG-INFO ADDED
@@ -0,0 +1,436 @@
1
+ Metadata-Version: 2.4
2
+ Name: multiCMD
3
+ Version: 1.52
4
+ Summary: Run commands simultaneously
5
+ Home-page: https://github.com/yufei-pan/multiCMD
6
+ Author: Yufei Pan
7
+ Author-email: pan@zopyr.us
8
+ License: GPLv3+
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Requires-Python: >=3.6
14
+ Description-Content-Type: text/markdown
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+ # multiCMD
26
+
27
+ Run many commands at the same time — from the shell or from Python.
28
+
29
+ `multiCMD` is a small, dependency-light helper (a single `multiCMD.py` module) for
30
+ launching and supervising multiple subprocesses concurrently. It streams each
31
+ command's output in real time (optionally colorized per command), captures
32
+ stdout/stderr/return codes, supports per-command inactivity timeouts, and can
33
+ expand range patterns like `host[1-10]` into many commands at once.
34
+
35
+ It works both as a command-line tool and as an importable wrapper around
36
+ `subprocess` for your own automation scripts.
37
+
38
+ ## Features
39
+
40
+ - Run a batch of commands in parallel with a bounded pool of worker threads.
41
+ - Live output that stays readable in parallel runs: `[#]` prefixes, per-command colors on a terminal, and progress lines that redraw in place.
42
+ - Capture `stdout`, `stderr`, return code and timing per command via the `Task` object.
43
+ - Per-command **inactivity timeout** that kills the command and everything it started (see [Timeout semantics](#timeout-semantics)).
44
+ - Range/pattern expansion: `host[1-3]`, `[01-03]`, `[a-f]`, `[A-C]`, `[1-2,a-b]`, variables, and `{...}` expressions.
45
+ - Commands from files or stdin (`-f`), `{}` templates filled from a list (`-a`), a `--dry-run` preview, and an end-of-run summary.
46
+ - pty mode, so commands print live instead of in buffered bursts.
47
+ - Fire-and-forget async mode (`wait_for_return=False`) plus an `AsyncExecutor` for managing long-lived batches.
48
+ - Optional `sudo` wrapping, and an opt-in shell mode for pipes and redirects.
49
+ - No third-party runtime dependencies; requires Python >= 3.6.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install multiCMD
55
+ ```
56
+
57
+ This installs the module and three equivalent console entry points:
58
+
59
+ ```bash
60
+ mcmd
61
+ multiCMD
62
+ multicmd
63
+ ```
64
+
65
+ You can also just drop `multiCMD.py` into your project and import it directly.
66
+
67
+ ## Command-line usage
68
+
69
+ ```bash
70
+ $ mcmd -h
71
+ usage: mcmd [-h] [-p] [-t SECONDS] [-m max_threads] [--sudo] [-q] [-n]
72
+ [--shell] [-f FILE] [-a FILE] [--summary | --no-summary]
73
+ [--no-prefix] [--no-color] [--pty | --no-pty] [-V]
74
+ [command ...]
75
+
76
+ Run multiple commands in parallel
77
+
78
+ positional arguments:
79
+ command commands to run ( with -a: templates, {} is replaced
80
+ by each input line )
81
+
82
+ options:
83
+ -h, --help show this help message and exit
84
+ -p, --parse Parse ranged input and expand them into multiple
85
+ commands
86
+ -t, --timeout SECONDS
87
+ kill a command after SECONDS with no output at all ( 0
88
+ = never, the default )
89
+ -m, --max_threads max_threads
90
+ maximum number of commands to run at once ( 0 = all )
91
+ --sudo use sudo for commands
92
+ -q, --quiet quiet mode: hide command output ( the summary still
93
+ shows )
94
+ -n, --dry-run print each command, quoted, instead of running it
95
+ --shell run each command through /bin/sh -c ( pipes, ;, > and
96
+ globs work )
97
+ -f, --file FILE read commands from FILE, one per line ( - for stdin;
98
+ blank lines and # comments are skipped )
99
+ -a, --arg-file FILE run each command template once per line of FILE, with
100
+ {} replaced by the line ( - for stdin )
101
+ --summary list every command in the end-of-run summary (
102
+ default: only failed ones )
103
+ --no-summary do not print the end-of-run summary
104
+ --no-prefix do not prefix output lines with [#] when commands run
105
+ at once
106
+ --no-color do not color output lines per command
107
+ --pty give commands a pseudo-terminal so they print live (
108
+ default when stdout is a terminal )
109
+ --no-pty give commands plain pipes
110
+ -V, --version show program's version number and exit
111
+
112
+ exit status:
113
+ 0 every command succeeded, or --dry-run
114
+ N one command: its own exit code ( killed by signal S: 128+S )
115
+ N several commands: how many failed, capped at 101
116
+ 2 usage error
117
+ 130 interrupted with Ctrl-C ( 128+S after SIGTERM / SIGHUP )
118
+ ```
119
+
120
+ ### Examples
121
+
122
+ Run two commands sequentially (default `--max_threads 1`):
123
+
124
+ ```bash
125
+ mcmd "echo hello" "echo world"
126
+ ```
127
+
128
+ Run them concurrently:
129
+
130
+ ```bash
131
+ mcmd -m 4 "echo hello" "echo world"
132
+ ```
133
+
134
+ Expand a range into many commands and run 8 at a time:
135
+
136
+ ```bash
137
+ mcmd -p -m 8 "ping -c1 192.168.1.[1-254]"
138
+ ```
139
+
140
+ Kill any command that goes silent for more than 30 seconds (along with everything it started):
141
+
142
+ ```bash
143
+ mcmd -t 30 -m 16 "long-running-task [1-100]"
144
+ ```
145
+
146
+ Run with `sudo` (falls back gracefully if `sudo` is unavailable or you are already root):
147
+
148
+ ```bash
149
+ mcmd --sudo "systemctl restart myservice"
150
+ ```
151
+
152
+ Preview what a range expands to without running anything:
153
+
154
+ ```bash
155
+ mcmd -n -p "ping -c1 192.168.1.[1-5]"
156
+ ```
157
+
158
+ Run one command per host from a file, 16 at a time:
159
+
160
+ ```bash
161
+ mcmd -m 16 -a hosts.txt "ssh {} uptime"
162
+ ```
163
+
164
+ Run a list of jobs from stdin:
165
+
166
+ ```bash
167
+ generate_jobs | mcmd -m 8 -f -
168
+ ```
169
+
170
+ Use pipes and redirects:
171
+
172
+ ```bash
173
+ mcmd --shell -m 4 "dmesg | tail -n 5" "journalctl -n 5 > /tmp/j.log"
174
+ ```
175
+
176
+ > Each positional argument is one command. It is split into arguments with shell
177
+ > quoting rules (`shlex`), but not run through a shell unless you pass `--shell`.
178
+ > With `-m/--max_threads > 1`, at most that many commands run at once, each with two
179
+ > threads draining its output. `stdin` is connected to `/dev/null` — multiCMD does
180
+ > not feed input to commands.
181
+
182
+ ### Exit status
183
+
184
+ | Situation | Exit code |
185
+ |---|---|
186
+ | Every command succeeded, or `--dry-run` | `0` |
187
+ | Exactly one command (after `-p` expansion and input files) | its own exit code; killed by signal N → `128+N` |
188
+ | Several commands | the number that failed, capped at `101` |
189
+ | Usage error (bad option, unbalanced quote, no commands) | `2` |
190
+ | Ctrl-C | `130` (`128+N` after SIGTERM / SIGHUP) |
191
+
192
+ ### Input files and templates
193
+
194
+ `-f FILE` reads one command per line (blank lines and `#` comments are skipped).
195
+ `-a FILE` turns each positional command into a template and runs it once per line of
196
+ FILE, with `{}` replaced by the line; a template without `{}` gets the line as its
197
+ last argument. `-` reads stdin. The line always stays one argument, and with `-p` only
198
+ the template is range-expanded, never the line.
199
+
200
+ With `--shell`, an `-a` line is never pasted into the shell code: it is passed to
201
+ `/bin/sh -c` as `"$1"`, so `{}` may be written bare, as `"{}"` or as `'{}'`, and the
202
+ line is still only data. A dry run shows this as `/bin/sh -c 'echo "$1"' mcmd LINE`.
203
+
204
+ ### Output in parallel runs
205
+
206
+ When more than one command can run at once, each output line starts with `[#]` (the
207
+ command's position, the same `#` the summary shows), and on a terminal each command
208
+ gets its own color. `--no-prefix` and `--no-color` (or the `NO_COLOR` environment
209
+ variable) turn them off. A command's `\r` progress line redraws in place; if another
210
+ command printed in between, the update starts a new line instead of overwriting it.
211
+
212
+ When the run ends with more than one command, mcmd prints a summary: a table of the
213
+ failed commands (all of them with `--summary`) and a tally line. `-q` hides command
214
+ output but keeps the summary; `--no-summary` hides it too.
215
+
216
+ ### pty mode
217
+
218
+ Many programs buffer their output in 4–8 KB blocks when it goes to a pipe, so they
219
+ look stuck and then print in bursts. When mcmd's own output is a terminal (and `-q` is
220
+ not set), commands get a pseudo-terminal instead, so they print line by line.
221
+ stdout and stderr are then merged, programs may print colors and progress bars, and
222
+ `PAGER`/`GIT_PAGER` are set to `cat` so nothing waits for a keypress. Use `--no-pty` to
223
+ turn it off, or `--pty` to force it. In Python, pass `pty=True`. POSIX only.
224
+
225
+ ## Range / pattern expansion (`-p` / `parse=True`)
226
+
227
+ When parsing is enabled, bracketed patterns are expanded into the cartesian
228
+ product of all options:
229
+
230
+ | Pattern | Expands to |
231
+ | -------------------- | ------------------------------------------- |
232
+ | `host[1-3]` | `host1`, `host2`, `host3` |
233
+ | `host[01-03]` | `host01`, `host02`, `host03` (zero-padded) |
234
+ | `item[a-c]` | `itema`, `itemb`, `itemc` |
235
+ | `v[a-f]` | `va` … `vf` (hex range) |
236
+ | `x[1-2,a-b]` | `x1`, `x2`, `xa`, `xb` (comma list) |
237
+ | `[1-2]-[a-b]` | `1-a`, `1-b`, `2-a`, `2-b` (multiple groups) |
238
+ | `host[A-C]` | `hostA`, `hostB`, `hostC` (case kept) |
239
+ | `n[08-0b]` | `n08`, `n09`, `n0a`, `n0b` (hex, padded) |
240
+ | `[web1\:8080]` | `web1:8080` (`\:` is a literal colon) |
241
+ | `[n:3]host[1-n]` | `host1`, `host2`, `host3` (variables) |
242
+ | `host[{2+3}]` | `host5` (`{...}` is evaluated as Python) |
243
+
244
+ Notes:
245
+
246
+ - Decimal padding follows the **shorter** endpoint, so `[0-10]` yields
247
+ `0..10` (unpadded) while `[01-10]` yields `01..10`.
248
+ - `name:value` inside brackets assigns a variable (the bracket itself produces
249
+ no output) that later brackets can reference.
250
+ - Variables are shared by the commands of one call (one `mcmd` run, one `run_commands`
251
+ call) and never leak into the next.
252
+ - `name:value` is an assignment only with exactly one `:` and a valid name before it;
253
+ IPv6 addresses like `[fe80::1]` pass through, and `\:` gives a literal colon.
254
+ - `{expr}` is evaluated as a Python expression with the current variables in
255
+ scope. Only use this with trusted input.
256
+
257
+ ## Python API
258
+
259
+ ```python
260
+ import multiCMD
261
+
262
+ # Run a single command, return its stdout lines
263
+ out = multiCMD.run_command(["echo", "hello"], quiet=True)
264
+ # -> ["hello"]
265
+
266
+ # Run several in parallel
267
+ results = multiCMD.run_commands(
268
+ [["echo", "hello"], ["echo", "world"]],
269
+ max_threads=4, quiet=True,
270
+ )
271
+ # -> [["hello"], ["world"]]
272
+ ```
273
+
274
+ ### Getting return codes and the full `Task`
275
+
276
+ ```python
277
+ # Just the return code
278
+ rc = multiCMD.run_command(["false"], return_code_only=True, quiet=True) # -> 1
279
+
280
+ # The full Task object (command, returncode, stdout, stderr)
281
+ task = multiCMD.run_command(["echo", "hi"], return_object=True, quiet=True)
282
+ print(task.returncode, task.stdout, task.stderr) # 0 ['hi'] []
283
+ ```
284
+
285
+ ### Shell mode, prefixes, colors and pty
286
+
287
+ ```python
288
+ multiCMD.run_command("dmesg | tail -n 3", shell=True) # run through /bin/sh -c
289
+ multiCMD.run_commands(cmds, max_threads=8, prefix=True) # [#] on each line
290
+ multiCMD.run_commands(cmds, max_threads=8, color=False) # never color
291
+ multiCMD.run_command(["python3", "train.py"], pty=True) # live output, stderr merged
292
+ ```
293
+
294
+ `raise_error=True` raises `multiCMD.CommandError`; its `.tasks` holds the failed `Task`
295
+ objects and its message lists each one with its return code and last error line.
296
+ `Task` also records `start_time`, `end_time` and `duration`. With `dry_run=True` nothing
297
+ runs; each command is printed once, quoted so it can be pasted into a shell.
298
+
299
+ ### Asynchronous / fire-and-forget
300
+
301
+ Use `quiet=True` with `wait_for_return=False` to launch commands on daemon
302
+ threads. The returned `Task` objects are updated in place as commands finish:
303
+
304
+ ```python
305
+ tasks = multiCMD.run_commands(
306
+ [["sleep", "2"], ["sleep", "1"]],
307
+ max_threads=2, quiet=True,
308
+ wait_for_return=False, return_object=True,
309
+ )
310
+ # tasks[i].returncode is None until that command completes
311
+
312
+ # Later, block until everything launched this way has finished:
313
+ multiCMD.join_threads()
314
+ ```
315
+
316
+ For managing larger or repeated batches, use `AsyncExecutor`:
317
+
318
+ ```python
319
+ ex = multiCMD.AsyncExecutor(max_threads=8, timeout=30, quiet=True)
320
+ ex.run_command(["./worker", "--job", "1"])
321
+ ex.run_commands([["./worker", "--job", "2"], ["./worker", "--job", "3"]])
322
+ ex.join() # wait and print any failures
323
+ print(ex.get_return_codes())
324
+ print(ex.get_results())
325
+ ```
326
+
327
+ `run_command(s)` returns right away and queues the commands. `wait()`, `join()` and
328
+ `stop()` wait until everything is done by default; a `timeout=` is one overall limit in
329
+ seconds (the constructor's `timeout` is only the per-command inactivity timeout).
330
+ `AsyncExecutor` takes the same `shell`, `prefix`, `color` and `pty` options as
331
+ `run_commands`, as constructor defaults or per call. One dispatcher thread
332
+ starts each queued command only after taking a permit from the semaphore, so the
333
+ executor holds at most one thread per permit plus the dispatcher, however many
334
+ commands are queued. A command's `task.thread` is `None` until it starts, and goes
335
+ back to `None` once it has finished and the executor has joined its thread, so a
336
+ large batch does not keep a `Thread` object per finished command. `stop()` skips
337
+ commands that have not started yet.
338
+
339
+ ### Range expansion from Python
340
+
341
+ ```python
342
+ multiCMD.run_commands([["echo", "[0-10]"]], quiet=True, parse=True)
343
+ # -> [["0"], ["1"], ..., ["10"]]
344
+ ```
345
+
346
+ ### Using sudo
347
+
348
+ ```python
349
+ multiCMD.set_sudo(True) # validates sudo is present and you aren't root
350
+ multiCMD.run_command(["systemctl", "restart", "nginx"])
351
+ # or per-call:
352
+ multiCMD.run_command(["id"], use_sudo=True)
353
+ ```
354
+
355
+ If `sudo` is not on `PATH`, or you are already root, the request is ignored with
356
+ a warning instead of failing.
357
+
358
+ ## Timeout semantics
359
+
360
+ `timeout` is an **inactivity timeout**, not a maximum total runtime. A command is
361
+ killed once it has produced **no output at all** on stdout or stderr for `timeout`
362
+ seconds. Any byte counts, including a progress line redrawn with `\r` and a partial
363
+ line without a newline.
364
+
365
+ On timeout, the command **and everything it started** are killed, the task's return
366
+ code is set to `124`, and `Timeout!` is appended to its stderr. `timeout=0` (the
367
+ default, in the API and on the command line) disables it.
368
+
369
+ ## Process groups and signals
370
+
371
+ Every command runs in a process group of its own, so a timeout, `stop()` or Ctrl-C
372
+ reaches the whole process tree, not just the first process. On Python 3.11+ this uses
373
+ `Popen(process_group=0)`; older Pythons start the command through a tiny exec wrapper.
374
+ Commands that try to read the terminal (a password prompt, for example) fail at once
375
+ instead of hanging. The library does this by blocking SIGTTIN/SIGTTOU for the command,
376
+ which covers commands started directly or through bash; `mcmd` also ignores them, which
377
+ every command inherits through any shell.
378
+
379
+ Because the commands are not in your program's process group, signals sent to that
380
+ group do not reach them. multiCMD handles this for you: a call waiting on the main
381
+ thread that gets Ctrl-C stops its commands before re-raising, `mcmd` stops them on
382
+ SIGTERM / SIGHUP (unless it was started with them ignored, as under `nohup`), and any
383
+ commands still running when the interpreter exits are killed. A program that handles
384
+ SIGTERM itself, or that runs multiCMD from its own threads (where Ctrl-C never
385
+ arrives), can call `multiCMD.kill_all()` from its handler.
386
+
387
+ ## Key functions and objects
388
+
389
+ ```python
390
+ run_command(command, timeout=0, max_threads=1, quiet=False, dry_run=False,
391
+ with_stdErr=False, return_code_only=False, return_object=False,
392
+ wait_for_return=True, sem=None, use_sudo=..., raise_error=False,
393
+ prefix=False, color=None, pty=False, shell=False)
394
+
395
+ run_commands(commands, timeout=0, max_threads=1, quiet=False, dry_run=False,
396
+ with_stdErr=False, return_code_only=False, return_object=False,
397
+ parse=False, wait_for_return=True, sem=None, use_sudo=...,
398
+ raise_error=False, prefix=False, color=None, pty=False, shell=False)
399
+
400
+ ping(hosts, timeout=1, max_threads=0, ...) # returns True/False reachability
401
+ join_threads(threads=..., timeout=None) # join fire-and-forget threads
402
+ kill_all(sig=None) # stop every running command and what it started
403
+ set_sudo(use_sudo) # enable/disable sudo globally
404
+
405
+ class Task: # command, returncode, stdout, stderr, start_time, end_time, duration
406
+ class CommandError: # raised by raise_error=True; .tasks = the failed Tasks
407
+ class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes
408
+ ```
409
+
410
+ The module also bundles a few terminal/formatting helpers used internally and
411
+ reusable on their own: `pretty_format_table`, `parseTable`, `print_progress_bar`,
412
+ `format_bytes`, `get_terminal_size`, `input_with_timeout_and_countdown`, and
413
+ `slugify`.
414
+
415
+ ## Platform support
416
+
417
+ Linux and macOS are fully supported. Windows is best-effort and untested: commands run
418
+ and time out, but a timeout or stop only reaches the first process, pty mode is not
419
+ available, and shell mode needs command strings (`-a` with `--shell` is not supported).
420
+
421
+ ## Development
422
+
423
+ Run the test suite with [pytest](https://pytest.org):
424
+
425
+ ```bash
426
+ pip install pytest
427
+ pytest # full suite under tests/
428
+ pytest -m "not slow" # skip intentional sleep-heavy cases
429
+ ```
430
+
431
+ `test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
432
+
433
+ ## License
434
+
435
+ GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).
436
+ ```