pbs-0.95/0000755000175000017500000000000011717347134013317 5ustar amoffatamoffat00000000000000pbs-0.95/LICENSE.txt0000644000175000017500000000205111711676171015140 0ustar amoffatamoffat00000000000000Copyright (C) 2011-2012 by Andrew Moffat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pbs-0.95/PKG-INFO0000644000175000017500000000165411717347134014422 0ustar amoffatamoffat00000000000000Metadata-Version: 1.0 Name: pbs Version: 0.95 Summary: Python subprocess wrapper Home-page: https://github.com/amoffat/pbs Author: Andrew Moffat Author-email: andrew.robert.moffat@gmail.com License: MIT Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Libraries :: Python Modules pbs-0.95/README.md0000644000175000017500000002264511717346752014614 0ustar amoffatamoffat00000000000000PBS is a unique subprocess wrapper that maps your system programs to Python functions dynamically. PBS helps you write shell scripts in Python by giving you the good features of Bash (easy command calling, easy piping) with all the power and flexibility of Python. ```python from pbs import ifconfig print ifconfig("eth0") ``` PBS is not a collection of system commands implemented in Python. # Getting $> pip install pbs # Usage The easiest way to get up and running is to import pbs directly or import your program from pbs: ```python import pbs print pbs.ifconfig("eth0") from pbs import ifconfig print ifconfig("eth0") ``` A less common usage pattern is through PBS Command wrapper, which takes a full path to a command and returns a callable object. This is useful for programs that have weird characters in their names or programs that aren't in your $PATH: ```python import pbs ffmpeg = pbs.Command("/usr/bin/ffmpeg") ffmpeg(movie_file) ``` The last usage pattern is for trying PBS through an interactive REPL. By default, this acts like a star import (so all of your system programs will be immediately available as functions): $> python pbs.py # Examples ## Executing Commands Commands work like you'd expect. **Just call your program's name like a function:** ```python # print the contents of this directory print ls("-l") # get the longest line of this file longest_line = wc(__file__, "-L") # get interface information print ifconfig("eth0") ``` Note that these aren't Python functions, these are running the binary commands on your system dynamically by resolving your PATH, much like Bash does. In this way, all the programs on your system are easily available in Python. You can also call attributes on commands. This translates to the command name followed by the attribute name: ```python from pbs import git # resolves to "git branch -v" print git.branch("-v") ``` It turns out this is extremely useful for commands whose first argument is often another sub-command (like git, svn, time, sudo, etc). See "Baking" for an advanced usage of this. ## Keyword Arguments Keyword arguments also work like you'd expect: they get replaced with the long-form and short-form commandline option: ```python # resolves to "curl http://duckduckgo.com/ -o page.html --silent" curl("http://duckduckgo.com/", o="page.html", silent=True) # or if you prefer not to use keyword arguments, these do the same thing: curl("http://duckduckgo.com/", "-o page.html", "--silent") curl("http://duckduckgo.com/", "-o", "page.html", "--silent") # resolves to "adduser amoffat --system --shell=/bin/bash --no-create-home" adduser("amoffat", system=True, shell="/bin/bash", no_create_home=True) # or adduser("amoffat", "--system", "--shell /bin/bash", "--no-create-home") ``` ## Piping Piping has become function composition: ```python # sort this directory by biggest file print sort(du(glob("*"), "-sb"), "-rn") # print the number of folders and files in /etc print wc(ls("/etc", "-1"), "-l") ``` ## Redirection PBS can redirect the standard and error output streams of a process to a file. This is done with the special _out and _err keyword arguments. You can pass a filename or a file object as the argument value. When the name of an already existing file is passed, the contents of the file will be overwritten. ```python ls(_out="files.list") ls("nonexistent", _err="error.txt") ``` PBS can also redirect the error output stream to the standard output stream, using the special _err_to_out=True keyword argument. ## Sudo and With Contexts Commands can be run within a "with" context. Popular commands using this might be "sudo" or "fakeroot": ```python with sudo: print ls("/root") ``` If you need to run a command in a with context AND call it, for example, specifying a -p prompt with sudo, you need to use the "_with" keyword argument. This let's the command know that it's being run from a with context so it can behave correctly. ```python with sudo(p=">", _with=True): print ls("/root") ``` ## Background Processes Commands can be run in the background with the special _bg=True keyword argument: ```python # blocks sleep(3) print "...3 seconds later" # doesn't block p = sleep(3, _bg=True) print "prints immediately!" p.wait() print "...and 3 seconds later" ``` You can also pipe together background processes! ```python p = wc(curl("http://github.com/", silent=True, _bg=True), "--bytes") print "prints immediately!" print "byte count of github: %d" % int(p) # lazily completes ``` This lets you start long-running commands at the beginning of your script (like a file download) and continue performing other commands in the foreground. ## Foreground Processes Foreground processes are processes that you want to interact directly with the default stdout and stdin of your terminal. In other words, these are processes that you do not want to return their output as a return value of their call. An example would be opening a text editor: ```python vim(file_to_edit) ``` This will block because pbs will be trying to aggregate the output of the command to python, without displaying anything to the screen. The solution is the "_fg" special keyword arg: ```python vim(file_to_edit, _fg=True) ``` This will open vim as expected and let you use it as expected, with all the input coming from the keyboard and the output going to the screen. The return value of a foreground process is an empty string. ## Finding Commands "Which" finds the full path of a program, or returns None if it doesn't exist. This command is one of the few commands implemented as a Python function, and therefore doesn't rely on the "which" program actually existing. ```python print which("python") # "/usr/bin/python" print which("ls") # "/bin/ls" print which("some_command") # None if not which("supervisorctl"): apt_get("install", "supervisor", "-y") ``` ## Baking PBS is capable of "baking" arguments into commands. This is similar to the stdlib functools.partial wrapper. An example can speak volumes: ```python from pbs import ls ls = ls.bake("-la") print ls # "/usr/bin/ls -la" # resolves to "ls / -la" print ls("/") ``` The idea is that calling "bake" on a command creates a callable object that automatically passes along all of the arguments passed into "bake". This gets **really interesting** when you combine this with the attribute access on a command: ```python from pbs import ssh # calling whoami on the server. this is tedious to do if you're running # any more than a few commands. iam1 = ssh("myserver.com", "-p 1393", "whoami") # wouldn't it be nice to bake the common parameters into the ssh command? myserver = ssh.bake("myserver.com", p=1393) print myserver # "/usr/bin/ssh myserver.com -p 1393" # resolves to "/usr/bin/ssh myserver.com -p 1393 whoami" iam2 = myserver.whoami() assert(iam1 == iam2) # True! ``` Now that the "myserver" callable represents a baked ssh command, you can call anything on the server easily: ```python # resolves to "/usr/bin/ssh myserver.com -p 1393 tail /var/log/dumb_daemon.log -n 100" print myserver.tail("/var/log/dumb_daemon.log", n=100) ``` ## Environment Variables Environment variables are available much like they are in Bash: ```python print HOME print SHELL print PS1 ``` You can set enviroment variables the usual way, through the os.environ mapping: ```python import os os.environ["TEST"] = "123" ``` Now any new subprocess commands called from the script will be able to access that environment variable. ## Exceptions Exceptions are dynamically generated based on the return code of the command. This lets you catch a specific return code, or catch all error return codes through the base class ErrorReturnCode: ```python try: print ls("/some/non-existant/folder") except ErrorReturnCode_2: print "folder doesn't exist!" create_the_folder() except ErrorReturnCode: print "unknown error" exit(1) ``` ## Globbing Glob-expansion is not done on your arguments. For example, this will not work: ```python from pbs import du print du("*") ``` You'll get an error to the effect of "cannot access '\*': No such file or directory". This is because the "\*" needs to be glob expanded: ```python from pbs import du, glob print du(glob("*")) ``` ## Commandline Arguments You can access commandline arguments similar to Bash's $1, $2, etc by using ARG1, ARG2, etc: ```python print ARG1, ARG2 # if an argument isn't defined, it's set to None if ARG10 is None: do_something() ``` You can access the entire argparse/optparse-friendly list of commandline arguments through "ARGV". This is recommended for flexibility: ```python import argparse parser = argparse.ArgumentParser(prog="PROG") parser.add_argument("-x", default=3, type=int) ns = parser.parse_args(ARGV) print ns.x ``` ## Weirdly-named Commands PBS automatically handles underscore-dash conversions. For example, if you want to call apt-get: ```python apt_get("install", "mplayer", y=True) ``` PBS looks for "apt_get", but if it doesn't find it, replaces all underscores with dashes and searches again. If the command still isn't found, a CommandNotFound exception is raised. Commands with other, less-commonly symbols in their names must be accessed directly through the "Command" class wrapper. The Command class takes the full path to the program as a string: ```python p27 = Command(which("python2.7")) print p27("-h") ``` The Command wrapper is also useful for commands that are not in your standard PATH: ```python script = Command("/tmp/temporary-script.sh") print script() ``` pbs-0.95/test.py0000644000175000017500000001030311717346752014652 0ustar amoffatamoffat00000000000000import os import unittest requires_posix = unittest.skipUnless(os.name == "posix", "Requires POSIX") class PbsTestSuite(unittest.TestCase): @requires_posix def test_print_command(self): from pbs import ls, which actual_location = which("ls") out = str(ls) self.assertEqual(out, actual_location) @requires_posix def test_which(self): from pbs import which, ls self.assertEqual(which("fjoawjefojawe"), None) self.assertEqual(which("ls"), str(ls)) @requires_posix def test_no_arg(self): import pwd from pbs import whoami u1 = unicode(whoami()).strip() u2 = pwd.getpwuid(os.geteuid())[0] self.assertEqual(u1, u2) @requires_posix def test_short_bool_option(self): from pbs import id i1 = int(id(u=True)) i2 = os.geteuid() self.assertEqual(i1, i2) @requires_posix def test_long_bool_option(self): from pbs import id i1 = int(id(user=True, real=True)) i2 = os.getuid() self.assertEqual(i1, i2) @requires_posix def test_composition(self): from pbs import ls, wc c1 = int(wc(ls(A=True), l=True)) c2 = len(os.listdir(".")) self.assertEqual(c1, c2) @requires_posix def test_short_option(self): from pbs import sh s1 = unicode(sh(c="echo test")).strip() s2 = "test" self.assertEqual(s1, s2) @requires_posix def test_long_option(self): from pbs import sed, echo out = unicode(sed(echo("test"), expression="s/test/lol/")).strip() self.assertEqual(out, "lol") @requires_posix def test_command_wrapper(self): from pbs import Command, which ls = Command(which("ls")) wc = Command(which("wc")) c1 = int(wc(ls(A=True), l=True)) c2 = len(os.listdir(".")) self.assertEqual(c1, c2) @requires_posix def test_background(self): from pbs import sleep import time start = time.time() sleep_time = 1 p = sleep(sleep_time, _bg=True) now = time.time() self.assertTrue(now - start < sleep_time) p.wait() now = time.time() self.assertTrue(now - start > sleep_time) @requires_posix def test_with_context(self): from pbs import time, ls with time: out = ls().stderr self.assertTrue("pagefaults" in out) @requires_posix def test_with_context_args(self): from pbs import time, ls with time(verbose=True, _with=True): out = ls().stderr self.assertTrue("Voluntary context switches" in out) @requires_posix def test_err_to_out(self): from pbs import time, ls with time(_with=True): out = ls(_err_to_out=True) self.assertTrue("pagefaults" in out) @requires_posix def test_out_redirection(self): import tempfile from pbs import ls file_obj = tempfile.TemporaryFile() out = ls(_out=file_obj) self.assertTrue(len(out) == 0) file_obj.seek(0) actual_out = file_obj.read() self.assertTrue(len(actual_out) != 0) @requires_posix def test_err_redirection(self): import tempfile from pbs import time, ls file_obj = tempfile.TemporaryFile() with time(_with=True): out = ls(_err=file_obj) file_obj.seek(0) actual_out = file_obj.read() self.assertTrue(len(actual_out) != 0) @requires_posix def test_subcommand(self): from pbs import time out = time.ls(_err_to_out=True) self.assertTrue("pagefaults" in out) @requires_posix def test_bake(self): from pbs import time, ls timed = time.bake("--verbose", _err_to_out=True) out = timed.ls() self.assertTrue("Voluntary context switches" in out) @requires_posix def test_output_equivalence(self): from pbs import whoami iam1 = whoami() iam2 = whoami() self.assertEqual(iam1, iam2) if __name__ == "__main__": unittest.main() pbs-0.95/AUTHORS.md0000644000175000017500000000036111713142116014753 0ustar amoffatamoffat00000000000000# Author * Andrew Moffat # Contributors * Dmitry Medvinsky * Jure Žiberna * Bahadır Kandemir * Jannis Leidel * tingletech * tdudziak * Arjen Stolk * nemec * fruch pbs-0.95/setup.py0000644000175000017500000000222211717346752015034 0ustar amoffatamoffat00000000000000import os import sys import pbs try: from distutils.core import setup except ImportError: from setuptools import setup if sys.argv[-1] == "test": os.system("python test.py") sys.exit() setup( name="pbs", version=pbs.__version__, description="Python subprocess wrapper", author="Andrew Moffat", author_email="andrew.robert.moffat@gmail.com", url="https://github.com/amoffat/pbs", license="MIT", py_modules=["pbs"], classifiers=[ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ], ) pbs-0.95/pbs.py0000644000175000017500000004326511717347117014470 0ustar amoffatamoffat00000000000000#=============================================================================== # Copyright (C) 2011-2012 by Andrew Moffat # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. #=============================================================================== import subprocess as subp import sys import traceback import os import re from glob import glob import shlex from types import ModuleType from functools import partial __version__ = "0.95" __project_url__ = "https://github.com/amoffat/pbs" IS_PY3 = sys.version_info[0] == 3 if IS_PY3: raw_input = input class ErrorReturnCode(Exception): truncate_cap = 200 def __init__(self, full_cmd, stdout, stderr): self.full_cmd = full_cmd self.stdout = stdout self.stderr = stderr if self.stdout is None: tstdout = "" else: tstdout = self.stdout[:self.truncate_cap] out_delta = len(self.stdout) - len(tstdout) if out_delta: tstdout += "... (%d more, please see e.stdout)" % out_delta if self.stderr is None: tstderr = "" else: tstderr = self.stderr[:self.truncate_cap] err_delta = len(self.stderr) - len(tstderr) if err_delta: tstderr += "... (%d more, please see e.stderr)" % err_delta msg = "\n\nRan: %r\n\nSTDOUT:\n\n %s\n\nSTDERR:\n\n %s" %\ (full_cmd, tstdout, tstderr) super(ErrorReturnCode, self).__init__(msg) class CommandNotFound(Exception): pass rc_exc_regex = re.compile("ErrorReturnCode_(\d+)") rc_exc_cache = {} def get_rc_exc(rc): try: return rc_exc_cache[rc] except KeyError: pass name = "ErrorReturnCode_%d" % rc exc = type(name, (ErrorReturnCode,), {}) rc_exc_cache[name] = exc return exc def which(program): def is_exe(fpath): return os.path.exists(fpath) and os.access(fpath, os.X_OK) fpath, fname = os.path.split(program) if fpath: if is_exe(program): return program else: for path in os.environ["PATH"].split(os.pathsep): exe_file = os.path.join(path, program) if is_exe(exe_file): return exe_file return None def resolve_program(program): path = which(program) if not path: # our actual command might have a dash in it, but we can't call # that from python (we have to use underscores), so we'll check # if a dash version of our underscore command exists and use that # if it does if "_" in program: path = which(program.replace("_", "-")) if not path: return None return path class RunningCommand(object): def __init__(self, command_ran, process, call_args, stdin=None): self.command_ran = command_ran self.process = process self._stdout = None self._stderr = None self.call_args = call_args # we're running in the background, return self and let us lazily # evaluate if self.call_args["bg"]: return # we're running this command as a with context, don't do anything # because nothing was started to run from Command.__call__ if self.call_args["with"]: return # run and block self._stdout, self._stderr = self.process.communicate(stdin) rc = self.process.wait() if rc != 0: raise get_rc_exc(rc)(self.command_ran, self._stdout, self._stderr) def __enter__(self): # we don't actually do anything here because anything that should # have been done would have been done in the Command.__call__ call. # essentially all that has to happen is the comand be pushed on # the prepend stack. pass def __exit__(self, typ, value, traceback): if self.call_args["with"] and Command._prepend_stack: Command._prepend_stack.pop() def __str__(self): if IS_PY3: return self.__unicode__() else: return unicode(self).encode("utf-8") def __unicode__(self): if self.process: if self.stdout: return self.stdout.decode("utf-8") # byte string else: return "" def __eq__(self, other): return str(self) == str(other) def __contains__(self, item): return item in str(self) def __getattr__(self, p): return getattr(str(self), p) def __repr__(self): return str(self) def __long__(self): return long(str(self).strip()) def __float__(self): return float(str(self).strip()) def __int__(self): return int(str(self).strip()) @property def stdout(self): if self.call_args["bg"]: self.wait() return self._stdout @property def stderr(self): if self.call_args["bg"]: self.wait() return self._stderr def wait(self): if self.process.returncode is not None: return self._stdout, self._stderr = self.process.communicate() rc = self.process.wait() if rc != 0: raise get_rc_exc(rc)(self.stdout, self.stderr) return self def __len__(self): return len(str(self)) class BakedCommand(object): def __init__(self, cmd, attr): self._cmd = cmd self._attr = attr self._partial = partial(cmd, attr) def __call__(self, *args, **kwargs): return self._partial(*args, **kwargs) def __str__(self): if IS_PY3: return self.__unicode__() else: return unicode(self).encode("utf-8") def __repr__(self): return str(self) def __unicode__(self): return "%s %s" % (self._cmd, self._attr) class Command(object): _prepend_stack = [] call_args = { "fg": False, # run command in foreground "bg": False, # run command in background "with": False, # prepend the command to every command after it "out": None, # redirect STDOUT "err": None, # redirect STDERR "err_to_out": None, # redirect STDERR to STDOUT } @classmethod def _create(cls, program): path = resolve_program(program) if not path: raise CommandNotFound(program) return cls(path) def __init__(self, path): self._path = path self._partial = False self._partial_baked_args = [] self._partial_call_args = {} def __getattribute__(self, name): # convenience getattr = partial(object.__getattribute__, self) # the logic here is, if an attribute starts with an # underscore, always try to find it, because it's very unlikely # that a first command will start with an underscore, example: # "git _command" will probably never exist. # after that, we check to see if the attribute actually exists # on the Command object, but only return that if we're not # a baked object. if name.startswith("_"): return getattr(name) try: attr = getattr(name) except AttributeError: return BakedCommand(self, name) if self._partial: return BakedCommand(self, name) return attr @staticmethod def _extract_call_args(kwargs): kwargs = kwargs.copy() call_args = Command.call_args.copy() for parg, default in call_args.items(): key = "_" + parg if key in kwargs: call_args[parg] = kwargs[key] del kwargs[key] return call_args, kwargs @staticmethod def _compile_args(args, kwargs): processed_args = [] # aggregate positional args for arg in args: if isinstance(arg, (list, tuple)): for sub_arg in arg: processed_args.append(repr(sub_arg)) else: processed_args.append(repr(arg)) # aggregate the keyword arguments for k,v in kwargs.items(): # we're passing a short arg as a kwarg, example: # cut(d="\t") if len(k) == 1: if v is True: arg = "-"+k else: arg = "-%s %r" % (k, v) # we're doing a long arg else: k = k.replace("_", "-") if v is True: arg = "--"+k else: arg = "--%s=%r" % (k, v) processed_args.append(arg) processed_args = shlex.split(" ".join(processed_args)) return processed_args def bake(self, *args, **kwargs): fn = Command(self._path) fn._partial = True fn._partial_call_args, kwargs = self._extract_call_args(kwargs) processed_args = self._compile_args(args, kwargs) fn._partial_baked_args = processed_args return fn def __str__(self): if IS_PY3: return self.__unicode__() else: return unicode(self).encode("utf-8") def __repr__(self): return str(self) def __unicode__(self): baked_args = " ".join(self._partial_baked_args) if baked_args: baked_args = " " + baked_args return self._path + baked_args def __enter__(self): Command._prepend_stack.append([self._path]) def __exit__(self, typ, value, traceback): Command._prepend_stack.pop() def __call__(self, *args, **kwargs): kwargs = kwargs.copy() args = list(args) cmd = [] # aggregate any with contexts for prepend in self._prepend_stack: cmd.extend(prepend) cmd.append(self._path) call_args, kwargs = self._extract_call_args(kwargs) call_args.update(self._partial_call_args) # set pipe to None if we're outputting straight to CLI pipe = None if call_args["fg"] else subp.PIPE # check if we're piping via composition stdin = pipe actual_stdin = None if args: first_arg = args.pop(0) if isinstance(first_arg, RunningCommand): # it makes sense that if the input pipe of a command is running # in the background, then this command should run in the # background as well if first_arg.call_args["bg"]: call_args["bg"] = True stdin = first_arg.process.stdout else: actual_stdin = first_arg.stdout else: args.insert(0, first_arg) processed_args = self._compile_args(args, kwargs) # makes sure our arguments are broken up correctly split_args = self._partial_baked_args + processed_args # we used to glob, but now we don't. the reason being, escaping globs # doesn't work. also, adding a _noglob attribute doesn't allow the # flexibility to glob some args and not others. so we have to leave # the globbing up to the user entirely #======================================================================= # # now glob-expand each arg and compose the final list # final_args = [] # for arg in split_args: # expanded = glob(arg) # if expanded: final_args.extend(expanded) # else: final_args.append(arg) #======================================================================= final_args = split_args cmd.extend(final_args) command_ran = " ".join(cmd) # with contexts shouldn't run at all yet, they prepend # to every command in the context if call_args["with"]: Command._prepend_stack.append(cmd) return RunningCommand(command_ran, None, call_args) # stdout redirection stdout = pipe out = call_args["out"] if out: if isinstance(out, file): stdout = out else: stdout = file(str(out), "w") # stderr redirection stderr = pipe err = call_args["err"] if err: if isinstance(err, file): stderr = err else: stderr = file(str(err), "w") if call_args["err_to_out"]: stderr = subp.STDOUT # leave shell=False process = subp.Popen(cmd, shell=False, env=os.environ, stdin=stdin, stdout=stdout, stderr=stderr) return RunningCommand(command_ran, process, call_args, actual_stdin) # this class is used directly when we do a "from pbs import *". it allows # lookups to names that aren't found in the global scope to be searched # for as a program. for example, if "ls" isn't found in the program's # scope, we consider it a system program and try to find it. class Environment(dict): def __init__(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) self["Command"] = Command self["CommandNotFound"] = CommandNotFound self["ErrorReturnCode"] = ErrorReturnCode self["ARGV"] = sys.argv[1:] for i, arg in enumerate(sys.argv): self["ARG%d" % i] = arg # this needs to be last self["env"] = os.environ def __setitem__(self, k, v): # are we altering an environment variable? if "env" in self and k in self["env"]: self["env"][k] = v # no? just setting a regular name else: dict.__setitem__(self, k, v) def __missing__(self, k): # the only way we'd get to here is if we've tried to # import * from a repl. so, raise an exception, since # that's really the only sensible thing to do if k == "__all__": raise ImportError("Cannot import * from pbs. \ Please import pbs or import programs individually.") # if we end with "_" just go ahead and skip searching # our namespace for python stuff. this was mainly for the # command "id", which is a popular program for finding # if a user exists, but also a python function for getting # the address of an object. so can call the python # version by "id" and the program version with "id_" if not k.endswith("_"): # check if we're naming a dynamically generated ReturnCode exception try: return rc_exc_cache[k] except KeyError: m = rc_exc_regex.match(k) if m: return get_rc_exc(int(m.group(1))) # are we naming a commandline argument? if k.startswith("ARG"): return None # is it a builtin? try: return getattr(self["__builtins__"], k) except AttributeError: pass elif not k.startswith("_"): k = k.rstrip("_") # how about an environment variable? try: return os.environ[k] except KeyError: pass # is it a custom builtin? builtin = getattr(self, "b_"+k, None) if builtin: return builtin # it must be a command then return Command._create(k) def b_cd(self, path): os.chdir(path) def b_which(self, program): return which(program) def run_repl(env): banner = "\n>> PBS v{version}\n>> https://github.com/amoffat/pbs\n" print(banner.format(version=__version__)) while True: try: line = raw_input("pbs> ") except (ValueError, EOFError): break try: exec(compile(line, "", "single"), env, env) except SystemExit: break except: print(traceback.format_exc()) # cleans up our last line print("") # this is a thin wrapper around THIS module (we patch sys.modules[__name__]). # this is in the case that the user does a "from pbs import whatever" # in other words, they only want to import certain programs, not the whole # system PATH worth of commands. in this case, we just proxy the # import lookup to our Environment class class SelfWrapper(ModuleType): def __init__(self, self_module): # this is super ugly to have to copy attributes like this, # but it seems to be the only way to make reload() behave # nicely. if i make these attributes dynamic lookups in # __getattr__, reload sometimes chokes in weird ways... for attr in ["__builtins__", "__doc__", "__name__", "__package__"]: setattr(self, attr, getattr(self_module, attr)) self.self_module = self_module self.env = Environment(globals()) def __getattr__(self, name): return self.env[name] # we're being run as a stand-alone script, fire up a REPL if __name__ == "__main__": globs = globals() f_globals = {} for k in ["__builtins__", "__doc__", "__name__", "__package__"]: f_globals[k] = globs[k] env = Environment(f_globals) run_repl(env) # we're being imported from somewhere else: self = sys.modules[__name__] sys.modules[__name__] = SelfWrapper(self)