runps 4.0.0__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.
- runps-4.0.0.dist-info/METADATA +502 -0
- runps-4.0.0.dist-info/RECORD +7 -0
- runps-4.0.0.dist-info/WHEEL +5 -0
- runps-4.0.0.dist-info/licenses/AUTHORS.md +22 -0
- runps-4.0.0.dist-info/licenses/LICENSE.txt +19 -0
- runps-4.0.0.dist-info/top_level.txt +1 -0
- runps.py +492 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runps
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: A small python utility for launching external processes easily..
|
|
5
|
+
Author: Andrew Moffat, Lucas Sinclair
|
|
6
|
+
License: Copyright (C) 2011-2012 by Andrew Moffat
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in
|
|
16
|
+
all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
24
|
+
THE SOFTWARE.
|
|
25
|
+
|
|
26
|
+
Project-URL: Homepage, https://github.com/xapple/runps/
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE.txt
|
|
29
|
+
License-File: AUTHORS.md
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
`runps` version 4.0.0
|
|
33
|
+
=====================
|
|
34
|
+
|
|
35
|
+
#### This is a fork of the `sh` package (formely `pbs` package) that works on Linux, macOS and Windows.
|
|
36
|
+
|
|
37
|
+
| Package | *nix / Python 2 | *nix / Python 3 | Windows / Python 2 | Windows / Python 3 | Compatible forking* |
|
|
38
|
+
|-------------|-----------------|-----------------| --------------- | --------------- | ------------------- |
|
|
39
|
+
| [`sh`](https://github.com/amoffat/sh) | ✅ Works | ✅ Works | 🔴 Not supported | 🔴 Not supported | 🔴 Fails |
|
|
40
|
+
| `runps` | ✅ Works | ✅ Works | ✅ Works | ✅ Works | ✅ Works |
|
|
41
|
+
|
|
42
|
+
\* By compatible forking we mean a method of creating subprocesses that can work successfully inside a debugging environment such as the one provided by PyCharm.
|
|
43
|
+
|
|
44
|
+
### Why do we need a different version of `sh`?
|
|
45
|
+
|
|
46
|
+
* First, in early 2012, the original `pbs` package was conceived by [@amoffat](https://github.com/amoffat/sh). It could launch subprocesses on both Unix and Windows environments with Python 2.
|
|
47
|
+
|
|
48
|
+
* In late 2012, the `pbs` project was renamed to `sh`, lots of functionality was added, but support for Windows [was completely dropped](http://amoffat.github.io/sh/sections/faq.html#will-windows-be-supported).
|
|
49
|
+
|
|
50
|
+
* For some time, the legacy `pbs` package was still used whenever one needs to launch external processes on Windows machines and was [still available](https://pypi.org/project/pbs/) at `pip install pbs`.
|
|
51
|
+
|
|
52
|
+
* The old `pbs` package was also used for its more compatible way of starting subprocesses. The current `sh` module is [not compatible](https://github.com/amoffat/sh/issues/475) with developing in PyCharm for instance.
|
|
53
|
+
|
|
54
|
+
* However, the last ever published version of `pbs` (v0.110 from Oct 20, 2012) does not work on Python 3. This package fixes that.
|
|
55
|
+
|
|
56
|
+
* `runps` works on all platforms and Python versions.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
### How do I install and use this package?
|
|
60
|
+
|
|
61
|
+
* You can install this version with this command:
|
|
62
|
+
|
|
63
|
+
`pip install runps`
|
|
64
|
+
|
|
65
|
+
* You can use this package with this import statement (if you are porting from `sh` code):
|
|
66
|
+
|
|
67
|
+
`import runps as sh`
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
### What exactly did not work in Python 3?
|
|
71
|
+
|
|
72
|
+
This would be the traceback that the old `pbs v0.110` would produce:
|
|
73
|
+
|
|
74
|
+
c:\python37\lib\site-packages\pbs.py in __call__(self, *args, **kwargs)
|
|
75
|
+
454 cwd=call_args["cwd"], stdin=stdin, stdout=stdout, stderr=stderr)
|
|
76
|
+
455
|
|
77
|
+
--> 456 return RunningCommand(command_ran, process, call_args, actual_stdin)
|
|
78
|
+
457
|
|
79
|
+
458
|
|
80
|
+
|
|
81
|
+
c:\python37\lib\site-packages\pbs.py in __init__(self, command_ran, process, call_args,
|
|
82
|
+
stdin)
|
|
83
|
+
166 if stdin: stdin = stdin.encode("utf8")
|
|
84
|
+
167 self._stdout, self._stderr = self.process.communicate(stdin)
|
|
85
|
+
--> 168 self._handle_exit_code(self.process.wait())
|
|
86
|
+
169
|
|
87
|
+
170 def __enter__(self):
|
|
88
|
+
|
|
89
|
+
c:\python37\lib\site-packages\pbs.py in _handle_exit_code(self, rc)
|
|
90
|
+
233 def _handle_exit_code(self, rc):
|
|
91
|
+
234 if rc not in self.call_args["ok_code"]:
|
|
92
|
+
--> 235 raise get_rc_exc(rc)(self.command_ran, self._stdout, self._stderr)
|
|
93
|
+
236
|
|
94
|
+
237 def __len__(self):
|
|
95
|
+
|
|
96
|
+
c:\python37\lib\site-packages\pbs.py in __init__(self, full_cmd, stdout, stderr)
|
|
97
|
+
93
|
|
98
|
+
94 msg = "\n\nRan: %r\n\nSTDOUT:\n\n %s\n\nSTDERR:\n\n %s" %\
|
|
99
|
+
---> 95 (full_cmd, tstdout.decode(), tstderr.decode())
|
|
100
|
+
96 super(ErrorReturnCode, self).__init__(msg)
|
|
101
|
+
97
|
|
102
|
+
|
|
103
|
+
AttributeError: 'str' object has no attribute 'decode'
|
|
104
|
+
|
|
105
|
+
This would occur when the program called exited with a non-zero return code.
|
|
106
|
+
|
|
107
|
+
* * *
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
Original documentation
|
|
111
|
+
======================
|
|
112
|
+
|
|
113
|
+
runps is a unique subprocess wrapper that maps your system programs to
|
|
114
|
+
Python functions dynamically. runps helps you write shell scripts in
|
|
115
|
+
Python by giving you the good features of Bash (easy command calling, easy
|
|
116
|
+
piping) with all the power and flexibility of Python.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from runps import ifconfig
|
|
120
|
+
print ifconfig("eth0")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
runps is not a collection of system commands implemented in Python.
|
|
124
|
+
|
|
125
|
+
# Getting
|
|
126
|
+
|
|
127
|
+
$> pip install runps
|
|
128
|
+
|
|
129
|
+
# Usage
|
|
130
|
+
|
|
131
|
+
The easiest way to get up and running is to import runps
|
|
132
|
+
directly or import your program from runps:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import runps
|
|
136
|
+
print runps.ifconfig("eth0")
|
|
137
|
+
|
|
138
|
+
from runps import ifconfig
|
|
139
|
+
print ifconfig("eth0")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A less common usage pattern is through runps Command wrapper, which takes a
|
|
143
|
+
full path to a command and returns a callable object. This is useful for
|
|
144
|
+
programs that have weird characters in their names or programs that aren't in
|
|
145
|
+
your $PATH:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import runps
|
|
149
|
+
ffmpeg = runps.Command("/usr/bin/ffmpeg")
|
|
150
|
+
ffmpeg(movie_file)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The last usage pattern is for trying runps through an interactive REPL. By
|
|
154
|
+
default, this acts like a star import (so all of your system programs will be
|
|
155
|
+
immediately available as functions):
|
|
156
|
+
|
|
157
|
+
$> python runps.py
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Examples
|
|
161
|
+
|
|
162
|
+
## Executing Commands
|
|
163
|
+
|
|
164
|
+
Commands work like you'd expect. **Just call your program's name like
|
|
165
|
+
a function:**
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# print the contents of this directory
|
|
169
|
+
print ls("-l")
|
|
170
|
+
|
|
171
|
+
# get the longest line of this file
|
|
172
|
+
longest_line = wc(__file__, "-L")
|
|
173
|
+
|
|
174
|
+
# get interface information
|
|
175
|
+
print ifconfig("eth0")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Note that these aren't Python functions, these are running the binary
|
|
179
|
+
commands on your system dynamically by resolving your PATH, much like Bash does.
|
|
180
|
+
In this way, all the programs on your system are easily available
|
|
181
|
+
in Python.
|
|
182
|
+
|
|
183
|
+
You can also call attributes on commands. This translates to the command
|
|
184
|
+
name followed by the attribute name:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from runps import git
|
|
188
|
+
|
|
189
|
+
# resolves to "git branch -v"
|
|
190
|
+
print git.branch("-v")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
It turns out this is extremely useful for commands whose first argument is often
|
|
194
|
+
another sub-command (like git, svn, time, sudo, etc).
|
|
195
|
+
See "Baking" for an advanced usage of this.
|
|
196
|
+
|
|
197
|
+
## Keyword Arguments
|
|
198
|
+
|
|
199
|
+
Keyword arguments also work like you'd expect: they get replaced with the
|
|
200
|
+
long-form and short-form commandline option:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# resolves to "curl http://duckduckgo.com/ -o page.html --silent"
|
|
204
|
+
curl("http://duckduckgo.com/", o="page.html", silent=True)
|
|
205
|
+
|
|
206
|
+
# or if you prefer not to use keyword arguments, this does the same thing:
|
|
207
|
+
curl("http://duckduckgo.com/", "-o", "page.html", "--silent")
|
|
208
|
+
|
|
209
|
+
# resolves to "adduser amoffat --system --shell=/bin/bash --no-create-home"
|
|
210
|
+
adduser("amoffat", system=True, shell="/bin/bash", no_create_home=True)
|
|
211
|
+
|
|
212
|
+
# or
|
|
213
|
+
adduser("amoffat", "--system", "--shell", "/bin/bash", "--no-create-home")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Piping
|
|
217
|
+
|
|
218
|
+
Piping has become function composition:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# sort this directory by biggest file
|
|
222
|
+
print sort(du(glob("*"), "-sb"), "-rn")
|
|
223
|
+
|
|
224
|
+
# print the number of folders and files in /etc
|
|
225
|
+
print wc(ls("/etc", "-1"), "-l")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Redirection
|
|
229
|
+
|
|
230
|
+
runps can redirect the standard and error output streams of a process to a file.
|
|
231
|
+
This is done with the special _out and _err keyword arguments. You can pass a
|
|
232
|
+
filename or a file object as the argument value. When the name of an already
|
|
233
|
+
existing file is passed, the contents of the file will be overwritten.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
ls(_out="files.list")
|
|
237
|
+
ls("nonexistent", _err="error.txt")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
runps can also redirect the error output stream to the standard output stream,
|
|
241
|
+
using the special _err_to_out=True keyword argument.
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
## Sudo and With Contexts
|
|
245
|
+
|
|
246
|
+
Commands can be run within a "with" context. Popular commands using this
|
|
247
|
+
might be "sudo" or "fakeroot":
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
with sudo:
|
|
251
|
+
print ls("/root")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If you need
|
|
255
|
+
to run a command in a with context AND call it, for example, specifying
|
|
256
|
+
a -p prompt with sudo, you need to use the "_with" keyword argument.
|
|
257
|
+
This let's the command know that it's being run from a with context so
|
|
258
|
+
it can behave correctly.
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
with sudo(p=">", _with=True):
|
|
262
|
+
print ls("/root")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Background Processes
|
|
266
|
+
|
|
267
|
+
Commands can be run in the background with the special _bg=True keyword
|
|
268
|
+
argument:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
# blocks
|
|
272
|
+
sleep(3)
|
|
273
|
+
print "...3 seconds later"
|
|
274
|
+
|
|
275
|
+
# doesn't block
|
|
276
|
+
p = sleep(3, _bg=True)
|
|
277
|
+
print "prints immediately!"
|
|
278
|
+
p.wait()
|
|
279
|
+
print "...and 3 seconds later"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
You can also pipe together background processes!
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
p = wc(curl("http://github.com/", silent=True, _bg=True), "--bytes")
|
|
286
|
+
print "prints immediately!"
|
|
287
|
+
print "byte count of github: %d" % int(p) # lazily completes
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
This lets you start long-running commands at the beginning of your script
|
|
291
|
+
(like a file download) and continue performing other commands in the
|
|
292
|
+
foreground.
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
## Foreground Processes
|
|
296
|
+
|
|
297
|
+
Foreground processes are processes that you want to interact directly with
|
|
298
|
+
the default stdout and stdin of your terminal. In other words, these are
|
|
299
|
+
processes that you do not want to return their output as a return value
|
|
300
|
+
of their call. An example would be opening a text editor:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
vim(file_to_edit)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
This will block because runps will be trying to aggregate the output
|
|
307
|
+
of the command to python, without displaying anything to the screen. The
|
|
308
|
+
solution is the "_fg" special keyword arg:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
vim(file_to_edit, _fg=True)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
This will open vim as expected and let you use it as expected, with all
|
|
315
|
+
the input coming from the keyboard and the output going to the screen.
|
|
316
|
+
The return value of a foreground process is an empty string.
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
## Finding Commands
|
|
320
|
+
|
|
321
|
+
"Which" finds the full path of a program, or returns None if it doesn't exist.
|
|
322
|
+
This command is one of the few commands implemented as a Python function,
|
|
323
|
+
and therefore doesn't rely on the "which" program actually existing.
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
print which("python") # "/usr/bin/python"
|
|
327
|
+
print which("ls") # "/bin/ls"
|
|
328
|
+
print which("some_command") # None
|
|
329
|
+
|
|
330
|
+
if not which("supervisorctl"): apt_get("install", "supervisor", "-y")
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Baking
|
|
334
|
+
|
|
335
|
+
runps is capable of "baking" arguments into commands. This is similar
|
|
336
|
+
to the stdlib functools.partial wrapper. An example can speak volumes:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
from runps import ls
|
|
340
|
+
|
|
341
|
+
ls = ls.bake("-la")
|
|
342
|
+
print ls # "/usr/bin/ls -la"
|
|
343
|
+
|
|
344
|
+
# resolves to "ls / -la"
|
|
345
|
+
print ls("/")
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The idea is that calling "bake" on a command creates a callable object
|
|
349
|
+
that automatically passes along all of the arguments passed into "bake".
|
|
350
|
+
This gets **really interesting** when you combine this with the attribute
|
|
351
|
+
access on a command:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from runps import ssh
|
|
355
|
+
|
|
356
|
+
# calling whoami on the server. this is tedious to do if you're running
|
|
357
|
+
# any more than a few commands.
|
|
358
|
+
iam1 = ssh("myserver.com", "-p 1393", "whoami")
|
|
359
|
+
|
|
360
|
+
# wouldn't it be nice to bake the common parameters into the ssh command?
|
|
361
|
+
myserver = ssh.bake("myserver.com", p=1393)
|
|
362
|
+
|
|
363
|
+
print myserver # "/usr/bin/ssh myserver.com -p 1393"
|
|
364
|
+
|
|
365
|
+
# resolves to "/usr/bin/ssh myserver.com -p 1393 whoami"
|
|
366
|
+
iam2 = myserver.whoami()
|
|
367
|
+
|
|
368
|
+
assert(iam1 == iam2) # True!
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Now that the "myserver" callable represents a baked ssh command, you
|
|
372
|
+
can call anything on the server easily:
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
# resolves to "/usr/bin/ssh myserver.com -p 1393 tail /var/log/dumb_daemon.log -n 100"
|
|
376
|
+
print myserver.tail("/var/log/dumb_daemon.log", n=100)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Environment Variables
|
|
380
|
+
|
|
381
|
+
Environment variables are available much like they are in Bash:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
print HOME
|
|
385
|
+
print SHELL
|
|
386
|
+
print PS1
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
You can set enviroment variables the usual way, through the os.environ
|
|
390
|
+
mapping:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
import os
|
|
394
|
+
os.environ["TEST"] = "123"
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Now any new subprocess commands called from the script will be able to
|
|
398
|
+
access that environment variable.
|
|
399
|
+
|
|
400
|
+
## Exceptions
|
|
401
|
+
|
|
402
|
+
Exceptions are dynamically generated based on the return code of the command.
|
|
403
|
+
This lets you catch a specific return code, or catch all error return codes
|
|
404
|
+
through the base class ErrorReturnCode:
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
try: print ls("/some/non-existant/folder")
|
|
408
|
+
except ErrorReturnCode_2:
|
|
409
|
+
print "folder doesn't exist!"
|
|
410
|
+
create_the_folder()
|
|
411
|
+
except ErrorReturnCode:
|
|
412
|
+
print "unknown error"
|
|
413
|
+
exit(1)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Globbing
|
|
417
|
+
|
|
418
|
+
Glob-expansion is not done on your arguments. For example, this will not work:
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
from runps import du
|
|
422
|
+
print du("*")
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
You'll get an error to the effect of "cannot access '\*': No such file or directory".
|
|
426
|
+
This is because the "\*" needs to be glob expanded:
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
from runps import du, glob
|
|
430
|
+
print du(glob("*"))
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
## Commandline Arguments
|
|
435
|
+
|
|
436
|
+
You can access commandline arguments similar to Bash's $1, $2, etc by using
|
|
437
|
+
ARG1, ARG2, etc:
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
print ARG1, ARG2
|
|
441
|
+
|
|
442
|
+
# if an argument isn't defined, it's set to None
|
|
443
|
+
if ARG10 is None: do_something()
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
You can access the entire argparse/optparse-friendly list of commandline
|
|
447
|
+
arguments through "ARGV". This is recommended for flexibility:
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
import argparse
|
|
451
|
+
parser = argparse.ArgumentParser(prog="PROG")
|
|
452
|
+
parser.add_argument("-x", default=3, type=int)
|
|
453
|
+
ns = parser.parse_args(ARGV)
|
|
454
|
+
print ns.x
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
## Weirdly-named Commands
|
|
459
|
+
|
|
460
|
+
runps automatically handles underscore-dash conversions. For example, if you want
|
|
461
|
+
to call apt-get:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
apt_get("install", "mplayer", y=True)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
runps looks for "apt_get", but if it doesn't find it, replaces all underscores
|
|
468
|
+
with dashes and searches again. If the command still isn't found, a
|
|
469
|
+
CommandNotFound exception is raised.
|
|
470
|
+
|
|
471
|
+
Commands with other, less-commonly symbols in their names must be accessed
|
|
472
|
+
directly through the "Command" class wrapper. The Command class takes the full
|
|
473
|
+
path to the program as a string:
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
p27 = Command(which("python2.7"))
|
|
477
|
+
print p27("-h")
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
The Command wrapper is also useful for commands that are not in your standard PATH:
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
script = Command("/tmp/temporary-script.sh")
|
|
484
|
+
print script()
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Non-standard Exit Codes
|
|
488
|
+
|
|
489
|
+
Normally, if a command returns an exit code that is not 0, runps raises an exception
|
|
490
|
+
based on that exit code. However, if you have determined that an error code
|
|
491
|
+
is normal and want to retrieve the output of the command without runps raising an
|
|
492
|
+
exception, you can use the "_ok_code" special argument to suppress the exception:
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
output = runps.ls("dir_that_exists", "dir_that_doesnt", _ok_code=2)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
In the above example, even though you're trying to list a directory that doesn't
|
|
499
|
+
exist, you can still get the output from the directory that does exist by telling
|
|
500
|
+
the command that 2 is an "ok" exit code, so don't raise an exception.
|
|
501
|
+
|
|
502
|
+
_ok_code can also take a list or tuple of numbers for multiple ok exit codes.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
runps.py,sha256=ZXJUcBu-4o3eHiv13vTHy3F37KUucq_YP9z4cERlLb0,17526
|
|
2
|
+
runps-4.0.0.dist-info/licenses/AUTHORS.md,sha256=UEit9bmRGS35YiCe-h24oVGFJpf_4d_i7QtpzT89ERo,281
|
|
3
|
+
runps-4.0.0.dist-info/licenses/LICENSE.txt,sha256=2EkcbiNlaNBPfIwH8vG49dFCcG2rariPbg0t1Ha2xwY,1065
|
|
4
|
+
runps-4.0.0.dist-info/METADATA,sha256=_hdaJ0-WKovcdjLL1rbV3PjTF8PVxHkfiHtQp3VFeFs,15665
|
|
5
|
+
runps-4.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
6
|
+
runps-4.0.0.dist-info/top_level.txt,sha256=xxRNhiYCmiH39JBo7m911yXLpHavc0cfB9HKnu9Idd0,6
|
|
7
|
+
runps-4.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Author
|
|
2
|
+
|
|
3
|
+
* Andrew Moffat <andrew.robert.moffat@gmail.com>
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Contributors
|
|
7
|
+
|
|
8
|
+
* Dmitry Medvinsky <dmedvinsky@gmail.com>
|
|
9
|
+
* Jure Žiberna
|
|
10
|
+
* Bahadır Kandemir
|
|
11
|
+
* Jannis Leidel <jezdez@enn.io>
|
|
12
|
+
* tingletech
|
|
13
|
+
* tdudziak
|
|
14
|
+
* Arjen Stolk
|
|
15
|
+
* nemec
|
|
16
|
+
* fruch
|
|
17
|
+
* Ralph Bean
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# `runps` fork
|
|
21
|
+
|
|
22
|
+
* xapple
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (C) 2011-2012 by Andrew Moffat
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
runps
|
runps.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Constants #
|
|
2
|
+
__version__ = "4.0.0"
|
|
3
|
+
__project_url__ = "https://github.com/xapple/runps"
|
|
4
|
+
|
|
5
|
+
# Modules #
|
|
6
|
+
import sys, os, re, warnings, functools, types, subprocess
|
|
7
|
+
from glob import glob as original_glob
|
|
8
|
+
|
|
9
|
+
# Python 3 hack #
|
|
10
|
+
IS_PY3 = sys.version_info[0] == 3
|
|
11
|
+
if IS_PY3: unicode = str
|
|
12
|
+
|
|
13
|
+
###############################################################################
|
|
14
|
+
class CommandNotFound(Exception): pass
|
|
15
|
+
|
|
16
|
+
class ErrorReturnCode(Exception):
|
|
17
|
+
truncate_cap = 200
|
|
18
|
+
|
|
19
|
+
def __init__(self, full_cmd, stdout, stderr, call_args):
|
|
20
|
+
# Attributes #
|
|
21
|
+
self.full_cmd = full_cmd
|
|
22
|
+
self.stdout = stdout
|
|
23
|
+
self.stderr = stderr
|
|
24
|
+
self.call_args = call_args
|
|
25
|
+
# Check stdout #
|
|
26
|
+
if self.stdout is None:
|
|
27
|
+
out = "<redirected to '%s'>" % self.call_args['out']
|
|
28
|
+
out = out.encode()
|
|
29
|
+
else:
|
|
30
|
+
out = self.stdout[:self.truncate_cap]
|
|
31
|
+
out_delta = len(self.stdout) - len(out)
|
|
32
|
+
if out_delta:
|
|
33
|
+
out += ("... (%d more, please see e.stdout)" % out_delta).encode()
|
|
34
|
+
# Check stderr #
|
|
35
|
+
if self.stderr is None:
|
|
36
|
+
err = "<redirected to '%s'>" % self.call_args['err']
|
|
37
|
+
err = err.encode()
|
|
38
|
+
else:
|
|
39
|
+
err = self.stderr[:self.truncate_cap]
|
|
40
|
+
err_delta = len(self.stderr) - len(err)
|
|
41
|
+
if err_delta:
|
|
42
|
+
err += ("... (%d more, please see e.stderr)" % err_delta).encode()
|
|
43
|
+
# Build message #
|
|
44
|
+
msg = "\n\nRan: %s\n\nSTDOUT:\n\n %s\n\nSTDERR:\n\n %s"
|
|
45
|
+
msg = msg % (full_cmd, out.decode(), err.decode())
|
|
46
|
+
# Call parent #
|
|
47
|
+
super(ErrorReturnCode, self).__init__(msg)
|
|
48
|
+
|
|
49
|
+
rc_exc_regex = re.compile(r"ErrorReturnCode_(\d+)")
|
|
50
|
+
rc_exc_cache = {}
|
|
51
|
+
|
|
52
|
+
def get_rc_exc(rc):
|
|
53
|
+
rc = int(rc)
|
|
54
|
+
try:
|
|
55
|
+
return rc_exc_cache[rc]
|
|
56
|
+
except KeyError:
|
|
57
|
+
pass
|
|
58
|
+
name = "ErrorReturnCode_%d" % rc
|
|
59
|
+
exc = type(name, (ErrorReturnCode,), {})
|
|
60
|
+
rc_exc_cache[rc] = exc
|
|
61
|
+
return exc
|
|
62
|
+
|
|
63
|
+
def which(program):
|
|
64
|
+
def is_exe(file_path):
|
|
65
|
+
return os.path.exists(file_path) and os.access(file_path, os.X_OK)
|
|
66
|
+
file_path, file_name = os.path.split(program)
|
|
67
|
+
if file_path:
|
|
68
|
+
if is_exe(program): return program
|
|
69
|
+
else:
|
|
70
|
+
for path in os.environ["PATH"].split(os.pathsep):
|
|
71
|
+
exe_file = os.path.join(path, program)
|
|
72
|
+
if is_exe(exe_file):
|
|
73
|
+
return exe_file
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def resolve_program(program):
|
|
77
|
+
"""Our actual command might have a dash in it, but we can't call
|
|
78
|
+
that from python (we have to use underscores), so we'll check
|
|
79
|
+
if a dash version of our underscore command exists and use that
|
|
80
|
+
if it does."""
|
|
81
|
+
path = which(program)
|
|
82
|
+
if not path:
|
|
83
|
+
if "_" in program: path = which(program.replace("_", "-"))
|
|
84
|
+
if not path: return None
|
|
85
|
+
return path
|
|
86
|
+
|
|
87
|
+
def glob(arg):
|
|
88
|
+
return original_glob(arg) or arg
|
|
89
|
+
|
|
90
|
+
###############################################################################
|
|
91
|
+
class RunningCommand(object):
|
|
92
|
+
def __init__(self, command_ran, process, call_args, stdin=None):
|
|
93
|
+
# Base attributes #
|
|
94
|
+
self.command_ran = command_ran
|
|
95
|
+
self.process = process
|
|
96
|
+
self._stdout = None
|
|
97
|
+
self._stderr = None
|
|
98
|
+
self.call_args = call_args
|
|
99
|
+
|
|
100
|
+
# We're running in the background, return self and let us lazily
|
|
101
|
+
# evaluate.
|
|
102
|
+
if self.call_args["bg"]: return
|
|
103
|
+
|
|
104
|
+
# We're running this command as a with context, don't do anything
|
|
105
|
+
# because nothing was started to run from Command.__call__
|
|
106
|
+
if self.call_args["with"]: return
|
|
107
|
+
|
|
108
|
+
# Run and block #
|
|
109
|
+
if stdin: stdin = stdin.encode("utf8")
|
|
110
|
+
self._stdout, self._stderr = self.process.communicate(stdin)
|
|
111
|
+
self._handle_exit_code(self.process.wait())
|
|
112
|
+
|
|
113
|
+
def __enter__(self):
|
|
114
|
+
# We don't actually do anything here because anything that should
|
|
115
|
+
# have been done or would have been done in the Command.__call__ call.
|
|
116
|
+
# essentially all that has to happen is the command be pushed on
|
|
117
|
+
# the prepend stack.
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def __exit__(self, typ, value, traceback):
|
|
121
|
+
if self.call_args["with"] and Command._prepend_stack:
|
|
122
|
+
Command._prepend_stack.pop()
|
|
123
|
+
|
|
124
|
+
def __repr__(self):
|
|
125
|
+
return "<RunningCommand %r, pid:%d, special_args:%r" % (
|
|
126
|
+
self.command_ran, self.process.pid, self.call_args)
|
|
127
|
+
|
|
128
|
+
def __str__(self):
|
|
129
|
+
if IS_PY3: return self.__unicode__()
|
|
130
|
+
else: return unicode(self).encode("utf8")
|
|
131
|
+
|
|
132
|
+
def __unicode__(self):
|
|
133
|
+
if self.process:
|
|
134
|
+
if self.call_args["bg"]: self.wait()
|
|
135
|
+
if self._stdout: return self.stdout
|
|
136
|
+
else: return ""
|
|
137
|
+
|
|
138
|
+
def __eq__(self, other):
|
|
139
|
+
return unicode(self) == unicode(other)
|
|
140
|
+
|
|
141
|
+
def __contains__(self, item):
|
|
142
|
+
return item in str(self)
|
|
143
|
+
|
|
144
|
+
def __getattr__(self, p):
|
|
145
|
+
# Let these three attributes pass through to the Popen object
|
|
146
|
+
if p in ("send_signal", "terminate", "kill"):
|
|
147
|
+
if self.process: return getattr(self.process, p)
|
|
148
|
+
else: raise AttributeError
|
|
149
|
+
return getattr(unicode(self), p)
|
|
150
|
+
|
|
151
|
+
def __long__(self):
|
|
152
|
+
return long(str(self).strip())
|
|
153
|
+
|
|
154
|
+
def __float__(self):
|
|
155
|
+
return float(str(self).strip())
|
|
156
|
+
|
|
157
|
+
def __int__(self):
|
|
158
|
+
return int(str(self).strip())
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def stdout(self):
|
|
162
|
+
if self.call_args["bg"]: self.wait()
|
|
163
|
+
return self._stdout.decode("utf8", "replace")
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def stderr(self):
|
|
167
|
+
if self.call_args["bg"]: self.wait()
|
|
168
|
+
return self._stderr.decode("utf8", "replace")
|
|
169
|
+
|
|
170
|
+
def wait(self):
|
|
171
|
+
if self.process.returncode is not None: return
|
|
172
|
+
self._stdout, self._stderr = self.process.communicate()
|
|
173
|
+
self._handle_exit_code(self.process.wait())
|
|
174
|
+
return str(self)
|
|
175
|
+
|
|
176
|
+
def _handle_exit_code(self, rc):
|
|
177
|
+
if rc not in self.call_args["ok_code"]:
|
|
178
|
+
raise get_rc_exc(rc)(self.command_ran, self._stdout, self._stderr, self.call_args)
|
|
179
|
+
|
|
180
|
+
def __len__(self):
|
|
181
|
+
return len(str(self))
|
|
182
|
+
|
|
183
|
+
###############################################################################
|
|
184
|
+
class Command(object):
|
|
185
|
+
_prepend_stack = []
|
|
186
|
+
|
|
187
|
+
call_args = {
|
|
188
|
+
"fg": False, # run command in foreground
|
|
189
|
+
"bg": False, # run command in background
|
|
190
|
+
"with": False, # prepend the command to every command after it
|
|
191
|
+
"out": None, # redirect STDOUT
|
|
192
|
+
"err": None, # redirect STDERR
|
|
193
|
+
"err_to_out": None, # redirect STDERR to STDOUT
|
|
194
|
+
"in": None,
|
|
195
|
+
"env": os.environ,
|
|
196
|
+
"cwd": None,
|
|
197
|
+
# This is for commands that may have a different exit status than the
|
|
198
|
+
# normal 0. This can either be an integer or a list/tuple of integers
|
|
199
|
+
"ok_code": 0,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def create(cls, program):
|
|
204
|
+
path = resolve_program(program)
|
|
205
|
+
if not path: raise CommandNotFound(program)
|
|
206
|
+
return cls(path)
|
|
207
|
+
|
|
208
|
+
def __init__(self, path):
|
|
209
|
+
# Path to executable #
|
|
210
|
+
self._path = path
|
|
211
|
+
# Partial #
|
|
212
|
+
self._partial = False
|
|
213
|
+
self._partial_baked_args = []
|
|
214
|
+
self._partial_call_args = {}
|
|
215
|
+
|
|
216
|
+
def __getattribute__(self, name):
|
|
217
|
+
# Convenience #
|
|
218
|
+
getattribute = functools.partial(object.__getattribute__, self)
|
|
219
|
+
if name.startswith("_"): return getattribute(name)
|
|
220
|
+
if name == "bake": return getattribute("bake")
|
|
221
|
+
else: return getattribute("bake")(name)
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _extract_call_args(kwargs):
|
|
225
|
+
kwargs = kwargs.copy()
|
|
226
|
+
call_args = Command.call_args.copy()
|
|
227
|
+
for arg, default in call_args.items():
|
|
228
|
+
key = "_" + arg
|
|
229
|
+
if key in kwargs:
|
|
230
|
+
call_args[arg] = kwargs[key]
|
|
231
|
+
del kwargs[key]
|
|
232
|
+
return call_args, kwargs
|
|
233
|
+
|
|
234
|
+
def _format_arg(self, arg):
|
|
235
|
+
if IS_PY3: arg = str(arg)
|
|
236
|
+
else: arg = unicode(arg).encode("utf8")
|
|
237
|
+
return arg
|
|
238
|
+
|
|
239
|
+
def _compile_args(self, args, kwargs):
|
|
240
|
+
processed_args = []
|
|
241
|
+
|
|
242
|
+
# Aggregate positional args
|
|
243
|
+
for arg in args:
|
|
244
|
+
if isinstance(arg, (list, tuple)):
|
|
245
|
+
if not arg:
|
|
246
|
+
message = "Empty list passed as an argument to '%r'."
|
|
247
|
+
message += " If you're using glob.glob(), please use runps.glob() instead."
|
|
248
|
+
warnings.warn(message % self.path, stacklevel=3)
|
|
249
|
+
for sub_arg in arg: processed_args.append(self._format_arg(sub_arg))
|
|
250
|
+
else: processed_args.append(self._format_arg(arg))
|
|
251
|
+
|
|
252
|
+
# Aggregate the keyword arguments
|
|
253
|
+
for k,v in kwargs.items():
|
|
254
|
+
# We're passing a short arg as a kwarg, example:
|
|
255
|
+
# cut(d="\t")
|
|
256
|
+
if len(k) == 1:
|
|
257
|
+
processed_args.append("-" + k)
|
|
258
|
+
if v is not True: processed_args.append(self._format_arg(v))
|
|
259
|
+
# we're doing a long arg
|
|
260
|
+
else:
|
|
261
|
+
k = k.replace("_", "-")
|
|
262
|
+
if v is True: processed_args.append("--" + k)
|
|
263
|
+
else: processed_args.append("--%s=%s" % (k, self._format_arg(v)))
|
|
264
|
+
return processed_args
|
|
265
|
+
|
|
266
|
+
def bake(self, *args, **kwargs):
|
|
267
|
+
fn = Command(self._path)
|
|
268
|
+
fn._partial = True
|
|
269
|
+
call_args, kwargs = self._extract_call_args(kwargs)
|
|
270
|
+
pruned_call_args = call_args
|
|
271
|
+
for k,v in Command.call_args.items():
|
|
272
|
+
try:
|
|
273
|
+
if pruned_call_args[k] == v:
|
|
274
|
+
del pruned_call_args[k]
|
|
275
|
+
except KeyError: continue
|
|
276
|
+
fn._partial_call_args.update(self._partial_call_args)
|
|
277
|
+
fn._partial_call_args.update(pruned_call_args)
|
|
278
|
+
fn._partial_baked_args.extend(self._partial_baked_args)
|
|
279
|
+
fn._partial_baked_args.extend(self._compile_args(args, kwargs))
|
|
280
|
+
return fn
|
|
281
|
+
|
|
282
|
+
def __str__(self):
|
|
283
|
+
if IS_PY3: return self.__unicode__()
|
|
284
|
+
else: return unicode(self).encode("utf-8")
|
|
285
|
+
|
|
286
|
+
def __repr__(self):
|
|
287
|
+
return str(self)
|
|
288
|
+
|
|
289
|
+
def __unicode__(self):
|
|
290
|
+
baked_args = " ".join(self._partial_baked_args)
|
|
291
|
+
if baked_args: baked_args = " " + baked_args
|
|
292
|
+
return self._path + baked_args
|
|
293
|
+
|
|
294
|
+
def __eq__(self, other):
|
|
295
|
+
try: return str(self) == str(other)
|
|
296
|
+
except: return False
|
|
297
|
+
|
|
298
|
+
def __enter__(self):
|
|
299
|
+
Command._prepend_stack.append([self._path])
|
|
300
|
+
|
|
301
|
+
def __exit__(self, typ, value, traceback):
|
|
302
|
+
Command._prepend_stack.pop()
|
|
303
|
+
|
|
304
|
+
def __call__(self, *args, **kwargs):
|
|
305
|
+
kwargs = kwargs.copy()
|
|
306
|
+
args = list(args)
|
|
307
|
+
cmd = []
|
|
308
|
+
|
|
309
|
+
# Aggregate any with contexts
|
|
310
|
+
for prepend in self._prepend_stack: cmd.extend(prepend)
|
|
311
|
+
|
|
312
|
+
cmd.append(self._path)
|
|
313
|
+
|
|
314
|
+
call_args, kwargs = self._extract_call_args(kwargs)
|
|
315
|
+
call_args.update(self._partial_call_args)
|
|
316
|
+
|
|
317
|
+
# Here we normalize the ok_code to be something we can do
|
|
318
|
+
# "if return_code in call_args["ok_code"]" on
|
|
319
|
+
if not isinstance(call_args["ok_code"], (tuple, list)):
|
|
320
|
+
call_args["ok_code"] = [call_args["ok_code"]]
|
|
321
|
+
|
|
322
|
+
# Set pipe to None if we're outputting straight to CLI
|
|
323
|
+
pipe = None if call_args["fg"] else subprocess.PIPE
|
|
324
|
+
|
|
325
|
+
# Check if we're piping via composition
|
|
326
|
+
stdin = pipe
|
|
327
|
+
actual_stdin = None
|
|
328
|
+
if args:
|
|
329
|
+
first_arg = args.pop(0)
|
|
330
|
+
if isinstance(first_arg, RunningCommand):
|
|
331
|
+
# It makes sense that if the input pipe of a command is running
|
|
332
|
+
# in the background, then this command should run in the
|
|
333
|
+
# background as well
|
|
334
|
+
if first_arg.call_args["bg"]:
|
|
335
|
+
call_args["bg"] = True
|
|
336
|
+
stdin = first_arg.process.stdout
|
|
337
|
+
else:
|
|
338
|
+
actual_stdin = first_arg.stdout
|
|
339
|
+
else: args.insert(0, first_arg)
|
|
340
|
+
|
|
341
|
+
processed_args = self._compile_args(args, kwargs)
|
|
342
|
+
|
|
343
|
+
# Makes sure our arguments are broken up correctly
|
|
344
|
+
split_args = self._partial_baked_args + processed_args
|
|
345
|
+
final_args = split_args
|
|
346
|
+
|
|
347
|
+
cmd.extend(final_args)
|
|
348
|
+
command_ran = " ".join(cmd)
|
|
349
|
+
|
|
350
|
+
# With contexts shouldn't run at all yet, they prepend
|
|
351
|
+
# to every command in the context
|
|
352
|
+
if call_args["with"]:
|
|
353
|
+
Command._prepend_stack.append(cmd)
|
|
354
|
+
return RunningCommand(command_ran, None, call_args)
|
|
355
|
+
|
|
356
|
+
# Stdin from string
|
|
357
|
+
input = call_args["in"]
|
|
358
|
+
if input:
|
|
359
|
+
actual_stdin = input
|
|
360
|
+
|
|
361
|
+
# Stdout redirection
|
|
362
|
+
stdout = pipe
|
|
363
|
+
out = call_args["out"]
|
|
364
|
+
if out:
|
|
365
|
+
if hasattr(out, "write"): stdout = out
|
|
366
|
+
else: stdout = open(str(out), "w")
|
|
367
|
+
|
|
368
|
+
# Stderr redirection
|
|
369
|
+
stderr = pipe
|
|
370
|
+
err = call_args["err"]
|
|
371
|
+
|
|
372
|
+
if err:
|
|
373
|
+
if hasattr(err, "write"): stderr = err
|
|
374
|
+
else: stderr = open(str(err), "w")
|
|
375
|
+
|
|
376
|
+
if call_args["err_to_out"]: stderr = subprocess.STDOUT
|
|
377
|
+
|
|
378
|
+
# Leave shell=False
|
|
379
|
+
process = subprocess.Popen(cmd, shell=False, env=call_args["env"],
|
|
380
|
+
cwd=call_args["cwd"], stdin=stdin, stdout=stdout, stderr=stderr)
|
|
381
|
+
|
|
382
|
+
return RunningCommand(command_ran, process, call_args, actual_stdin)
|
|
383
|
+
|
|
384
|
+
###############################################################################
|
|
385
|
+
class Environment(dict):
|
|
386
|
+
"""
|
|
387
|
+
This class is used directly when we do a "from runps import *". It allows
|
|
388
|
+
lookups to names that aren't found in the global scope to be searched
|
|
389
|
+
for as a program. For example, if "ls" isn't found in the program's
|
|
390
|
+
scope, we consider it a system program and try to find it.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(self, *args, **kwargs):
|
|
394
|
+
dict.__init__(self, *args, **kwargs)
|
|
395
|
+
self["Command"] = Command
|
|
396
|
+
self["CommandNotFound"] = CommandNotFound
|
|
397
|
+
self["ErrorReturnCode"] = ErrorReturnCode
|
|
398
|
+
self["ARGV"] = sys.argv[1:]
|
|
399
|
+
for i, arg in enumerate(sys.argv):
|
|
400
|
+
self["ARG%d" % i] = arg
|
|
401
|
+
# This needs to be last
|
|
402
|
+
self["env"] = os.environ
|
|
403
|
+
|
|
404
|
+
def __setitem__(self, k, v):
|
|
405
|
+
# Are we altering an environment variable?
|
|
406
|
+
if "env" in self and k in self["env"]: self["env"][k] = v
|
|
407
|
+
# No? Just setting a regular name
|
|
408
|
+
else: dict.__setitem__(self, k, v)
|
|
409
|
+
|
|
410
|
+
def __missing__(self, key):
|
|
411
|
+
# This seems to happen in Python 3
|
|
412
|
+
if key == "__path__":
|
|
413
|
+
message = "You cannot use the form 'from runps import x' in Python 3."
|
|
414
|
+
message += "Please use x = runps.Command('x') instead."
|
|
415
|
+
raise ImportError(message)
|
|
416
|
+
|
|
417
|
+
# The only way we'd get to here is if we've tried to
|
|
418
|
+
# import * from a repl. So, raise an exception, since
|
|
419
|
+
# that's really the only sensible thing to do
|
|
420
|
+
if key == "__all__":
|
|
421
|
+
message = "Cannot import * from runps."
|
|
422
|
+
message += "Please import runps or import programs individually."
|
|
423
|
+
raise ImportError(message)
|
|
424
|
+
|
|
425
|
+
# If we end with "_" just go ahead and skip searching
|
|
426
|
+
# our namespace for python stuff. This was mainly for the
|
|
427
|
+
# command "id", which is a popular program for finding
|
|
428
|
+
# if a user exists, but also a python function for getting
|
|
429
|
+
# the address of an object. So can call the python
|
|
430
|
+
# version by "id" and the program version with "id_"
|
|
431
|
+
if not key.endswith("_"):
|
|
432
|
+
# check if we're naming a dynamically generated ReturnCode exception
|
|
433
|
+
try: return rc_exc_cache[key]
|
|
434
|
+
except KeyError:
|
|
435
|
+
m = rc_exc_regex.match(key)
|
|
436
|
+
if m: return get_rc_exc(int(m.group(1)))
|
|
437
|
+
|
|
438
|
+
# are we naming a command-line argument?
|
|
439
|
+
if key.startswith("ARG"):
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
# is it a built-in?
|
|
443
|
+
try: return getattr(self["__builtins__"], key)
|
|
444
|
+
except AttributeError: pass
|
|
445
|
+
elif not key.startswith("_"): key = key.rstrip("_")
|
|
446
|
+
|
|
447
|
+
# how about an environment variable?
|
|
448
|
+
try: return os.environ[key]
|
|
449
|
+
except KeyError: pass
|
|
450
|
+
|
|
451
|
+
# is it a custom built-in?
|
|
452
|
+
builtin = getattr(self, "b_" + key, None)
|
|
453
|
+
if builtin: return builtin
|
|
454
|
+
|
|
455
|
+
# it must be a command then
|
|
456
|
+
return Command.create(key)
|
|
457
|
+
|
|
458
|
+
def b_cd(self, path):
|
|
459
|
+
os.chdir(path)
|
|
460
|
+
|
|
461
|
+
def b_which(self, program):
|
|
462
|
+
return which(program)
|
|
463
|
+
|
|
464
|
+
###############################################################################
|
|
465
|
+
class SelfWrapper(types.ModuleType):
|
|
466
|
+
"""
|
|
467
|
+
This is a thin wrapper around THIS module (we patch sys.modules[__name__]).
|
|
468
|
+
this is in the case that the user does a "from runps import whatever"
|
|
469
|
+
in other words, they only want to import certain programs, not the whole
|
|
470
|
+
system PATH worth of commands. In this case, we just proxy the
|
|
471
|
+
import lookup to our Environment class.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
def __init__(self, self_module):
|
|
475
|
+
"""
|
|
476
|
+
This is super ugly to have to copy attributes like this,
|
|
477
|
+
but it seems to be the only way to make reload() behave
|
|
478
|
+
nicely. If one makes these attributes dynamic lookups in
|
|
479
|
+
__getattr__, reload sometimes chokes in weird ways.
|
|
480
|
+
"""
|
|
481
|
+
for attr in ["__builtins__", "__doc__", "__name__", "__package__"]:
|
|
482
|
+
setattr(self, attr, getattr(self_module, attr))
|
|
483
|
+
|
|
484
|
+
self.self_module = self_module
|
|
485
|
+
self.env = Environment(globals())
|
|
486
|
+
|
|
487
|
+
def __getattr__(self, name):
|
|
488
|
+
return self.env[name]
|
|
489
|
+
|
|
490
|
+
###############################################################################
|
|
491
|
+
self = sys.modules[__name__]
|
|
492
|
+
sys.modules[__name__] = SelfWrapper(self)
|