diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 7f46e780467d7fd2a594e3b50ed942c316807919..96f59e61235dd6bc3b8a775e1a3ca01b2c19cd2e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,25 @@ This document contains the ``fslpy`` release history in reverse chronological
 order.
 
 
+3.3.0 (Under development)
+-------------------------
+
+
+Added
+^^^^^
+
+* New ported versions of various core FSL tools, including ``imrm``, ``imln``,
+  ``imtest``, ``fsl_abspath``, ``remove_ext``, ``Text2Vest``, and
+  ``Vest2Text``.
+
+
+Changed
+^^^^^^^
+
+
+* Adjustments to tests and documentation
+
+
 3.2.2 (Thursday 9th July 2020)
 ------------------------------
 
diff --git a/fsl/data/image.py b/fsl/data/image.py
index ce01645b4c544f263c0ea05cd2cd38be7fa0f604..7c2bdbcba722e8f0017ce0bba46269283f8e4d8b 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -1625,18 +1625,20 @@ def removeExt(filename):
     return fslpath.removeExt(filename, ALLOWED_EXTENSIONS)
 
 
-def fixExt(filename):
+def fixExt(filename, **kwargs):
     """Fix the extension of ``filename``.
 
     For example, if a file name is passed in as ``file.nii.gz``, but the
     file is actually ``file.nii``, this function will fix the file name.
 
     If ``filename`` already exists, it is returned unchanged.
+
+    All other arguments are passed through to :func:`addExt`.
     """
     if op.exists(filename):
         return filename
     else:
-        return addExt(removeExt(filename))
+        return addExt(removeExt(filename), **kwargs)
 
 
 def defaultExt():
diff --git a/fsl/data/vest.py b/fsl/data/vest.py
index b0b33c73fa41ab045c849d079f52d73d40baccf6..9fe10c103ad8dc278d4ec4b163883803ecc6830f 100644
--- a/fsl/data/vest.py
+++ b/fsl/data/vest.py
@@ -11,9 +11,14 @@
 
    looksLikeVestLutFile
    loadVestLutFile
+   loadVestFile
+   generateVest
 """
 
 
+import textwrap as tw
+import             io
+
 import numpy as np
 
 
@@ -76,3 +81,70 @@ def loadVestLutFile(path, normalise=True):
 
     else:
         return colours
+
+
+def loadVestFile(path, ignoreHeader=True):
+    """Loads numeric data from a VEST file, returning it as a ``numpy`` array.
+
+    :arg ignoreHeader: if ``True`` (the default), the matrix shape specified
+                       in the VEST header information is ignored, and the shape
+                       inferred from the data. Otherwise, if the number of
+                       rows/columns specified in the VEST header information
+                       does not match the matrix shape, a ``ValueError`` is
+                       raised.
+
+    :returns:          a ``numpy`` array containing the matrix data in the
+                       VEST file.
+    """
+
+    data = np.loadtxt(path, comments=['#', '/'])
+
+    if not ignoreHeader:
+        nrows, ncols = None, None
+        with open(path, 'rt') as f:
+            for line in f:
+                if   'NumWaves'  in line: ncols = int(line.split()[1])
+                elif 'NumPoints' in line: nrows = int(line.split()[1])
+                else: continue
+
+                if (ncols is not None) and (nrows is not None):
+                    break
+
+        if tuple(data.shape) != (nrows, ncols):
+            raise ValueError(f'Invalid VEST file ({path}) - data shape '
+                             f'({data.shape}) does not match header '
+                             f'({nrows}, {ncols})')
+
+    return data
+
+
+def generateVest(data):
+    """Generates VEST-formatted text for the given ``numpy`` array.
+
+    :arg data: A 1D or 2D numpy array.
+    :returns:  A string containing a VEST header, and the ``data``.
+    """
+
+    data = np.asanyarray(data)
+
+    if len(data.shape) not in (1, 2):
+        raise ValueError(f'unsupported number of dimensions: {data.shape}')
+
+    data = np.atleast_2d(data)
+
+    if np.issubdtype(data.dtype, np.integer): fmt = '%d'
+    else:                                     fmt = '%0.12f'
+
+    sdata = io.StringIO()
+    np.savetxt(sdata, data, fmt=fmt)
+    sdata = sdata.getvalue()
+
+    nrows, ncols = data.shape
+
+    vest = tw.dedent(f"""
+    /NumWaves {ncols}
+    /NumPoints {nrows}
+    /Matrix
+    """).strip() + '\n' + sdata
+
+    return vest.strip()
diff --git a/fsl/scripts/Text2Vest.py b/fsl/scripts/Text2Vest.py
new file mode 100644
index 0000000000000000000000000000000000000000..15989f945f0a39ef73163e9c6840eff893f2e5c7
--- /dev/null
+++ b/fsl/scripts/Text2Vest.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+# Text2Vest.py - Convert an ASCII text matrix file into a VEST file.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""``Text2Vest`` simply takes a plain text ASCII text matrix file, and
+adds a VEST header.
+"""
+
+
+import sys
+
+import numpy as np
+
+import fsl.data.vest as fslvest
+
+
+usage = "Usage: Text2Vest <text_file> <vest_file>"
+
+
+def main(argv=None):
+    """Convert a plain text file to a VEST file. """
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    if len(argv) != 2:
+        print(usage)
+        return 0
+
+    infile, outfile = argv
+    data            = np.loadtxt(infile)
+    vest            = fslvest.generateVest(data)
+
+    with open(outfile, 'wt') as f:
+        f.write(vest)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/fsl/scripts/Vest2Text.py b/fsl/scripts/Vest2Text.py
new file mode 100644
index 0000000000000000000000000000000000000000..b12d8166a32a8ffd8eb2de33ada2e886ba9f8b39
--- /dev/null
+++ b/fsl/scripts/Vest2Text.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+# Vest2Text.py - Convert a VEST matrix file into a plain text ASCII file.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""``Vest2Text`` takes a VEST file containing a 2D matrix, and converts it
+into a plain-text ASCII file.
+"""
+
+
+import sys
+
+import numpy as np
+
+import fsl.data.vest as fslvest
+
+
+usage = "Usage: Vest2Text <vest_file> <text_file>"
+
+
+def main(argv=None):
+    """Convert a VEST file to a plain text file. """
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    if len(argv) != 2:
+        print(usage)
+        return 0
+
+    infile, outfile = argv
+    data            = fslvest.loadVestFile(infile)
+
+    if np.issubdtype(data.dtype, np.integer): fmt = '%d'
+    else:                                     fmt = '%0.12f'
+
+    np.savetxt(outfile, data, fmt=fmt)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/fsl/scripts/__init__.py b/fsl/scripts/__init__.py
index 4ef1e04becbabc6280ce9106f84d943d0e8e923b..499b221767c7e66e8ff8bac70c7cc1ff14cf5120 100644
--- a/fsl/scripts/__init__.py
+++ b/fsl/scripts/__init__.py
@@ -5,5 +5,12 @@
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 """The ``fsl.scripts`` package contains all of the executable scripts provided
-by ``fslpy``.
+by ``fslpy``, and other python-based FSL packages.
+
+.. note:: The ``fsl.scripts`` namespace is a ``pkgutil``-style *namespace
+          package* - it can be used across different projects - see
+          https://packaging.python.org/guides/packaging-namespace-packages/ for
+          details.
 """
+
+__path__ = __import__('pkgutil').extend_path(__path__, __name__)  # noqa
diff --git a/fsl/scripts/fsl_abspath.py b/fsl/scripts/fsl_abspath.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed4775874326abda6c6d3881892d6988a006f2a4
--- /dev/null
+++ b/fsl/scripts/fsl_abspath.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+#
+# fsl_abspath.py - Make a path absolute
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""The fsl_abspath command - makes relative paths absolute.
+"""
+
+
+import os.path as op
+import            sys
+
+
+usage = """
+usage: fsl_abspath path
+""".strip()
+
+
+def main(argv=None):
+    """fsl_abspath - make a relative path absolute. """
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    if len(argv) != 1:
+        print(usage)
+        return 1
+
+    print(op.realpath(argv[0]))
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/fsl/scripts/fsl_ents.py b/fsl/scripts/fsl_ents.py
index eb78ed83ebb585787d5864c67aac98b7b3786296..1590c9b4491865c7256a3ccb87a27c047b88b772 100644
--- a/fsl/scripts/fsl_ents.py
+++ b/fsl/scripts/fsl_ents.py
@@ -9,8 +9,6 @@ time series from a MELODIC ``.ica`` directory.
 """
 
 
-from __future__ import print_function
-
 import os.path as op
 import            sys
 import            argparse
diff --git a/fsl/scripts/imcp.py b/fsl/scripts/imcp.py
index 81f53a83d5fdd682c80f41a208641c3273a8985d..d2fcc1eda66a6c01f43bbdb246ce46ade699e82f 100755
--- a/fsl/scripts/imcp.py
+++ b/fsl/scripts/imcp.py
@@ -12,8 +12,6 @@ The :func:`main` function is essentially a wrapper around the
 """
 
 
-from __future__ import print_function
-
 import os.path        as op
 import                   sys
 import                   warnings
diff --git a/fsl/scripts/imglob.py b/fsl/scripts/imglob.py
index ed85e4120d2e9024f2848e9be28eab43ce18e068..1e756dee6f79d5e3f4a5f087ba355c5a795771bd 100644
--- a/fsl/scripts/imglob.py
+++ b/fsl/scripts/imglob.py
@@ -9,8 +9,6 @@ NIFTI/ANALYZE image files.
 """
 
 
-from __future__ import print_function
-
 import                   sys
 import                   warnings
 import fsl.utils.path as fslpath
diff --git a/fsl/scripts/imln.py b/fsl/scripts/imln.py
new file mode 100644
index 0000000000000000000000000000000000000000..88955f3cb36300853f58f276f6e4dedb6ffda18e
--- /dev/null
+++ b/fsl/scripts/imln.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+#
+# imln.py - Create symbolic links to image files.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This module defines the ``imln`` application, for creating sym-links
+to NIFTI image files.
+
+.. note:: When creating links to relative paths, ln requires that the path is
+          relative to the link location, rather than the invocation
+          location. This is *not* currently supported by imln, and possibly
+          never will be.
+"""
+
+
+import os.path        as op
+import                   os
+import                   sys
+import                   warnings
+
+import fsl.utils.path as fslpath
+
+
+# See atlasq.py for explanation
+with warnings.catch_warnings():
+    warnings.filterwarnings("ignore", category=FutureWarning)
+    import fsl.data.image as fslimage
+
+
+ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz']
+"""List of file extensions that are supported by ``imln``. """
+
+
+usage = """
+Usage: imln <file1> <file2>
+  Makes a link (called file2) to file1
+  NB: filenames can be basenames or include an extension
+""".strip()
+
+
+def main(argv=None):
+    """``imln`` - create sym-links to images. """
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    if len(argv) != 2:
+        print(usage)
+        return 1
+
+    target, linkbase = argv
+    target           = fslpath.removeExt(target,   ALLOWED_EXTENSIONS)
+    linkbase         = fslpath.removeExt(linkbase, ALLOWED_EXTENSIONS)
+
+    # Target must exist, so we can
+    # infer the correct extension(s).
+    # Error on incomplete file groups
+    # (e.g. a.img without a.hdr).
+    try:
+        targets = fslpath.getFileGroup(target,
+                                       allowedExts=ALLOWED_EXTENSIONS,
+                                       fileGroups=fslimage.FILE_GROUPS,
+                                       unambiguous=True)
+    except Exception as e:
+        print(f'Error: {e}')
+        return 1
+
+    for target in targets:
+        if not op.exists(target):
+            continue
+
+        ext  = fslpath.getExt(target, ALLOWED_EXTENSIONS)
+        link = f'{linkbase}{ext}'
+
+        try:
+
+            # emulate old imln behaviour - if
+            # link already exists, it is removed
+            if op.exists(link):
+                os.remove(link)
+
+            os.symlink(target, link)
+
+        except Exception as e:
+            print(f'Error: {e}')
+            return 1
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/fsl/scripts/immv.py b/fsl/scripts/immv.py
index c25a99e04c535f3cd26690f5600213e4d3d23ebe..febe422747c27558c98cf6243816980837aaa603 100755
--- a/fsl/scripts/immv.py
+++ b/fsl/scripts/immv.py
@@ -13,8 +13,6 @@ The :func:`main` function is essentially a wrapper around the
 """
 
 
-from __future__ import print_function
-
 import os.path        as op
 import                   sys
 import                   warnings
diff --git a/fsl/scripts/imrm.py b/fsl/scripts/imrm.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd81103325b6d8dea25f0b88f02c3bbb40555f6c
--- /dev/null
+++ b/fsl/scripts/imrm.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+#
+# imrm.py - Remove image files.
+#
+# Author: Paul McCarthy <paulmc@fmrib.ox.ac.uk>
+#
+"""This module defines the ``imrm`` application, for removing NIFTI image
+files.
+"""
+
+
+import itertools      as it
+import os.path        as op
+import                   os
+import                   sys
+import                   warnings
+
+import fsl.utils.path as fslpath
+
+# See atlasq.py for explanation
+with warnings.catch_warnings():
+    warnings.filterwarnings("ignore", category=FutureWarning)
+    import fsl.data.image as fslimage
+
+
+usage = """Usage: imrm <list of image names to remove>
+NB: filenames can be basenames or not
+""".strip()
+
+
+ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz']
+"""List of file extensions that are removed by ``imrm``. """
+
+
+def main(argv=None):
+    """Removes all images which are specified on the command line. """
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    if len(argv) < 1:
+        print(usage)
+        return 1
+
+    prefixes = [fslpath.removeExt(p, ALLOWED_EXTENSIONS) for p in argv]
+
+    for prefix, ext in it.product(prefixes, ALLOWED_EXTENSIONS):
+
+        path = f'{prefix}{ext}'
+
+        if op.exists(path):
+            os.remove(path)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/fsl/scripts/imtest.py b/fsl/scripts/imtest.py
new file mode 100644
index 0000000000000000000000000000000000000000..f03c28442b52ed79f535b12aae444e310cf83bc3
--- /dev/null
+++ b/fsl/scripts/imtest.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+#
+# imtest.py - Test whether an image file exists or not.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""The ``imtest`` script can be used to test whether an image file exists or
+not, without having to know the file suffix (.nii, .nii.gz, etc).
+"""
+
+
+import os.path        as op
+import                   sys
+import                   warnings
+
+import fsl.utils.path as fslpath
+
+# See atlasq.py for explanation
+with warnings.catch_warnings():
+    warnings.filterwarnings("ignore", category=FutureWarning)
+    import fsl.data.image as fslimage
+
+
+ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz']
+"""List of file extensions that are supported by ``imln``. """
+
+
+def main(argv=None):
+    """Test if an image path exists, and prints ``'1'`` if it does or ``'0'``
+    if it doesn't.
+    """
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    # emulate old fslio/imtest - always return 0
+    if len(argv) != 1:
+        print('0')
+        return 0
+
+    path = fslpath.removeExt(argv[0], ALLOWED_EXTENSIONS)
+    path = op.realpath(path)
+
+    # getFileGroup will raise an error
+    # if the image (including all
+    # components - i.e. header and
+    # image) does not exist
+    try:
+        fslpath.getFileGroup(path,
+                             allowedExts=ALLOWED_EXTENSIONS,
+                             fileGroups=fslimage.FILE_GROUPS,
+                             unambiguous=True)
+        print('1')
+    except fslpath.PathError:
+        print('0')
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/fsl/scripts/remove_ext.py b/fsl/scripts/remove_ext.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc21094ff113576cc808f25fc3dbde1b7f38ff4f
--- /dev/null
+++ b/fsl/scripts/remove_ext.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+#
+# remove_ext.py - Remove file extensions from NIFTI image paths
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+
+import sys
+import warnings
+
+import fsl.utils.path as fslpath
+
+# See atlasq.py for explanation
+with warnings.catch_warnings():
+    warnings.filterwarnings("ignore", category=FutureWarning)
+    import fsl.data.image as fslimage
+
+
+usage = """Usage: remove_ext <list of image paths to remove extension from>
+""".strip()
+
+
+ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz']
+"""List of file extensions that are removed by ``remove_ext``. """
+
+
+def main(argv=None):
+    """Removes file extensions from all paths which are specified on the
+    command line.
+    """
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    if len(argv) < 1:
+        print(usage)
+        return 1
+
+    removed = []
+
+    for path in argv:
+        removed.append(fslpath.removeExt(path, ALLOWED_EXTENSIONS))
+
+    print(' '.join(removed))
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/setup.py b/setup.py
index eee141bd0972ba372349a7d1037d076d7b9dd021..1a971bf2a3eb5facc9b5b4ca56168ee8aa9b485d 100644
--- a/setup.py
+++ b/setup.py
@@ -118,9 +118,16 @@ setup(
 
     entry_points={
         'console_scripts' : [
-            'immv           = fsl.scripts.immv:main',
             'imcp           = fsl.scripts.imcp:main',
+            'imln           = fsl.scripts.imln:main',
+            'immv           = fsl.scripts.immv:main',
+            'imrm           = fsl.scripts.imrm:main',
             'imglob         = fsl.scripts.imglob:main',
+            'imtest         = fsl.scripts.imtest:main',
+            'remove_ext     = fsl.scripts.remove_ext:main',
+            'fsl_abspath    = fsl.scripts.fsl_abspath:main',
+            'Text2Vest      = fsl.scripts.Text2Vest:main',
+            'Vest2Text      = fsl.scripts.Vest2Text:main',
             'atlasq         = fsl.scripts.atlasq:main',
             'atlasquery     = fsl.scripts.atlasq:atlasquery_emulation',
             'fsl_ents       = fsl.scripts.fsl_ents:main',
diff --git a/tests/test_imglob.py b/tests/test_imglob.py
deleted file mode 100644
index 9a8e838d1321fd45c91c1d94188700067423097f..0000000000000000000000000000000000000000
--- a/tests/test_imglob.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env python
-#
-# test_imglob.py -
-#
-# Author: Paul McCarthy <pauldmccarthy@gmail.com>
-#
-
-import pytest
-
-import fsl.scripts.imglob as imglob
-
-from . import testdir
-
-
-def test_imglob_shouldPass():
-
-    # (files to create, paths, output, expected)
-    tests = [
-
-        # normal usage, one file
-        ('file.hdr file.img', 'file',  None,     'file'),
-        ('file.hdr file.img', 'file', 'prefix',  'file'),
-        ('file.hdr file.img', 'file', 'primary', 'file.hdr'),
-        ('file.hdr file.img', 'file', 'all',     'file.hdr file.img'),
-
-        # incomplete file pair
-        ('file.hdr', 'file',               'prefix',  'file'),
-        ('file.hdr', 'file.hdr',           'prefix',  'file'),
-        ('file.hdr', 'file.img',           'prefix',  'file'),
-        ('file.hdr', 'file',               'primary', 'file.hdr'),
-        ('file.hdr', 'file.hdr',           'primary', 'file.hdr'),
-        ('file.hdr', 'file.img',           'primary', 'file.hdr'),
-        ('file.hdr', 'file',               'all',     'file.hdr'),
-        ('file.hdr', 'file.hdr',           'all',     'file.hdr'),
-        ('file.hdr', 'file.img',           'all',     'file.hdr'),
-
-        # same file specified multiple times
-        ('file.hdr file.img', 'file file',              'prefix',  'file'),
-        ('file.hdr file.img', 'file file.img',          'prefix',  'file'),
-        ('file.hdr file.img', 'file file.img file.hdr', 'prefix',  'file'),
-        ('file.hdr file.img', 'file file',              'primary', 'file.hdr'),
-        ('file.hdr file.img', 'file file.img',          'primary', 'file.hdr'),
-        ('file.hdr file.img', 'file file.img file.hdr', 'primary', 'file.hdr'),
-        ('file.hdr file.img', 'file file',              'all',     'file.hdr file.img'),
-        ('file.hdr file.img', 'file file.img',          'all',     'file.hdr file.img'),
-        ('file.hdr file.img', 'file file.img file.hdr', 'all',     'file.hdr file.img'),
-
-        # multiple files same prefix
-        ('file.hdr file.img file.nii', 'file', 'prefix',  'file'),
-        ('file.hdr file.img file.nii', 'file', 'primary', 'file.hdr file.nii'),
-        ('file.hdr file.img file.nii', 'file', 'all',     'file.hdr file.img file.nii'),
-
-        # multiple files
-        ('file1.hdr file1.img file2.nii', 'file1', 'prefix',  'file1'),
-        ('file1.hdr file1.img file2.nii', 'file1', 'primary', 'file1.hdr'),
-        ('file1.hdr file1.img file2.nii', 'file1', 'all',     'file1.hdr file1.img'),
-
-        ('file1.hdr file1.img file2.nii', 'file1 file2', 'prefix',  'file1 file2'),
-        ('file1.hdr file1.img file2.nii', 'file1 file2', 'primary', 'file1.hdr file2.nii'),
-        ('file1.hdr file1.img file2.nii', 'file1 file2', 'all',     'file1.hdr file1.img file2.nii'),
-
-        # no file
-        ('file.nii', 'bag', 'prefix',  ''),
-        ('file.nii', 'bag', 'primary', ''),
-        ('file.nii', 'bag', 'all',     ''),
-
-        # incomplete prefix
-        ('file.nii', 'fi', 'prefix',  ''),
-        ('file.nii', 'fi', 'primary', ''),
-        ('file.nii', 'fi', 'all',     ''),
-    ]
-
-
-    for to_create, paths, output, expected in tests:
-        with testdir(to_create.split()) as td:
-
-            paths    = paths.split()
-            expected = expected.split()
-            result   = imglob.imglob(paths, output)
-
-            assert sorted(result) == sorted(expected)
-
-
-def test_imglob_shouldFail():
-
-    with pytest.raises(ValueError):
-        imglob.imglob([], 'bag')
diff --git a/tests/test_scripts/test_fsl_abspath.py b/tests/test_scripts/test_fsl_abspath.py
new file mode 100644
index 0000000000000000000000000000000000000000..1657128978f003dc551789e00d4f24f161c797ae
--- /dev/null
+++ b/tests/test_scripts/test_fsl_abspath.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+#
+# test_fsl_abspath.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+
+import os.path as op
+
+import fsl.scripts.fsl_abspath as fsl_abspath
+
+from tests import tempdir, CaptureStdout
+
+
+def test_usage():
+    assert fsl_abspath.main([]) != 0
+
+
+def test_fsl_abspath():
+
+    # fsl_abspath just calls os.path.realpath
+
+    with tempdir() as td:
+
+        oneup = op.dirname(td)
+
+        # (input, expected)
+        tests = [
+            ('file',    f'{td}/file'),
+            ('./file',  f'{td}/file'),
+            ('../file', f'{oneup}/file'),
+
+            ('/file',         '/file'),
+            ('/one/two/file', '/one/two/file'),
+        ]
+
+        for input, expected in tests:
+            cap = CaptureStdout()
+
+            with cap:
+                ret = fsl_abspath.main([input])
+
+            assert ret == 0
+            assert cap.stdout.strip() == expected
diff --git a/tests/test_scripts/test_imglob.py b/tests/test_scripts/test_imglob.py
index 0f8e934509ce68359d17a8a8794bbaaa47f1a691..5ce2c2bc517dfcc00fa5c4be69886fd360de570e 100644
--- a/tests/test_scripts/test_imglob.py
+++ b/tests/test_scripts/test_imglob.py
@@ -1,6 +1,9 @@
 #!/usr/bin/env python
 
-import mock
+
+from unittest import mock
+
+import pytest
 
 import fsl.scripts.imglob as imglob
 
@@ -8,7 +11,7 @@ from .. import testdir
 from .. import CaptureStdout
 
 
-def test_imglob_script_shouldPass():
+def test_imglob_shouldPass1():
 
     # (files to create, args, expected)
     tests = [
@@ -50,8 +53,81 @@ def test_imglob_script_shouldPass():
             assert capture.stdout.strip().split() == expected.split()
 
 
+def test_imglob_shouldPass2():
+
+    # (files to create, paths, output, expected)
+    tests = [
+
+        # normal usage, one file
+        ('file.hdr file.img', 'file',  None,     'file'),
+        ('file.hdr file.img', 'file', 'prefix',  'file'),
+        ('file.hdr file.img', 'file', 'primary', 'file.hdr'),
+        ('file.hdr file.img', 'file', 'all',     'file.hdr file.img'),
+
+        # incomplete file pair
+        ('file.hdr', 'file',               'prefix',  'file'),
+        ('file.hdr', 'file.hdr',           'prefix',  'file'),
+        ('file.hdr', 'file.img',           'prefix',  'file'),
+        ('file.hdr', 'file',               'primary', 'file.hdr'),
+        ('file.hdr', 'file.hdr',           'primary', 'file.hdr'),
+        ('file.hdr', 'file.img',           'primary', 'file.hdr'),
+        ('file.hdr', 'file',               'all',     'file.hdr'),
+        ('file.hdr', 'file.hdr',           'all',     'file.hdr'),
+        ('file.hdr', 'file.img',           'all',     'file.hdr'),
+
+        # same file specified multiple times
+        ('file.hdr file.img', 'file file',              'prefix',  'file'),
+        ('file.hdr file.img', 'file file.img',          'prefix',  'file'),
+        ('file.hdr file.img', 'file file.img file.hdr', 'prefix',  'file'),
+        ('file.hdr file.img', 'file file',              'primary', 'file.hdr'),
+        ('file.hdr file.img', 'file file.img',          'primary', 'file.hdr'),
+        ('file.hdr file.img', 'file file.img file.hdr', 'primary', 'file.hdr'),
+        ('file.hdr file.img', 'file file',              'all',     'file.hdr file.img'),
+        ('file.hdr file.img', 'file file.img',          'all',     'file.hdr file.img'),
+        ('file.hdr file.img', 'file file.img file.hdr', 'all',     'file.hdr file.img'),
+
+        # multiple files same prefix
+        ('file.hdr file.img file.nii', 'file', 'prefix',  'file'),
+        ('file.hdr file.img file.nii', 'file', 'primary', 'file.hdr file.nii'),
+        ('file.hdr file.img file.nii', 'file', 'all',     'file.hdr file.img file.nii'),
+
+        # multiple files
+        ('file1.hdr file1.img file2.nii', 'file1', 'prefix',  'file1'),
+        ('file1.hdr file1.img file2.nii', 'file1', 'primary', 'file1.hdr'),
+        ('file1.hdr file1.img file2.nii', 'file1', 'all',     'file1.hdr file1.img'),
+
+        ('file1.hdr file1.img file2.nii', 'file1 file2', 'prefix',  'file1 file2'),
+        ('file1.hdr file1.img file2.nii', 'file1 file2', 'primary', 'file1.hdr file2.nii'),
+        ('file1.hdr file1.img file2.nii', 'file1 file2', 'all',     'file1.hdr file1.img file2.nii'),
+
+        # no file
+        ('file.nii', 'bag', 'prefix',  ''),
+        ('file.nii', 'bag', 'primary', ''),
+        ('file.nii', 'bag', 'all',     ''),
+
+        # incomplete prefix
+        ('file.nii', 'fi', 'prefix',  ''),
+        ('file.nii', 'fi', 'primary', ''),
+        ('file.nii', 'fi', 'all',     ''),
+    ]
+
+
+    for to_create, paths, output, expected in tests:
+        with testdir(to_create.split()) as td:
+
+            paths    = paths.split()
+            expected = expected.split()
+            result   = imglob.imglob(paths, output)
+
+            assert sorted(result) == sorted(expected)
+
+
+
 def test_imglob_script_shouldFail():
 
+    with pytest.raises(ValueError):
+        imglob.imglob([], 'bag')
+
     capture = CaptureStdout()
 
     with capture:
diff --git a/tests/test_scripts/test_imln.py b/tests/test_scripts/test_imln.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9fea4e4fc00f9cf9b94370c801f4fb1d9a4606b
--- /dev/null
+++ b/tests/test_scripts/test_imln.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+#
+# test_imln.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import os
+import os.path as op
+from unittest import mock
+
+import pytest
+
+from   fsl.utils.tempdir import tempdir
+import fsl.utils.path    as     fslpath
+import fsl.scripts.imln  as     imln
+
+from tests import touch
+
+
+def test_usage():
+    assert imln.main([]) != 0
+    with mock.patch('sys.argv', []):
+        assert imln.main() != 0
+
+
+def test_imln():
+
+    # (files, command, expected links)
+    tests = [
+        ('a.nii',    'a.nii    link.nii',    'link.nii'),
+        ('a.nii',    'a        link',        'link.nii'),
+        ('a.nii',    'a.nii    link',        'link.nii'),
+        ('a.nii',    'a        link.nii',    'link.nii'),
+        ('a.nii',    'a        link.nii.gz', 'link.nii'),
+        ('a.nii.gz', 'a.nii.gz link.nii.gz', 'link.nii.gz'),
+        ('a.nii.gz', 'a        link',        'link.nii.gz'),
+        ('a.nii.gz', 'a.nii.gz link',        'link.nii.gz'),
+        ('a.nii.gz', 'a        link.nii.gz', 'link.nii.gz'),
+        ('a.nii.gz', 'a        link.nii',    'link.nii.gz'),
+
+        ('a.img a.hdr', 'a      link',     'link.img link.hdr'),
+        ('a.img a.hdr', 'a      link.img', 'link.img link.hdr'),
+        ('a.img a.hdr', 'a      link.hdr', 'link.img link.hdr'),
+        ('a.img a.hdr', 'a.img  link',     'link.img link.hdr'),
+        ('a.img a.hdr', 'a.hdr  link',     'link.img link.hdr'),
+        ('a.img a.hdr', 'a.img  link.img', 'link.img link.hdr'),
+        ('a.img a.hdr', 'a.hdr  link.hdr', 'link.img link.hdr'),
+        ('a.img a.hdr', 'a.img  link.hdr', 'link.img link.hdr'),
+        ('a.img a.hdr', 'a.hdr  link.img', 'link.img link.hdr'),
+    ]
+
+
+    for files, command, explinks in tests:
+        with tempdir():
+            files    = files.split()
+            command  = command.split()
+            explinks = explinks.split()
+
+            for f in files:
+                touch(f)
+
+            assert imln.main(command) == 0
+
+            assert sorted(os.listdir('.')) == sorted(files + explinks)
+
+            for f, l in zip(sorted(files), sorted(explinks)):
+                assert op.islink(l)
+                assert op.isfile(f) and not op.islink(f)
+                assert op.realpath(l) == op.abspath(f)
+
+
+    # subdirs - imln currently only
+    # works with absolute paths (we
+    # make all paths absolute below)
+    tests = [
+        ('dir/a.nii',           'dir/a dir/link', 'dir/link.nii'),
+        ('dir/a.img dir/a.hdr', 'dir/a dir/link', 'dir/link.img dir/link.hdr'),
+        ('dir/a.nii',           'dir/a link',     'link.nii'),
+        ('dir/a.img dir/a.hdr', 'dir/a link',     'link.img link.hdr'),
+        ('a.nii',               'a     dir/link', 'dir/link.nii'),
+        ('a.img a.hdr',         'a     dir/link', 'dir/link.img dir/link.hdr'),
+    ]
+    for files, command, explinks in tests:
+        with tempdir():
+            files    = files.split()
+            command  = [op.abspath(c) for c in command.split()]
+            explinks = explinks.split()
+
+            os.mkdir('dir')
+
+            for f in files:
+                touch(f)
+
+            assert imln.main(command) == 0
+
+            for f, l in zip(sorted(files), sorted(explinks)):
+                assert op.islink(l)
+                assert op.isfile(f) and not op.islink(f)
+                assert op.realpath(l) == op.abspath(f)
+
+    # error cases
+    # (files, commnad)
+    tests = [
+        ('a.img',             'a link'),
+        ('a.nii a.img a.hdr', 'a link'),
+    ]
+    for files, command in tests:
+        with tempdir():
+            files   = files.split()
+            command = command.split()
+
+            for f in files:
+                touch(f)
+
+            assert imln.main(command) != 0
+            assert sorted(os.listdir('.')) == sorted(files)
diff --git a/tests/test_scripts/test_imrm.py b/tests/test_scripts/test_imrm.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6682400a4d93696b996c5f78884be181ae04016
--- /dev/null
+++ b/tests/test_scripts/test_imrm.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+#
+# test_imrm.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import os
+
+from fsl.utils.tempdir import tempdir
+import fsl.scripts.imrm as imrm
+
+from tests import touch
+
+def test_imrm_usage():
+    assert imrm.main([]) != 0
+
+
+def test_imrm():
+    # (files present, command, expected)
+    tests = [
+        ('a.nii',       'a',       ''),
+        ('a.nii.gz',    'a',       ''),
+        ('a.img a.hdr', 'a',       ''),
+        ('a.img',       'a',       ''),
+        ('a.hdr',       'a',       ''),
+        ('a.nii b.nii', 'a',       'b.nii'),
+        ('a.nii b.nii', 'a b',     ''),
+        ('a.nii b.nii', 'a b.nii', ''),
+
+        # suffix doesn't have to be correct
+        ('a.nii.gz', 'a.nii',  ''),
+
+        # files don't exist -> no problem
+        ('a.nii', 'b',  'a.nii'),
+    ]
+
+    for files, command, expected in tests:
+        with tempdir():
+
+            for f in files.split():
+                touch(f)
+
+            print('files',    files)
+            print('command',  command)
+            print('expected', expected)
+
+            ret = imrm.main(('imrm ' + command).split())
+
+            assert ret == 0
+            assert sorted(os.listdir()) == sorted(expected.split())
diff --git a/tests/test_scripts/test_imtest.py b/tests/test_scripts/test_imtest.py
new file mode 100644
index 0000000000000000000000000000000000000000..819afa959175391d8b6fc44c986fe7b12e2021b3
--- /dev/null
+++ b/tests/test_scripts/test_imtest.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+#
+# test_imtest.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import os
+import os.path as op
+
+import fsl.utils.path     as fslpath
+from   fsl.utils.tempdir  import tempdir
+
+import fsl.scripts.imtest as imtest
+
+from tests import CaptureStdout, touch
+
+
+def test_wrongargs():
+    cap = CaptureStdout()
+    with cap:
+        assert imtest.main([]) == 0
+    assert cap.stdout.strip() == '0'
+
+
+def test_imtest():
+    # (files, input, expected)
+    tests = [
+        ('a.nii', 'a',        '1'),
+        ('a.nii', 'a.nii',    '1'),
+        ('a.nii', 'a.nii.gz', '1'), # imtest is suffix-agnostic
+
+        ('a.img a.hdr', 'a',     '1'),
+        ('a.img a.hdr', 'a.img', '1'),
+        ('a.img a.hdr', 'a.hdr', '1'),
+        ('a.img',       'a',     '0'),
+        ('a.img',       'a.img', '0'),
+        ('a.img',       'a.hdr', '0'),
+        ('a.hdr',       'a',     '0'),
+        ('a.hdr',       'a.img', '0'),
+        ('a.hdr',       'a.hdr', '0'),
+
+        ('dir/a.nii',           'dir/a', '1'),
+        ('dir/a.img dir/a.hdr', 'dir/a', '1'),
+    ]
+
+    for files, input, expected in tests:
+        with tempdir():
+            for f in files.split():
+                dirname = op.dirname(f)
+                if dirname != '':
+                    os.makedirs(dirname, exist_ok=True)
+                touch(f)
+
+            cap = CaptureStdout()
+            with cap:
+                assert imtest.main([input]) == 0
+
+            assert cap.stdout.strip() == expected
+
+    # test that sym-links are
+    # followed correctly
+    with tempdir():
+        touch('image.nii.gz')
+        os.symlink('image.nii.gz', 'link.nii.gz')
+        cap = CaptureStdout()
+        with cap:
+            assert imtest.main(['link']) == 0
+        assert cap.stdout.strip() == '1'
+
+    # sym-links in sub-directories
+    # (old imtest would not work
+    # in this scenario)
+    with tempdir():
+        os.mkdir('subdir')
+        impath = op.join('subdir', 'image.nii.gz')
+        lnpath = op.join('subdir', 'link.nii.gz')
+        touch(impath)
+        os.symlink('image.nii.gz', lnpath)
+        cap = CaptureStdout()
+        with cap:
+            assert imtest.main([lnpath]) == 0
+        assert cap.stdout.strip() == '1'
diff --git a/tests/test_scripts/test_remove_ext.py b/tests/test_scripts/test_remove_ext.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb65a893af9d8dd6f3b6abcb5f60b1f7b4e576d9
--- /dev/null
+++ b/tests/test_scripts/test_remove_ext.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+#
+# test_remove_ext.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+
+import fsl.scripts.remove_ext as remove_ext
+
+from tests import CaptureStdout
+
+def test_usage():
+    assert remove_ext.main([]) != 0
+
+def test_remove_ext():
+    # (input, expected output)
+    tests = [
+        ('a',                 'a'),
+        ('a.nii',             'a'),
+        ('a.nii.gz',          'a'),
+        ('a.txt',             'a.txt'),
+        ('a.nii b.img c.hdr', 'a b c'),
+        ('a.nii b.img b.hdr', 'a b b'),
+        ('a b.img c.txt',     'a b c.txt'),
+        ('a.nii.gz b c.mnc',  'a b c'),
+    ]
+
+    for input, expected in tests:
+
+        cap = CaptureStdout()
+        with cap:
+            ret = remove_ext.main(input.split())
+
+        assert ret == 0
+
+        got = cap.stdout.split()
+        assert sorted(got) == sorted(expected.split())
diff --git a/tests/test_scripts/test_vest2text.py b/tests/test_scripts/test_vest2text.py
new file mode 100644
index 0000000000000000000000000000000000000000..15cca4f5a22eb0d96cff4827ff4a3309489b4f05
--- /dev/null
+++ b/tests/test_scripts/test_vest2text.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# test_vest2text.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import numpy as np
+
+import fsl.data.vest         as fslvest
+import fsl.scripts.Text2Vest as Text2Vest
+import fsl.scripts.Vest2Text as Vest2Text
+
+from tests import tempdir
+
+
+def test_usage():
+    assert Vest2Text.main([]) == 0
+    assert Text2Vest.main([]) == 0
+
+
+def test_Vest2Text():
+    with tempdir():
+        data = np.random.random((20, 10))
+        vest = fslvest.generateVest(data)
+
+        with open('data.vest', 'wt') as f:
+            f.write(vest)
+
+        assert Vest2Text.main(['data.vest', 'data.txt']) == 0
+
+        got = np.loadtxt('data.txt')
+
+        assert np.all(np.isclose(data, got))
+
+
+def test_Text2Vest():
+    with tempdir():
+        data = np.random.random((20, 10))
+
+        np.savetxt('data.txt', data)
+
+        assert Text2Vest.main(['data.txt', 'data.vest']) == 0
+
+        got = fslvest.loadVestFile('data.vest', ignoreHeader=False)
+
+        assert np.all(np.isclose(data, got))
diff --git a/tests/test_vest.py b/tests/test_vest.py
index 407c502ef633880cfb072581be1a2a47e7a97581..a49f5fc3afb7c5469438a8facf70aed35a079ca1 100644
--- a/tests/test_vest.py
+++ b/tests/test_vest.py
@@ -6,16 +6,20 @@
 #
 
 
-import os.path as op
-import            shutil
-import            tempfile
-import            warnings
+import os.path  as op
+import textwrap as tw
+import             io
+import             shutil
+import             tempfile
+import             warnings
 
-import numpy   as np
-import            pytest
+import numpy    as np
+import             pytest
 
 import fsl.data.vest as vest
 
+from tests import tempdir
+
 
 testfile1 = """%!VEST-LUT
 %%BeginInstance
@@ -214,3 +218,75 @@ def test_loadVestLutFile():
 
     finally:
         shutil.rmtree(testdir)
+
+
+def test_generateVest():
+    def readvest(vstr):
+        lines = vstr.split('\n')
+        nrows = [l for l in lines if 'NumPoints' in l][0]
+        ncols = [l for l in lines if 'NumWaves'  in l][0]
+        nrows = int(nrows.split()[1])
+        ncols = int(ncols.split()[1])
+        data  = '\n'.join(lines[3:])
+        data  = np.loadtxt(io.StringIO(data)).reshape((nrows, ncols))
+
+        return ((nrows, ncols), data)
+
+    # shape, expectedshape
+    tests = [
+        ((10,   ), ( 1, 10)),
+        ((10,  1), (10,  1)),
+        (( 1, 10), ( 1, 10)),
+        (( 3,  5), ( 3,  5)),
+        (( 5,  3), ( 5,  3))
+    ]
+
+    for shape, expshape in tests:
+        data = np.random.random(shape)
+        vstr = vest.generateVest(data)
+
+        gotshape, gotdata = readvest(vstr)
+
+        data = data.reshape(expshape)
+
+        assert expshape == gotshape
+        assert np.all(np.isclose(data, gotdata))
+
+
+def test_loadVestFile():
+    def genvest(data, path, shapeOverride=None):
+        if shapeOverride is None:
+            nrows, ncols = data.shape
+        else:
+            nrows, ncols = shapeOverride
+
+        with open(path, 'wt') as f:
+            f.write(f'/NumWaves {ncols}\n')
+            f.write(f'/NumPoints {nrows}\n')
+            f.write( '/Matrix\n')
+
+            if np.issubdtype(data.dtype, np.integer): fmt = '%d'
+            else:                                     fmt = '%0.12f'
+
+            np.savetxt(f, data, fmt=fmt)
+
+    with tempdir():
+        data = np.random.randint(1, 100, (10, 20))
+        genvest(data, 'data.vest')
+        assert np.all(data == vest.loadVestFile('data.vest'))
+
+        data = np.random.random((20, 10))
+        genvest(data, 'data.vest')
+        assert np.all(np.isclose(data, vest.loadVestFile('data.vest')))
+
+        # should pass
+        vest.loadVestFile('data.vest', ignoreHeader=False)
+
+        # invalid VEST header
+        genvest(data, 'data.vest', (10, 20))
+
+        # default behaviour - ignore header
+        assert np.all(np.isclose(data, vest.loadVestFile('data.vest')))
+
+        with pytest.raises(ValueError):
+            vest.loadVestFile('data.vest', ignoreHeader=False)