diff --git a/module/VuFindConsole/Module.php b/module/VuFindConsole/Module.php
index 2c18e1d4d335e67f4a02c15e3a3a683f58e2d203..f5178b8345c28a36836e4bc0694618fd4ab09423 100644
--- a/module/VuFindConsole/Module.php
+++ b/module/VuFindConsole/Module.php
@@ -76,6 +76,7 @@ class Module implements \Zend\ModuleManager\Feature\ConsoleUsageProviderInterfac
     public function getConsoleUsage(Console $console)
     {
         return array(
+            'generate extendservice' => 'Override a service with a new child class',
             'harvest harvest_oai' => 'OAI-PMH harvester',
             'harvest merge-marc' => 'MARC merge tool',
             'import import-xsl' => 'XSLT importer',
diff --git a/module/VuFindConsole/config/module.config.php b/module/VuFindConsole/config/module.config.php
index 29823c90d59ee5482039ccc319cfbccb1c9e0b6e..a99e4f80cb78be6fdd7f435d51d93bedbb22dab8 100644
--- a/module/VuFindConsole/config/module.config.php
+++ b/module/VuFindConsole/config/module.config.php
@@ -4,6 +4,7 @@ namespace VuFindConsole\Module\Configuration;
 $config = array(
     'controllers' => array(
         'invokables' => array(
+            'generate' => 'VuFindConsole\Controller\GenerateController',
             'harvest' => 'VuFindConsole\Controller\HarvestController',
             'import' => 'VuFindConsole\Controller\ImportController',
             'language' => 'VuFindConsole\Controller\LanguageController',
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
new file mode 100644
index 0000000000000000000000000000000000000000..2fb40088598b7cbe69223dda36310750880a465c
--- /dev/null
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
@@ -0,0 +1,271 @@
+<?php
+/**
+ * CLI Controller Module (language tools)
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/vufind2:building_a_controller Wiki
+ */
+namespace VuFindConsole\Controller;
+use Zend\Code\Generator\ClassGenerator;
+use Zend\Code\Generator\FileGenerator;
+use Zend\Console\Console;
+
+/**
+ * This controller handles various command-line tools for dealing with language files
+ *
+ * @category VuFind2
+ * @package  Controller
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/vufind2:building_a_controller Wiki
+ */
+class GenerateController extends AbstractBase
+{
+    /**
+     * Copy one language string to another
+     *
+     * @return \Zend\Console\Response
+     */
+    public function extendserviceAction()
+    {
+        // Display help message if parameters missing:
+        $argv = $this->consoleOpts->getRemainingArgs();
+        if (!isset($argv[1])) {
+            Console::writeLine(
+                "Usage: {$_SERVER['argv'][0]} [config_path] [target_module]"
+            );
+            Console::writeLine(
+                "\tconfig_path - the path to the service in the framework config"
+            );
+            Console::writeLine("\t\te.g. controllers/invokables/generate");
+            Console::writeLine(
+                "\ttarget_module - the module where the new class will be generated"
+            );
+            return $this->getFailureResponse();
+        }
+
+        $source = $argv[0];
+        $target = $argv[1];
+
+        $parts = explode('/', $source);
+        $partCount = count($parts);
+        if ($partCount < 3) {
+            Console::writeLine("Config path too short.");
+            return $this->getFailureResponse();
+        }
+        $sourceType = $parts[$partCount - 2];
+
+        $supportedTypes = array('factories', 'invokables');
+        if (!in_array($sourceType, $supportedTypes)) {
+            Console::writeLine(
+                'Unsupported service type; supported values: '
+                . implode(', ', $supportedTypes)
+            );
+            return $this->getFailureResponse();
+        }
+
+        $config = $this->retrieveConfig($parts);
+        if (!$config) {
+            Console::writeLine("{$source} not found in configuration.");
+            return $this->getFailureResponse();
+        }
+
+        try {
+            switch ($sourceType) {
+            case 'factories':
+                $newConfig = $this->cloneFactory($config, $target);
+                break;
+            case 'invokables':
+                $newConfig = $this->createSubclassInModule($config, $target);
+                break;
+            default:
+                throw new \Exception('Reached unreachable code!');
+            }
+            $this->writeNewConfig($parts, $newConfig, $target);
+        } catch (\Exception $e) {
+            Console::writeLine($e->getMessage());
+            return $this->getFailureResponse();
+        }
+
+        return $this->getSuccessResponse();
+    }
+
+    /**
+     * Create a new subclass and factory to override a factory-generated
+     * service.
+     *
+     * @param mixed  $factory Factory configuration for class to extend
+     * @param string $module  Module in which to create the new factory
+     *
+     * @return string
+     * @throws \Exception
+     */
+    protected function cloneFactory($factory, $module)
+    {
+        throw new \Exception('Factories not supported yet.');
+    }
+
+    /**
+     * Extend a specified class within a specified module. Return the name of
+     * the new subclass.
+     *
+     * @param string $class  Name of class to extend
+     * @param string $module Module in which to create the new invokable
+     *
+     * @return string
+     * @throws \Exception
+     */
+    protected function createSubclassInModule($class, $module)
+    {
+        // Determine the name of the new class by exploding the old class and
+        // replacing the namespace:
+        $parts = explode('\\', $class);
+        if (count($parts) < 2) {
+            throw new \Exception('Expected a namespaced class; found ' . $class);
+        }
+        $parts[0] = $module;
+        $newClass = implode('\\', $parts);
+
+        // Use the class name parts from the previous step to determine a path
+        // and filename, then create the new path.
+        $filename = array_pop($parts) . '.php';
+        array_unshift($parts, 'module', $module, 'src');
+        $this->createTree($parts);
+
+        // Generate the new class:
+        $generator = FileGenerator::fromArray(
+            [
+                'classes' => [new ClassGenerator($newClass, null, null, "\\$class")]
+            ]
+        );
+        $fullPath = APPLICATION_PATH . '/' . implode('/', $parts) . '/' . $filename;
+        if (file_exists($fullPath)) {
+            throw new \Exception("$fullPath already exists.");
+        }
+        if (!file_put_contents($fullPath, $generator->generate())) {
+            throw new \Exception("Problem writing to $fullPath.");
+        }
+        Console::writeLine("Generated new file: $fullPath");
+
+        // Send back the name of the new class:
+        return $newClass;
+    }
+
+    /**
+     * Create a directory tree.
+     *
+     * @param array $path Array of subdirectories to create relative to
+     * APPLICATION_PATH
+     *
+     * @return void
+     * @throws \Exception
+     */
+    protected function createTree($path)
+    {
+        $fullPath = APPLICATION_PATH;
+        foreach ($path as $part) {
+            $fullPath .= '/' . $part;
+            if (!file_exists($fullPath)) {
+                if (!mkdir($fullPath)) {
+                    throw new \Exception("Problem creating $fullPath");
+                }
+            }
+            if (!is_dir($fullPath)) {
+                throw new \Exception("$fullPath is not a directory!");
+            }
+        }
+    }
+
+    /**
+     * Update the configuration of a target module.
+     *
+     * @param array  $path    Representation of path in config array
+     * @param string $setting New setting to write into config
+     * @param string $module  Module in which to write the configuration
+     *
+     * @return void
+     * @throws \Exception
+     */
+    protected function writeNewConfig($path, $setting, $module)
+    {
+        // Locate module configuration
+        $configPath = APPLICATION_PATH . "/module/$module/config/module.config.php";
+        if (!file_exists($configPath)) {
+            throw new \Exception("Cannot find $configPath");
+        }
+
+        // Create backup of configuration
+        $backup = $configPath . '.' . time() . '.bak';
+        if (!copy($configPath, $backup)) {
+            throw new \Exception("Problem generating backup file: $backup");
+        }
+        Console::writeLine("Created configuration backup: $backup");
+
+        $config = require $configPath;
+        $current = & $config;
+        $finalStep = array_pop($path);
+        foreach ($path as $step) {
+            if (!is_array($current)) {
+                throw new \Exception('Unexpected non-array: ' . $current);
+            }
+            if (!isset($current[$step])) {
+                $current[$step] = [];
+            }
+            $current = & $current[$step];
+        }
+        if (!is_array($current)) {
+            throw new \Exception('Unexpected non-array: ' . $current);
+        }
+        $current[$finalStep] = $setting;
+
+        $generator = FileGenerator::fromArray(
+            [
+                'body' => 'return ' . var_export($config, true) . ';'
+            ]
+        );
+        if (!file_put_contents($configPath, $generator->generate())) {
+            throw new \Exception("Cannot write to $configPath");
+        }
+        Console::writeLine("Successfully updated $configPath");
+    }
+
+    /**
+     * Retrieve a value from the application configuration (or return false
+     * if the path is not found).
+     *
+     * @param array $path Path to walk through configuration
+     *
+     * @return mixed
+     */
+    protected function retrieveConfig(array $path)
+    {
+        $config = $this->getServiceLocator()->get('config');
+        foreach ($path as $part) {
+            if (!isset($config[$part])) {
+                return false;
+            }
+            $config = $config[$part];
+        }
+        return $config;
+    }
+}