diff --git a/fsl/fsleyes/actions/runscript.py b/fsl/fsleyes/actions/runscript.py
index 17e37bca9bf6a20dc61223f66d1d9a5b15f4ab05..18637312fd9a95a66d8e4dd7ede28997a879f65a 100644
--- a/fsl/fsleyes/actions/runscript.py
+++ b/fsl/fsleyes/actions/runscript.py
@@ -6,6 +6,15 @@
 #
 """This module provides the :class:`RunScriptAction` class, which allows
 the user to run a custom Python script.
+
+The following functions are used by the :class:`RunScriptAction`, and are
+available for other purposes:
+
+ .. autosummary::
+    :nosignatures:
+
+    runScript
+    fsleyesScriptEnvironment
 """
 
 
@@ -45,35 +54,47 @@ class RunScriptAction(action.Action):
         self.__displayCtx  = displayCtx
         
 
-    def __doAction(self):
-        """Called when this :class:`.Action` is invoked. Prompts the user to
-        select a script file, then compiles and runs the script.
+    def __doAction(self, script=None):
+        """Called when this :class:`.Action` is invoked. If the ``script``
+        argument is ``None``, the user is prompted  to select a script file.
+        The script is then compiled and executed.
         """
 
         import wx
 
-        lastDir = fslsettings.read('runScriptLastDir')
+        if script is None:
 
-        if lastDir is None:
-            lastDir = os.getcwd()
+            lastDir = fslsettings.read('runScriptLastDir')
 
-        msg = strings.messages[self, 'runScript']
+            if lastDir is None:
+                lastDir = os.getcwd()
 
-        # Ask the user what script
-        # they want to run
-        dlg    = wx.FileDialog(self.__frame,
-                               message=msg,
-                               defaultDir=lastDir,
-                               wildcard='*.py',
-                               style=wx.FD_OPEN)
+            msg = strings.messages[self, 'runScript']
 
-        if dlg.ShowModal() != wx.ID_OK:
-            return
+            # Ask the user what script
+            # they want to run
+            dlg    = wx.FileDialog(self.__frame,
+                                   message=msg,
+                                   defaultDir=lastDir,
+                                   wildcard='*.py',
+                                   style=wx.FD_OPEN)
 
-        script = dlg.GetPath()
-        
+            if dlg.ShowModal() != wx.ID_OK:
+                return
+
+            script = dlg.GetPath()
+
+            # Save the script directory for the
+            # next time the user is prompted 
+            fslsettings.write('runScriptLastDir', op.dirname(script))
+
+        # Run the script, show an
+        # error if it crashes
         try:
-            self.__runScript(script)
+            runScript(self.__frame,
+                      self.__overlayList,
+                      self.__displayCtx,
+                      script)
             
         except Exception as e:
             log.warning('Script ({}) could not be executed: {}'.format(
@@ -91,49 +112,74 @@ class RunScriptAction(action.Action):
             
             return
 
-        # Save the script directory
-        # for the next time the user
-        # is prompted 
-        fslsettings.write('runScriptLastDir', op.dirname(script))
-
         
-    def __runScript(self, script):
-        """Compiles and executes the given file, assumed to be a Python script.
-        An ``Error`` is raised if the script cannot be compiled or executed.
-        """
+def runScript(frame, overlayList, displayCtx, script):
+    """Compiles and executes the given file, assumed to be a Python script.
+    An ``Error`` is raised if the script cannot be compiled or executed.
+    """
 
-        # Set up the script environment. It's
-        # quite difficult to truly sand-box an
-        # eval'ed piece of code, but setting
-        # __builtins__ to None will deter simple
-        # attack attempts.
-        _globals = {
-            '__builtins__' : None
-        }
-        
-        _locals = {
-            'overlayList' : self.__overlayList,
-            'displayCtx'  : self.__displayCtx,
-            'frame'       : self.__frame,
-            'viewPanels'  : self.__frame.getViewPanels()
-        }
-
-        # We want scripts to be Python3-like
-        flags = (futures.print_function  .compiler_flag |
-                 futures.absolute_import .compiler_flag |
-                 futures.division        .compiler_flag | 
-                 futures.unicode_literals.compiler_flag)
-
-        # Compile the script
-        with open(script, 'rt') as f:
-            log.debug('Compiling {}...'.format(script))
-            code = f.read()
-            code = compile(code,
-                           script,
-                           mode='exec',
-                           flags=flags,
-                           dont_inherit=True)
-
-        # Run the script
-        log.debug('Running {}...'.format(script))
-        eval(code, _globals, _locals)
+    # We want scripts to be Python3-like
+    flags = (futures.print_function  .compiler_flag |
+             futures.absolute_import .compiler_flag |
+             futures.division        .compiler_flag | 
+             futures.unicode_literals.compiler_flag)
+
+    # Compile the script
+    with open(script, 'rt') as f:
+        log.debug('Compiling {}...'.format(script))
+        code = f.read()
+        code = compile(code,
+                       script,
+                       mode='exec',
+                       flags=flags,
+                       dont_inherit=True)
+
+    _globals, _locals = fsleyesScriptEnvironment(frame,
+                                                 overlayList,
+                                                 displayCtx)
+
+    # Run the script
+    log.debug('Running {}...'.format(script))
+    eval(code, _globals, _locals)
+
+
+def fsleyesScriptEnvironment(frame, overlayList, displayCtx):
+    """Creates and returns two dictionaries, to be used as the ``globals``
+    and ``locals`` dictionaries when executing a custom FSLeyes python
+    script.
+    """
+    
+    # Set up the script environment. It's
+    # quite difficult to truly sand-box an
+    # eval'ed piece of code, but restricting
+    # the things contained in__builtins__
+    # will deter simple attack attempts.
+    _globals = {
+        '__builtins__' : {
+            'True'  : True,
+            'False' : False
+        },
+    }
+    
+    import fsl.fsleyes.views     as views
+    import fsl.fsleyes.controls  as controls
+    import fsl.data.image        as image
+    import fsl.data.featimage    as featimage
+    import fsl.data.melodicimage as melimage
+    import fsl.data.tensorimage  as tensorimage
+    import fsl.data.model        as model
+    
+    _locals = {
+        'Image'        : image.Image,
+        'FEATImage'    : featimage.FEATImage,
+        'MelodicImage' : melimage.MelodicImage,
+        'TensorImage'  : tensorimage.TensorImage,
+        'Model'        : model.Model,
+        'views'        : views,
+        'controls'     : controls,
+        'overlayList'  : overlayList,
+        'displayCtx'   : displayCtx,
+        'frame'        : frame,
+    }
+
+    return _globals, _locals
diff --git a/fsl/fsleyes/frame.py b/fsl/fsleyes/frame.py
index d71d3850d2af1acf61ff00072cba6cc9ef6e95db..c4921a49995cb51d3bc3c42802c06449cbce0a56 100644
--- a/fsl/fsleyes/frame.py
+++ b/fsl/fsleyes/frame.py
@@ -404,9 +404,13 @@ class FSLEyesFrame(wx.Frame):
         self.__makePerspectiveMenu()
 
 
-    def runScript(self):
+    def runScript(self, script=None):
         """Runs a custom python script, via a :class:`.RunScriptAction`. """
-        actions.RunScriptAction(self.__overlayList, self.__displayCtx, self)()
+        
+        rsa = actions.RunScriptAction(self.__overlayList,
+                                      self.__displayCtx,
+                                      self)
+        rsa(script)
 
         
     def __addViewPanelMenu(self, panel, title):
diff --git a/fsl/fsleyes/views/shellpanel.py b/fsl/fsleyes/views/shellpanel.py
index fc145d9fe70016b7f84cc92161174d81783e3976..36152f3e0161816e5b27a43e32ee6dd7474c3419 100644
--- a/fsl/fsleyes/views/shellpanel.py
+++ b/fsl/fsleyes/views/shellpanel.py
@@ -15,6 +15,8 @@ import wx.py.shell as wxshell
 
 from . import viewpanel
 
+import fsl.fsleyes.actions.runscript as runscript
+
 
 class ShellPanel(viewpanel.ViewPanel):
     """A ``ShellPanel`` is a :class:`.ViewPanel` which contains an
@@ -42,22 +44,19 @@ class ShellPanel(viewpanel.ViewPanel):
         """
         viewpanel.ViewPanel.__init__(self, parent, overlayList, displayCtx)
 
-        lcls = {
-            'displayCtx'  : displayCtx,
-            'overlayList' : overlayList,
-            'frame'       : frame,
-            'viewPanel'   : parent,
-        }
+        _globals, _locals = runscript.fsleyesScriptEnvironment(frame,
+                                                               overlayList,
+                                                               displayCtx)
+
 
         shell = wxshell.Shell(
             self,
             introText='   FSLEyes python shell\n\n'
-                      'Available variables are:\n'
+                      'Available FSLeyes variables are:\n'
                       '  - overlayList\n' 
                       '  - displayCtx\n'
-                      '  - frame\n'
-                      '  - viewPanel\n\n', 
-            locals=lcls,
+                      '  - frame\n',
+            locals=_locals,
             showInterpIntro=False)
 
         # TODO set up environment so that users can
@@ -66,14 +65,12 @@ class ShellPanel(viewpanel.ViewPanel):
         #
         #   - Load overlays from a URL
         #
-        #   - make plots - already possible with pylab, but make
-        #     sure it works properly (i.e. doesn't clobber the shell)
+        #   - make plots
         #
         #   - run scripts (add a 'load/run' button)
         #
         #   - open/close view panels, and manipulate existing view panels
         #   
-        shell.push('from pylab import *\n')
 
         font = shell.GetFont()
 
diff --git a/fsl/tools/fsleyes.py b/fsl/tools/fsleyes.py
index aedafad4b627b68ec61667bb2cc39ff0737659fe..1a119166adff2a9dae99bba369707af984d23992 100644
--- a/fsl/tools/fsleyes.py
+++ b/fsl/tools/fsleyes.py
@@ -66,6 +66,10 @@ def parseArgs(argv):
         add_help=False,
         formatter_class=argparse.RawDescriptionHelpFormatter)
 
+    parser.add_argument('-r', '--runscript',
+                        metavar='SCRIPTFILE',
+                        help='Run custom FSLeyes script')
+
     # TODO Dynamically generate perspective list
     # in description. To do this, you will need
     # to make fsl.utils.settings work without a
@@ -78,7 +82,10 @@ def parseArgs(argv):
         Use the '--scene' option to load a saved perspective (e.g. 'default',
         'melodic', 'feat', 'ortho', or 'lightbox').
         
-        If no '--scene' is specified, the previous layout is restored.
+        If no '--scene' is specified, the previous layout is restored, unless
+        a script is provided via the '--runscript' argument, in which case
+        it is assumed that the script sets up the scene, so the previous
+        layout is not restored.
         """)
 
     # Options for configuring the scene are
@@ -86,7 +93,8 @@ def parseArgs(argv):
     return fsleyes_parseargs.parseArgs(parser,
                                        argv,
                                        name,
-                                       description)
+                                       description,
+                                       fileOpts=['r', 'runscript'])
 
 
 def context(args, splash):
@@ -205,6 +213,7 @@ def interface(parent, args, ctx):
 
     overlayList, displayCtx, splashFrame = ctx
 
+    # Set up the frame scene (a.k.a. layout, perspective)
     # The scene argument can be:
     #
     #   - 'lightbox' or 'ortho', specifying a single view
@@ -212,13 +221,16 @@ def interface(parent, args, ctx):
     # 
     #   - The name of a saved (or built-in) perspective
     # 
-    #   - None, in which case the previous layout is restored
-    scene = args.scene
+    #   - None, in which case the previous layout is restored,
+    #     unless a custom script has been provided.
+    script = args.runscript 
+    scene  = args.scene
 
-    # If a scene or perspective has not been
-    # specified, the default behaviour is to
-    # restore the previous frame layout. 
-    restore = scene is None
+    # If a scene/perspective or custom script
+    # has not been specified, the default
+    # behaviour is to restore the previous
+    # frame layout. 
+    restore = (scene is None) and (script is None)
 
     status.update('Creating FSLeyes interface...')
     
@@ -253,17 +265,15 @@ def interface(parent, args, ctx):
     else:
         wx.CallLater(250, splashFrame.Close)
 
+    status.update('Setting up scene...')
+
     # If a perspective has been specified,
     # we load the perspective
     if args.scene is not None:
         perspectives.loadPerspective(frame, args.scene)
 
-    # The viewPanel is assumed to be a CanvasPanel 
-    # (i.e. either OrthoPanel or LightBoxPanel)
+    # Apply any view-panel specific arguments
     viewPanels = frame.getViewPanels()
-
-    status.update('Setting up scene...')
-
     for viewPanel in viewPanels:
 
         if not isinstance(viewPanel, views.CanvasPanel):
@@ -286,6 +296,15 @@ def interface(parent, args, ctx):
 
         if isinstance(viewPanel, views.OrthoPanel):
             async.idle(centre)
+
+    # If a script has been specified, we run
+    # the script. This has to be done on the
+    # idle loop, because overlays specified
+    # on the command line are loaded on the
+    # idle loop, and the script may assume
+    # that they have already been loaded.
+    if script is not None:
+        async.idle(frame.runScript, script)
             
     return frame