multiCMD 1.46__tar.gz → 1.48__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.48/PKG-INFO ADDED
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.4
2
+ Name: multiCMD
3
+ Version: 1.48
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
272
+ ```
273
+
274
+ ## License
275
+
276
+ GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).
277
+ ```
@@ -0,0 +1,251 @@
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
246
+ ```
247
+
248
+ ## License
249
+
250
+ GPLv3+ — see the package metadata. Authored by Yufei Pan (<pan@zopyr.us>).
251
+ ```