Fixing the Python subprocess interface
amoffat.github.com
amoffat.github.com
All that said, this is horribly un-Pythonic. A much better route to so is something like envoy[1] which simply wraps `subprocess` in a sane API.
Also, it's not clear on what basis you make this assertion:
> FWIW, re-replaying the stack trace to figure out what was imported and re-implementing it is a horrible idea.
Does it not work?
Line 17 of PEP-20 sums up my thoughts on this code as more than an intellectual exercise.
> If the implementation is hard to explain, it's a bad idea.
Regarding using Python as a replacement for shell scripting: if you have to rewire the language to do what you want, why are you using a different tool for the job?
I don't think this principle is universal in software, and I don't think you need to embrace it as a prerequisite for writing or distributing Python code. My personal preference would be for a statement more like this, "If the implementation is more complicated than it needs to be to do what you want, it's probably a bad idea."
> Regarding using Python as a replacement for shell scripting: if you have to rewire the language to do what you want, why are you using a different tool for the job?
Well that's easy to answer. It's cleaner and more readable than bash, has some nice features that shell scripts lack, can be used to call into python libraries, allows a single unified codebase if you're already writing python code . . . I could go on.
I think the more comfortable (and Pythonic?) way to do this would be to explicitly create these command objects:
>>> import pbs
>>> ffmpeg = pbs.Command('ffmpeg') # or '/usr/bin/ffmpeg', perhaps
>>> result = ffmpeg(...)
[Edit: I really like the concept of using Python's positional and keyword arguments to construct a shell command, though. Great insight there.]In your example, what pbs is doing is no different than a bash shell script reporting a "command not found". It isn't what I would want if I were writing a complex piece of software interacting with ffmpeg, but for a simple script to process a bunch of files in a directory, I like it.
I've tended to shy away from using Python for shell scripting because subprocess is so ugly (even its out-of-favor, crippled relative os.system is nasty), and pbs looks like it does a really great job at addressing that.
# magical, designed only for single shell scripts
from pbs import *
ffmpeg()
# less magical
from pbs import ffmpeg
ffmpeg()
# or
import pbs
pbs.ffmpeg()
# no magic
import pbs
ffmpeg = pbs.Command(pbs.which("ffmpeg")) # command takes full path
ffmpeg()
I tried to cover the main use cases adequately. My goal was to ease a pain point myself and others have experienced, that other popular packages don't address well unfortunately.If it's helped anyone like it's helped me, I'm happy and glad to share!
Perhaps the commands should accessible from an object you import. That's slightly more typing but more explicit and would not require ugly magic. E.g.
from pbs import sh
print sh.ifconfig('eth')
If it's not clear, the 'sh' object could override __getattr__ or __getattribute__ and wrap commands as necessary.--And its not a "global" name space, its just the namespace of your script.--
somewhat naive example:
>>> time = 'blah'
>>> time
'blah'
>>> from datetime import *
>>> time
<type 'datetime.time'>
>>>It's especially bad in this case where adding an executable in the system can suddenly shadow any built-in name (e.g. imagine someone adds a "print" or "sys" executable).
let's say you have
from a import foo from b import *
what does 'foo' refer to? 'b' could contain 'foo', which would overwrite your previous reference to 'a.foo'. its hard to figure out what 'foo' now refers to from inspecting the code. its better to be explicit:
from a import foo from b import bar, baz
ifconfig = pbs.Command("/path/to/ifconfig")It might not be pythonic, it might be a crime against Guido and everything he represents, but it's pretty awesome.
# sort this directory by biggest file
print sort(du("*", "-sb"), "-rn")
# print the number of folders and files in /etc
print wc(ls("/etc", "-1"), "-l")
The reason that I like that method over, say, envoy's [1] method is that envoy.run('uptime | pbcopy') has what I consider code in strings. When I'm writing a script, the programs I'm calling, and how they interact, is part of the "code" to me. I would prefer that they're at the language level, and not represented as strings.[1] I only just learned about envoy in this thread. Thanks! Sadly, one of the places where I run my Python scripts is a location where I can't install my own packages, and I don't want to deal with using my own install of Python, so I tend to just implement something like this:
def checked_exec(seq):
p = Popen(seq, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
print 'err: ' + stderr
print "'" + seq + "' failed."
sys.exit()
return stdoutI've written what I feel is a much better solution to this problem: Envoy.
https://github.com/kennethreitz/envoy
It's pythonic and makes far fewer assumptions about both your code and what you're running.
Clint and Requests are worth a look too.. Thanks Kenn!
However:
Please, please, please don't use or recommend things like "from pbs import * ". Namespace pollution is bad enough when importing a documented collection of functions. Importing functions that are named based on whatever happens to be in my path at the moment ... that's seriously scary.
But "import pbs" looks like fun. :-) And "pbs.ls" isn't that much to type.
As The Zen of Python says:
] Namespaces are one honking great idea -- let's do more of those!
P.S. Hmmm ... but does "import pbs" work? Haven't tried it.
For a sane alternative I'd recommend Kenneth Reitz' awesome Envoy (https://github.com/kennethreitz/envoy)
https://github.com/JulienPalard/Pipe
It would be really cool (though admittedly less Pythonic) to combine the infix notation provided by the Pipe library to allow more shell-like function chaining.
Instead of this...
print wc(ls("/etc", "-1"), "-l")
You would have this... print ls("/etc", "-1") | wc("-l")If you think "horrible" hacks are a slight against something that provides an amazing level of functionality to an end user, then you've lost sight of what we're coding for.
I code in Ruby for a living, and this is even a bit much magic for me. I suppose you can use it in magic-less mode with `from pbs import Command`, and set up your command set manually.
If you'd like to handle missing system executables, catch the exception. You should be writing in that style anyways.
Honestly, this cleans up a ton of system scripting code, making it way more readable/maintainable.
Maybe the code could be cleaned up, but this is the direction that Python should be heading. Abstract away the complications when possible, keep low-level stuff around for when it's absolutely needed.
Beautiful is better than ugly.
curl("http://duckduckgo.com/", "-o page.html", "--silent")
curl("http://duckduckgo.com/", "-o", "page.html", "--silent")
are equivalent. This worries me, I would much rather always have it be one argument corresponding to exactly one shell argument. Here it looks like in some (maybe all?) cases arguments are split on spaces, which means always having to be extremely cautious about escaping, something a good abstraction shouldn't force you to deal with.So cat("filename with spaces in it") will fail, but cat("'filename with spaces in it') ought to succeed.
It's a neat experiment, but using this module in production would not be a great idea.
I'd feel more comfortable if it only exported one variable.
Figuring out a way around this (mocking up an fd to pass to sort at instantiation time that blocks until stdout on du is available) is left as an exercise to the reader. I've already said too much. ;)
Anyway, this is a great idea. :)