diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 63302ceedba7fac56d09169f04a99a6185972b2b..462a26fd447effd2aed45217753c53636c8c3ade 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,42 @@ This document contains the ``fslpy`` release history in reverse chronological
 order.
 
 
+1.9.0 (Under development)
+-------------------------
+
+
+Added
+^^^^^
+
+
+* New :meth:`.Image.data` property method, for easy access to image data
+  as a ``numpy`` array.
+* New ``log`` option to the :func:`.run.run` function, allowing more
+  fine-grained control over sub-process output streams.
+* New :meth:`.Platform.fsldevdir` property, allowing the ``$FSLDEVDIR``
+  environment variable to be queried/changed.
+
+
+Changed
+^^^^^^^
+
+
+* :meth:`.Image.ndims` has been renamed to :meth:`.Image.ndim`, to align
+  more closely with ``numpy`` naming conventions.
+* The ``err`` and ``ret`` parameters to the :func:`.run.run` function have
+  been renamed to ``stderr`` and ``exitcode`` respectively.
+* The :func:`.runfsl` function will give priority to the ``$FSLDEVDIR``
+  environment variable if it is set.
+
+
+Deprecated
+^^^^^^^^^^
+
+
+* :meth:`.Image.ndims`.
+* The ``err`` and ``ret`` parameters to :func:`.run.run`.
+
+
 1.8.1 (Friday May 11th 2018)
 ----------------------------
 
diff --git a/fsl/data/image.py b/fsl/data/image.py
index c03618d856931ee96c0ab037ba530aa42d4c1a76..77889897f2610ac6d5735c81ffff7de85a226f40 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -491,7 +491,7 @@ class Nifti(notifier.Notifier, meta.Meta):
 
 
     @property
-    def ndims(self):
+    def ndim(self):
         """Returns the number of dimensions in this image. This number may not
         match the number of dimensions specified in the NIFTI header, as
         trailing dimensions of length 1 are ignored. But it is guaranteed to be
@@ -500,6 +500,15 @@ class Nifti(notifier.Notifier, meta.Meta):
         return len(self.__shape)
 
 
+    @property
+    @deprecation.deprecated(deprecated_in='1.9.0',
+                            removed_in='2.0.0',
+                            details='Use ndim instead')
+    def ndims(self):
+        """Deprecated - use :mod::meth:``ndim`` instead. """
+        return self.ndim()
+
+
     @deprecation.deprecated(deprecated_in='1.1.0',
                             removed_in='2.0.0',
                             details='Use ndims instead')
@@ -1006,6 +1015,17 @@ class Image(Nifti):
         return self.__nibImage
 
 
+    @property
+    def data(self):
+        """Returns the image data as a ``numpy`` array.
+
+        .. warning:: Calling this method will cause the entire image to be
+                     loaded into memory.
+        """
+        self.__imageWrapper.loadData()
+        return self[:]
+
+
     @property
     def saveState(self):
         """Returns ``True`` if this ``Image`` has been saved to disk, ``False``
diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py
index 4d7f05809f5be53ea7f24344df97364a0e97c1d0..ad8922f85dbeb5adf792c9a384db7757615beb5a 100644
--- a/fsl/data/melodicimage.py
+++ b/fsl/data/melodicimage.py
@@ -77,7 +77,7 @@ class MelodicImage(fslimage.Image):
             dataImage = fslimage.Image(dataFile,
                                        loadData=False,
                                        calcRange=False)
-            if dataImage.ndims >= 4:
+            if dataImage.ndim >= 4:
                 self.__tr = dataImage.pixdim[3]
 
 
diff --git a/fsl/utils/platform.py b/fsl/utils/platform.py
index 63c1d697c441664fca6bff9f8d96ffc2fec5da4e..cd2764ef9a9accc9a9f0f1a9d4f7e7837a687a8f 100644
--- a/fsl/utils/platform.py
+++ b/fsl/utils/platform.py
@@ -88,7 +88,6 @@ def isWidgetAlive(widget):
 
     import wx
 
-
     if platform.wxFlavour == platform.WX_PHOENIX:
         excType = RuntimeError
     elif platform.wxFlavour == platform.WX_PYTHON:
@@ -116,6 +115,7 @@ class Platform(notifier.Notifier):
        os
        frozen
        fsldir
+       fsldevdir
        haveGui
        canHaveGui
        inSSHSession
@@ -146,8 +146,9 @@ class Platform(notifier.Notifier):
         self.__glRenderer   = None
         self.__glIsSoftware = None
         self.__fslVersion   = None
-        self.__fsldir       = None
-        self.fsldir         = os.environ.get('FSLDIR', None)
+
+        # initialise fsldir - see fsldir.setter
+        self.fsldir = self.fsldir
 
         # Determine if a display is available. We do
         # this once at init (instead of on-demand in
@@ -284,7 +285,13 @@ class Platform(notifier.Notifier):
                   any registered listeners are notified via the
                   :class:`.Notifier` interface.
         """
-        return self.__fsldir
+        return os.environ.get('FSLDIR', None)
+
+
+    @property
+    def fsldevdir(self):
+        """The FSL development directory location. """
+        return os.environ.get('FSLDEVDIR', None)
 
 
     @fsldir.setter
@@ -301,9 +308,9 @@ class Platform(notifier.Notifier):
         elif not op.exists(value): value = None
         elif not op.isdir(value):  value = None
 
-        self.__fsldir = value
-
-        if value is not None:
+        if value is None:
+            os.environ.pop('FSLDIR', None)
+        else:
             os.environ['FSLDIR'] = value
 
             # Set the FSL version field if we can
@@ -316,6 +323,26 @@ class Platform(notifier.Notifier):
         self.notify(value=value)
 
 
+    @fsldevdir.setter
+    def fsldevdir(self, value):
+        """Changes the value of the :attr:`fsldevdir` property, and notifies
+        any registered listeners.
+        """
+
+        if value is not None:
+            value = value.strip()
+
+        if   value is None:        pass
+        elif value == '':          value = None
+        elif not op.exists(value): value = None
+        elif not op.isdir(value):  value = None
+
+        if value is None:
+            os.environ.pop('FSLDEVDIR', None)
+        else:
+            os.environ['FSLDEVDIR'] = value
+
+
     @property
     def fslVersion(self):
         """Returns the FSL version as a string, e.g. ``'5.0.9'``. Returns
diff --git a/fsl/utils/run.py b/fsl/utils/run.py
index 032e07d97a91951e61026b5e49f757675b799bd9..a8adb80b879a198485970284d562ef6064c96d96 100644
--- a/fsl/utils/run.py
+++ b/fsl/utils/run.py
@@ -16,7 +16,10 @@
 """
 
 
+import               sys
 import               logging
+import               warnings
+import               threading
 import               contextlib
 import               collections
 import subprocess as sp
@@ -26,6 +29,7 @@ import               six
 
 from   fsl.utils.platform import platform as fslplatform
 import fsl.utils.fslsub                   as fslsub
+import fsl.utils.tempdir                  as tempdir
 
 
 log = logging.getLogger(__name__)
@@ -37,6 +41,10 @@ execute them.
 """
 
 
+FSL_PREFIX = None
+"""Global override for the FSL executable location used by :func:`runfsl`. """
+
+
 class FSLNotPresent(Exception):
     """Error raised by the :func:`runfsl` function when ``$FSLDIR`` cannot
     be found.
@@ -80,6 +88,36 @@ def _prepareArgs(args):
     return list(args)
 
 
+real_stdout = sys.stdout
+def _forwardStream(in_, *outs):
+    """Creates and starts a daemon thread which forwards the given input stream
+    to one or more output streams. Used by the :func:`run` function to redirect
+    a command's standard output/error streams to more than one destination.
+
+    It is necessary to read the process stdout/ stderr on separate threads to
+    avoid deadlocks.
+
+    :arg in_:  Input stream
+    :arg outs: Output stream(s)
+    :returns:  The thread that has been started.
+    """
+
+    # not all file-likes have a mode attribute -
+    # if not present, assume a string stream
+    omodes = [getattr(o, 'mode', 'w') for o in outs]
+
+    def realForward():
+        for line in in_:
+            for i, o in enumerate(outs):
+                if 'b' in omodes[i]: o.write(line)
+                else:                o.write(line.decode('utf-8'))
+
+    t = threading.Thread(target=realForward)
+    t.daemon = True
+    t.start()
+    return t
+
+
 def run(*args, **kwargs):
     """Call a command and return its output. You can pass the command and
     arguments as a single string, or as a regular or unpacked sequence.
@@ -90,39 +128,79 @@ def run(*args, **kwargs):
     An exception is raised if the command returns a non-zero exit code, unless
     the ``ret`` option is set to ``True``.
 
-    :arg submit: Must be passed as a keyword argument. Defaults to ``None``.
-                 Accepted values are ``True`` or a
-                 If ``True``, the command is submitted as a cluster job via
-                 the :func:`.fslsub.submit` function.  May also be a
-                 dictionary containing arguments to that function.
-
-    :arg err:    Must be passed as a keyword argument. Defaults to
-                 ``False``. If ``True``, standard error is captured and
-                 returned. Ignored if ``submit`` is specified.
-
-    :arg ret:    Must be passed as a keyword argument. Defaults to ``False``.
-                 If ``True``, and the command's return code is non-0, an
-                 exception is not raised.  Ignored if ``submit`` is specified.
-
-    :returns:    If ``submit`` is provided, the cluster job ID is returned.
-                 Otherwise if ``err is False and ret is False`` (the default)
-                 a string containing the command's standard output.  is
-                 returned. Or, if ``err is True`` and/or ``ret is True``, a
-                 tuple containing the standard output, standard error (if
-                 ``err``), and return code (if ``ret``).
+    :arg stdout:   Must be passed as a keyword argument. Defaults to ``True``.
+                   If ``True``, standard output is captured and returned.
+                   Ignored if ``submit`` is specified.
+
+    :arg stderr:   Must be passed as a keyword argument. Defaults to ``False``.
+                   If ``True``, standard error is captured and returned.
+                   Ignored if ``submit`` is specified.
+
+    :arg exitcode: Must be passed as a keyword argument. Defaults to ``False``.
+                   If ``True``, and the command's return code is non-0, an
+                   exception is not raised.  Ignored if ``submit`` is
+                   specified.
+
+    :arg err:      Deprecated - use ``stderr`` instead.
+
+    :arg ret:      Deprecated - use ``exitcode`` instead.
+
+    :arg submit:   Must be passed as a keyword argument. Defaults to ``None``.
+                   If ``True``, the command is submitted as a cluster job via
+                   the :func:`.fslsub.submit` function.  May also be a
+                   dictionary containing arguments to that function.
+
+    :arg log:      Must be passed as a keyword argument.  An optional ``dict``
+                   which may be used to redirect the command's standard output
+                   and error. The following keys are recognised:
+
+                     - tee:    If ``True``, the command's standard output/error
+                               streams are forwarded to this processes streams.
+
+                     - stdout: Optional file-like object to which the command's
+                               standard output stream can be forwarded.
+
+                     - stderr: Optional file-like object to which the command's
+                               standard error stream can be forwarded.
+
+                     - cmd:    If ``True``, the command itself is logged to the
+                               standard output stream(s).
+
+    :returns:      If ``submit`` is provided, the return value of
+                   :func:`.fslsub` is returned. Otherwise returns a single
+                   value or a tuple, based on the based on the ``stdout``,
+                   ``stderr``, and ``exitcode`` arguments.
     """
 
-    err    = kwargs.get('err',    False)
-    ret    = kwargs.get('ret',    False)
-    submit = kwargs.get('submit', None)
-    args   = _prepareArgs(args)
+    if 'err' in kwargs:
+        warnings.warn('err is deprecated and will be removed '
+                      'in fslpy 2.0.0 - use stderr instead',
+                      DeprecationWarning)
+        kwargs['stderr'] = kwargs.get('stderr', kwargs['err'])
+    if 'ret' in kwargs:
+        warnings.warn('ret is deprecated and will be removed '
+                      'in fslpy 2.0.0 - use exitcode instead',
+                      DeprecationWarning)
+        kwargs['exitcode'] = kwargs.get('exitcode', kwargs['ret'])
+
+    returnStdout   = kwargs.get('stdout',   True)
+    returnStderr   = kwargs.get('stderr',   False)
+    returnExitcode = kwargs.get('exitcode', False)
+    submit         = kwargs.get('submit',   {})
+    log            = kwargs.get('log',      {})
+    tee            = log   .get('tee',      False)
+    logStdout      = log   .get('stdout',   None)
+    logStderr      = log   .get('stderr',   None)
+    logCmd         = log   .get('cmd',      False)
+    args           = _prepareArgs(args)
 
     if not bool(submit):
         submit = None
 
     if submit is not None:
-        err = False
-        ret = False
+        returnStdout   = False
+        returnStderr   = False
+        returnExitcode = False
 
         if submit is True:
             submit = dict()
@@ -132,54 +210,150 @@ def run(*args, **kwargs):
                          'options for fsl.utils.fslsub.submit')
 
     if DRY_RUN:
-        log.debug('dryrun: {}'.format(' '.join(args)))
-    else:
-        log.debug('run: {}'.format(' '.join(args)))
+        return _dryrun(
+            submit, returnStdout, returnStderr, returnExitcode, *args)
 
-    if DRY_RUN:
-        stderr = ''
-        if submit is None:
-            stdout = ' '.join(args)
-        else:
-            stdout = '[submit] ' + ' '.join(args)
-
-    elif submit is not None:
+    # submit - delegate to fslsub
+    if submit is not None:
         return fslsub.submit(' '.join(args), **submit)
 
-    else:
-        proc           = sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE)
-        stdout, stderr = proc.communicate()
-        retcode        = proc.returncode
+    # Run directly - delegate to _realrun
+    stdout, stderr, exitcode = _realrun(
+        tee, logStdout, logStderr, logCmd, *args)
+
+    if not returnExitcode and (exitcode != 0):
+        raise RuntimeError('{} returned non-zero exit code: {}'.format(
+            args[0], exitcode))
 
-        stdout = stdout.decode('utf-8').strip()
-        stderr = stderr.decode('utf-8').strip()
+    results = []
+    if returnStdout:   results.append(stdout)
+    if returnStderr:   results.append(stderr)
+    if returnExitcode: results.append(exitcode)
 
-        log.debug('stdout: {}'.format(stdout))
-        log.debug('stderr: {}'.format(stderr))
+    if len(results) == 1: return results[0]
+    else:                 return tuple(results)
+
+
+
+def _dryrun(submit, returnStdout, returnStderr, returnExitcode, *args):
+    """Used by the :func:`run` function when the :attr:`DRY_RUN` flag is
+    active.
+    """
 
-        if not ret and (retcode != 0):
-            raise RuntimeError('{} returned non-zero exit code: {}'.format(
-                args[0], retcode))
+    if submit:
+        return ('0',)
 
-    results = [stdout]
+    results = []
+    stderr  = ''
+    stdout  = ' '.join(args)
 
-    if err: results.append(stderr)
-    if ret: results.append(retcode)
+    if returnStdout:   results.append(stdout)
+    if returnStderr:   results.append(stderr)
+    if returnExitcode: results.append(0)
 
     if len(results) == 1: return results[0]
     else:                 return tuple(results)
 
 
+def _realrun(tee, logStdout, logStderr, logCmd, *args):
+    """Used by :func:`run`. Runs the given command and manages its standard
+    output and error streams.
+
+    :arg tee:       If ``True``, the command's standard output and error
+                    streams are forwarded to this process' standard output/
+                    error.
+
+    :arg logStdout: Optional file-like object to which the command's standard
+                    output stream can be forwarded.
+
+    :arg logStderr: Optional file-like object to which the command's standard
+                    error stream can be forwarded.
+
+    :arg logCmd:    If ``True``, the command itself is logged to the standard
+                    output stream(s).
+
+    :arg args:      Command to run
+
+    :returns:       A tuple containing:
+                      - the command's standard output as a string.
+                      - the command's standard error as a string.
+                      - the command's exit code.
+    """
+    proc = sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE)
+    with tempdir.tempdir(changeto=False) as td:
+
+        # We always direct the command's stdout/
+        # stderr to two temporary files
+        stdoutf = op.join(td, 'stdout')
+        stderrf = op.join(td, 'stderr')
+
+        with open(stdoutf, 'wb') as stdout, \
+             open(stderrf, 'wb') as stderr:  # noqa
+
+            outstreams = [stdout]
+            errstreams = [stderr]
+
+            # if tee, we duplicate the command's
+            # stdout/stderr to this process'
+            # stdout/stderr
+            if tee:
+                outstreams.append(sys.stdout)
+                errstreams.append(sys.stderr)
+
+            # And we also duplicate to caller-
+            # provided streams if they're given.
+            if logStdout is not None: outstreams.append(logStdout)
+            if logStderr is not None: errstreams.append(logStderr)
+
+            # log the command to
+            # stdout if requested
+            if logCmd:
+                cmd = ' '.join(args) + '\n'
+                for o in outstreams:
+                    if 'b' in getattr(o, 'mode', 'w'):
+                        o.write(cmd.encode('utf-8'))
+                    else:
+                        o.write(cmd)
+
+            stdoutt = _forwardStream(proc.stdout, *outstreams)
+            stderrt = _forwardStream(proc.stderr, *errstreams)
+
+            # Wait until the forwarding threads
+            # have finished cleanly, and the
+            # command has terminated.
+            stdoutt.join()
+            stderrt.join()
+            proc.communicate()
+
+        # Read in the command's stdout/stderr
+        with open(stdoutf, 'rb') as f: stdout = f.read()
+        with open(stderrf, 'rb') as f: stderr = f.read()
+
+    exitcode = proc.returncode
+    stdout   = stdout.decode('utf-8')
+    stderr   = stderr.decode('utf-8')
+
+    return stdout, stderr, exitcode
+
+
 def runfsl(*args, **kwargs):
     """Call a FSL command and return its output. This function simply prepends
     ``$FSLDIR/bin/`` to the command before passing it to :func:`run`.
     """
 
-    if fslplatform.fsldir is None:
+    prefix = None
+
+    if FSL_PREFIX is not None:
+        prefix = FSL_PREFIX
+    elif fslplatform.fsldevdir is not None:
+        prefix = op.join(fslplatform.fsldevdir, 'bin')
+    elif fslplatform.fsldir is not None:
+        prefix = op.join(fslplatform.fsldir, 'bin')
+    else:
         raise FSLNotPresent('$FSLDIR is not set - FSL cannot be found!')
 
     args    = _prepareArgs(args)
-    args[0] = op.join(fslplatform.fsldir, 'bin', args[0])
+    args[0] = op.join(prefix, args[0])
 
     return run(*args, **kwargs)
 
diff --git a/fsl/wrappers/bet.py b/fsl/wrappers/bet.py
index 45e7870de684fb7a0d23c15a2994000b37869524..969dcb3cdf7920f20d8dcb15fd05e6bd6adf03eb 100644
--- a/fsl/wrappers/bet.py
+++ b/fsl/wrappers/bet.py
@@ -39,8 +39,19 @@ def bet(input, output, **kwargs):
 
     valmap = {
         'm' : wutils.SHOW_IF_TRUE,
-        'R' : wutils.SHOW_IF_TRUE,
+        'o' : wutils.SHOW_IF_TRUE,
+        's' : wutils.SHOW_IF_TRUE,
         'n' : wutils.HIDE_IF_TRUE,
+        't' : wutils.SHOW_IF_TRUE,
+        'e' : wutils.SHOW_IF_TRUE,
+        'R' : wutils.SHOW_IF_TRUE,
+        'S' : wutils.SHOW_IF_TRUE,
+        'B' : wutils.SHOW_IF_TRUE,
+        'Z' : wutils.SHOW_IF_TRUE,
+        'F' : wutils.SHOW_IF_TRUE,
+        'A' : wutils.SHOW_IF_TRUE,
+        'v' : wutils.SHOW_IF_TRUE,
+        'd' : wutils.SHOW_IF_TRUE,
     }
 
     cmd  = ['bet', input, output]
diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py
index a1bc938d96ba57ddd95956a45c139fc348f3cb18..cfeed50ffa9b6dcdeb87c32075c22202772c77b9 100644
--- a/fsl/wrappers/wrapperutils.py
+++ b/fsl/wrappers/wrapperutils.py
@@ -142,8 +142,10 @@ def cmdwrapper(func):
     """
     def wrapper(*args, **kwargs):
         submit = kwargs.pop('submit', None)
-        cmd = func(*args, **kwargs)
-        return run.run(cmd, err=True, submit=submit)
+        stderr = kwargs.pop('stderr', True)
+        log    = kwargs.pop('log',    {'tee' : True})
+        cmd    = func(*args, **kwargs)
+        return run.run(cmd, stderr=stderr, log=log, submit=submit)
     return _update_wrapper(wrapper, func)
 
 
@@ -154,8 +156,10 @@ def fslwrapper(func):
     """
     def wrapper(*args, **kwargs):
         submit = kwargs.pop('submit', None)
-        cmd = func(*args, **kwargs)
-        return run.runfsl(cmd, err=True, submit=submit)
+        stderr = kwargs.pop('stderr', True)
+        log    = kwargs.pop('log',    {'tee' : True})
+        cmd    = func(*args, **kwargs)
+        return run.runfsl(cmd, stderr=stderr, log=log, submit=submit)
     return _update_wrapper(wrapper, func)
 
 
diff --git a/tests/__init__.py b/tests/__init__.py
index 57794123f0bbb3aa3ee448f742039781015cbb49..d8647089277169add9aead66a38bd911615448e3 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -36,7 +36,8 @@ logging.getLogger().setLevel(logging.WARNING)
 @contextlib.contextmanager
 def mockFSLDIR():
 
-    oldval = fslplatform.fsldir
+    oldfsldir    = fslplatform.fsldir
+    oldfsldevdir = fslplatform.fsldevdir
 
     try:
         with tempdir() as td:
@@ -44,13 +45,15 @@ def mockFSLDIR():
             bindir = op.join(fsldir, 'bin')
             os.makedirs(bindir)
             fslplatform.fsldir = fsldir
+            fslplatform.fsldevdir = None
 
             path = op.pathsep.join((bindir, os.environ['PATH']))
 
             with mock.patch.dict(os.environ, {'PATH': path}):
                 yield fsldir
     finally:
-        fslplatform.fsldir = oldval
+        fslplatform.fsldir    = oldfsldir
+        fslplatform.fsldevdir = oldfsldevdir
 
 
 def touch(fname):
@@ -67,6 +70,9 @@ class CaptureStdout(object):
     def reset(self):
         self.__mock_stdout = StringIO('')
         self.__mock_stderr = StringIO('')
+        self.__mock_stdout.mode = 'w'
+        self.__mock_stderr.mode = 'w'
+        return self
 
     def __enter__(self):
         self.__real_stdout = sys.stdout
diff --git a/tests/test_image.py b/tests/test_image.py
index 841e1620ffde74b403131758e271de393ad94177..74e4a9a72575d243abe666b27aa43bea9f4b07e6 100644
--- a/tests/test_image.py
+++ b/tests/test_image.py
@@ -285,11 +285,12 @@ def _test_Image_atts(imgtype):
             i    = fslimage.Image(path)
 
             assert tuple(i.shape)                       == tuple(expdims)
+            assert tuple(i.data.shape)                  == tuple(expdims)
             assert tuple(i.pixdim)                      == tuple(exppixdims)
             assert tuple(i.nibImage.shape)              == tuple(dims)
             assert tuple(i.nibImage.header.get_zooms()) == tuple(pixdims)
 
-            assert i.ndims      == expndims
+            assert i.ndim       == expndims
             assert i.dtype      == dtype
             assert i.name       == op.basename(path)
             assert i.dataSource == fslpath.addExt(path,
@@ -859,8 +860,9 @@ def _test_Image_5D(imgtype):
 
             img = fslimage.Image(path)
 
-            assert img.shape == dims
-            assert img.ndims == 5
+            assert img.shape      == dims
+            assert img.ndim       == 5
+            assert img.data.shape == dims
 
 
 def test_Image_voxToScaledVox_analyze(): _test_Image_voxToScaledVox(0)
diff --git a/tests/test_run.py b/tests/test_run.py
index 4b482fb090f9bc46753c0f13f504b234b3a5d709..9a260de5c543b71132b42b6ca716633b8dfbda1f 100644
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -8,6 +8,7 @@
 
 import os.path as op
 import            os
+import            shutil
 import            textwrap
 
 # python 3
@@ -23,7 +24,13 @@ from   fsl.utils.platform import platform as fslplatform
 import fsl.utils.run                      as run
 import fsl.utils.fslsub                   as fslsub
 
-from . import make_random_image, mockFSLDIR
+from . import make_random_image, mockFSLDIR, CaptureStdout
+
+
+def mkexec(path, contents):
+    with open(path, 'wt') as f:
+        f.write(contents)
+    os.chmod(path, 0o755)
 
 
 def test_run():
@@ -39,50 +46,106 @@ def test_run():
     with tempdir.tempdir():
 
         # return code == 0
-        with open('script.sh', 'wt') as f:
-            f.write(test_script.format(0))
-        os.chmod('script.sh', 0o755)
+        mkexec('script.sh', test_script.format(0))
 
-        expstdout = "standard output - arguments: 1 2 3"
-        expstderr = "standard error"
+        expstdout = "standard output - arguments: 1 2 3\n"
+        expstderr = "standard error\n"
 
         # test:
         #   - single string
         #   - packed sequence
         #   - unpacked sequence
-        assert run.run('./script.sh 1 2 3').strip() == expstdout
-        assert run.run(('./script.sh', '1', '2', '3')) == expstdout
+        assert run.run('./script.sh 1 2 3')             == expstdout
+        assert run.run(('./script.sh', '1', '2', '3'))  == expstdout
         assert run.run(*('./script.sh', '1', '2', '3')) == expstdout
 
         # test stdout/stderr
-        stdout, stderr = run.run('./script.sh 1 2 3', err=True)
-        assert stdout.strip() == expstdout
-        assert stderr.strip() == expstderr
+        stdout, stderr = run.run('./script.sh 1 2 3', stderr=True)
+        assert stdout == expstdout
+        assert stderr == expstderr
 
         # test return code
-        res = run.run('./script.sh 1 2 3', ret=True)
-        print(res)
+        res = run.run('./script.sh 1 2 3', exitcode=True)
         stdout, ret = res
-        assert stdout.strip() == expstdout
+        assert stdout == expstdout
         assert ret == 0
-        stdout, stderr, ret = run.run('./script.sh 1 2 3', err=True, ret=True)
-        assert stdout.strip() == expstdout
-        assert stderr.strip() == expstderr
+        stdout, stderr, ret = run.run('./script.sh 1 2 3', stderr=True,
+                                      exitcode=True)
+        assert stdout == expstdout
+        assert stderr == expstderr
         assert ret == 0
 
+        # stdout=False
+        res = run.run('./script.sh 1 2 3', stdout=False)
+        assert res == ()
+        stderr = run.run('./script.sh 1 2 3', stdout=False, stderr=True)
+        assert stderr == expstderr
+
         # return code != 0
-        with open('./script.sh', 'wt') as f:
-            f.write(test_script.format(255))
-        os.chmod('./script.sh', 0o755)
+        mkexec('./script.sh', test_script.format(255))
 
         with pytest.raises(RuntimeError):
             run.run('./script.sh 1 2 3')
 
-        stdout, ret = run.run('./script.sh 1 2 3', ret=True)
-        assert stdout.strip() == expstdout
+        stdout, ret = run.run('./script.sh 1 2 3', exitcode=True)
+        assert stdout == expstdout
         assert ret == 255
 
 
+def test_run_tee():
+    test_script = textwrap.dedent("""
+    #!/bin/bash
+
+    echo "standard output - arguments: $@"
+    echo "standard error" >&2
+    exit 0
+    """).strip()
+
+    with tempdir.tempdir():
+        mkexec('script.sh', test_script)
+
+        expstdout = "standard output - arguments: 1 2 3\n"
+        expstderr = "standard error\n"
+
+        capture = CaptureStdout()
+
+        with capture:
+            stdout = run.run('./script.sh 1 2 3', log={'tee' : True})
+
+        assert stdout         == expstdout
+        assert capture.stdout == expstdout
+
+        with capture.reset():
+            stdout, stderr = run.run('./script.sh 1 2 3', stderr=True,
+                                     log={'tee' : True})
+
+        assert stdout         == expstdout
+        assert stderr         == expstderr
+        assert capture.stdout == expstdout
+        assert capture.stderr == expstderr
+
+        with capture.reset():
+            stdout, stderr, ret = run.run('./script.sh 1 2 3',
+                                          stderr=True,
+                                          exitcode=True,
+                                          log={'tee' : True})
+
+        assert ret            == 0
+        assert stdout         == expstdout
+        assert stderr         == expstderr
+        assert capture.stdout == expstdout
+        assert capture.stderr == expstderr
+
+        with capture.reset():
+            stdout, ret = run.run('./script.sh 1 2 3',
+                                  exitcode=True,
+                                  log={'tee' : True})
+
+        assert ret            == 0
+        assert stdout         == expstdout
+        assert capture.stdout == expstdout
+
+
 def test_dryrun():
 
     test_script = textwrap.dedent("""
@@ -91,9 +154,7 @@ def test_dryrun():
     """).strip()
 
     with tempdir.tempdir():
-        with open('./script.sh', 'wt') as f:
-            f.write(test_script)
-        os.chmod('./script.sh', 0o755)
+        mkexec('./script.sh', test_script)
 
         run.run('./script.sh')
         assert op.exists('foo')
@@ -111,11 +172,12 @@ def test_runfsl():
 
     test_script = textwrap.dedent("""
     #!/bin/bash
-    echo $@
+    echo {}
     exit 0
     """).strip()
 
-    old_fsldir = fslplatform.fsldir
+    old_fsldir    = fslplatform.fsldir
+    old_fsldevdir = fslplatform.fsldevdir
 
     try:
         with tempdir.tempdir():
@@ -123,24 +185,47 @@ def test_runfsl():
             make_random_image('image.nii.gz')
 
             # no FSLDIR - should error
-            fslplatform.fsldir = None
+            fslplatform.fsldir    = None
+            fslplatform.fsldevdir = None
             with pytest.raises(run.FSLNotPresent):
-                run.runfsl('fslhd image')
+                run.runfsl('fslhd')
 
             # FSLDIR/bin exists - should be good
             fsldir = op.abspath('./fsl')
             fslhd  = op.join(fsldir, 'bin', 'fslhd')
             os.makedirs(op.join(fsldir, 'bin'))
-            with open(fslhd, 'wt') as f:
-                f.write(test_script)
-            os.chmod(fslhd, 0o777)
+
+            mkexec(fslhd, test_script.format('fsldir'))
 
             fslplatform.fsldir = fsldir
-            path = op.pathsep.join((fsldir, os.environ['PATH']))
-            with mock.patch.dict(os.environ, {'PATH' : path}):
-                assert run.runfsl('fslhd image').strip() == 'image'
+            assert run.runfsl('fslhd').strip() == 'fsldir'
+
+            # FSLDEVDIR should take precedence
+            fsldevdir = './fsldev'
+            fslhd  = op.join(fsldevdir, 'bin', 'fslhd')
+            shutil.copytree(fsldir, fsldevdir)
+
+            mkexec(fslhd, test_script.format('fsldevdir'))
+
+            fslplatform.fsldevdir = fsldevdir
+            fslplatform.fsldir    = None
+            assert run.runfsl('fslhd').strip() == 'fsldevdir'
+
+            # FSL_PREFIX should override all
+            override = './override'
+            fslhd    = op.join(override, 'fslhd')
+            os.makedirs(override)
+            mkexec(fslhd, test_script.format('override'))
+
+            fslplatform.fsldir    = None
+            fslplatform.fsldevdir = None
+            run.FSL_PREFIX = override
+            assert run.runfsl('fslhd').strip() == 'override'
+
     finally:
-        fslplatform.fsldir = old_fsldir
+        fslplatform.fsldir    = old_fsldir
+        fslplatform.fsldevdir = old_fsldevdir
+        run.FSL_PREFIX        = None
 
 
 def mock_submit(cmd, **kwargs):
@@ -189,8 +274,8 @@ def test_run_submit():
 
         stdout, stderr = fslsub.output(jid)
 
-        assert stdout.strip() == 'test_script running'
-        assert stderr.strip() == ''
+        assert stdout == 'test_script running\n'
+        assert stderr == ''
 
         kwargs = {'name' : 'abcde', 'ram' : '4GB'}
 
@@ -201,7 +286,84 @@ def test_run_submit():
         stdout, stderr = fslsub.output(jid)
 
         experr = '\n'.join(['{}: {}'.format(k, kwargs[k])
-                            for k in sorted(kwargs.keys())])
+                            for k in sorted(kwargs.keys())]) + '\n'
+
+        assert stdout == 'test_script running\n'
+        assert stderr == experr
+
+
+def test_run_streams():
+    """
+    """
+
+    test_script = textwrap.dedent("""
+    #!/usr/bin/env bash
+    echo standard output
+    echo standard error >&2
+    exit 0
+    """).strip()
+
+    expstdout = 'standard output\n'
+    expstderr = 'standard error\n'
+
+    with tempdir.tempdir():
+        mkexec('./script.sh', test_script)
+
+        with open('my_stdout', 'wt') as stdout, \
+             open('my_stderr', 'wt') as stderr:
+
+            stdout, stderr = run.run('./script.sh',
+                                     stderr=True,
+                                     log={'stdout' : stdout,
+                                          'stderr' : stderr})
+
+        assert stdout                         == expstdout
+        assert stderr                         == expstderr
+        assert open('my_stdout', 'rt').read() == expstdout
+        assert open('my_stderr', 'rt').read() == expstderr
+
+
+        capture = CaptureStdout()
+
+        with open('my_stdout', 'wt') as stdout, \
+             open('my_stderr', 'wt') as stderr, \
+             capture.reset():
+
+            stdout, stderr = run.run('./script.sh',
+                                     stderr=True,
+                                     log={'tee'    : True,
+                                          'stdout' : stdout,
+                                          'stderr' : stderr})
+
+        assert stdout                         == expstdout
+        assert stderr                         == expstderr
+        assert capture.stdout                 == expstdout
+        assert capture.stderr                 == expstderr
+        assert open('my_stdout', 'rt').read() == expstdout
+        assert open('my_stderr', 'rt').read() == expstderr
+
+
+def test_run_logcmd():
+    test_script = textwrap.dedent("""
+    #!/usr/bin/env bash
+    echo output $@
+    exit 0
+    """).strip()
+
+    expstdout = './script.sh 1 2 3\noutput 1 2 3\n'
+
+    with tempdir.tempdir():
+        mkexec('script.sh', test_script)
+        stdout = run.run('./script.sh 1 2 3', log={'cmd' : True})
+        assert stdout == expstdout
+
+        mkexec('script.sh', test_script)
+        stdout = run.run('./script.sh 1 2 3', log={'cmd' : True})
+        assert stdout == expstdout
+
+        with open('my_stdout', 'wt') as stdoutf:
+            stdout = run.run('./script.sh 1 2 3',
+                             log={'cmd' : True, 'stdout' : stdoutf})
 
-        assert stdout.strip() == 'test_script running'
-        assert stderr.strip() == experr
+        assert stdout                         == expstdout
+        assert open('my_stdout', 'rt').read() == expstdout