| """ |
| http://amoffat.github.io/sh/ |
| """ |
| #=============================================================================== |
| # Copyright (C) 2011-2015 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. |
| #=============================================================================== |
| |
| |
| __version__ = "1.11" |
| __project_url__ = "https://github.com/amoffat/sh" |
| |
| |
| |
| import platform |
| |
| if "windows" in platform.system().lower(): |
| raise ImportError("sh %s is currently only supported on linux and osx. \ |
| please install pbs 0.110 (http://pypi.python.org/pypi/pbs) for windows \ |
| support." % __version__) |
| |
| |
| import sys |
| IS_PY3 = sys.version_info[0] == 3 |
| |
| import traceback |
| import os |
| import re |
| from glob import glob as original_glob |
| import time |
| from types import ModuleType |
| from functools import partial |
| import inspect |
| from contextlib import contextmanager |
| |
| from locale import getpreferredencoding |
| DEFAULT_ENCODING = getpreferredencoding() or "UTF-8" |
| |
| |
| if IS_PY3: |
| from io import StringIO |
| from io import BytesIO as cStringIO |
| from queue import Queue, Empty |
| |
| # for some reason, python 3.1 removed the builtin "callable", wtf |
| if not hasattr(__builtins__, "callable"): |
| def callable(ob): |
| return hasattr(ob, "__call__") |
| else: |
| from StringIO import StringIO |
| from cStringIO import OutputType as cStringIO |
| from Queue import Queue, Empty |
| |
| IS_OSX = platform.system() == "Darwin" |
| THIS_DIR = os.path.dirname(os.path.realpath(__file__)) |
| SH_LOGGER_NAME = "sh" |
| |
| |
| import errno |
| import warnings |
| |
| import pty |
| import termios |
| import signal |
| import gc |
| import select |
| import threading |
| import tty |
| import fcntl |
| import struct |
| import resource |
| from collections import deque |
| import logging |
| import weakref |
| |
| |
| # TODO remove with contexts in next version |
| def with_context_warning(): |
| warnings.warn(""" |
| with contexts are deprecated because they are not thread safe. they will be \ |
| removed in the next version. use subcommands instead \ |
| http://amoffat.github.io/sh/#sub-commands. see \ |
| https://github.com/amoffat/sh/issues/195 |
| """.strip(), stacklevel=3) |
| |
| |
| |
| if IS_PY3: |
| raw_input = input |
| unicode = str |
| basestring = str |
| |
| |
| _unicode_methods = set(dir(unicode())) |
| |
| |
| def encode_to_py3bytes_or_py2str(s): |
| """ takes anything and attempts to return a py2 string or py3 bytes. this |
| is typically used when creating command + arguments to be executed via |
| os.exec* """ |
| |
| fallback_encoding = "utf8" |
| |
| if IS_PY3: |
| # if we're already bytes, do nothing |
| if isinstance(s, bytes): |
| pass |
| else: |
| s = str(s) |
| try: |
| s = bytes(s, DEFAULT_ENCODING) |
| except UnicodeEncodeError: |
| s = bytes(s, fallback_encoding) |
| else: |
| # attempt to convert the thing to unicode from the system's encoding |
| try: |
| s = unicode(s, DEFAULT_ENCODING) |
| # if the thing is already unicode, or it's a number, it can't be |
| # coerced to unicode with an encoding argument, but if we leave out |
| # the encoding argument, it will convert it to a string, then to unicode |
| except TypeError: |
| s = unicode(s) |
| |
| # now that we have guaranteed unicode, encode to our system encoding, |
| # but attempt to fall back to something |
| try: |
| s = s.encode(DEFAULT_ENCODING) |
| except: |
| s = s.encode(fallback_encoding) |
| return s |
| |
| |
| class ErrorReturnCode(Exception): |
| """ base class for all exceptions as a result of a command's exit status |
| being deemed an error. this base class is dynamically subclassed into |
| derived classes with the format: ErrorReturnCode_NNN where NNN is the exit |
| code number. the reason for this is it reduces boiler plate code when |
| testing error return codes: |
| |
| try: |
| some_cmd() |
| except ErrorReturnCode_12: |
| print("couldn't do X") |
| |
| vs: |
| try: |
| some_cmd() |
| except ErrorReturnCode as e: |
| if e.exit_code == 12: |
| print("couldn't do X") |
| |
| it's not much of a savings, but i believe it makes the code easier to read """ |
| |
| truncate_cap = 750 |
| |
| def __init__(self, full_cmd, stdout, stderr): |
| self.full_cmd = full_cmd |
| self.stdout = stdout |
| self.stderr = stderr |
| |
| |
| if self.stdout is None: |
| exc_stdout = "<redirected>" |
| else: |
| exc_stdout = self.stdout[:self.truncate_cap] |
| out_delta = len(self.stdout) - len(exc_stdout) |
| if out_delta: |
| exc_stdout += ("... (%d more, please see e.stdout)" % out_delta).encode() |
| |
| if self.stderr is None: |
| exc_stderr = "<redirected>" |
| else: |
| exc_stderr = self.stderr[:self.truncate_cap] |
| err_delta = len(self.stderr) - len(exc_stderr) |
| if err_delta: |
| exc_stderr += ("... (%d more, please see e.stderr)" % err_delta).encode() |
| |
| msg = "\n\n RAN: %r\n\n STDOUT:\n%s\n\n STDERR:\n%s" % \ |
| (full_cmd, exc_stdout.decode(DEFAULT_ENCODING, "replace"), |
| exc_stderr.decode(DEFAULT_ENCODING, "replace")) |
| super(ErrorReturnCode, self).__init__(msg) |
| |
| |
| class SignalException(ErrorReturnCode): pass |
| class TimeoutException(Exception): |
| """ the exception thrown when a command is killed because a specified |
| timeout (via _timeout) was hit """ |
| def __init__(self, exit_code): |
| self.exit_code = exit_code |
| super(Exception, self).__init__() |
| |
| SIGNALS_THAT_SHOULD_THROW_EXCEPTION = ( |
| signal.SIGABRT, |
| signal.SIGBUS, |
| signal.SIGFPE, |
| signal.SIGILL, |
| signal.SIGINT, |
| signal.SIGKILL, |
| signal.SIGPIPE, |
| signal.SIGQUIT, |
| signal.SIGSEGV, |
| signal.SIGTERM, |
| signal.SIGSYS, |
| ) |
| |
| |
| # we subclass AttributeError because: |
| # https://github.com/ipython/ipython/issues/2577 |
| # https://github.com/amoffat/sh/issues/97#issuecomment-10610629 |
| class CommandNotFound(AttributeError): pass |
| |
| |
| |
| |
| rc_exc_regex = re.compile("(ErrorReturnCode|SignalException)_((\d+)|SIG\w+)") |
| rc_exc_cache = {} |
| |
| |
| def get_exc_from_name(name): |
| """ takes an exception name, like: |
| |
| ErrorReturnCode_1 |
| SignalException_9 |
| SignalException_SIGHUP |
| |
| and returns the corresponding exception. this is primarily used for |
| importing exceptions from sh into user code, for instance, to capture those |
| exceptions """ |
| |
| exc = None |
| try: |
| return rc_exc_cache[name] |
| except KeyError: |
| m = rc_exc_regex.match(name) |
| if m: |
| base = m.group(1) |
| rc_or_sig_name = m.group(2) |
| |
| if base == "SignalException": |
| try: |
| rc = -int(rc_or_sig_name) |
| except ValueError: |
| rc = -getattr(signal, rc_or_sig_name) |
| else: |
| rc = int(rc_or_sig_name) |
| |
| exc = get_rc_exc(rc) |
| return exc |
| |
| |
| def get_rc_exc(rc_or_sig_name): |
| """ takes a exit code, signal number, or signal name, and produces an |
| exception that corresponds to that return code. positive return codes yield |
| ErrorReturnCode exception, negative return codes yield SignalException |
| |
| we also cache the generated exception so that only one signal of that type |
| exists, preserving identity """ |
| |
| try: |
| rc = int(rc_or_sig_name) |
| except ValueError: |
| rc = -getattr(signal, rc_or_sig_name) |
| |
| try: |
| return rc_exc_cache[rc] |
| except KeyError: |
| pass |
| |
| if rc > 0: |
| name = "ErrorReturnCode_%d" % rc |
| base = ErrorReturnCode |
| else: |
| name = "SignalException_%d" % abs(rc) |
| base = SignalException |
| |
| exc = type(name, (base,), {"exit_code": rc}) |
| rc_exc_cache[rc] = exc |
| return exc |
| |
| |
| |
| |
| def which(program): |
| def is_exe(fpath): |
| return (os.path.exists(fpath) and |
| os.access(fpath, os.X_OK) and |
| os.path.isfile(os.path.realpath(fpath))) |
| |
| fpath, fname = os.path.split(program) |
| if fpath: |
| if is_exe(program): |
| return program |
| else: |
| if "PATH" not in os.environ: |
| return None |
| 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 |
| |
| |
| # we add this thin wrapper to glob.glob because of a specific edge case where |
| # glob does not expand to anything. for example, if you try to do |
| # glob.glob("*.py") and there are no *.py files in the directory, glob.glob |
| # returns an empty list. this empty list gets passed to the command, and |
| # then the command fails with a misleading error message. this thin wrapper |
| # ensures that if there is no expansion, we pass in the original argument, |
| # so that when the command fails, the error message is clearer |
| def glob(arg): |
| return original_glob(arg) or arg |
| |
| |
| |
| class Logger(object): |
| """ provides a memory-inexpensive logger. a gotcha about python's builtin |
| logger is that logger objects are never garbage collected. if you create a |
| thousand loggers with unique names, they'll sit there in memory until your |
| script is done. with sh, it's easy to create loggers with unique names if |
| we want our loggers to include our command arguments. for example, these |
| are all unique loggers: |
| |
| ls -l |
| ls -l /tmp |
| ls /tmp |
| |
| so instead of creating unique loggers, and without sacrificing logging |
| output, we use this class, which maintains as part of its state, the logging |
| "context", which will be the very unique name. this allows us to get a |
| logger with a very general name, eg: "command", and have a unique name |
| appended to it via the context, eg: "ls -l /tmp" """ |
| def __init__(self, name, context=None): |
| self.name = name |
| if context: |
| context = context.replace("%", "%%") |
| self.context = context |
| self.log = logging.getLogger("%s.%s" % (SH_LOGGER_NAME, name)) |
| |
| def _format_msg(self, msg, *args): |
| if self.context: |
| msg = "%s: %s" % (self.context, msg) |
| return msg % args |
| |
| def get_child(self, name, context): |
| new_name = self.name + "." + name |
| new_context = self.context + "." + context |
| l = Logger(new_name, new_context) |
| return l |
| |
| def info(self, msg, *args): |
| self.log.info(self._format_msg(msg, *args)) |
| |
| def debug(self, msg, *args): |
| self.log.debug(self._format_msg(msg, *args)) |
| |
| def error(self, msg, *args): |
| self.log.error(self._format_msg(msg, *args)) |
| |
| def exception(self, msg, *args): |
| self.log.exception(self._format_msg(msg, *args)) |
| |
| |
| def friendly_truncate(s, max_len): |
| if len(s) > max_len: |
| s = "%s...(%d more)" % (s[:max_len], len(s) - max_len) |
| return s |
| |
| |
| class RunningCommand(object): |
| """ this represents an executing Command object. it is returned as the |
| result of __call__() being executed on a Command instance. this creates a |
| reference to a OProc instance, which is a low-level wrapper around the |
| process that was exec'd |
| |
| this is the class that gets manipulated the most by user code, and so it |
| implements various convenience methods and logical mechanisms for the |
| underlying process. for example, if a user tries to access a |
| backgrounded-process's stdout/err, the RunningCommand object is smart enough |
| to know to wait() on the process to finish first. and when the process |
| finishes, RunningCommand is smart enough to translate exit codes to |
| exceptions. """ |
| |
| def __init__(self, cmd, call_args, stdin, stdout, stderr): |
| # self.ran is used for auditing what actually ran. for example, in |
| # exceptions, or if you just want to know what was ran after the |
| # command ran |
| if IS_PY3: |
| self.ran = " ".join([arg.decode(DEFAULT_ENCODING, "ignore") for arg in cmd]) |
| else: |
| self.ran = " ".join(cmd) |
| |
| |
| friendly_cmd = friendly_truncate(self.ran, 20) |
| friendly_call_args = friendly_truncate(str(call_args), 20) |
| |
| # we're setting up the logger string here, instead of __repr__ because |
| # we reserve __repr__ to behave as if it was evaluating the child |
| # process's output |
| logger_str = "<Command %r call_args %s>" % (friendly_cmd, |
| friendly_call_args) |
| |
| self.log = Logger("command", logger_str) |
| self.call_args = call_args |
| self.cmd = cmd |
| |
| self.process = None |
| self._process_completed = False |
| should_wait = True |
| spawn_process = True |
| |
| |
| # with contexts shouldn't run at all yet, they prepend |
| # to every command in the context |
| if call_args["with"]: |
| spawn_process = False |
| Command._prepend_stack.append(self) |
| |
| |
| if call_args["piped"] or call_args["iter"] or call_args["iter_noblock"]: |
| should_wait = False |
| |
| # we're running in the background, return self and let us lazily |
| # evaluate |
| if call_args["bg"]: |
| should_wait = False |
| |
| # redirection |
| if call_args["err_to_out"]: |
| stderr = OProc.STDOUT |
| |
| |
| # set up which stream should write to the pipe |
| # TODO, make pipe None by default and limit the size of the Queue |
| # in oproc.OProc |
| pipe = OProc.STDOUT |
| if call_args["iter"] == "out" or call_args["iter"] is True: |
| pipe = OProc.STDOUT |
| elif call_args["iter"] == "err": |
| pipe = OProc.STDERR |
| |
| if call_args["iter_noblock"] == "out" or call_args["iter_noblock"] is True: |
| pipe = OProc.STDOUT |
| elif call_args["iter_noblock"] == "err": |
| pipe = OProc.STDERR |
| |
| |
| # there's currently only one case where we wouldn't spawn a child |
| # process, and that's if we're using a with-context with our command |
| if spawn_process: |
| self.log.info("starting process") |
| self.process = OProc(self.log, cmd, stdin, stdout, stderr, |
| self.call_args, pipe) |
| |
| if should_wait: |
| self.wait() |
| |
| |
| def wait(self): |
| if not self._process_completed: |
| self._process_completed = True |
| |
| exit_code = self.process.wait() |
| if self.process.timed_out: |
| # if we timed out, our exit code represents a signal, which is |
| # negative, so let's make it positive to store in our |
| # TimeoutException |
| raise TimeoutException(-exit_code) |
| else: |
| self.handle_command_exit_code(exit_code) |
| |
| # https://github.com/amoffat/sh/issues/185 |
| if self.call_args["done"]: |
| self.call_args["done"](self) |
| |
| return self |
| |
| |
| def handle_command_exit_code(self, code): |
| """ here we determine if we had an exception, or an error code that we |
| weren't expecting to see. if we did, we create and raise an exception |
| """ |
| if (code not in self.call_args["ok_code"] and (code > 0 or -code in |
| SIGNALS_THAT_SHOULD_THROW_EXCEPTION)): |
| exc = get_rc_exc(code) |
| raise exc(self.ran, self.process.stdout, self.process.stderr) |
| |
| |
| |
| @property |
| def stdout(self): |
| self.wait() |
| return self.process.stdout |
| |
| @property |
| def stderr(self): |
| self.wait() |
| return self.process.stderr |
| |
| @property |
| def exit_code(self): |
| self.wait() |
| return self.process.exit_code |
| |
| @property |
| def pid(self): |
| return self.process.pid |
| |
| def __len__(self): |
| return len(str(self)) |
| |
| 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. """ |
| with_context_warning() |
| |
| def __iter__(self): |
| return self |
| |
| def next(self): |
| """ allow us to iterate over the output of our command """ |
| |
| # we do this because if get blocks, we can't catch a KeyboardInterrupt |
| # so the slight timeout allows for that. |
| while True: |
| try: |
| chunk = self.process._pipe_queue.get(True, 0.001) |
| except Empty: |
| if self.call_args["iter_noblock"]: |
| return errno.EWOULDBLOCK |
| else: |
| if chunk is None: |
| self.wait() |
| raise StopIteration() |
| try: |
| return chunk.decode(self.call_args["encoding"], |
| self.call_args["decode_errors"]) |
| except UnicodeDecodeError: |
| return chunk |
| |
| # python 3 |
| __next__ = next |
| |
| def __exit__(self, typ, value, traceback): |
| if self.call_args["with"] and Command._prepend_stack: |
| Command._prepend_stack.pop() |
| |
| def __str__(self): |
| """ in python3, should return unicode. in python2, should return a |
| string of bytes """ |
| if IS_PY3: |
| return self.__unicode__() |
| else: |
| return unicode(self).encode(self.call_args["encoding"]) |
| |
| def __unicode__(self): |
| """ a magic method defined for python2. calling unicode() on a |
| RunningCommand object will call this """ |
| if self.process and self.stdout: |
| return self.stdout.decode(self.call_args["encoding"], |
| self.call_args["decode_errors"]) |
| elif IS_PY3: |
| return "" |
| else: |
| return unicode("") |
| |
| def __eq__(self, other): |
| return unicode(self) == unicode(other) |
| __hash__ = None # Avoid DeprecationWarning in Python < 3 |
| |
| def __contains__(self, item): |
| return item in str(self) |
| |
| def __getattr__(self, p): |
| # let these three attributes pass through to the OProc object |
| if p in ("signal", "terminate", "kill"): |
| if self.process: |
| return getattr(self.process, p) |
| else: |
| raise AttributeError |
| |
| # see if strings have what we're looking for. we're looking at the |
| # method names explicitly because we don't want to evaluate self unless |
| # we absolutely have to, the reason being, in python2, hasattr swallows |
| # exceptions, and if we try to run hasattr on a command that failed and |
| # is being run with _iter=True, the command will be evaluated, throw an |
| # exception, but hasattr will discard it |
| if p in _unicode_methods: |
| return getattr(unicode(self), p) |
| |
| raise AttributeError |
| |
| def __repr__(self): |
| """ in python3, should return unicode. in python2, should return a |
| string of bytes """ |
| try: |
| return str(self) |
| except UnicodeDecodeError: |
| if self.process: |
| if self.stdout: |
| return repr(self.stdout) |
| return repr("") |
| |
| def __long__(self): |
| return long(str(self).strip()) |
| |
| def __float__(self): |
| return float(str(self).strip()) |
| |
| def __int__(self): |
| return int(str(self).strip()) |
| |
| |
| |
| def output_redirect_is_filename(out): |
| return out \ |
| and not callable(out) \ |
| and not hasattr(out, "write") \ |
| and not isinstance(out, (cStringIO, StringIO)) |
| |
| |
| |
| |
| |
| |
| class Command(object): |
| """ represents an un-run system program, like "ls" or "cd". because it |
| represents the program itself (and not a running instance of it), it should |
| hold very little state. in fact, the only state it does hold is baked |
| arguments. |
| |
| when a Command object is called, the result that is returned is a |
| RunningCommand object, which represents the Command put into an execution |
| state. """ |
| _prepend_stack = [] |
| |
| _call_args = { |
| # currently unsupported |
| #"fg": False, # run command in foreground |
| |
| # run a command in the background. commands run in the background |
| # ignore SIGHUP and do not automatically exit when the parent process |
| # ends |
| "bg": False, |
| |
| "with": False, # prepend the command to every command after it |
| "in": None, |
| "out": None, # redirect STDOUT |
| "err": None, # redirect STDERR |
| "err_to_out": None, # redirect STDERR to STDOUT |
| |
| # stdin buffer size |
| # 1 for line, 0 for unbuffered, any other number for that amount |
| "in_bufsize": 0, |
| # stdout buffer size, same values as above |
| "out_bufsize": 1, |
| "err_bufsize": 1, |
| |
| # this is how big the output buffers will be for stdout and stderr. |
| # this is essentially how much output they will store from the process. |
| # we use a deque, so if it overflows past this amount, the first items |
| # get pushed off as each new item gets added. |
| # |
| # NOTICE |
| # this is not a *BYTE* size, this is a *CHUNK* size...meaning, that if |
| # you're buffering out/err at 1024 bytes, the internal buffer size will |
| # be "internal_bufsize" CHUNKS of 1024 bytes |
| "internal_bufsize": 3 * 1024 ** 2, |
| |
| "env": None, |
| "piped": None, |
| "iter": None, |
| "iter_noblock": None, |
| "ok_code": 0, |
| "cwd": None, |
| |
| # the separator delimiting between a long-argument's name and its value |
| # for example, --arg=derp, '=' is the long_sep |
| "long_sep": "=", |
| |
| # this is for programs that expect their input to be from a terminal. |
| # ssh is one of those programs |
| "tty_in": False, |
| "tty_out": True, |
| |
| "encoding": DEFAULT_ENCODING, |
| "decode_errors": "strict", |
| |
| # how long the process should run before it is auto-killed |
| "timeout": 0, |
| "timeout_signal": signal.SIGKILL, |
| |
| # TODO write some docs on "long-running processes" |
| # these control whether or not stdout/err will get aggregated together |
| # as the process runs. this has memory usage implications, so sometimes |
| # with long-running processes with a lot of data, it makes sense to |
| # set these to true |
| "no_out": False, |
| "no_err": False, |
| "no_pipe": False, |
| |
| # if any redirection is used for stdout or stderr, internal buffering |
| # of that data is not stored. this forces it to be stored, as if |
| # the output is being T'd to both the redirected destination and our |
| # internal buffers |
| "tee": None, |
| |
| # will be called when a process terminates without exception. this |
| # option also puts the command in the background, since it doesn't make |
| # sense to have an un-backgrounded command with a done callback |
| "done": None, |
| |
| # a tuple (rows, columns) of the desired size of both the stdout and |
| # stdin ttys, if ttys are being used |
| "tty_size": (20, 80), |
| } |
| |
| # these are arguments that cannot be called together, because they wouldn't |
| # make any sense |
| _incompatible_call_args = ( |
| #("fg", "bg", "Command can't be run in the foreground and background"), |
| ("err", "err_to_out", "Stderr is already being redirected"), |
| ("piped", "iter", "You cannot iterate when this command is being piped"), |
| ("piped", "no_pipe", "Using a pipe doesn't make sense if you've \ |
| disabled the pipe"), |
| ("no_out", "iter", "You cannot iterate over output if there is no \ |
| output"), |
| ) |
| |
| |
| # this method exists because of the need to have some way of letting |
| # manual object instantiation not perform the underscore-to-dash command |
| # conversion that resolve_program uses. |
| # |
| # there are 2 ways to create a Command object. using sh.Command(<program>) |
| # or by using sh.<program>. the method fed into sh.Command must be taken |
| # literally, and so no underscore-dash conversion is performed. the one |
| # for sh.<program> must do the underscore-dash converesion, because we |
| # can't type dashes in method names |
| @classmethod |
| def _create(cls, program, **default_kwargs): |
| path = resolve_program(program) |
| if not path: |
| raise CommandNotFound(program) |
| |
| cmd = cls(path) |
| if default_kwargs: |
| cmd = cmd.bake(**default_kwargs) |
| |
| return cmd |
| |
| |
| def __init__(self, path): |
| found = which(path) |
| if not found: |
| raise CommandNotFound(path) |
| |
| self._path = encode_to_py3bytes_or_py2str(found) |
| |
| self._partial = False |
| self._partial_baked_args = [] |
| self._partial_call_args = {} |
| |
| # bugfix for functools.wraps. issue #121 |
| self.__name__ = str(self) |
| |
| |
| def __getattribute__(self, name): |
| # convenience |
| getattr = partial(object.__getattribute__, self) |
| |
| if name.startswith("_"): |
| return getattr(name) |
| if name == "bake": |
| return getattr("bake") |
| if name.endswith("_"): |
| name = name[:-1] |
| |
| return getattr("bake")(name) |
| |
| |
| @staticmethod |
| def _extract_call_args(kwargs, to_override={}): |
| kwargs = kwargs.copy() |
| call_args = {} |
| for parg, default in Command._call_args.items(): |
| key = "_" + parg |
| |
| if key in kwargs: |
| call_args[parg] = kwargs[key] |
| del kwargs[key] |
| elif parg in to_override: |
| call_args[parg] = to_override[parg] |
| |
| # test for incompatible call args |
| s1 = set(call_args.keys()) |
| for args in Command._incompatible_call_args: |
| args = list(args) |
| error = args.pop() |
| |
| if s1.issuperset(args): |
| raise TypeError("Invalid special arguments %r: %s" % (args, error)) |
| |
| return call_args, kwargs |
| |
| |
| def _aggregate_keywords(self, keywords, sep, raw=False): |
| processed = [] |
| for k, v in keywords.items(): |
| # we're passing a short arg as a kwarg, example: |
| # cut(d="\t") |
| if len(k) == 1: |
| if v is not False: |
| processed.append(encode_to_py3bytes_or_py2str("-" + k)) |
| if v is not True: |
| processed.append(encode_to_py3bytes_or_py2str(v)) |
| |
| # we're doing a long arg |
| else: |
| if not raw: |
| k = k.replace("_", "-") |
| |
| if v is True: |
| processed.append(encode_to_py3bytes_or_py2str("--" + k)) |
| elif v is False: |
| pass |
| else: |
| arg = encode_to_py3bytes_or_py2str("--%s%s%s" % (k, sep, v)) |
| processed.append(arg) |
| return processed |
| |
| |
| def _compile_args(self, args, kwargs, sep): |
| processed_args = [] |
| |
| # aggregate positional args |
| for arg in args: |
| if isinstance(arg, (list, tuple)): |
| if not arg: |
| warnings.warn("Empty list passed as an argument to %r. \ |
| If you're using glob.glob(), please use sh.glob() instead." % self._path, stacklevel=3) |
| for sub_arg in arg: |
| processed_args.append(encode_to_py3bytes_or_py2str(sub_arg)) |
| elif isinstance(arg, dict): |
| processed_args += self._aggregate_keywords(arg, sep, raw=True) |
| else: |
| processed_args.append(encode_to_py3bytes_or_py2str(arg)) |
| |
| # aggregate the keyword arguments |
| processed_args += self._aggregate_keywords(kwargs, sep) |
| |
| return processed_args |
| |
| |
| # TODO needs documentation |
| def bake(self, *args, **kwargs): |
| fn = Command(self._path) |
| fn._partial = True |
| |
| call_args, kwargs = self._extract_call_args(kwargs) |
| |
| pruned_call_args = call_args |
| for k, v in Command._call_args.items(): |
| try: |
| if pruned_call_args[k] == v: |
| del pruned_call_args[k] |
| except KeyError: |
| continue |
| |
| fn._partial_call_args.update(self._partial_call_args) |
| fn._partial_call_args.update(pruned_call_args) |
| fn._partial_baked_args.extend(self._partial_baked_args) |
| sep = pruned_call_args.get("long_sep", self._call_args["long_sep"]) |
| fn._partial_baked_args.extend(self._compile_args(args, kwargs, sep)) |
| return fn |
| |
| def __str__(self): |
| """ in python3, should return unicode. in python2, should return a |
| string of bytes """ |
| if IS_PY3: |
| return self.__unicode__() |
| else: |
| return self.__unicode__().encode(DEFAULT_ENCODING) |
| |
| |
| def __eq__(self, other): |
| try: |
| return str(self) == str(other) |
| except: |
| return False |
| __hash__ = None # Avoid DeprecationWarning in Python < 3 |
| |
| |
| def __repr__(self): |
| """ in python3, should return unicode. in python2, should return a |
| string of bytes """ |
| return "<Command %r>" % str(self) |
| |
| |
| def __unicode__(self): |
| """ a magic method defined for python2. calling unicode() on a |
| self will call this """ |
| baked_args = " ".join(item.decode(DEFAULT_ENCODING) for item in self._partial_baked_args) |
| if baked_args: |
| baked_args = " " + baked_args |
| return self._path.decode(DEFAULT_ENCODING) + baked_args |
| |
| def __enter__(self): |
| with_context_warning() |
| self(_with=True) |
| |
| 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 |
| call_args = Command._call_args.copy() |
| for prepend in self._prepend_stack: |
| # don't pass the 'with' call arg |
| pcall_args = prepend.call_args.copy() |
| try: |
| del pcall_args["with"] |
| except: |
| pass |
| |
| call_args.update(pcall_args) |
| cmd.extend(prepend.cmd) |
| |
| cmd.append(self._path) |
| |
| # here we extract the special kwargs and override any |
| # special kwargs from the possibly baked command |
| tmp_call_args, kwargs = self._extract_call_args(kwargs, self._partial_call_args) |
| call_args.update(tmp_call_args) |
| |
| if not getattr(call_args["ok_code"], "__iter__", None): |
| call_args["ok_code"] = [call_args["ok_code"]] |
| |
| |
| if call_args["done"]: |
| call_args["bg"] = True |
| |
| # check if we're piping via composition |
| stdin = call_args["in"] |
| 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 |
| |
| if first_arg.call_args["piped"] == "direct": |
| stdin = first_arg.process |
| else: |
| stdin = first_arg.process._pipe_queue |
| |
| else: |
| args.insert(0, first_arg) |
| |
| processed_args = self._compile_args(args, kwargs, call_args["long_sep"]) |
| |
| # makes sure our arguments are broken up correctly |
| split_args = self._partial_baked_args + processed_args |
| |
| final_args = split_args |
| |
| cmd.extend(final_args) |
| |
| |
| # stdout redirection |
| stdout = call_args["out"] |
| if output_redirect_is_filename(stdout): |
| stdout = open(str(stdout), "wb") |
| |
| # stderr redirection |
| stderr = call_args["err"] |
| if output_redirect_is_filename(stderr): |
| stderr = open(str(stderr), "wb") |
| |
| |
| return RunningCommand(cmd, call_args, stdin, stdout, stderr) |
| |
| |
| |
| |
| def _start_daemon_thread(fn, *args): |
| thrd = threading.Thread(target=fn, args=args) |
| thrd.daemon = True |
| thrd.start() |
| return thrd |
| |
| |
| def setwinsize(fd, rows_cols): |
| """ set the terminal size of a tty file descriptor. borrowed logic |
| from pexpect.py """ |
| rows, cols = rows_cols |
| TIOCSWINSZ = getattr(termios, 'TIOCSWINSZ', -2146929561) |
| |
| s = struct.pack('HHHH', rows, cols, 0, 0) |
| fcntl.ioctl(fd, TIOCSWINSZ, s) |
| |
| def construct_streamreader_callback(process, handler): |
| """ here we're constructing a closure for our streamreader callback. this |
| is used in the case that we pass a callback into _out or _err, meaning we |
| want to our callback to handle each bit of output |
| |
| we construct the closure based on how many arguments it takes. the reason |
| for this is to make it as easy as possible for people to use, without |
| limiting them. a new user will assume the callback takes 1 argument (the |
| data). as they get more advanced, they may want to terminate the process, |
| or pass some stdin back, and will realize that they can pass a callback of |
| more args """ |
| |
| |
| # implied arg refers to the "self" that methods will pass in. we need to |
| # account for this implied arg when figuring out what function the user |
| # passed in based on number of args |
| implied_arg = 0 |
| |
| partial_args = 0 |
| handler_to_inspect = handler |
| |
| if isinstance(handler, partial): |
| partial_args = len(handler.args) |
| handler_to_inspect = handler.func |
| |
| if inspect.ismethod(handler_to_inspect): |
| implied_arg = 1 |
| num_args = len(inspect.getargspec(handler_to_inspect).args) |
| |
| else: |
| if inspect.isfunction(handler_to_inspect): |
| num_args = len(inspect.getargspec(handler_to_inspect).args) |
| |
| # is an object instance with __call__ method |
| else: |
| implied_arg = 1 |
| num_args = len(inspect.getargspec(handler_to_inspect.__call__).args) |
| |
| |
| net_args = num_args - implied_arg - partial_args |
| |
| handler_args = () |
| |
| # just the chunk |
| if net_args == 1: |
| handler_args = () |
| |
| # chunk, stdin |
| if net_args == 2: |
| handler_args = (process.stdin,) |
| |
| # chunk, stdin, process |
| elif net_args == 3: |
| # notice we're only storing a weakref, to prevent cyclic references |
| # (where the process holds a streamreader, and a streamreader holds a |
| # handler-closure with a reference to the process |
| handler_args = (process.stdin, weakref.ref(process)) |
| |
| def fn(chunk): |
| # this is pretty ugly, but we're evaluating the process at call-time, |
| # because it's a weakref |
| args = handler_args |
| if len(args) == 2: |
| args = (handler_args[0], handler_args[1]()) |
| return handler(chunk, *args) |
| |
| return fn |
| |
| |
| |
| def handle_process_exit_code(exit_code): |
| """ this should only ever be called once for each child process """ |
| # if we exited from a signal, let our exit code reflect that |
| if os.WIFSIGNALED(exit_code): |
| return -os.WTERMSIG(exit_code) |
| # otherwise just give us a normal exit code |
| elif os.WIFEXITED(exit_code): |
| return os.WEXITSTATUS(exit_code) |
| else: |
| raise RuntimeError("Unknown child exit status!") |
| |
| |
| |
| |
| class OProc(object): |
| """ this class is instantiated by RunningCommand for a command to be exec'd. |
| it handles all the nasty business involved with correctly setting up the |
| input/output to the child process. it gets its name for subprocess.Popen |
| (process open) but we're calling ours OProc (open process) """ |
| |
| _default_window_size = (24, 80) |
| |
| # used in redirecting |
| STDOUT = -1 |
| STDERR = -2 |
| |
| def __init__(self, parent_log, cmd, stdin, stdout, stderr, call_args, pipe): |
| """ |
| cmd is the full string that will be exec'd. it includes the program |
| name and all its arguments |
| |
| stdin, stdout, stderr are what the child will use for standard |
| input/output/err |
| |
| call_args is a mapping of all the special keyword arguments to apply |
| to the child process |
| """ |
| |
| self.call_args = call_args |
| |
| # I had issues with getting 'Input/Output error reading stdin' from dd, |
| # until I set _tty_out=False |
| if self.call_args["piped"] == "direct": |
| self.call_args["tty_out"] = False |
| |
| self._single_tty = self.call_args["tty_in"] and self.call_args["tty_out"] |
| |
| # this logic is a little convoluted, but basically this top-level |
| # if/else is for consolidating input and output TTYs into a single |
| # TTY. this is the only way some secure programs like ssh will |
| # output correctly (is if stdout and stdin are both the same TTY) |
| if self._single_tty: |
| self._stdin_fd, self._slave_stdin_fd = pty.openpty() |
| |
| self._stdout_fd = self._stdin_fd |
| self._slave_stdout_fd = self._slave_stdin_fd |
| |
| self._stderr_fd = self._stdin_fd |
| self._slave_stderr_fd = self._slave_stdin_fd |
| |
| # do not consolidate stdin and stdout. this is the most common use- |
| # case |
| else: |
| # this check here is because we may be doing "direct" piping |
| # (_piped="direct"), and so our stdin might be an instance of |
| # OProc |
| if isinstance(stdin, OProc): |
| self._slave_stdin_fd = stdin._stdout_fd |
| self._stdin_fd = None |
| elif self.call_args["tty_in"]: |
| self._slave_stdin_fd, self._stdin_fd = pty.openpty() |
| # tty_in=False is the default |
| else: |
| self._slave_stdin_fd, self._stdin_fd = os.pipe() |
| |
| |
| # tty_out=True is the default |
| if self.call_args["tty_out"]: |
| self._stdout_fd, self._slave_stdout_fd = pty.openpty() |
| else: |
| self._stdout_fd, self._slave_stdout_fd = os.pipe() |
| |
| # unless STDERR is going to STDOUT, it ALWAYS needs to be a pipe, |
| # and never a PTY. the reason for this is not totally clear to me, |
| # but it has to do with the fact that if STDERR isn't set as the |
| # CTTY (because STDOUT is), the STDERR buffer won't always flush |
| # by the time the process exits, and the data will be lost. |
| # i've only seen this on OSX. |
| if stderr is not OProc.STDOUT: |
| self._stderr_fd, self._slave_stderr_fd = os.pipe() |
| |
| |
| # this is a hack, but what we're doing here is intentionally throwing an |
| # OSError exception if our child processes's directory doesn't exist, |
| # but we're doing it BEFORE we fork. the reason for before the fork is |
| # error handling. i'm currently too lazy to implement what |
| # subprocess.py did and set up a error pipe to handle exceptions that |
| # happen in the child between fork and exec. it has only been seen in |
| # the wild for a missing cwd, so we'll handle it here. |
| cwd = self.call_args["cwd"] |
| if cwd is not None and not os.path.exists(cwd): |
| os.chdir(cwd) |
| |
| |
| gc_enabled = gc.isenabled() |
| if gc_enabled: |
| gc.disable() |
| self.pid = os.fork() |
| |
| |
| # child |
| if self.pid == 0: # pragma: no cover |
| try: |
| # ignoring SIGHUP lets us persist even after the parent process |
| # exits. only ignore if we're backgrounded |
| if self.call_args["bg"] is True: |
| signal.signal(signal.SIGHUP, signal.SIG_IGN) |
| |
| # this piece of ugliness is due to a bug where we can lose output |
| # if we do os.close(self._slave_stdout_fd) in the parent after |
| # the child starts writing. |
| # see http://bugs.python.org/issue15898 |
| if IS_OSX: |
| time.sleep(0.01) |
| |
| os.setsid() |
| |
| if self.call_args["tty_out"]: |
| # set raw mode, so there isn't any weird translation of |
| # newlines to \r\n and other oddities. we're not outputting |
| # to a terminal anyways |
| # |
| # we HAVE to do this here, and not in the parent process, |
| # because we have to guarantee that this is set before the |
| # child process is run, and we can't do it twice. |
| tty.setraw(self._slave_stdout_fd) |
| |
| |
| # if the parent-side fd for stdin exists, close it. the case |
| # where it may not exist is if we're using piped="direct" |
| if self._stdin_fd: |
| os.close(self._stdin_fd) |
| |
| if not self._single_tty: |
| os.close(self._stdout_fd) |
| if stderr is not OProc.STDOUT: |
| os.close(self._stderr_fd) |
| |
| |
| if cwd: |
| os.chdir(cwd) |
| |
| os.dup2(self._slave_stdin_fd, 0) |
| os.dup2(self._slave_stdout_fd, 1) |
| |
| # we're not directing stderr to stdout? then set self._slave_stderr_fd to |
| # fd 2, the common stderr fd |
| if stderr is OProc.STDOUT: |
| os.dup2(self._slave_stdout_fd, 2) |
| else: |
| os.dup2(self._slave_stderr_fd, 2) |
| |
| # don't inherit file descriptors |
| max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[0] |
| os.closerange(3, max_fd) |
| |
| |
| # set our controlling terminal. tty_out defaults to true |
| if self.call_args["tty_out"]: |
| tmp_fd = os.open(os.ttyname(1), os.O_RDWR) |
| os.close(tmp_fd) |
| |
| |
| if self.call_args["tty_out"]: |
| setwinsize(1, self.call_args["tty_size"]) |
| |
| # actually execute the process |
| if self.call_args["env"] is None: |
| os.execv(cmd[0], cmd) |
| else: |
| os.execve(cmd[0], cmd, self.call_args["env"]) |
| |
| # we must ensure that we ALWAYS exit the child process, otherwise |
| # the parent process code will be executed twice on exception |
| # https://github.com/amoffat/sh/issues/202 |
| # |
| # if your parent process experiences an exit code 255, it is most |
| # likely that an exception occurred between the fork of the child |
| # and the exec. this should be reported. |
| finally: |
| os._exit(255) |
| |
| # parent |
| else: |
| if gc_enabled: |
| gc.enable() |
| |
| # used to determine what exception to raise. if our process was |
| # killed via a timeout counter, we'll raise something different than |
| # a SIGKILL exception |
| self.timed_out = False |
| |
| self.started = time.time() |
| self.cmd = cmd |
| |
| # exit code should only be manipulated from within self._wait_lock |
| # to prevent race conditions |
| self.exit_code = None |
| |
| self.stdin = stdin or Queue() |
| |
| # _pipe_queue is used internally to hand off stdout from one process |
| # to another. by default, all stdout from a process gets dumped |
| # into this pipe queue, to be consumed in real time (hence the |
| # thread-safe Queue), or at a potentially later time |
| self._pipe_queue = Queue() |
| |
| # this is used to prevent a race condition when we're waiting for |
| # a process to end, and the OProc's internal threads are also checking |
| # for the processes's end |
| self._wait_lock = threading.Lock() |
| |
| # these are for aggregating the stdout and stderr. we use a deque |
| # because we don't want to overflow |
| self._stdout = deque(maxlen=self.call_args["internal_bufsize"]) |
| self._stderr = deque(maxlen=self.call_args["internal_bufsize"]) |
| |
| if self.call_args["tty_in"]: |
| setwinsize(self._stdin_fd, self.call_args["tty_size"]) |
| |
| |
| self.log = parent_log.get_child("process", repr(self)) |
| |
| os.close(self._slave_stdin_fd) |
| if not self._single_tty: |
| os.close(self._slave_stdout_fd) |
| if stderr is not OProc.STDOUT: |
| os.close(self._slave_stderr_fd) |
| |
| self.log.debug("started process") |
| |
| |
| if self.call_args["tty_in"]: |
| attr = termios.tcgetattr(self._stdin_fd) |
| attr[3] &= ~termios.ECHO |
| termios.tcsetattr(self._stdin_fd, termios.TCSANOW, attr) |
| |
| # this represents the connection from a Queue object (or whatever |
| # we're using to feed STDIN) to the process's STDIN fd |
| self._stdin_stream = None |
| if not isinstance(self.stdin, OProc): |
| self._stdin_stream = \ |
| StreamWriter(self.log.get_child("streamwriter", |
| "stdin"), self._stdin_fd, self.stdin, |
| self.call_args["in_bufsize"], |
| self.call_args["encoding"], |
| self.call_args["tty_in"]) |
| |
| stdout_pipe = None |
| if pipe is OProc.STDOUT and not self.call_args["no_pipe"]: |
| stdout_pipe = self._pipe_queue |
| |
| |
| # this represents the connection from a process's STDOUT fd to |
| # wherever it has to go, sometimes a pipe Queue (that we will use |
| # to pipe data to other processes), and also an internal deque |
| # that we use to aggregate all the output |
| save_stdout = not self.call_args["no_out"] and \ |
| (self.call_args["tee"] in (True, "out") or stdout is None) |
| |
| |
| # if we're piping directly into another process's filedescriptor, we |
| # bypass reading from the stdout stream altogether, because we've |
| # already hooked up this processes's stdout fd to the other |
| # processes's stdin fd |
| self._stdout_stream = None |
| if self.call_args["piped"] != "direct": |
| if callable(stdout): |
| stdout = construct_streamreader_callback(self, stdout) |
| self._stdout_stream = \ |
| StreamReader(self.log.get_child("streamreader", |
| "stdout"), self._stdout_fd, stdout, self._stdout, |
| self.call_args["out_bufsize"], |
| self.call_args["encoding"], |
| self.call_args["decode_errors"], stdout_pipe, |
| save_data=save_stdout) |
| |
| if stderr is OProc.STDOUT or self._single_tty: |
| self._stderr_stream = None |
| else: |
| stderr_pipe = None |
| if pipe is OProc.STDERR and not self.call_args["no_pipe"]: |
| stderr_pipe = self._pipe_queue |
| |
| save_stderr = not self.call_args["no_err"] and \ |
| (self.call_args["tee"] in ("err",) or stderr is None) |
| |
| if callable(stderr): |
| stderr = construct_streamreader_callback(self, stderr) |
| |
| self._stderr_stream = StreamReader(Logger("streamreader"), |
| self._stderr_fd, stderr, self._stderr, |
| self.call_args["err_bufsize"], self.call_args["encoding"], |
| self.call_args["decode_errors"], stderr_pipe, |
| save_data=save_stderr) |
| |
| |
| # start the main io threads |
| # stdin thread is not needed if we are connecting from another process's stdout pipe |
| self._input_thread = None |
| if self._stdin_stream: |
| self._input_thread = _start_daemon_thread(self.input_thread, |
| self._stdin_stream) |
| |
| self._output_thread = _start_daemon_thread(self.output_thread, |
| self._stdout_stream, self._stderr_stream, |
| self.call_args["timeout"], self.started, |
| self.call_args["timeout_signal"]) |
| |
| |
| def __repr__(self): |
| return "<Process %d %r>" % (self.pid, self.cmd[:500]) |
| |
| |
| def change_in_bufsize(self, buf): |
| self._stdin_stream.stream_bufferer.change_buffering(buf) |
| |
| def change_out_bufsize(self, buf): |
| self._stdout_stream.stream_bufferer.change_buffering(buf) |
| |
| def change_err_bufsize(self, buf): |
| self._stderr_stream.stream_bufferer.change_buffering(buf) |
| |
| |
| def input_thread(self, stdin): |
| """ this is run in a separate thread. it writes into our process's |
| stdin (a streamwriter) and waits the process to end AND everything that |
| can be written to be written """ |
| done = False |
| while not done and self.is_alive(): |
| self.log.debug("%r ready for more input", stdin) |
| done = stdin.write() |
| |
| stdin.close() |
| |
| |
| def output_thread(self, stdout, stderr, timeout, started, timeout_exc): |
| """ this function is run in a separate thread. it reads from the |
| process's stdout stream (a streamreader), and waits for it to claim that |
| its done """ |
| |
| readers = [] |
| errors = [] |
| |
| if stdout is not None: |
| readers.append(stdout) |
| errors.append(stdout) |
| if stderr is not None: |
| readers.append(stderr) |
| errors.append(stderr) |
| |
| # this is our select loop for polling stdout or stderr that is ready to |
| # be read and processed. if one of those streamreaders indicate that it |
| # is done altogether being read from, we remove it from our list of |
| # things to poll. when no more things are left to poll, we leave this |
| # loop and clean up |
| while readers: |
| outputs, inputs, err = select.select(readers, [], errors, 0.1) |
| |
| # stdout and stderr |
| for stream in outputs: |
| self.log.debug("%r ready to be read from", stream) |
| done = stream.read() |
| if done: |
| readers.remove(stream) |
| |
| for stream in err: |
| pass |
| |
| # test if the process has been running too long |
| if timeout: |
| now = time.time() |
| if now - started > timeout: |
| self.log.debug("we've been running too long") |
| self.timed_out = True |
| self.signal(timeout_exc) |
| |
| |
| # this is here because stdout may be the controlling TTY, and |
| # we can't close it until the process has ended, otherwise the |
| # child will get SIGHUP. typically, if we've broken out of |
| # the above loop, and we're here, the process is just about to |
| # end, so it's probably ok to aggressively poll self.is_alive() |
| # |
| # the other option to this would be to do the CTTY close from |
| # the method that does the actual os.waitpid() call, but the |
| # problem with that is that the above loop might still be |
| # running, and closing the fd will cause some operation to |
| # fail. this is less complex than wrapping all the ops |
| # in the above loop with out-of-band fd-close exceptions |
| while self.is_alive(): |
| time.sleep(0.001) |
| |
| if stdout: |
| stdout.close() |
| |
| if stderr: |
| stderr.close() |
| |
| |
| @property |
| def stdout(self): |
| return "".encode(self.call_args["encoding"]).join(self._stdout) |
| |
| @property |
| def stderr(self): |
| return "".encode(self.call_args["encoding"]).join(self._stderr) |
| |
| |
| def signal(self, sig): |
| self.log.debug("sending signal %d", sig) |
| try: |
| os.kill(self.pid, sig) |
| except OSError: |
| pass |
| |
| def kill(self): |
| self.log.debug("killing") |
| self.signal(signal.SIGKILL) |
| |
| def terminate(self): |
| self.log.debug("terminating") |
| self.signal(signal.SIGTERM) |
| |
| |
| def is_alive(self): |
| """ polls if our child process has completed, without blocking. this |
| method has side-effects, such as setting our exit_code, if we happen to |
| see our child exit while this is running """ |
| |
| if self.exit_code is not None: |
| return False |
| |
| # what we're doing here essentially is making sure that the main thread |
| # (or another thread), isn't calling .wait() on the process. because |
| # .wait() calls os.waitpid(self.pid, 0), we can't do an os.waitpid |
| # here...because if we did, and the process exited while in this |
| # thread, the main thread's os.waitpid(self.pid, 0) would raise OSError |
| # (because the process ended in another thread). |
| # |
| # so essentially what we're doing is, using this lock, checking if |
| # we're calling .wait(), and if we are, let .wait() get the exit code |
| # and handle the status, otherwise let us do it. |
| acquired = self._wait_lock.acquire(False) |
| if not acquired: |
| if self.exit_code is not None: |
| return False |
| return True |
| |
| try: |
| # WNOHANG is just that...we're calling waitpid without hanging... |
| # essentially polling the process. the return result is (0, 0) if |
| # there's no process status, so we check that pid == self.pid below |
| # in order to determine how to proceed |
| pid, exit_code = os.waitpid(self.pid, os.WNOHANG) |
| if pid == self.pid: |
| self.exit_code = handle_process_exit_code(exit_code) |
| return False |
| |
| # no child process |
| except OSError: |
| return False |
| else: |
| return True |
| finally: |
| self._wait_lock.release() |
| |
| |
| def wait(self): |
| """ waits for the process to complete, handles the exit code """ |
| |
| self.log.debug("acquiring wait lock to wait for completion") |
| # using the lock in a with-context blocks, which is what we want if |
| # we're running wait() |
| with self._wait_lock: |
| self.log.debug("got wait lock") |
| |
| if self.exit_code is None: |
| self.log.debug("exit code not set, waiting on pid") |
| pid, exit_code = os.waitpid(self.pid, 0) # blocks |
| self.exit_code = handle_process_exit_code(exit_code) |
| else: |
| self.log.debug("exit code already set (%d), no need to wait", self.exit_code) |
| |
| # we may not have a thread for stdin, if the pipe has been connected |
| # via _piped="direct" |
| if self._input_thread: |
| self._input_thread.join() |
| |
| # wait for our stdout and stderr streamreaders to finish reading and |
| # aggregating the process output |
| self._output_thread.join() |
| |
| return self.exit_code |
| |
| |
| |
| |
| class DoneReadingForever(Exception): pass |
| class NotYetReadyToRead(Exception): pass |
| |
| |
| def determine_how_to_read_input(input_obj): |
| """ given some kind of input object, return a function that knows how to |
| read chunks of that input object. |
| |
| each reader function should return a chunk and raise a DoneReadingForever |
| exception, or return None, when there's no more data to read |
| |
| NOTE: the function returned does not need to care much about the requested |
| buffering type (eg, unbuffered vs newline-buffered). the StreamBufferer |
| will take care of that. these functions just need to return a |
| reasonably-sized chunk of data. """ |
| |
| get_chunk = None |
| |
| if isinstance(input_obj, Queue): |
| log_msg = "queue" |
| get_chunk = get_queue_chunk_reader(input_obj) |
| |
| elif callable(input_obj): |
| log_msg = "callable" |
| get_chunk = get_callable_chunk_reader(input_obj) |
| |
| # also handles stringio |
| elif hasattr(input_obj, "read"): |
| log_msg = "file descriptor" |
| get_chunk = get_file_chunk_reader(input_obj) |
| |
| elif isinstance(input_obj, basestring): |
| log_msg = "string" |
| get_chunk = get_iter_string_reader(input_obj) |
| |
| else: |
| log_msg = "general iterable" |
| get_chunk = get_iter_chunk_reader(iter(input_obj)) |
| |
| return get_chunk, log_msg |
| |
| |
| |
| def get_queue_chunk_reader(stdin): |
| def fn(): |
| try: |
| chunk = stdin.get(True, 0.01) |
| except Empty: |
| raise NotYetReadyToRead |
| if chunk is None: |
| raise DoneReadingForever |
| return chunk |
| return fn |
| |
| |
| def get_callable_chunk_reader(stdin): |
| def fn(): |
| try: |
| return stdin() |
| except: |
| raise DoneReadingForever |
| return fn |
| |
| |
| def get_iter_string_reader(stdin): |
| """ return an iterator that returns a chunk of a string every time it is |
| called. notice that even though bufsize_type might be line buffered, we're |
| not doing any line buffering here. that's because our StreamBufferer |
| handles all buffering. we just need to return a reasonable-sized chunk. """ |
| bufsize = 1024 |
| iter_str = (stdin[i:i + bufsize] for i in range(0, len(stdin), bufsize)) |
| return get_iter_chunk_reader(iter_str) |
| |
| |
| def get_iter_chunk_reader(stdin): |
| def fn(): |
| try: |
| if IS_PY3: |
| chunk = stdin.__next__() |
| else: |
| chunk = stdin.next() |
| return chunk |
| except StopIteration: |
| raise DoneReadingForever |
| return fn |
| |
| def get_file_chunk_reader(stdin): |
| bufsize = 1024 |
| |
| def fn(): |
| chunk = stdin.read(bufsize) |
| if not chunk: |
| raise DoneReadingForever |
| else: |
| return chunk |
| return fn |
| |
| |
| def bufsize_type_to_bufsize(bf_type): |
| """ for a given bufsize type, return the actual bufsize we will read. |
| notice that although 1 means "newline-buffered", we're reading a chunk size |
| of 1024. this is because we have to read something. we let a |
| StreamBufferer instance handle splitting our chunk on newlines """ |
| |
| # newlines |
| if bf_type == 1: |
| bufsize = 1024 |
| # unbuffered |
| elif bf_type == 0: |
| bufsize = 1 |
| # or buffered by specific amount |
| else: |
| bufsize = bf_type |
| |
| return bufsize |
| |
| |
| |
| class StreamWriter(object): |
| """ StreamWriter reads from some input (the stdin param) and writes to a fd |
| (the stream param). the stdin may be a Queue, a callable, something with |
| the "read" method, a string, or an iterable """ |
| |
| def __init__(self, log, stream, stdin, bufsize_type, encoding, tty_in): |
| self.stream = stream |
| self.stdin = stdin |
| |
| self.log = log |
| self.encoding = encoding |
| self.tty_in = tty_in |
| |
| |
| self.stream_bufferer = StreamBufferer(bufsize_type, self.encoding) |
| self.get_chunk, log_msg = determine_how_to_read_input(stdin) |
| self.log.debug("parsed stdin as a %s", log_msg) |
| |
| |
| def fileno(self): |
| """ defining this allows us to do select.select on an instance of this |
| class """ |
| return self.stream |
| |
| |
| |
| def write(self): |
| """ attempt to get a chunk of data to write to our child process's |
| stdin, then write it. the return value answers the questions "are we |
| done writing forever?" """ |
| |
| # get_chunk may sometimes return bytes, and sometimes returns trings |
| # because of the nature of the different types of STDIN objects we |
| # support |
| try: |
| chunk = self.get_chunk() |
| if chunk is None: |
| raise DoneReadingForever |
| |
| except DoneReadingForever: |
| self.log.debug("done reading") |
| |
| if self.tty_in: |
| # EOF time |
| try: |
| char = termios.tcgetattr(self.stream)[6][termios.VEOF] |
| except: |
| char = chr(4).encode() |
| os.write(self.stream, char) |
| |
| return True |
| |
| except NotYetReadyToRead: |
| self.log.debug("received no data") |
| return False |
| |
| # if we're not bytes, make us bytes |
| if IS_PY3 and hasattr(chunk, "encode"): |
| chunk = chunk.encode(self.encoding) |
| |
| for proc_chunk in self.stream_bufferer.process(chunk): |
| self.log.debug("got chunk size %d: %r", len(proc_chunk), |
| proc_chunk[:30]) |
| |
| self.log.debug("writing chunk to process") |
| try: |
| os.write(self.stream, proc_chunk) |
| except OSError: |
| self.log.debug("OSError writing stdin chunk") |
| return True |
| |
| |
| def close(self): |
| self.log.debug("closing, but flushing first") |
| chunk = self.stream_bufferer.flush() |
| self.log.debug("got chunk size %d to flush: %r", len(chunk), chunk[:30]) |
| try: |
| if chunk: |
| os.write(self.stream, chunk) |
| |
| if not self.tty_in: |
| self.log.debug("we used a TTY, so closing the stream") |
| os.close(self.stream) |
| |
| except OSError: |
| pass |
| |
| |
| |
| def determine_how_to_feed_output(handler, encoding, decode_errors): |
| if callable(handler): |
| process, finish = get_callback_chunk_consumer(handler, encoding, |
| decode_errors) |
| elif isinstance(handler, cStringIO): |
| process, finish = get_cstringio_chunk_consumer(handler) |
| elif isinstance(handler, StringIO): |
| process, finish = get_stringio_chunk_consumer(handler, encoding, |
| decode_errors) |
| elif hasattr(handler, "write"): |
| process, finish = get_file_chunk_consumer(handler) |
| else: |
| process = lambda chunk: False |
| finish = lambda: None |
| |
| return process, finish |
| |
| |
| def get_file_chunk_consumer(handler): |
| def process(chunk): |
| handler.write(chunk) |
| # we should flush on an fd. chunk is already the correctly-buffered |
| # size, so we don't need the fd buffering as well |
| handler.flush() |
| return False |
| |
| def finish(): |
| if hasattr(handler, "flush"): |
| handler.flush() |
| |
| return process, finish |
| |
| def get_callback_chunk_consumer(handler, encoding, decode_errors): |
| def process(chunk): |
| # try to use the encoding first, if that doesn't work, send |
| # the bytes, because it might be binary |
| try: |
| chunk = chunk.decode(encoding, decode_errors) |
| except UnicodeDecodeError: |
| pass |
| return handler(chunk) |
| |
| def finish(): |
| pass |
| |
| return process, finish |
| |
| def get_cstringio_chunk_consumer(handler): |
| def process(chunk): |
| handler.write(chunk) |
| return False |
| |
| def finish(): |
| pass |
| |
| return process, finish |
| |
| |
| def get_stringio_chunk_consumer(handler, encoding, decode_errors): |
| def process(chunk): |
| handler.write(chunk.decode(encoding, decode_errors)) |
| return False |
| |
| def finish(): |
| pass |
| |
| return process, finish |
| |
| |
| class StreamReader(object): |
| """ reads from some output (the stream) and sends what it just read to the |
| handler. """ |
| def __init__(self, log, stream, handler, buffer, bufsize_type, encoding, |
| decode_errors, pipe_queue=None, save_data=True): |
| self.stream = stream |
| self.buffer = buffer |
| self.save_data = save_data |
| self.encoding = encoding |
| self.decode_errors = decode_errors |
| |
| self.pipe_queue = None |
| if pipe_queue: |
| self.pipe_queue = weakref.ref(pipe_queue) |
| |
| self.log = log |
| |
| self.stream_bufferer = StreamBufferer(bufsize_type, self.encoding, |
| self.decode_errors) |
| self.bufsize = bufsize_type_to_bufsize(bufsize_type) |
| |
| self.process_chunk, self.finish_chunk_processor = \ |
| determine_how_to_feed_output(handler, encoding, decode_errors) |
| |
| self.should_quit = False |
| |
| |
| def fileno(self): |
| """ defining this allows us to do select.select on an instance of this |
| class """ |
| return self.stream |
| |
| def close(self): |
| chunk = self.stream_bufferer.flush() |
| self.log.debug("got chunk size %d to flush: %r", len(chunk), chunk[:30]) |
| if chunk: |
| self.write_chunk(chunk) |
| |
| self.finish_chunk_processor() |
| |
| if self.pipe_queue and self.save_data: |
| self.pipe_queue().put(None) |
| |
| try: |
| os.close(self.stream) |
| except OSError: |
| pass |
| |
| |
| def write_chunk(self, chunk): |
| # in PY3, the chunk coming in will be bytes, so keep that in mind |
| |
| if not self.should_quit: |
| self.should_quit = self.process_chunk(chunk) |
| |
| |
| if self.save_data: |
| self.buffer.append(chunk) |
| |
| if self.pipe_queue: |
| self.log.debug("putting chunk onto pipe: %r", chunk[:30]) |
| self.pipe_queue().put(chunk) |
| |
| |
| def read(self): |
| # if we're PY3, we're reading bytes, otherwise we're reading |
| # str |
| try: |
| chunk = os.read(self.stream, self.bufsize) |
| except OSError as e: |
| self.log.debug("got errno %d, done reading", e.errno) |
| return True |
| if not chunk: |
| self.log.debug("got no chunk, done reading") |
| return True |
| |
| self.log.debug("got chunk size %d: %r", len(chunk), chunk[:30]) |
| for chunk in self.stream_bufferer.process(chunk): |
| self.write_chunk(chunk) |
| |
| |
| |
| |
| class StreamBufferer(object): |
| """ this is used for feeding in chunks of stdout/stderr, and breaking it up |
| into chunks that will actually be put into the internal buffers. for |
| example, if you have two processes, one being piped to the other, and you |
| want that, first process to feed lines of data (instead of the chunks |
| however they come in), OProc will use an instance of this class to chop up |
| the data and feed it as lines to be sent down the pipe """ |
| |
| def __init__(self, buffer_type, encoding=DEFAULT_ENCODING, |
| decode_errors="strict"): |
| # 0 for unbuffered, 1 for line, everything else for that amount |
| self.type = buffer_type |
| self.buffer = [] |
| self.n_buffer_count = 0 |
| self.encoding = encoding |
| self.decode_errors = decode_errors |
| |
| # this is for if we change buffering types. if we change from line |
| # buffered to unbuffered, its very possible that our self.buffer list |
| # has data that was being saved up (while we searched for a newline). |
| # we need to use that up, so we don't lose it |
| self._use_up_buffer_first = False |
| |
| # the buffering lock is used because we might chance the buffering |
| # types from a different thread. for example, if we have a stdout |
| # callback, we might use it to change the way stdin buffers. so we |
| # lock |
| self._buffering_lock = threading.RLock() |
| self.log = Logger("stream_bufferer") |
| |
| |
| def change_buffering(self, new_type): |
| # TODO, when we stop supporting 2.6, make this a with context |
| self.log.debug("acquiring buffering lock for changing buffering") |
| self._buffering_lock.acquire() |
| self.log.debug("got buffering lock for changing buffering") |
| try: |
| if new_type == 0: |
| self._use_up_buffer_first = True |
| |
| self.type = new_type |
| finally: |
| self._buffering_lock.release() |
| self.log.debug("released buffering lock for changing buffering") |
| |
| |
| def process(self, chunk): |
| # MAKE SURE THAT THE INPUT IS PY3 BYTES |
| # THE OUTPUT IS ALWAYS PY3 BYTES |
| |
| # TODO, when we stop supporting 2.6, make this a with context |
| self.log.debug("acquiring buffering lock to process chunk (buffering: %d)", self.type) |
| self._buffering_lock.acquire() |
| self.log.debug("got buffering lock to process chunk (buffering: %d)", self.type) |
| try: |
| # we've encountered binary, permanently switch to N size buffering |
| # since matching on newline doesn't make sense anymore |
| if self.type == 1: |
| try: |
| chunk.decode(self.encoding, self.decode_errors) |
| except: |
| self.log.debug("detected binary data, changing buffering") |
| self.change_buffering(1024) |
| |
| # unbuffered |
| if self.type == 0: |
| if self._use_up_buffer_first: |
| self._use_up_buffer_first = False |
| to_write = self.buffer |
| self.buffer = [] |
| to_write.append(chunk) |
| return to_write |
| |
| return [chunk] |
| |
| # line buffered |
| # we must decode the bytes before we try to match on newline |
| elif self.type == 1: |
| total_to_write = [] |
| chunk = chunk.decode(self.encoding, self.decode_errors) |
| while True: |
| newline = chunk.find("\n") |
| if newline == -1: |
| break |
| |
| chunk_to_write = chunk[:newline + 1] |
| if self.buffer: |
| # this is ugly, but it's designed to take the existing |
| # bytes buffer, join it together, tack on our latest |
| # chunk, then convert the whole thing to a string. |
| # it's necessary, i'm sure. read the whole block to |
| # see why. |
| chunk_to_write = "".encode(self.encoding).join(self.buffer) \ |
| + chunk_to_write.encode(self.encoding) |
| chunk_to_write = chunk_to_write.decode(self.encoding) |
| |
| self.buffer = [] |
| self.n_buffer_count = 0 |
| |
| chunk = chunk[newline + 1:] |
| total_to_write.append(chunk_to_write.encode(self.encoding)) |
| |
| if chunk: |
| self.buffer.append(chunk.encode(self.encoding)) |
| self.n_buffer_count += len(chunk) |
| return total_to_write |
| |
| # N size buffered |
| else: |
| total_to_write = [] |
| while True: |
| overage = self.n_buffer_count + len(chunk) - self.type |
| if overage >= 0: |
| ret = "".encode(self.encoding).join(self.buffer) + chunk |
| chunk_to_write = ret[:self.type] |
| chunk = ret[self.type:] |
| total_to_write.append(chunk_to_write) |
| self.buffer = [] |
| self.n_buffer_count = 0 |
| else: |
| self.buffer.append(chunk) |
| self.n_buffer_count += len(chunk) |
| break |
| return total_to_write |
| finally: |
| self._buffering_lock.release() |
| self.log.debug("released buffering lock for processing chunk (buffering: %d)", self.type) |
| |
| |
| def flush(self): |
| self.log.debug("acquiring buffering lock for flushing buffer") |
| self._buffering_lock.acquire() |
| self.log.debug("got buffering lock for flushing buffer") |
| try: |
| ret = "".encode(self.encoding).join(self.buffer) |
| self.buffer = [] |
| return ret |
| finally: |
| self._buffering_lock.release() |
| self.log.debug("released buffering lock for flushing buffer") |
| |
| |
| |
| @contextmanager |
| def pushd(path): |
| """ pushd is just a specialized form of args, where we're passing in the |
| current working directory """ |
| with args(_cwd=path): |
| yield |
| |
| |
| @contextmanager |
| def args(*args, **kwargs): |
| """ allows us to temporarily override all the special keyword parameters in |
| a with context """ |
| call_args = Command._call_args |
| old_args = call_args.copy() |
| |
| for key,value in kwargs.items(): |
| key = key.lstrip("_") |
| call_args[key] = value |
| |
| yield |
| call_args.update(old_args) |
| |
| |
| |
| class Environment(dict): |
| """ this allows lookups to names that aren't found in the global scope to be |
| searched for as a program name. for example, if "ls" isn't found in this |
| module's scope, we consider it a system program and try to find it. |
| |
| we use a dict instead of just a regular object as the base class because the |
| exec() statement used in this file requires the "globals" argument to be a |
| dictionary """ |
| |
| |
| # this is a list of all of the names that the sh module exports that will |
| # not resolve to functions. we don't want to accidentally shadow real |
| # commands with functions/imports that we define in sh.py. for example, |
| # "import time" may override the time system program |
| whitelist = set([ |
| "Command", |
| "CommandNotFound", |
| "DEFAULT_ENCODING", |
| "DoneReadingForever", |
| "ErrorReturnCode", |
| "NotYetReadyToRead", |
| "SignalException", |
| "TimeoutException", |
| "__project_url__", |
| "__version__", |
| "args", |
| "glob", |
| "pushd", |
| ]) |
| |
| def __init__(self, globs, baked_args={}): |
| self.globs = globs |
| self.baked_args = baked_args |
| self.disable_whitelist = False |
| |
| def __setitem__(self, k, v): |
| self.globs[k] = v |
| |
| def __getitem__(self, k): |
| # if we first import "_disable_whitelist" from sh, we can import |
| # anything defined in the global scope of sh.py. this is useful for our |
| # tests |
| if k == "_disable_whitelist": |
| self.disable_whitelist = True |
| return None |
| |
| # we're trying to import something real (maybe), see if it's in our |
| # global scope |
| if k in self.whitelist or self.disable_whitelist: |
| try: |
| return self.globs[k] |
| except KeyError: |
| pass |
| |
| # somebody tried to be funny and do "from sh import *" |
| if k == "__all__": |
| raise AttributeError("Cannot import * from sh. \ |
| Please import sh or import programs individually.") |
| |
| |
| # check if we're naming a dynamically generated ReturnCode exception |
| exc = get_exc_from_name(k) |
| if exc: |
| return exc |
| |
| |
| # https://github.com/ipython/ipython/issues/2577 |
| # https://github.com/amoffat/sh/issues/97#issuecomment-10610629 |
| if k.startswith("__") and k.endswith("__"): |
| raise AttributeError |
| |
| # 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 |
| # we use _create instead of instantiating the class directly because |
| # _create uses resolve_program, which will automatically do underscore- |
| # to-dash conversions. instantiating directly does not use that |
| return Command._create(k, **self.baked_args) |
| |
| |
| # methods that begin with "b_" are custom builtins and will override any |
| # program that exists in our path. this is useful for things like |
| # common shell builtins that people are used to, but which aren't actually |
| # full-fledged system binaries |
| |
| def b_cd(self, path): |
| os.chdir(path) |
| |
| def b_which(self, program): |
| return which(program) |
| |
| |
| |
| |
| def run_repl(env): # pragma: no cover |
| banner = "\n>> sh v{version}\n>> https://github.com/amoffat/sh\n" |
| |
| print(banner.format(version=__version__)) |
| while True: |
| try: |
| line = raw_input("sh> ") |
| except (ValueError, EOFError): |
| break |
| |
| try: |
| exec(compile(line, "<dummy>", "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 sh 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, baked_args={}): |
| # 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, None)) |
| |
| # python 3.2 (2.7 and 3.3 work fine) breaks on osx (not ubuntu) |
| # if we set this to None. and 3.3 needs a value for __path__ |
| self.__path__ = [] |
| self.__self_module = self_module |
| self.__env = Environment(globals(), baked_args) |
| |
| def __setattr__(self, name, value): |
| if hasattr(self, "__env"): |
| self.__env[name] = value |
| else: |
| ModuleType.__setattr__(self, name, value) |
| |
| def __getattr__(self, name): |
| if name == "__env": |
| raise AttributeError |
| return self.__env[name] |
| |
| # accept special keywords argument to define defaults for all operations |
| # that will be processed with given by return SelfWrapper |
| def __call__(self, **kwargs): |
| return SelfWrapper(self.__self_module, kwargs) |
| |
| |
| |
| # we're being run as a stand-alone script |
| if __name__ == "__main__": # pragma: no cover |
| try: |
| arg = sys.argv.pop(1) |
| except: |
| arg = None |
| |
| if arg == "test": |
| import subprocess |
| |
| def run_test(version, locale): |
| py_version = "python%s" % version |
| py_bin = which(py_version) |
| |
| if py_bin: |
| print("Testing %s, locale %r" % (py_version.capitalize(), |
| locale)) |
| |
| env = os.environ.copy() |
| env["LANG"] = locale |
| p = subprocess.Popen([py_bin, os.path.join(THIS_DIR, "test.py")] |
| + sys.argv[1:], env=env) |
| return_code = p.wait() |
| |
| if return_code != 0: |
| exit(1) |
| else: |
| print("Couldn't find %s, skipping" % py_version.capitalize()) |
| |
| versions = ("2.6", "2.7", "3.1", "3.2", "3.3", "3.4") |
| locales = ("en_US.UTF-8", "C") |
| for locale in locales: |
| for version in versions: |
| run_test(version, locale) |
| |
| else: |
| env = Environment(globals()) |
| run_repl(env) |
| |
| # we're being imported from somewhere |
| else: |
| self = sys.modules[__name__] |
| sys.modules[__name__] = SelfWrapper(self) |