diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 09d15c5fca9ef6fac5b8d8dc67705e816e8c8ed1..f85840ef56f69a20c2fbdd3c2f80466358e97a98 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,20 @@
 This document contains the ``fslpy`` release history in reverse chronological
 order.
 
+
+3.3.2 (Tuesday 12th October 2020)
+---------------------------------
+
+
+Changed
+^^^^^^^
+
+
+* Most :func:`.wrapper` functions now accept an argument called ``cmdonly``
+  which, if ``True``, will cause the generated command-line call to be
+  returned, instead of executed.
+
+
 3.3.1 (Thursday 8th October 2020)
 ---------------------------------
 
diff --git a/fsl/utils/run.py b/fsl/utils/run.py
index 3ed799ca8330f2c17585d8959da448c3b9ccf6eb..cf75fc505ce80e7db8046133970061fdca4ead90 100644
--- a/fsl/utils/run.py
+++ b/fsl/utils/run.py
@@ -151,6 +151,10 @@ def run(*args, **kwargs):
                    the :func:`.fslsub.submit` function.  May also be a
                    dictionary containing arguments to that function.
 
+    :arg cmdonly:  Defaults to ``False``. If ``True``, the command is not
+                   executed, but rather is returned directly, as a list of
+                   arguments.
+
     :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:
@@ -181,6 +185,7 @@ def run(*args, **kwargs):
     returnStderr   = kwargs.pop('stderr',   False)
     returnExitcode = kwargs.pop('exitcode', False)
     submit         = kwargs.pop('submit',   {})
+    cmdonly        = kwargs.pop('cmdonly',  False)
     log            = kwargs.pop('log',      None)
     args           = prepareArgs(args)
 
@@ -207,6 +212,9 @@ def run(*args, **kwargs):
         raise ValueError('submit must be a mapping containing '
                          'options for fsl.utils.fslsub.submit')
 
+    if cmdonly:
+        return args
+
     if DRY_RUN:
         return _dryrun(
             submit, returnStdout, returnStderr, returnExitcode, *args)
diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py
index 7a58c02317928c83dea9c9e7c899de0f666ff78d..2bfbf500c69dcef76c1e3b0a3a2de6fb8a043eb1 100644
--- a/fsl/wrappers/wrapperutils.py
+++ b/fsl/wrappers/wrapperutils.py
@@ -149,46 +149,73 @@ def _unwrap(func):
     return func
 
 
-def cmdwrapper(func):
-    """This decorator can be used on functions which generate a command line.
-    It will pass the return value of the function to the
-    :func:`fsl.utils.run.run` function in a standardised manner.
+def genxwrapper(func, runner):
+    """This function is used by :func:`cmdwrapper` and :func:`fslwrapper`.
+    It is not intended to be used in any other circumstances.
+
+    This function generates a wrapper function which calls ``func`` to
+    generate a command-line call, and then uses ``runner`` to invoke that
+    command.
+
+    ``func`` is assumed to be a wrapper function which generates a command-
+    line. ``runner`` is assumed to be Either :func:`.run.run` or
+    :func:`.run.runfsl`.
+
+    The generated wrapper function will pass all of its arguments to ``func``,
+    and will then pass the generated command-line to ``runner``, returning
+    whatever is returned.
+
+    The following keyword arguments will be intercepted by the wrapper
+    function, and will *not* be passed to ``func``:
+      - ``stdout``:   Passed to ``runner``. Defaults to ``True``.
+      - ``stderr``:   Passed to ``runner``. Defaults to ``True``.
+      - ``exitcode``: Passed to ``runner``. Defaults to ``False``.
+      - ``submit``:   Passed to ``runner``. Defaults to ``None``.
+      - ``log``:      Passed to ``runner``. Defaults to ``{'tee':True}``.
+      - ``cmdonly``:  Passed to ``runner``. Defaults to ``False``.
+
+    :arg func:   A function which generates a command line.
+    :arg runner: Either :func:`.run.run` or :func:`.run.runfsl`.
     """
+
     def wrapper(*args, **kwargs):
         stdout   = kwargs.pop('stdout',   True)
         stderr   = kwargs.pop('stderr',   True)
         exitcode = kwargs.pop('exitcode', False)
         submit   = kwargs.pop('submit',   None)
+        cmdonly  = kwargs.pop('cmdonly',  False)
         log      = kwargs.pop('log',      {'tee' : True})
         cmd      = func(*args, **kwargs)
-        return run.run(cmd,
-                       stderr=stderr,
-                       log=log,
-                       submit=submit,
-                       stdout=stdout,
-                       exitcode=exitcode)
+
+        return runner(cmd,
+                      stderr=stderr,
+                      log=log,
+                      submit=submit,
+                      cmdonly=cmdonly,
+                      stdout=stdout,
+                      exitcode=exitcode)
+
     return _update_wrapper(wrapper, func)
 
 
+def cmdwrapper(func):
+    """This decorator can be used on functions which generate a command line.
+    It will pass the return value of the function to the
+    :func:`fsl.utils.run.run` function in a standardised manner.
+
+    See the :func:`genxwrapper` function for details.
+    """
+    return genxwrapper(func, run.run)
+
+
 def fslwrapper(func):
     """This decorator can be used on functions which generate a FSL command
     line. It will pass the return value of the function to the
     :func:`fsl.utils.run.runfsl` function in a standardised manner.
+
+    See the :func:`genxwrapper` function for details.
     """
-    def wrapper(*args, **kwargs):
-        stdout   = kwargs.pop('stdout',   True)
-        stderr   = kwargs.pop('stderr',   True)
-        exitcode = kwargs.pop('exitcode', False)
-        submit   = kwargs.pop('submit',   None)
-        log      = kwargs.pop('log',      {'tee' : True})
-        cmd      = func(*args, **kwargs)
-        return run.runfsl(cmd,
-                          stderr=stderr,
-                          log=log,
-                          submit=submit,
-                          stdout=stdout,
-                          exitcode=exitcode)
-    return _update_wrapper(wrapper, func)
+    return genxwrapper(func, run.runfsl)
 
 
 SHOW_IF_TRUE = object()
diff --git a/tests/test_run.py b/tests/test_run.py
index 363d4489ec30d25799e152f23e96becf12f06ab9..7308b849c72f6f82afd00eb417c4d479b58d0f30 100644
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -179,6 +179,13 @@ def test_run_passthrough():
         assert run.run('./script.sh', env=env) == expstdout
 
 
+def test_cmdonly():
+    assert run.run('script.sh',        cmdonly=True) == ['script.sh']
+    assert run.run('script.sh 1 2 3',  cmdonly=True) == ['script.sh', '1', '2', '3']
+    assert run.run(['script.sh'],      cmdonly=True) == ['script.sh']
+    assert run.run(['script.sh', '1'], cmdonly=True) == ['script.sh', '1']
+
+
 def test_dryrun():
 
     test_script = textwrap.dedent("""
diff --git a/tests/test_wrappers/test_wrapperutils.py b/tests/test_wrappers/test_wrapperutils.py
index e928057ff858a37b030f149782cf33400fe54e02..d19e3ed13374217fb4d7e393ee5d87ccb5327802 100644
--- a/tests/test_wrappers/test_wrapperutils.py
+++ b/tests/test_wrappers/test_wrapperutils.py
@@ -753,15 +753,20 @@ def test_cmdwrapper():
     with run.dryrun():
         assert func(1, 2)[0] == 'func 1 2'
 
+    assert func(1, 2, cmdonly=True) == ['func', '1', '2']
+
 
 def test_fslwrapper():
     @wutils.fslwrapper
     def func(a, b):
         return ['func', str(a), str(b)]
 
-    with run.dryrun(), mockFSLDIR(bin=('func',)) as fsldir:
+    with mockFSLDIR(bin=('func',)) as fsldir:
         expected = '{} 1 2'.format(op.join(fsldir, 'bin', 'func'))
-        assert func(1, 2)[0] == expected
+        with run.dryrun():
+            assert func(1, 2)[0] == expected
+
+        func(1, 2, cmdonly=True)[0] == list(shlex.split(expected))
 
 
 _test_script = textwrap.dedent("""