multiCMD 1.47__tar.gz → 1.49__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.
- multicmd-1.49/PKG-INFO +280 -0
- multicmd-1.49/README.md +254 -0
- multicmd-1.49/multiCMD.egg-info/PKG-INFO +280 -0
- multicmd-1.49/multiCMD.egg-info/SOURCES.txt +19 -0
- {multicmd-1.47 → multicmd-1.49}/multiCMD.py +117 -35
- multicmd-1.49/tests/test_async_executor.py +52 -0
- multicmd-1.49/tests/test_cli.py +45 -0
- multicmd-1.49/tests/test_expand_ranges.py +48 -0
- multicmd-1.49/tests/test_helpers.py +76 -0
- multicmd-1.49/tests/test_ping.py +31 -0
- multicmd-1.49/tests/test_regressions.py +68 -0
- multicmd-1.49/tests/test_run_command.py +97 -0
- multicmd-1.49/tests/test_run_commands.py +119 -0
- multicmd-1.49/tests/test_sudo.py +45 -0
- multicmd-1.49/tests/test_task.py +22 -0
- multicmd-1.47/PKG-INFO +0 -183
- multicmd-1.47/README.md +0 -157
- multicmd-1.47/multiCMD.egg-info/PKG-INFO +0 -183
- multicmd-1.47/multiCMD.egg-info/SOURCES.txt +0 -9
- {multicmd-1.47 → multicmd-1.49}/multiCMD.egg-info/dependency_links.txt +0 -0
- {multicmd-1.47 → multicmd-1.49}/multiCMD.egg-info/entry_points.txt +0 -0
- {multicmd-1.47 → multicmd-1.49}/multiCMD.egg-info/requires.txt +0 -0
- {multicmd-1.47 → multicmd-1.49}/multiCMD.egg-info/top_level.txt +0 -0
- {multicmd-1.47 → multicmd-1.49}/setup.cfg +0 -0
- {multicmd-1.47 → multicmd-1.49}/setup.py +0 -0
multicmd-1.49/PKG-INFO
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: multiCMD
|
|
3
|
+
Version: 1.49
|
|
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
|
+
Requires-Dist: argparse
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: license
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# multiCMD
|
|
28
|
+
|
|
29
|
+
Run many commands at the same time — from the shell or from Python.
|
|
30
|
+
|
|
31
|
+
`multiCMD` is a small, dependency-light helper (a single `multiCMD.py` module) for
|
|
32
|
+
launching and supervising multiple subprocesses concurrently. It streams each
|
|
33
|
+
command's output in real time (optionally colorized per command), captures
|
|
34
|
+
stdout/stderr/return codes, supports per-command inactivity timeouts, and can
|
|
35
|
+
expand range patterns like `host[1-10]` into many commands at once.
|
|
36
|
+
|
|
37
|
+
It works both as a command-line tool and as an importable wrapper around
|
|
38
|
+
`subprocess` for your own automation scripts.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- Run a batch of commands in parallel with a bounded thread pool.
|
|
43
|
+
- Live, non-blocking, optionally per-command colorized output.
|
|
44
|
+
- Capture `stdout`, `stderr`, and return code per command via the `Task` object.
|
|
45
|
+
- Per-command **inactivity timeout** (see [Timeout semantics](#timeout-semantics)).
|
|
46
|
+
- Range/pattern expansion: `host[1-3]`, `[01-03]`, `[a-f]`, `[1-2,a-b]`, variables, and `{...}` expressions.
|
|
47
|
+
- Fire-and-forget async mode (`wait_for_return=False`) plus an `AsyncExecutor` for managing long-lived batches.
|
|
48
|
+
- Optional `sudo` wrapping.
|
|
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 timeout] [-m max_threads] [--sudo] [-q] [-V]
|
|
72
|
+
command [command ...]
|
|
73
|
+
|
|
74
|
+
Run multiple commands in parallel
|
|
75
|
+
|
|
76
|
+
positional arguments:
|
|
77
|
+
command commands to run
|
|
78
|
+
|
|
79
|
+
options:
|
|
80
|
+
-h, --help show this help message and exit
|
|
81
|
+
-p, --parse Parse ranged input and expand them into multiple commands
|
|
82
|
+
-t, --timeout timeout
|
|
83
|
+
timeout for each command
|
|
84
|
+
-m, --max_threads max_threads
|
|
85
|
+
maximum number of threads to use
|
|
86
|
+
--sudo use sudo for commands
|
|
87
|
+
-q, --quiet quiet mode
|
|
88
|
+
-V, --version show program's version number and exit
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Examples
|
|
92
|
+
|
|
93
|
+
Run two commands sequentially (default `--max_threads 1`):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
mcmd "echo hello" "echo world"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Run them concurrently:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
mcmd -m 4 "echo hello" "echo world"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Expand a range into many commands and run 8 at a time:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
mcmd -p -m 8 "ping -c1 192.168.1.[1-254]"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Kill any command that goes silent for more than 30 seconds:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
mcmd -t 30 -m 16 "long-running-task [1-100]"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Run with `sudo` (falls back gracefully if `sudo` is unavailable or you are already root):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
mcmd --sudo "systemctl restart myservice"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> Each positional argument is one command. With `-m/--max_threads > 1`, a worker
|
|
124
|
+
> thread is spawned per command; each worker uses `subprocess` to run the command
|
|
125
|
+
> and two extra threads to drain stdout/stderr without blocking. `stdin` is
|
|
126
|
+
> connected to `/dev/null` — multiCMD does not feed live input to commands.
|
|
127
|
+
|
|
128
|
+
## Range / pattern expansion (`-p` / `parse=True`)
|
|
129
|
+
|
|
130
|
+
When parsing is enabled, bracketed patterns are expanded into the cartesian
|
|
131
|
+
product of all options:
|
|
132
|
+
|
|
133
|
+
| Pattern | Expands to |
|
|
134
|
+
| -------------------- | ------------------------------------------- |
|
|
135
|
+
| `host[1-3]` | `host1`, `host2`, `host3` |
|
|
136
|
+
| `host[01-03]` | `host01`, `host02`, `host03` (zero-padded) |
|
|
137
|
+
| `item[a-c]` | `itema`, `itemb`, `itemc` |
|
|
138
|
+
| `v[a-f]` | `va` … `vf` (hex range) |
|
|
139
|
+
| `x[1-2,a-b]` | `x1`, `x2`, `xa`, `xb` (comma list) |
|
|
140
|
+
| `[1-2]-[a-b]` | `1-a`, `1-b`, `2-a`, `2-b` (multiple groups) |
|
|
141
|
+
| `[n:3]host[1-n]` | `host1`, `host2`, `host3` (variables) |
|
|
142
|
+
| `host[{2+3}]` | `host5` (`{...}` is evaluated as Python) |
|
|
143
|
+
|
|
144
|
+
Notes:
|
|
145
|
+
|
|
146
|
+
- Decimal padding follows the **shorter** endpoint, so `[0-10]` yields
|
|
147
|
+
`0..10` (unpadded) while `[01-10]` yields `01..10`.
|
|
148
|
+
- `name:value` inside brackets assigns a variable (the bracket itself produces
|
|
149
|
+
no output) that later brackets can reference.
|
|
150
|
+
- `{expr}` is evaluated as a Python expression with the current variables in
|
|
151
|
+
scope. Only use this with trusted input.
|
|
152
|
+
|
|
153
|
+
## Python API
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import multiCMD
|
|
157
|
+
|
|
158
|
+
# Run a single command, return its stdout lines
|
|
159
|
+
out = multiCMD.run_command(["echo", "hello"], quiet=True)
|
|
160
|
+
# -> ["hello"]
|
|
161
|
+
|
|
162
|
+
# Run several in parallel
|
|
163
|
+
results = multiCMD.run_commands(
|
|
164
|
+
[["echo", "hello"], ["echo", "world"]],
|
|
165
|
+
max_threads=4, quiet=True,
|
|
166
|
+
)
|
|
167
|
+
# -> [["hello"], ["world"]]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Getting return codes and the full `Task`
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# Just the return code
|
|
174
|
+
rc = multiCMD.run_command(["false"], return_code_only=True, quiet=True) # -> 1
|
|
175
|
+
|
|
176
|
+
# The full Task object (command, returncode, stdout, stderr)
|
|
177
|
+
task = multiCMD.run_command(["echo", "hi"], return_object=True, quiet=True)
|
|
178
|
+
print(task.returncode, task.stdout, task.stderr) # 0 ['hi'] []
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Asynchronous / fire-and-forget
|
|
182
|
+
|
|
183
|
+
Use `quiet=True` with `wait_for_return=False` to launch commands on daemon
|
|
184
|
+
threads. The returned `Task` objects are updated in place as commands finish:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
tasks = multiCMD.run_commands(
|
|
188
|
+
[["sleep", "2"], ["sleep", "1"]],
|
|
189
|
+
max_threads=2, quiet=True,
|
|
190
|
+
wait_for_return=False, return_object=True,
|
|
191
|
+
)
|
|
192
|
+
# tasks[i].returncode is None until that command completes
|
|
193
|
+
|
|
194
|
+
# Later, block until everything launched this way has finished:
|
|
195
|
+
multiCMD.join_threads()
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For managing larger or repeated batches, use `AsyncExecutor`:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
ex = multiCMD.AsyncExecutor(max_threads=8, timeout=30, quiet=True)
|
|
202
|
+
ex.run_command(["./worker", "--job", "1"])
|
|
203
|
+
ex.run_commands([["./worker", "--job", "2"], ["./worker", "--job", "3"]])
|
|
204
|
+
ex.join() # wait and print any failures
|
|
205
|
+
print(ex.get_return_codes())
|
|
206
|
+
print(ex.get_results())
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Range expansion from Python
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
multiCMD.run_commands([["echo", "[0-10]"]], quiet=True, parse=True)
|
|
213
|
+
# -> [["0"], ["1"], ..., ["10"]]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Using sudo
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
multiCMD.set_sudo(True) # validates sudo is present and you aren't root
|
|
220
|
+
multiCMD.run_command(["systemctl", "restart", "nginx"])
|
|
221
|
+
# or per-call:
|
|
222
|
+
multiCMD.run_command(["id"], use_sudo=True)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
If `sudo` is not on `PATH`, or you are already root, the request is ignored with
|
|
226
|
+
a warning instead of failing.
|
|
227
|
+
|
|
228
|
+
## Timeout semantics
|
|
229
|
+
|
|
230
|
+
`timeout` is an **inactivity timeout**, not a maximum total runtime. A command is
|
|
231
|
+
killed only after it has produced **no new committed output line** for `timeout`
|
|
232
|
+
seconds. An output line is "committed" when the stream handler encounters a `\n`
|
|
233
|
+
or `\r`.
|
|
234
|
+
|
|
235
|
+
This means a command that keeps printing output will keep running, while one that
|
|
236
|
+
hangs silently will be terminated after `timeout` seconds. Set `timeout=0` (the
|
|
237
|
+
default in the API) to disable the timeout entirely. On timeout, the task's
|
|
238
|
+
return code is set to `124` and `Timeout!` is appended to its stderr.
|
|
239
|
+
|
|
240
|
+
## Key functions and objects
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
run_command(command, timeout=0, max_threads=1, quiet=False, dry_run=False,
|
|
244
|
+
with_stdErr=False, return_code_only=False, return_object=False,
|
|
245
|
+
wait_for_return=True, sem=None, use_sudo=..., raise_error=False)
|
|
246
|
+
|
|
247
|
+
run_commands(commands, timeout=0, max_threads=1, quiet=False, dry_run=False,
|
|
248
|
+
with_stdErr=False, return_code_only=False, return_object=False,
|
|
249
|
+
parse=False, wait_for_return=True, sem=None, use_sudo=...,
|
|
250
|
+
raise_error=False)
|
|
251
|
+
|
|
252
|
+
ping(hosts, timeout=1, max_threads=0, ...) # returns True/False reachability
|
|
253
|
+
join_threads(threads=..., timeout=None) # join fire-and-forget threads
|
|
254
|
+
set_sudo(use_sudo) # enable/disable sudo globally
|
|
255
|
+
|
|
256
|
+
class Task: # command, returncode, stdout (list[str]), stderr (list[str])
|
|
257
|
+
class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The module also bundles a few terminal/formatting helpers used internally and
|
|
261
|
+
reusable on their own: `pretty_format_table`, `parseTable`, `print_progress_bar`,
|
|
262
|
+
`format_bytes`, `get_terminal_size`, `input_with_timeout_and_countdown`, and
|
|
263
|
+
`slugify`.
|
|
264
|
+
|
|
265
|
+
## Development
|
|
266
|
+
|
|
267
|
+
Run the test suite with [pytest](https://pytest.org):
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
pip install pytest
|
|
271
|
+
pytest # full suite under tests/
|
|
272
|
+
pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).
|
|
280
|
+
```
|
multicmd-1.49/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# multiCMD
|
|
2
|
+
|
|
3
|
+
Run many commands at the same time — from the shell or from Python.
|
|
4
|
+
|
|
5
|
+
`multiCMD` is a small, dependency-light helper (a single `multiCMD.py` module) for
|
|
6
|
+
launching and supervising multiple subprocesses concurrently. It streams each
|
|
7
|
+
command's output in real time (optionally colorized per command), captures
|
|
8
|
+
stdout/stderr/return codes, supports per-command inactivity timeouts, and can
|
|
9
|
+
expand range patterns like `host[1-10]` into many commands at once.
|
|
10
|
+
|
|
11
|
+
It works both as a command-line tool and as an importable wrapper around
|
|
12
|
+
`subprocess` for your own automation scripts.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Run a batch of commands in parallel with a bounded thread pool.
|
|
17
|
+
- Live, non-blocking, optionally per-command colorized output.
|
|
18
|
+
- Capture `stdout`, `stderr`, and return code per command via the `Task` object.
|
|
19
|
+
- Per-command **inactivity timeout** (see [Timeout semantics](#timeout-semantics)).
|
|
20
|
+
- Range/pattern expansion: `host[1-3]`, `[01-03]`, `[a-f]`, `[1-2,a-b]`, variables, and `{...}` expressions.
|
|
21
|
+
- Fire-and-forget async mode (`wait_for_return=False`) plus an `AsyncExecutor` for managing long-lived batches.
|
|
22
|
+
- Optional `sudo` wrapping.
|
|
23
|
+
- No third-party runtime dependencies; requires Python >= 3.6.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install multiCMD
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This installs the module and three equivalent console entry points:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mcmd
|
|
35
|
+
multiCMD
|
|
36
|
+
multicmd
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You can also just drop `multiCMD.py` into your project and import it directly.
|
|
40
|
+
|
|
41
|
+
## Command-line usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
$ mcmd -h
|
|
45
|
+
usage: mcmd [-h] [-p] [-t timeout] [-m max_threads] [--sudo] [-q] [-V]
|
|
46
|
+
command [command ...]
|
|
47
|
+
|
|
48
|
+
Run multiple commands in parallel
|
|
49
|
+
|
|
50
|
+
positional arguments:
|
|
51
|
+
command commands to run
|
|
52
|
+
|
|
53
|
+
options:
|
|
54
|
+
-h, --help show this help message and exit
|
|
55
|
+
-p, --parse Parse ranged input and expand them into multiple commands
|
|
56
|
+
-t, --timeout timeout
|
|
57
|
+
timeout for each command
|
|
58
|
+
-m, --max_threads max_threads
|
|
59
|
+
maximum number of threads to use
|
|
60
|
+
--sudo use sudo for commands
|
|
61
|
+
-q, --quiet quiet mode
|
|
62
|
+
-V, --version show program's version number and exit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Examples
|
|
66
|
+
|
|
67
|
+
Run two commands sequentially (default `--max_threads 1`):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
mcmd "echo hello" "echo world"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Run them concurrently:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
mcmd -m 4 "echo hello" "echo world"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Expand a range into many commands and run 8 at a time:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
mcmd -p -m 8 "ping -c1 192.168.1.[1-254]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Kill any command that goes silent for more than 30 seconds:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
mcmd -t 30 -m 16 "long-running-task [1-100]"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run with `sudo` (falls back gracefully if `sudo` is unavailable or you are already root):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
mcmd --sudo "systemctl restart myservice"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> Each positional argument is one command. With `-m/--max_threads > 1`, a worker
|
|
98
|
+
> thread is spawned per command; each worker uses `subprocess` to run the command
|
|
99
|
+
> and two extra threads to drain stdout/stderr without blocking. `stdin` is
|
|
100
|
+
> connected to `/dev/null` — multiCMD does not feed live input to commands.
|
|
101
|
+
|
|
102
|
+
## Range / pattern expansion (`-p` / `parse=True`)
|
|
103
|
+
|
|
104
|
+
When parsing is enabled, bracketed patterns are expanded into the cartesian
|
|
105
|
+
product of all options:
|
|
106
|
+
|
|
107
|
+
| Pattern | Expands to |
|
|
108
|
+
| -------------------- | ------------------------------------------- |
|
|
109
|
+
| `host[1-3]` | `host1`, `host2`, `host3` |
|
|
110
|
+
| `host[01-03]` | `host01`, `host02`, `host03` (zero-padded) |
|
|
111
|
+
| `item[a-c]` | `itema`, `itemb`, `itemc` |
|
|
112
|
+
| `v[a-f]` | `va` … `vf` (hex range) |
|
|
113
|
+
| `x[1-2,a-b]` | `x1`, `x2`, `xa`, `xb` (comma list) |
|
|
114
|
+
| `[1-2]-[a-b]` | `1-a`, `1-b`, `2-a`, `2-b` (multiple groups) |
|
|
115
|
+
| `[n:3]host[1-n]` | `host1`, `host2`, `host3` (variables) |
|
|
116
|
+
| `host[{2+3}]` | `host5` (`{...}` is evaluated as Python) |
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
|
|
120
|
+
- Decimal padding follows the **shorter** endpoint, so `[0-10]` yields
|
|
121
|
+
`0..10` (unpadded) while `[01-10]` yields `01..10`.
|
|
122
|
+
- `name:value` inside brackets assigns a variable (the bracket itself produces
|
|
123
|
+
no output) that later brackets can reference.
|
|
124
|
+
- `{expr}` is evaluated as a Python expression with the current variables in
|
|
125
|
+
scope. Only use this with trusted input.
|
|
126
|
+
|
|
127
|
+
## Python API
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import multiCMD
|
|
131
|
+
|
|
132
|
+
# Run a single command, return its stdout lines
|
|
133
|
+
out = multiCMD.run_command(["echo", "hello"], quiet=True)
|
|
134
|
+
# -> ["hello"]
|
|
135
|
+
|
|
136
|
+
# Run several in parallel
|
|
137
|
+
results = multiCMD.run_commands(
|
|
138
|
+
[["echo", "hello"], ["echo", "world"]],
|
|
139
|
+
max_threads=4, quiet=True,
|
|
140
|
+
)
|
|
141
|
+
# -> [["hello"], ["world"]]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Getting return codes and the full `Task`
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# Just the return code
|
|
148
|
+
rc = multiCMD.run_command(["false"], return_code_only=True, quiet=True) # -> 1
|
|
149
|
+
|
|
150
|
+
# The full Task object (command, returncode, stdout, stderr)
|
|
151
|
+
task = multiCMD.run_command(["echo", "hi"], return_object=True, quiet=True)
|
|
152
|
+
print(task.returncode, task.stdout, task.stderr) # 0 ['hi'] []
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Asynchronous / fire-and-forget
|
|
156
|
+
|
|
157
|
+
Use `quiet=True` with `wait_for_return=False` to launch commands on daemon
|
|
158
|
+
threads. The returned `Task` objects are updated in place as commands finish:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
tasks = multiCMD.run_commands(
|
|
162
|
+
[["sleep", "2"], ["sleep", "1"]],
|
|
163
|
+
max_threads=2, quiet=True,
|
|
164
|
+
wait_for_return=False, return_object=True,
|
|
165
|
+
)
|
|
166
|
+
# tasks[i].returncode is None until that command completes
|
|
167
|
+
|
|
168
|
+
# Later, block until everything launched this way has finished:
|
|
169
|
+
multiCMD.join_threads()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
For managing larger or repeated batches, use `AsyncExecutor`:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
ex = multiCMD.AsyncExecutor(max_threads=8, timeout=30, quiet=True)
|
|
176
|
+
ex.run_command(["./worker", "--job", "1"])
|
|
177
|
+
ex.run_commands([["./worker", "--job", "2"], ["./worker", "--job", "3"]])
|
|
178
|
+
ex.join() # wait and print any failures
|
|
179
|
+
print(ex.get_return_codes())
|
|
180
|
+
print(ex.get_results())
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Range expansion from Python
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
multiCMD.run_commands([["echo", "[0-10]"]], quiet=True, parse=True)
|
|
187
|
+
# -> [["0"], ["1"], ..., ["10"]]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Using sudo
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
multiCMD.set_sudo(True) # validates sudo is present and you aren't root
|
|
194
|
+
multiCMD.run_command(["systemctl", "restart", "nginx"])
|
|
195
|
+
# or per-call:
|
|
196
|
+
multiCMD.run_command(["id"], use_sudo=True)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If `sudo` is not on `PATH`, or you are already root, the request is ignored with
|
|
200
|
+
a warning instead of failing.
|
|
201
|
+
|
|
202
|
+
## Timeout semantics
|
|
203
|
+
|
|
204
|
+
`timeout` is an **inactivity timeout**, not a maximum total runtime. A command is
|
|
205
|
+
killed only after it has produced **no new committed output line** for `timeout`
|
|
206
|
+
seconds. An output line is "committed" when the stream handler encounters a `\n`
|
|
207
|
+
or `\r`.
|
|
208
|
+
|
|
209
|
+
This means a command that keeps printing output will keep running, while one that
|
|
210
|
+
hangs silently will be terminated after `timeout` seconds. Set `timeout=0` (the
|
|
211
|
+
default in the API) to disable the timeout entirely. On timeout, the task's
|
|
212
|
+
return code is set to `124` and `Timeout!` is appended to its stderr.
|
|
213
|
+
|
|
214
|
+
## Key functions and objects
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
run_command(command, timeout=0, max_threads=1, quiet=False, dry_run=False,
|
|
218
|
+
with_stdErr=False, return_code_only=False, return_object=False,
|
|
219
|
+
wait_for_return=True, sem=None, use_sudo=..., raise_error=False)
|
|
220
|
+
|
|
221
|
+
run_commands(commands, timeout=0, max_threads=1, quiet=False, dry_run=False,
|
|
222
|
+
with_stdErr=False, return_code_only=False, return_object=False,
|
|
223
|
+
parse=False, wait_for_return=True, sem=None, use_sudo=...,
|
|
224
|
+
raise_error=False)
|
|
225
|
+
|
|
226
|
+
ping(hosts, timeout=1, max_threads=0, ...) # returns True/False reachability
|
|
227
|
+
join_threads(threads=..., timeout=None) # join fire-and-forget threads
|
|
228
|
+
set_sudo(use_sudo) # enable/disable sudo globally
|
|
229
|
+
|
|
230
|
+
class Task: # command, returncode, stdout (list[str]), stderr (list[str])
|
|
231
|
+
class AsyncExecutor: # run_command(s), wait, join, stop, cleanup, get_results, get_return_codes
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The module also bundles a few terminal/formatting helpers used internally and
|
|
235
|
+
reusable on their own: `pretty_format_table`, `parseTable`, `print_progress_bar`,
|
|
236
|
+
`format_bytes`, `get_terminal_size`, `input_with_timeout_and_countdown`, and
|
|
237
|
+
`slugify`.
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
Run the test suite with [pytest](https://pytest.org):
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
pip install pytest
|
|
245
|
+
pytest # full suite under tests/
|
|
246
|
+
pytest -m "not slow" # skip intentional sleep-heavy cases
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`test_expand_ranges_perf.py` is a manual performance microbenchmark and is not collected by pytest.
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).
|
|
254
|
+
```
|