diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f85840ef56f69a20c2fbdd3c2f80466358e97a98..c6e789223f5e5b398572c612185b0a967266f8dc 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,20 @@ This document contains the ``fslpy`` release history in reverse chronological
 order.
 
 
+3.3.3 (Wednesday 13th October 2020)
+-----------------------------------
+
+
+Changed
+^^^^^^^
+
+
+* The :func:`.fileOrImage` (and related) decorators will not manipulate the
+  return value of a decorated function if an argument ``cmdonly=True`` is
+  passed. This is so that wrapper functions will directly return the command
+  that would be executed when ``cmdonly=True``.
+
+
 3.3.2 (Tuesday 12th October 2020)
 ---------------------------------
 
diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py
index 2bfbf500c69dcef76c1e3b0a3a2de6fb8a043eb1..51f269bcbdf3f700c17145b14cf3a5aa67dbf246 100644
--- a/fsl/wrappers/wrapperutils.py
+++ b/fsl/wrappers/wrapperutils.py
@@ -482,25 +482,27 @@ class FileOrThing(object):
     the dictionary.
 
 
-    **Cluster submission**
+    **Exceptions**
 
 
-    The above description holds in all situations, except when an argument
-    called ``submit`` is passed, and is set to a value which evaluates to
-    ``True``. In this case, the ``FileOrThing`` decorator will pass all
-    arguments straight through to the decorated function, and will return its
-    return value unchanged.
-
+    The above description holds in all situations, except when arguments called
+    ``submit`` and/or ``cmdonly`` are passed, and are set to values which
+    evaluate to ``True``. In this case, the ``FileOrThing`` decorator will pass
+    all arguments straight through to the decorated function, and will return
+    its return value unchanged.
 
     This is because most functions that are decorated with the
     :func:`fileOrImage` or :func:`fileOrArray` decorators will invoke a call
-    to :func:`.run.run` or :func:`.runfsl`, where a value of ``submit=True``
-    will cause the command to be executed asynchronously on a cluster
-    platform.
+    to :func:`.run.run` or :func:`.runfsl`, where:
 
+      - a value of ``submit=True`` will cause the command to be executed
+        asynchronously on a cluster platform.
+      - a value of ``cmdonly=True`` will cause the command to *not* be executed,
+        but instead the command that would have been executed is returned.
 
     A :exc:`ValueError` will be raised if the decorated function is called
-    with ``submit=True``, and with any in-memory objects or ``LOAD`` symbols.
+    with ``submit=True`` and/or ``cmdonly=True``, and with any in-memory
+    objects or ``LOAD`` symbols.
 
 
     **Example**
@@ -684,9 +686,11 @@ class FileOrThing(object):
 
         # Special case - if fsl.utils.run[fsl] is
         # being decorated (e.g. via cmdwrapper/
-        # fslwrapper), and submit=True, this call
-        # will ultimately submit the job to the
-        # cluster, and will return immediately.
+        # fslwrapper), and submit=True or
+        # cmdonly=True, this call will ultimately
+        # submit the job to the cluster, or will
+        # return the command that would have been
+        # executed, and will return immediately.
         #
         # We error if we are given any in-memory
         # things, or LOAD symbols.
@@ -694,7 +698,8 @@ class FileOrThing(object):
         # n.b. testing values to be strings could
         # interfere with the fileOrText decorator.
         # Possible solution is to use pathlib?
-        if kwargs.get('submit', False):
+        if kwargs.get('submit',  False) or \
+           kwargs.get('cmdonly', False):
             allargs = {**dict(zip(argnames, args)), **kwargs}
             for name, val in allargs.items():
                 if (name in self.__things) and \
diff --git a/tests/test_wrappers/test_wrapperutils.py b/tests/test_wrappers/test_wrapperutils.py
index d19e3ed13374217fb4d7e393ee5d87ccb5327802..7e2116d1d3c3a7d5cf4c457fff4d84447b31de93 100644
--- a/tests/test_wrappers/test_wrapperutils.py
+++ b/tests/test_wrappers/test_wrapperutils.py
@@ -714,13 +714,15 @@ def test_fileOrThing_chained_outprefix():
         assert np.all(res['out_array'] == exparr)
 
 
-def test_fileOrThing_submit():
+def test_fileOrThing_submit_cmdonly():
 
     @wutils.fileOrImage('input', 'output')
-    def func(input, output, submit=False):
+    def func(input, output, submit=False, cmdonly=False):
 
         if submit:
             return 'submitted!'
+        if cmdonly:
+            return 'cmdonly!'
 
         img = nib.load(input)
         img = nib.nifti1.Nifti1Image(np.asanyarray(img.dataobj) * 2, np.eye(4))
@@ -735,7 +737,8 @@ def test_fileOrThing_submit():
         result = func(img, wutils.LOAD)
         assert np.all(np.asanyarray(result['output'].dataobj) == exp)
 
-        assert func('input.nii.gz', 'output.nii.gz', submit=True) == 'submitted!'
+        assert func('input.nii.gz', 'output.nii.gz', submit=True)  == 'submitted!'
+        assert func('input.nii.gz', 'output.nii.gz', cmdonly=True) == 'cmdonly!'
 
         with pytest.raises(ValueError):
             func(img, wutils.LOAD, submit=True)
@@ -766,7 +769,7 @@ def test_fslwrapper():
         with run.dryrun():
             assert func(1, 2)[0] == expected
 
-        func(1, 2, cmdonly=True)[0] == list(shlex.split(expected))
+        assert func(1, 2, cmdonly=True) == list(shlex.split(expected))
 
 
 _test_script = textwrap.dedent("""
@@ -840,3 +843,21 @@ def test_fslwrapper_submit():
 
         assert stdout.strip() == 'test_script running: 1 2'
         assert stderr.strip() == experr
+
+
+@pytest.mark.unixtest
+def test_cmdwrapper_fileorthing_cmdonly():
+
+    test_func = wutils.fileOrImage('a')(wutils.cmdwrapper(_test_script_func))
+    newpath   = op.pathsep.join(('.', os.environ['PATH']))
+    with tempdir.tempdir(), \
+         mock.patch.dict(os.environ, {'PATH' : newpath}):
+
+        with open('test_script', 'wt') as f:
+            f.write(_test_script)
+        os.chmod('test_script', 0o755)
+
+        ran = test_func('1', '2')
+        cmd = test_func('1', '2', cmdonly=True)
+        assert ran.stdout[0].strip() == 'test_script running: 1 2'
+        assert cmd                   == ['test_script', '1', '2']