diff --git a/.ci/test_template.sh b/.ci/test_template.sh
index dd3e0022662614b764b5ac6331893f58c505c808..00bc034b1b76c8db636817e86f3f7820150a250c 100644
--- a/.ci/test_template.sh
+++ b/.ci/test_template.sh
@@ -28,7 +28,7 @@ if [ "$TEST_STYLE"x != "x" ]; then exit 0; fi
 # tests, and need $FSLDIR to be defined
 export FSLDIR=/fsl/
 mkdir -p $FSLDIR/data/
-rsync -rv "fsldownload:data/atlases/" "$FSLDIR/data/atlases/"
+rsync -rv "fsldownload:$FSL_ATLAS_DIR" "$FSLDIR/data/atlases/"
 
 # Finally, run the damned tests.
 TEST_OPTS="--cov-report= --cov-append"
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 792aee38e5d79796997101754ff0023b476f5aff..c07fcec57d0c65d3607255a8a5833642023b76f5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -72,6 +72,9 @@ stages:
 #   - FSL_HOST:                      - Username@host to download FSL data from
 #                                      (e.g. "paulmc@jalapeno.fmrib.ox.ac.uk")
 #
+#   - FSL_ATLAS_DIR:                 - Location of the FSL atlas data on
+#                                      FSL_HOST.
+#
 #   - TWINE_USERNAME:                - Username to use when uploading to pypi
 #
 #   - TWINE_PASSWORD:                - Password to use when uploading to pypi
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 42815f289500ee9892044ca6276ccd8c9fb8496b..bd49656fa72a595585c318bbc14dd7df0389c3c0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,8 +2,8 @@ This document contains the ``fslpy`` release history in reverse chronological
 order.
 
 
-2.0.0 (Under development)
--------------------------
+2.0.0 (Friday March 20th 2019)
+------------------------------
 
 
 Added
@@ -13,18 +13,23 @@ Added
   file/directory templates (Michiel Cottaar).
 * Simple built-in :mod:`.deprecated` decorator.
 * New :mod:`fsl.data.utils` module, which currently contains one function
-  :func:`.guessType`, which guess the data type of a file/directory path.
+  :func:`.guessType`, which guesses the data type of a file/directory path.
+* New `.commonBase` function for finding the common prefix of a set of
+  file/directory paths.
 
 
 Changed
 ^^^^^^^
 
+
 * Removed support for Python 2.7 and 3.4.
 * Minimum required version of ``nibabel`` is now 2.3.
 * The :class:`.Image` class now fully delegates to ``nibabel`` for managing
   file handles.
 * The :class:`.GiftiMesh` class can now load surface files which contain
-  vertex data.
+  vertex data, and will accept surface files which end in ``.gii``, rather
+  than requiring files which end in ``.surf.gii``.
+* The ``name`` property of :class:`.Mesh` instances can now be updated.
 
 
 Removed
@@ -33,6 +38,14 @@ Removed
 * Many deprecated items removed.
 
 
+Deprecated
+^^^^^^^^^^
+
+
+* Deprecated the :func:`.loadIndexedImageFile`  function, and the ``indexed``
+  flag to the :class:`.Image` constructor.
+
+
 1.13.3 (Friday February 8th 2019)
 ---------------------------------
 
diff --git a/doc/fsl.utils.filetree.query.rst b/doc/fsl.utils.filetree.query.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d56577ba5087f3230c54b25573abc35df046043d
--- /dev/null
+++ b/doc/fsl.utils.filetree.query.rst
@@ -0,0 +1,7 @@
+``fsl.utils.filetree.query``
+============================
+
+.. automodule:: fsl.utils.filetree.query
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/doc/fsl.utils.filetree.rst b/doc/fsl.utils.filetree.rst
index 28ed519ced0fc52bee9c0b0f858953bfc6a91fb8..7dcdbf406eeb4caff66f25df69de9501f95de006 100644
--- a/doc/fsl.utils.filetree.rst
+++ b/doc/fsl.utils.filetree.rst
@@ -6,6 +6,7 @@
 
    fsl.utils.filetree.filetree
    fsl.utils.filetree.parse
+   fsl.utils.filetree.query
    fsl.utils.filetree.utils
 
 .. automodule:: fsl.utils.filetree
diff --git a/fsl/data/mesh.py b/fsl/data/mesh.py
index 55b996069dfcb54b616bcc616b8b70eff90e02a7..f5ca20adea3747ca53945755f61c85dc28c539d1 100644
--- a/fsl/data/mesh.py
+++ b/fsl/data/mesh.py
@@ -241,6 +241,12 @@ class Mesh(notifier.Notifier, meta.Meta):
         return self.__name
 
 
+    @name.setter
+    def name(self, name):
+        """Set the name of this ``Mesh``. """
+        self.__name = name
+
+
     @property
     def dataSource(self):
         """Returns the data source of this ``Mesh``. """
diff --git a/fsl/utils/filetree/__init__.py b/fsl/utils/filetree/__init__.py
index 9d31383db6c525d926f9eeb5e558f530700a376c..736ad99983ab438191632a4514543a7f511719ef 100644
--- a/fsl/utils/filetree/__init__.py
+++ b/fsl/utils/filetree/__init__.py
@@ -277,4 +277,5 @@ of the short variable names defined in the
 __author__ = 'Michiel Cottaar <Michiel.Cottaar@ndcn.ox.ac.uk>'
 
 from .filetree import FileTree, register_tree, MissingVariable
-from .parse import tree_directories
+from .parse import tree_directories, list_all_trees
+from .query import FileTreeQuery
diff --git a/fsl/utils/filetree/filetree.py b/fsl/utils/filetree/filetree.py
index 69c3d75b0c039e06d2d9e9e349763aa8e5532f73..9ba5fede6ad1d7f4ee6a9b9a24980941b6d7fc0a 100644
--- a/fsl/utils/filetree/filetree.py
+++ b/fsl/utils/filetree/filetree.py
@@ -1,5 +1,5 @@
 from pathlib import Path, PurePath
-from typing import Tuple, Optional, Dict, Any, Set
+from typing import Tuple, Optional, List, Dict, Any, Set
 from copy import deepcopy
 from . import parse
 import pickle
diff --git a/fsl/utils/filetree/parse.py b/fsl/utils/filetree/parse.py
index c89b381fd38142158f78319c7b070a331bb875a5..11a8f1f221713b775caf35dfbfae7ba2f65102bb 100644
--- a/fsl/utils/filetree/parse.py
+++ b/fsl/utils/filetree/parse.py
@@ -1,7 +1,8 @@
+import glob
 import os.path as op
 from . import filetree
 from pathlib import PurePath
-from typing import Tuple
+from typing import Tuple, List
 import re
 
 
@@ -26,6 +27,17 @@ def search_tree(name: str) -> str:
     raise ValueError("No file tree found for %s" % name)
 
 
+def list_all_trees() -> List[str]:
+    """Return a list containing paths to all tree files that can be found in
+    :data:`tree_directories`
+    """
+    trees = []
+    for directory in tree_directories:
+        directory = op.abspath(directory)
+        trees.extend(glob.glob(op.join(directory, '*.tree')))
+    return trees
+
+
 def read_line(line: str) -> Tuple[int, PurePath, str]:
     """
     Parses line from the tree file
diff --git a/fsl/utils/filetree/query.py b/fsl/utils/filetree/query.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1f1f46eddb6d007ab366852a4328df97a84547c
--- /dev/null
+++ b/fsl/utils/filetree/query.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python
+#
+# query.py - The FileTreeQuery class
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+# Author: Michiel Cottaar <michiel.cottaar@.ndcn.ox.ac.uk>
+#
+"""This module contains the :class:`FileTreeQuery` class, which can be used to
+search for files in a directory described by a `.FileTree`. A
+``FileTreeQuery`` object returns :class:`Match` objects which each represent a
+file that is described by the ``FileTree``, and which is present in the
+directory.
+
+The following utility functions, used by the ``FileTreeQuery`` class, are also
+defined in this module:
+
+.. autosummary::
+   :nosignatures:
+
+   scan
+   allVariables
+"""
+
+
+import logging
+import collections
+
+import os.path as op
+from typing import Dict, List, Tuple
+
+import numpy as np
+
+from . import FileTree
+
+
+log = logging.getLogger(__name__)
+
+
+class FileTreeQuery(object):
+    """The ``FileTreeQuery`` class uses a :class:`.FileTree` to search
+    a directory for files which match a specific query.
+
+    A ``FileTreeQuery`` scans the contents of a directory which is described
+    by a :class:`.FileTree`, and identifies all file types (a.k.a. _templates_
+    or _short names_) that are present, and the values of variables within each
+    short name that are present. The :meth:`query` method can be used to
+    retrieve files which match a specific short name, and variable values.
+
+    The :meth:`query` method returns a multi-dimensional ``numpy.array``
+    which contains :class:`Match` objects, where each dimension one
+    represents variable for the short name in question.
+
+    Example usage::
+
+        >>> from fsl.utils.filetree import FileTree, FileTreeQuery
+
+        >>> tree  = FileTree.read('bids_raw', './my_bids_data')
+        >>> query = FileTreeQuery(tree)
+
+        >>> query.axes('anat_image')
+        ['acq', 'ext', 'modality', 'participant', 'rec', 'run_index',
+         'session']
+
+        >>> query.variables('anat_image')
+        {'acq': [None],
+         'ext': ['.nii.gz'],
+         'modality': ['T1w', 'T2w'],
+         'participant': ['01', '02', '03'],
+         'rec': [None],
+         'run_index': [None, '01', '02', '03'],
+         'session': [None]}
+
+        >>> query.query('anat_image', participant='01')
+        array([[[[[[[Match(./my_bids_data/sub-01/anat/sub-01_T1w.nii.gz)],
+                    [nan],
+                    [nan],
+                    [nan]]]],
+
+                 [[[[Match(./my_bids_data/sub-01/anat/sub-01_T2w.nii.gz)],
+                    [nan],
+                    [nan],
+                    [nan]]]]]]], dtype=object)
+    """
+
+
+    def __init__(self, tree):
+        """Create a ``FileTreeQuery``. The contents of the tree directory are
+        scanned via the :func:`scan` function, which may take some time for
+        large data sets.
+
+        :arg tree: The :class:`.FileTree` object
+        """
+
+        # Find all files present in the directory
+        # (as Match objects), and find all variables,
+        # plus their values, and all short names,
+        # that are present in the directory.
+        matches                = scan(tree)
+        allvars, shortnamevars = allVariables(tree, matches)
+
+        # Now we are going to build a series of ND
+        # arrays to store Match objects. We create
+        # one array for each short name. Each axis
+        # in an array corresponds to a variable
+        # present in files of that short name type,
+        # and each position along an axis corresponds
+        # to one value of that variable.
+        #
+        # These arrays will be used to store and
+        # retrieve Match objects - given a short
+        # name and a set of variable values, we
+        # can quickly find the corresponding Match
+        # object (or objects).
+
+        # matcharrays contains {shortname : ndarray}
+        # mappings, and varidxs contains
+        # {shortname : {varvalue : index}} mappings
+        matcharrays = {}
+        varidxs     = {}
+
+        for shortname in shortnamevars.keys():
+
+            snvars    = shortnamevars[shortname]
+            snvarlens = [len(allvars[v]) for v in snvars]
+
+            # An ND array for this short
+            # name. Each element is a
+            # Match object, or nan.
+            matcharray    = np.zeros(snvarlens, dtype=np.object)
+            matcharray[:] = np.nan
+
+            # indices into the match array
+            # for each variable value
+            snvaridxs = {}
+            for v in snvars:
+                snvaridxs[v] = {n : i for i, n in enumerate(allvars[v])}
+
+            matcharrays[shortname] = matcharray
+            varidxs[    shortname] = snvaridxs
+
+        # Populate the match arrays
+        for match in matches:
+            snvars    = shortnamevars[match.short_name]
+            snvaridxs = varidxs[      match.short_name]
+            snarr     = matcharrays[  match.short_name]
+            idx       = []
+            for var in snvars:
+
+                val = match.variables[var]
+                idx.append(snvaridxs[var][val])
+
+            snarr[tuple(idx)] = match
+
+        self.__allvars       = allvars
+        self.__shortnamevars = shortnamevars
+        self.__matches       = matches
+        self.__matcharrays   = matcharrays
+        self.__varidxs       = varidxs
+
+
+    def axes(self, short_name) -> List[str]:
+        """Returns a list containing the names of variables present in files
+        of the given ``short_name`` type, in the same order of the axes of
+        :class:`Match` arrays that are returned by the :meth:`query` method.
+        """
+        return self.__shortnamevars[short_name]
+
+
+    def variables(self, short_name=None) -> Dict[str, List]:
+        """Return a dict of ``{variable : [values]}`` mappings.
+        This dict describes all variables and their possible values in
+        the tree.
+
+        If a ``short_name`` is specified, only variables which are present in
+        files of that ``short_name`` type are returned.
+        """
+        if short_name is None:
+            return {var : list(vals) for var, vals in self.__allvars.items()}
+        else:
+            varnames = self.__shortnamevars[short_name]
+            return {var : list(self.__allvars[var]) for var in varnames}
+
+
+    @property
+    def short_names(self) -> List[str]:
+        """Returns a list containing all short names of the ``FileTree`` that
+        are present in the directory.
+        """
+        return list(self.__shortnamevars.keys())
+
+
+    def query(self, short_name, asarray=False, **variables):
+        """Search for files of the given ``short_name``, which match
+        the specified ``variables``. All hits are returned for variables
+        that are unspecified.
+
+        :arg short_name:  Short name of files to search for.
+
+        :arg asarray: If ``True``, the relevant :class:`Match` objects are
+                      returned in a in a ND ``numpy.array`` where each
+                      dimension corresponds to a variable for the
+                      ``short_name`` in question (as returned by
+                      :meth:`axes`). Otherwise (the default), they are
+                      returned in a list.
+
+        All other arguments are assumed to be ``variable=value`` pairs,
+        used to restrict which matches are returned. All values are returned
+        for variables that are not specified, or variables which are given a
+        value of ``'*'``.
+
+        :returns: A list  of ``Match`` objects, (or a ``numpy.array`` if
+                  ``asarray=True``).
+        """
+
+        varnames    = list(variables.keys())
+        allvarnames = self.__shortnamevars[short_name]
+        varidxs     = self.__varidxs[    short_name]
+        matcharray  = self.__matcharrays[short_name]
+        slc         = []
+
+        for var in allvarnames:
+
+            if var in varnames: val = variables[var]
+            else:               val = '*'
+
+            # We're using np.newaxis to retain
+            # the full dimensionality of the
+            # array, so that the axis labels
+            # returned by the axes() method
+            # are valid.
+            if val == '*': slc.append(slice(None))
+            else:          slc.extend([np.newaxis, varidxs[var][val]])
+
+        result = matcharray[tuple(slc)]
+
+        if asarray: return result
+        else:       return [m for m in result.flat if isinstance(m, Match)]
+
+
+class Match(object):
+    """A ``Match`` object represents a file with a name matching a template in
+    a ``FileTree``.  The :func:`scan` function and :meth:`FileTree.query`
+    method both return ``Match`` objects.
+    """
+
+
+    def __init__(self, filename, short_name, variables):
+        """Create a ``Match`` object. All arguments are added as attributes.
+
+        :arg filename:   name of existing file
+        :arg short_name: template identifier
+        :arg variables:  Dictionary of ``{variable : value}`` mappings
+                         containing all variables present in the file name.
+        """
+        self.__filename   = filename
+        self.__short_name = short_name
+        self.__variables  = dict(variables)
+
+
+    @property
+    def filename(self):
+        return self.__filename
+
+
+    @property
+    def short_name(self):
+        return self.__short_name
+
+
+    @property
+    def variables(self):
+        return dict(self.__variables)
+
+
+    def __eq__(self, other):
+        return (isinstance(other, Match)            and
+                self.filename   == other.filename   and
+                self.short_name == other.short_name and
+                self.variables  == other.variables)
+
+
+    def __lt__(self, other):
+        return isinstance(other, Match) and self.filename < other.filename
+
+
+    def __le__(self, other):
+        return isinstance(other, Match) and self.filename <= other.filename
+
+
+    def __repr__(self):
+        """Returns a string representation of this ``Match``. """
+        return 'Match({})'.format(self.filename)
+
+
+    def __str__(self):
+        """Returns a string representation of this ``Match``. """
+        return repr(self)
+
+
+def scan(tree : FileTree) -> List[Match]:
+    """Scans the directory of the given ``FileTree`` to find all files which
+    match a tree template.
+
+    :return: list of :class:`Match` objects
+    """
+
+    matches = []
+    for template in tree.templates:
+        for filename in tree.get_all(template, glob_vars='all'):
+
+            if not op.isfile(filename):
+                continue
+
+            variables = dict(tree.extract_variables(template, filename))
+
+            matches.append(Match(filename, template, variables))
+
+    for tree_name, sub_tree in tree.sub_trees.items():
+        matches.extend(scan(sub_tree))
+
+    return matches
+
+
+def allVariables(
+        tree    : FileTree,
+        matches : List[Match]) -> Tuple[Dict[str, List], Dict[str, List]]:
+    """Identifies the ``FileTree`` variables which are actually represented
+    in files in the directory.
+
+    :arg filetree: The ``FileTree``object
+    :arg matches:  list of ``Match`` objects (e.g. as returned by :func:`scan`)
+
+    :returns: a tuple containing two dicts:
+
+               - A dict of ``{ variable : [values] }`` mappings containing all
+                 variables and their possible values present in the given list
+                 of ``Match`` objects.
+
+               - A dict of ``{ short_name : [variables] }`` mappings,
+                 containing the variables which are relevant to each short
+                 name.
+    """
+    allvars       = collections.defaultdict(set)
+    allshortnames = collections.defaultdict(set)
+
+    for m in matches:
+        for var, val in m.variables.items():
+            allvars[      var]         .add(val)
+            allshortnames[m.short_name].add(var)
+
+    # allow us to compare None with strings
+    def key(v):
+        if v is None: return ''
+        else:         return v
+
+    allvars       = {var : list(sorted(vals, key=key))
+                     for var, vals in allvars.items()}
+    allshortnames = {sn  : list(sorted(vars))
+                     for sn, vars in allshortnames.items()}
+
+    return allvars, allshortnames
diff --git a/fsl/utils/filetree/utils.py b/fsl/utils/filetree/utils.py
index e7dd4aad3347c678e32217e09eecf33d2ecdee35..0b82929491c9a8994d252f24704f66dff87f8411 100644
--- a/fsl/utils/filetree/utils.py
+++ b/fsl/utils/filetree/utils.py
@@ -158,6 +158,7 @@ def extract_variables(template, filename, known_vars=None):
         ))
         while '//' in sub_re:
             sub_re = sub_re.replace('//', '/')
+        sub_re = sub_re.replace('.', '\.')
         if re.match(sub_re, filename) is None:
             continue
 
diff --git a/fsl/utils/path.py b/fsl/utils/path.py
index 4190865a820dab8b52b8119ed305c0d17006a721..e45f82213f0b44495bd1c6e4573bb5ab169b7c1d 100644
--- a/fsl/utils/path.py
+++ b/fsl/utils/path.py
@@ -22,12 +22,14 @@ paths.
    getFileGroup
    removeDuplicates
    uniquePrefix
+   commonBase
 """
 
 
 import os.path as op
 import            os
 import            glob
+import            operator
 
 
 class PathError(Exception):
@@ -471,3 +473,30 @@ def uniquePrefix(path):
             hits    = [h for h in hits if h.startswith(prefix)]
 
     return prefix
+
+
+def commonBase(paths):
+    """Identifies the deepest common base directory shared by all files
+    in ``paths``.
+
+    Raises a :exc:`PathError` if the paths have no common base. This will
+    never happen for absolute paths (as the base will be e.g. ``'/'``).
+    """
+
+    depths = [len(p.split(op.sep)) for p in paths]
+    base   = max(zip(depths, paths), key=operator.itemgetter(0))[1]
+    last   = base
+
+    while True:
+
+        base = op.split(base)[0]
+
+        if base == last or len(base) == 0:
+            break
+
+        last = base
+
+        if all([p.startswith(base) for p in paths]):
+            return base
+
+    raise PathError('No common base')
diff --git a/tests/test_filetree/__init__.py b/tests/test_filetree/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/test_filetree/test_query.py b/tests/test_filetree/test_query.py
new file mode 100644
index 0000000000000000000000000000000000000000..66e50c1f8d0c494b721e2e2ad0846e0629ef4b73
--- /dev/null
+++ b/tests/test_filetree/test_query.py
@@ -0,0 +1,438 @@
+#!/usr/bin/env python
+#
+# test_query.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import os
+import glob
+import shutil
+import os.path as op
+import contextlib
+import textwrap as tw
+import itertools as it
+
+from .. import testdir
+
+import fsl.utils.filetree       as filetree
+import fsl.utils.filetree.query as ftquery
+
+
+_test_tree = """
+subj-{participant}
+  [ses-{session}]
+    T1w.nii.gz (T1w)
+    T2w.nii.gz (T2w)
+    {hemi}.{surf}.gii (surface)
+""".strip()
+
+_subjs = ['01', '02', '03']
+_sess  = ['1', '2']
+_hemis = ['L', 'R']
+_surfs = ['midthickness', 'pial', 'white']
+
+
+@contextlib.contextmanager
+def _test_data():
+
+    files = []
+
+    for subj, ses in it.product(_subjs, _sess):
+        sesdir = op.join('subj-{}'.format(subj), 'ses-{}'.format(ses))
+        files.append(op.join(sesdir, 'T1w.nii.gz'))
+        files.append(op.join(sesdir, 'T2w.nii.gz'))
+
+        for hemi, surf in it.product(_hemis, _surfs):
+            files.append(op.join(sesdir, '{}.{}.gii'.format(hemi, surf)))
+
+    with testdir(files):
+        with open('_test_tree.tree', 'wt') as f:
+            f.write(_test_tree)
+        yield
+
+
+def _expected_matches(short_name, **kwargs):
+
+    matches = []
+    subjs   = kwargs.get('participant', _subjs)
+    sess    = kwargs.get('session',     _sess)
+    surfs   = kwargs.get('surf',        _surfs)
+    hemis   = kwargs.get('hemi',        _hemis)
+
+    for subj, ses in it.product(subjs, sess):
+
+        sesdir = op.join('subj-{}'.format(subj), 'ses-{}'.format(ses))
+
+        if short_name in ('T1w', 'T2w'):
+            f = op.join(sesdir, '{}.nii.gz'.format(short_name))
+            matches.append(ftquery.Match(f,
+                                         short_name,
+                                         {'participant' : subj,
+                                          'session'     : ses}))
+
+        elif short_name == 'surface':
+            for hemi, surf in it.product(hemis, surfs):
+                f = op.join(sesdir, '{}.{}.gii'.format(hemi, surf))
+                matches.append(ftquery.Match(f,
+                                             short_name,
+                                             {'participant' : subj,
+                                              'session'     : ses,
+                                              'hemi'        : hemi,
+                                              'surf'        : surf}))
+
+    return matches
+
+
+def _run_and_check_query(query, short_name, asarray=False, **vars):
+
+    gotmatches = query.query(      short_name, asarray=asarray, **vars)
+    expmatches = _expected_matches(short_name, **{k : [v]
+                                                  for k, v
+                                                  in vars.items()})
+
+    if not asarray:
+        assert len(gotmatches) == len(expmatches)
+        for got, exp in zip(sorted(gotmatches), sorted(expmatches)):
+            assert got == exp
+    else:
+        snvars = query.variables(short_name)
+
+        assert len(snvars) == len(gotmatches.shape)
+
+        for i, var in enumerate(sorted(snvars.keys())):
+            if var not in vars or vars[var] == '*':
+                assert len(snvars[var]) == gotmatches.shape[i]
+            else:
+                assert gotmatches.shape[i] == 1
+
+        for expmatch in expmatches:
+            slc = []
+            for var in query.axes(short_name):
+                if var not in vars or vars[var] == '*':
+                    vidx = snvars[var].index(expmatch.variables[var])
+                    slc.append(vidx)
+                else:
+                    slc.append(0)
+
+
+def test_query_properties():
+    with _test_data():
+
+        tree  = filetree.FileTree.read('_test_tree.tree', '.')
+        query = filetree.FileTreeQuery(tree)
+
+        assert sorted(query.axes('T1w'))     == ['participant', 'session']
+        assert sorted(query.axes('T2w'))     == ['participant', 'session']
+        assert sorted(query.axes('surface')) == ['hemi',
+                                                 'participant',
+                                                 'session',
+                                                 'surf']
+        assert sorted(query.short_names)     == ['T1w', 'T2w', 'surface']
+
+        assert query.variables('T1w')     == {'participant' : ['01', '02', '03'],
+                                              'session'     : ['1', '2']}
+        assert query.variables('T2w')     == {'participant' : ['01', '02', '03'],
+                                              'session'     : ['1', '2']}
+        assert query.variables('surface') == {'participant' : ['01', '02', '03'],
+                                              'session'     : ['1', '2'],
+                                              'surf'        : ['midthickness',
+                                                               'pial',
+                                                               'white'],
+                                              'hemi'        : ['L', 'R']}
+        assert query.variables()          == {'participant' : ['01', '02', '03'],
+                                              'session'     : ['1', '2'],
+                                              'surf'        : ['midthickness',
+                                                               'pial',
+                                                               'white'],
+                                              'hemi'        : ['L', 'R']}
+
+
+def test_query():
+    with _test_data():
+        tree  = filetree.FileTree.read('_test_tree.tree', '.')
+        query = filetree.FileTreeQuery(tree)
+
+    _run_and_check_query(query, 'T1w')
+    _run_and_check_query(query, 'T1w', participant='01')
+    _run_and_check_query(query, 'T1w', session='2')
+    _run_and_check_query(query, 'T1w', participant='02', session='1')
+    _run_and_check_query(query, 'T2w')
+    _run_and_check_query(query, 'T2w', participant='01')
+    _run_and_check_query(query, 'T2w', session='2')
+    _run_and_check_query(query, 'T2w', participant='02', session='1')
+    _run_and_check_query(query, 'surface')
+    _run_and_check_query(query, 'surface', hemi='L')
+    _run_and_check_query(query, 'surface', surf='midthickness')
+    _run_and_check_query(query, 'surface', hemi='R', surf='pial')
+    _run_and_check_query(query, 'surface', participant='03', surf='pial')
+    _run_and_check_query(query, 'surface', participant='03', sssion='2')
+
+
+def test_query_optional_var_folder():
+    with _test_data():
+
+        # make subj-01 have no session sub-directories
+        for f in glob.glob(op.join('subj-01', 'ses-1', '*')):
+            shutil.move(f, 'subj-01')
+        shutil.rmtree(op.join('subj-01', 'ses-1'))
+        shutil.rmtree(op.join('subj-01', 'ses-2'))
+
+        tree  = filetree.FileTree.read('_test_tree.tree', '.')
+        query = filetree.FileTreeQuery(tree)
+
+        assert query.variables()['session'] == [None, '1', '2']
+
+        m = query.query('T1w', participant='01')
+        assert len(m) == 1
+        assert m[0].filename == op.join('subj-01', 'T1w.nii.gz')
+
+
+def test_query_optional_var_filename():
+
+    treefile = tw.dedent("""
+    sub-{subject}
+        img[-{modality}].nii.gz (image)
+    """).strip()
+
+    files = [
+        op.join('sub-01', 'img.nii.gz'),
+        op.join('sub-02', 'img-t1.nii.gz'),
+        op.join('sub-02', 'img-t2.nii.gz'),
+        op.join('sub-03', 'img-t1.nii.gz'),
+        op.join('sub-04', 'img.nii.gz')]
+
+    with testdir(files):
+        with open('_test_tree.tree', 'wt') as f:
+            f.write(treefile)
+
+        tree  = filetree.FileTree.read('_test_tree.tree', '.')
+        query = filetree.FileTreeQuery(tree)
+
+        qvars = query.variables()
+
+        assert sorted(qvars.keys()) == ['modality', 'subject']
+        assert qvars['subject']  == ['01', '02', '03', '04']
+        assert qvars['modality'] == [None, 't1', 't2']
+
+        got = query.query('image', modality=None)
+        assert [m.filename for m in sorted(got)] == [
+            op.join('sub-01', 'img.nii.gz'),
+            op.join('sub-04', 'img.nii.gz')]
+
+        got = query.query('image', modality='t1')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('sub-02', 'img-t1.nii.gz'),
+            op.join('sub-03', 'img-t1.nii.gz')]
+
+        got = query.query('image', modality='t2')
+        assert len(got) == 1
+        assert got[0].filename == op.join('sub-02', 'img-t2.nii.gz')
+
+
+def test_query_missing_files():
+    with _test_data():
+
+        os.remove(op.join('subj-01', 'ses-1', 'T1w.nii.gz'))
+        os.remove(op.join('subj-02', 'ses-2', 'T2w.nii.gz'))
+        os.remove(op.join('subj-03', 'ses-1', 'L.white.gii'))
+        os.remove(op.join('subj-03', 'ses-1', 'L.midthickness.gii'))
+        os.remove(op.join('subj-03', 'ses-1', 'L.pial.gii'))
+
+        tree  = filetree.FileTree.read('_test_tree.tree', '.')
+        query = filetree.FileTreeQuery(tree)
+
+        got = query.query('T1w', session='1')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-02', 'ses-1', 'T1w.nii.gz'),
+            op.join('subj-03', 'ses-1', 'T1w.nii.gz')]
+
+        got = query.query('T2w', session='2')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-01', 'ses-2', 'T2w.nii.gz'),
+            op.join('subj-03', 'ses-2', 'T2w.nii.gz')]
+
+        got = query.query('surface', session='1', hemi='L')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-01', 'ses-1', 'L.midthickness.gii'),
+            op.join('subj-01', 'ses-1', 'L.pial.gii'),
+            op.join('subj-01', 'ses-1', 'L.white.gii'),
+            op.join('subj-02', 'ses-1', 'L.midthickness.gii'),
+            op.join('subj-02', 'ses-1', 'L.pial.gii'),
+            op.join('subj-02', 'ses-1', 'L.white.gii')]
+
+
+def test_query_asarray():
+    with _test_data():
+        tree  = filetree.FileTree.read('_test_tree.tree', '.')
+        query = filetree.FileTreeQuery(tree)
+
+        _run_and_check_query(query, 'T1w', asarray=True)
+        _run_and_check_query(query, 'T1w', asarray=True, participant='01')
+        _run_and_check_query(query, 'T1w', asarray=True, session='2')
+        _run_and_check_query(query, 'T1w', asarray=True, participant='02', session='1')
+        _run_and_check_query(query, 'T2w', asarray=True)
+        _run_and_check_query(query, 'T2w', asarray=True, participant='01')
+        _run_and_check_query(query, 'T2w', asarray=True, session='2')
+        _run_and_check_query(query, 'T2w', asarray=True, participant='02', session='1')
+        _run_and_check_query(query, 'surface', asarray=True)
+        _run_and_check_query(query, 'surface', asarray=True, hemi='L')
+        _run_and_check_query(query, 'surface', asarray=True, surf='midthickness')
+        _run_and_check_query(query, 'surface', asarray=True, hemi='R', surf='pial')
+        _run_and_check_query(query, 'surface', asarray=True, participant='03', surf='pial')
+        _run_and_check_query(query, 'surface', asarray=True, participant='03', sssion='2')
+
+
+def test_query_subtree():
+    tree1 = tw.dedent("""
+    subj-{participant}
+        T1w.nii.gz (T1w)
+        surf
+            ->surface (surfdir)
+    """)
+    tree2 = tw.dedent("""
+    {hemi}.{surf}.gii (surface)
+    """)
+
+    files = [
+        op.join('subj-01', 'T1w.nii.gz'),
+        op.join('subj-01', 'surf', 'L.pial.gii'),
+        op.join('subj-01', 'surf', 'R.pial.gii'),
+        op.join('subj-01', 'surf', 'L.white.gii'),
+        op.join('subj-01', 'surf', 'R.white.gii'),
+        op.join('subj-02', 'T1w.nii.gz'),
+        op.join('subj-02', 'surf', 'L.pial.gii'),
+        op.join('subj-02', 'surf', 'R.pial.gii'),
+        op.join('subj-02', 'surf', 'L.white.gii'),
+        op.join('subj-02', 'surf', 'R.white.gii'),
+        op.join('subj-03', 'T1w.nii.gz'),
+        op.join('subj-03', 'surf', 'L.pial.gii'),
+        op.join('subj-03', 'surf', 'R.pial.gii'),
+        op.join('subj-03', 'surf', 'L.white.gii'),
+        op.join('subj-03', 'surf', 'R.white.gii')]
+
+    with testdir(files):
+        with open('tree1.tree',   'wt') as f: f.write(tree1)
+        with open('surface.tree', 'wt') as f: f.write(tree2)
+
+        tree = filetree.FileTree.read('tree1.tree', '.')
+        query = filetree.FileTreeQuery(tree)
+
+        assert sorted(query.short_names) == ['T1w', 'surface']
+
+        qvars = query.variables()
+        assert sorted(qvars.keys()) == ['hemi', 'participant', 'surf']
+        assert qvars['hemi']        == ['L', 'R']
+        assert qvars['participant'] == ['01', '02', '03']
+        assert qvars['surf']        == ['pial', 'white']
+
+        qvars = query.variables('T1w')
+        assert sorted(qvars.keys()) == ['participant']
+        assert qvars['participant'] == ['01', '02', '03']
+
+        qvars = query.variables('surface')
+        assert sorted(qvars.keys()) == ['hemi', 'participant', 'surf']
+        assert qvars['hemi']        == ['L', 'R']
+        assert qvars['participant'] == ['01', '02', '03']
+        assert qvars['surf']        == ['pial', 'white']
+
+        got = query.query('T1w')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-01', 'T1w.nii.gz'),
+            op.join('subj-02', 'T1w.nii.gz'),
+            op.join('subj-03', 'T1w.nii.gz')]
+
+        got = query.query('T1w', participant='01')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-01', 'T1w.nii.gz')]
+
+        got = query.query('surface')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-01', 'surf', 'L.pial.gii'),
+            op.join('subj-01', 'surf', 'L.white.gii'),
+            op.join('subj-01', 'surf', 'R.pial.gii'),
+            op.join('subj-01', 'surf', 'R.white.gii'),
+            op.join('subj-02', 'surf', 'L.pial.gii'),
+            op.join('subj-02', 'surf', 'L.white.gii'),
+            op.join('subj-02', 'surf', 'R.pial.gii'),
+            op.join('subj-02', 'surf', 'R.white.gii'),
+            op.join('subj-03', 'surf', 'L.pial.gii'),
+            op.join('subj-03', 'surf', 'L.white.gii'),
+            op.join('subj-03', 'surf', 'R.pial.gii'),
+            op.join('subj-03', 'surf', 'R.white.gii')]
+
+        got = query.query('surface', hemi='L')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-01', 'surf', 'L.pial.gii'),
+            op.join('subj-01', 'surf', 'L.white.gii'),
+            op.join('subj-02', 'surf', 'L.pial.gii'),
+            op.join('subj-02', 'surf', 'L.white.gii'),
+            op.join('subj-03', 'surf', 'L.pial.gii'),
+            op.join('subj-03', 'surf', 'L.white.gii')]
+
+        got = query.query('surface', surf='white')
+        assert [m.filename for m in sorted(got)] == [
+            op.join('subj-01', 'surf', 'L.white.gii'),
+            op.join('subj-01', 'surf', 'R.white.gii'),
+            op.join('subj-02', 'surf', 'L.white.gii'),
+            op.join('subj-02', 'surf', 'R.white.gii'),
+            op.join('subj-03', 'surf', 'L.white.gii'),
+            op.join('subj-03', 'surf', 'R.white.gii')]
+
+
+def test_scan():
+
+    with _test_data():
+        tree    = filetree.FileTree.read('_test_tree.tree', '.')
+        gotmatches = ftquery.scan(tree)
+
+    expmatches = []
+
+    for subj, ses in it.product(_subjs, _sess):
+
+        sesdir = op.join('subj-{}'.format(subj), 'ses-{}'.format(ses))
+        t1wf   = op.join(sesdir, 'T1w.nii.gz')
+        t2wf   = op.join(sesdir, 'T2w.nii.gz')
+
+        expmatches.append(ftquery.Match(t1wf, 'T1w', {'participant' : subj,
+                                                      'session'     : ses}))
+        expmatches.append(ftquery.Match(t2wf, 'T2w', {'participant' : subj,
+                                                      'session'     : ses}))
+
+        for hemi, surf in it.product(_hemis, _surfs):
+            surff = op.join(sesdir, '{}.{}.gii'.format(hemi, surf))
+
+            expmatches.append(ftquery.Match(surff,
+                                            'surface',
+                                            {'participant' : subj,
+                                             'session'     : ses,
+                                             'surf'        : surf,
+                                             'hemi'        : hemi}))
+
+
+    assert len(gotmatches) == len(expmatches)
+
+    for got, exp in zip(sorted(gotmatches), sorted(expmatches)):
+        assert got.filename   == exp.filename
+        assert got.short_name == exp.short_name
+        assert got.variables  == exp.variables
+
+
+def test_allVariables():
+    with _test_data():
+        tree          = filetree.FileTree.read('_test_tree.tree', '.')
+        matches       = ftquery.scan(tree)
+        qvars, snames = ftquery.allVariables(tree, matches)
+
+        expqvars = {
+            'participant' : _subjs,
+            'session'     : _sess,
+            'surf'        : _surfs,
+            'hemi'        : _hemis}
+        expsnames = {
+            'T1w'     : ['participant', 'session'],
+            'T2w'     : ['participant', 'session'],
+            'surface' : ['hemi', 'participant', 'session', 'surf']}
+
+        assert qvars  == expqvars
+        assert snames == expsnames
diff --git a/tests/test_fsl_utils_path.py b/tests/test_fsl_utils_path.py
index 0820f4ed642c9499047b6a02f811027d52522d15..f5e798377e5e45dff555c2aa90d2b41fa5ae3ed9 100644
--- a/tests/test_fsl_utils_path.py
+++ b/tests/test_fsl_utils_path.py
@@ -1356,3 +1356,32 @@ def test_uniquePrefix():
 
     finally:
         shutil.rmtree(workdir)
+
+
+def test_commonBase():
+
+    tests = [
+        ('/',
+         ['/a/b/c',
+          '/d/e',
+          '/f/g/h/i']),
+        ('/a',
+         ['/a/b/c',
+          '/a/d/e/f/g',
+          '/a/d/h/g/h/i']),
+        ('a',
+         ['a/b/c/d',
+          'a/e/f/g/h',
+          'a/i/j/k/'])
+    ]
+    for exp, paths in tests:
+        assert fslpath.commonBase(paths) == exp
+
+    failtests = [
+        ['a/b/c', 'd/e/f'],
+        ['/a/b/c', 'd/e/f'],
+        ['a', 'b/c/d']]
+
+    for ft in failtests:
+        with pytest.raises(fslpath.PathError):
+            fslpath.commonBase(ft)