12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
- import asyncio
16
15
import subprocess
17
16
import sys
18
- from typing import List , Optional , Tuple , Union , IO , Any , cast , NamedTuple
19
-
20
- from collections .abc import AsyncIterable
21
-
22
- CommandOutput = NamedTuple (
23
- "CommandOutput" , [('out' , Optional [str ]), ('err' , Optional [str ]), ('exit_code' , int )]
24
- )
17
+ from typing import List , Tuple , Union
25
18
26
19
27
20
BOLD = 1
@@ -45,69 +38,6 @@ def highlight(text: str, color_code: int, bold: bool = False) -> str:
45
38
return '{}\033 [{}m{}\033 [0m' .format ('\033 [1m' if bold else '' , color_code , text )
46
39
47
40
48
- class TeeCapture :
49
- """Marker class indicating desire to capture output written to a pipe.
50
-
51
- If out_pipe is None, the caller just wants to capture output without
52
- writing it to anything in particular.
53
- """
54
-
55
- def __init__ (self , out_pipe : Optional [IO [str ]] = None ) -> None :
56
- self .out_pipe = out_pipe
57
-
58
-
59
- async def _async_forward (
60
- async_chunks : AsyncIterable , out : Optional [Union [TeeCapture , IO [str ]]]
61
- ) -> Optional [str ]:
62
- """Prints/captures output from the given asynchronous iterable.
63
-
64
- Args:
65
- async_chunks: An asynchronous source of bytes or str.
66
- out: Where to put the chunks.
67
-
68
- Returns:
69
- The complete captured output, or else None if the out argument wasn't a
70
- TeeCapture instance.
71
- """
72
- capture = isinstance (out , TeeCapture )
73
- out_pipe = out .out_pipe if isinstance (out , TeeCapture ) else out
74
-
75
- chunks : Optional [List [str ]] = [] if capture else None
76
- async for chunk in async_chunks :
77
- if not isinstance (chunk , str ):
78
- chunk = chunk .decode ()
79
- if out_pipe :
80
- print (chunk , file = out_pipe , end = '' )
81
- if chunks is not None :
82
- chunks .append (chunk )
83
-
84
- return '' .join (chunks ) if chunks is not None else None
85
-
86
-
87
- async def _async_wait_for_process (
88
- future_process : Any ,
89
- out : Optional [Union [TeeCapture , IO [str ]]] = sys .stdout ,
90
- err : Optional [Union [TeeCapture , IO [str ]]] = sys .stderr ,
91
- ) -> CommandOutput :
92
- """Awaits the creation and completion of an asynchronous process.
93
-
94
- Args:
95
- future_process: The eventually created process.
96
- out: Where to write stuff emitted by the process' stdout.
97
- err: Where to write stuff emitted by the process' stderr.
98
-
99
- Returns:
100
- A (captured output, captured error output, return code) triplet.
101
- """
102
- process = await future_process
103
- future_output = _async_forward (process .stdout , out )
104
- future_err_output = _async_forward (process .stderr , err )
105
- output , err_output = await asyncio .gather (future_output , future_err_output )
106
- await process .wait ()
107
-
108
- return CommandOutput (output , err_output , process .returncode )
109
-
110
-
111
41
def abbreviate_command_arguments_after_switches (cmd : Tuple [str , ...]) -> Tuple [str , ...]:
112
42
result = [cmd [0 ]]
113
43
for i in range (1 , len (cmd )):
@@ -165,126 +95,6 @@ def run(
165
95
return subprocess .run (args , ** subprocess_run_kwargs )
166
96
167
97
168
- def run_cmd (
169
- * cmd : Optional [str ],
170
- out : Optional [Union [TeeCapture , IO [str ]]] = sys .stdout ,
171
- err : Optional [Union [TeeCapture , IO [str ]]] = sys .stderr ,
172
- raise_on_fail : bool = True ,
173
- log_run_to_stderr : bool = True ,
174
- abbreviate_non_option_arguments : bool = False ,
175
- ** kwargs ,
176
- ) -> CommandOutput :
177
- """Invokes a subprocess and waits for it to finish.
178
-
179
- Args:
180
- *cmd: Components of the command to execute, e.g. ["echo", "dog"].
181
- out: Where to write the process' stdout. Defaults to sys.stdout. Can be
182
- anything accepted by print's 'file' parameter, or None if the
183
- output should be dropped, or a TeeCapture instance. If a TeeCapture
184
- instance is given, the first element of the returned tuple will be
185
- the captured output.
186
- err: Where to write the process' stderr. Defaults to sys.stderr. Can be
187
- anything accepted by print's 'file' parameter, or None if the
188
- output should be dropped, or a TeeCapture instance. If a TeeCapture
189
- instance is given, the second element of the returned tuple will be
190
- the captured error output.
191
- raise_on_fail: If the process returns a non-zero error code
192
- and this flag is set, a CalledProcessError will be raised.
193
- Otherwise the return code is the third element of the returned
194
- tuple.
195
- log_run_to_stderr: Determines whether the fact that this shell command
196
- was executed is logged to sys.stderr or not.
197
- abbreviate_non_option_arguments: When logging to stderr, this cuts off
198
- the potentially-huge tail of the command listing off e.g. hundreds
199
- of file paths. No effect if log_run_to_stderr is not set.
200
- **kwargs: Extra arguments for asyncio.create_subprocess_shell, such as
201
- a cwd (current working directory) argument.
202
-
203
- Returns:
204
- A (captured output, captured error output, return code) triplet. The
205
- captured outputs will be None if the out or err parameters were not set
206
- to an instance of TeeCapture.
207
-
208
- Raises:
209
- subprocess.CalledProcessError: The process returned a non-zero error
210
- code and raise_on_fail was set.
211
- """
212
- kept_cmd = tuple (cast (str , e ) for e in cmd if e is not None )
213
- if log_run_to_stderr :
214
- cmd_desc = kept_cmd
215
- if abbreviate_non_option_arguments :
216
- cmd_desc = abbreviate_command_arguments_after_switches (cmd_desc )
217
- print ('run:' , cmd_desc , file = sys .stderr )
218
- result = asyncio .get_event_loop ().run_until_complete (
219
- _async_wait_for_process (
220
- asyncio .create_subprocess_exec (
221
- * kept_cmd , stdout = asyncio .subprocess .PIPE , stderr = asyncio .subprocess .PIPE , ** kwargs
222
- ),
223
- out ,
224
- err ,
225
- )
226
- )
227
- if raise_on_fail and result [2 ]:
228
- raise subprocess .CalledProcessError (result [2 ], kept_cmd )
229
- return result
230
-
231
-
232
- def run_shell (
233
- cmd : str ,
234
- out : Optional [Union [TeeCapture , IO [str ]]] = sys .stdout ,
235
- err : Optional [Union [TeeCapture , IO [str ]]] = sys .stderr ,
236
- raise_on_fail : bool = True ,
237
- log_run_to_stderr : bool = True ,
238
- ** kwargs ,
239
- ) -> CommandOutput :
240
- """Invokes a shell command and waits for it to finish.
241
-
242
- Args:
243
- cmd: The command line string to execute, e.g. "echo dog | cat > file".
244
- out: Where to write the process' stdout. Defaults to sys.stdout. Can be
245
- anything accepted by print's 'file' parameter, or None if the
246
- output should be dropped, or a TeeCapture instance. If a TeeCapture
247
- instance is given, the first element of the returned tuple will be
248
- the captured output.
249
- err: Where to write the process' stderr. Defaults to sys.stderr. Can be
250
- anything accepted by print's 'file' parameter, or None if the
251
- output should be dropped, or a TeeCapture instance. If a TeeCapture
252
- instance is given, the second element of the returned tuple will be
253
- the captured error output.
254
- raise_on_fail: If the process returns a non-zero error code
255
- and this flag is set, a CalledProcessError will be raised.
256
- Otherwise the return code is the third element of the returned
257
- tuple.
258
- log_run_to_stderr: Determines whether the fact that this shell command
259
- was executed is logged to sys.stderr or not.
260
- **kwargs: Extra arguments for asyncio.create_subprocess_shell, such as
261
- a cwd (current working directory) argument.
262
-
263
- Returns:
264
- A (captured output, captured error output, return code) triplet. The
265
- captured outputs will be None if the out or err parameters were not set
266
- to an instance of TeeCapture.
267
-
268
- Raises:
269
- subprocess.CalledProcessError: The process returned a non-zero error
270
- code and raise_on_fail was set.
271
- """
272
- if log_run_to_stderr :
273
- print ('shell:' , cmd , file = sys .stderr )
274
- result = asyncio .get_event_loop ().run_until_complete (
275
- _async_wait_for_process (
276
- asyncio .create_subprocess_shell (
277
- cmd , stdout = asyncio .subprocess .PIPE , stderr = asyncio .subprocess .PIPE , ** kwargs
278
- ),
279
- out ,
280
- err ,
281
- )
282
- )
283
- if raise_on_fail and result [2 ]:
284
- raise subprocess .CalledProcessError (result [2 ], cmd )
285
- return result
286
-
287
-
288
98
def output_of (args : Union [str , List [str ]], ** kwargs ) -> str :
289
99
"""Invokes a subprocess and returns its output as a string.
290
100
0 commit comments