From 3ecfa4288667503fcfc9c934444835e956500afe Mon Sep 17 00:00:00 2001
From: Demian Katz <demian.katz@villanova.edu>
Date: Wed, 9 Nov 2016 09:27:54 -0500
Subject: [PATCH] Eliminate custom console router. (#824)

- Uses Zend Framework console router more extensively.
- Simplifies/improves parameter processing.
- Reduces dependence on direct access to PHP superglobals.
---
 harvest/harvest_oai.php                       |   6 +-
 harvest/merge-marc.php                        |   6 +-
 import/import-xsl.php                         |   6 +-
 import/webcrawl.php                           |   6 +-
 module/VuFindConsole/config/module.config.php |  46 ++++-
 .../VuFindConsole/Controller/AbstractBase.php |  14 +-
 .../Controller/GenerateController.php         |  67 ++++---
 .../Controller/HarvestController.php          |   6 +-
 .../Controller/ImportController.php           |  44 ++---
 .../Controller/LanguageController.php         |  43 +++--
 .../Controller/RedirectController.php         | 101 +++++++++++
 .../Controller/UtilController.php             | 164 +++++++----------
 .../Mvc/Router/ConsoleRouter.php              | 167 ------------------
 .../VuFindConsole/Route/RouteGenerator.php}   |  42 +++--
 .../VuFindTest/Route/RouteGeneratorTest.php   |  85 +++++++++
 util/commit.php                               |   6 +-
 util/createHierarchyTrees.php                 |   6 +-
 util/cssBuilder.php                           |   6 +-
 util/deletes.php                              |   6 +-
 util/expire_searches.php                      |   6 +-
 util/expire_sessions.php                      |   6 +-
 util/index_reserves.php                       |   6 +-
 util/optimize.php                             |   6 +-
 util/sitemap.php                              |   6 +-
 util/suppressed.php                           |   6 +-
 25 files changed, 443 insertions(+), 420 deletions(-)
 create mode 100644 module/VuFindConsole/src/VuFindConsole/Controller/RedirectController.php
 delete mode 100644 module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php
 rename module/VuFindConsole/{tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php => src/VuFindConsole/Route/RouteGenerator.php} (54%)
 create mode 100644 module/VuFindConsole/tests/unit-tests/src/VuFindTest/Route/RouteGeneratorTest.php

diff --git a/harvest/harvest_oai.php b/harvest/harvest_oai.php
index c08364b9fcf..a364a9a31b9 100644
--- a/harvest/harvest_oai.php
+++ b/harvest/harvest_oai.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki/indexing:oai-pmh Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'harvest', 'harvest_oai');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
\ No newline at end of file
diff --git a/harvest/merge-marc.php b/harvest/merge-marc.php
index 87335875b6d..779f18766fe 100644
--- a/harvest/merge-marc.php
+++ b/harvest/merge-marc.php
@@ -27,7 +27,7 @@
  * @link     https://vufind.org/wiki/development:architecture:authority_control Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'harvest', 'merge-marc');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
\ No newline at end of file
diff --git a/import/import-xsl.php b/import/import-xsl.php
index db1230ce550..0aec5d90e4a 100644
--- a/import/import-xsl.php
+++ b/import/import-xsl.php
@@ -27,7 +27,7 @@
  * @link     https://vufind.org/wiki/indexing Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'import', 'import-xsl');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/import/webcrawl.php b/import/webcrawl.php
index 24857961c8a..f7e4e53f0da 100644
--- a/import/webcrawl.php
+++ b/import/webcrawl.php
@@ -27,7 +27,7 @@
  * @link     https://vufind.org/wiki/indexing Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'import', 'webcrawl');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/module/VuFindConsole/config/module.config.php b/module/VuFindConsole/config/module.config.php
index 5f14cab092d..d0fef6e0c1a 100644
--- a/module/VuFindConsole/config/module.config.php
+++ b/module/VuFindConsole/config/module.config.php
@@ -8,12 +8,24 @@ $config = [
             'harvest' => 'VuFindConsole\Controller\HarvestController',
             'import' => 'VuFindConsole\Controller\ImportController',
             'language' => 'VuFindConsole\Controller\LanguageController',
+            'redirect' => 'VuFindConsole\Controller\RedirectController',
             'util' => 'VuFindConsole\Controller\UtilController',
         ],
     ],
     'console' => [
         'router'  => [
-          'router_class'  => 'VuFindConsole\Mvc\Router\ConsoleRouter',
+            'routes'  => [
+                'default-route' => [
+                    'type' => 'catchall',
+                    'options' => [
+                        'route' => '',
+                        'defaults' => [
+                            'controller' => 'redirect',
+                            'action' => 'consoledefault',
+                        ],
+                    ],
+                ],
+            ],
         ],
     ],
     'view_manager' => [
@@ -22,4 +34,36 @@ $config = [
     ],
 ];
 
+$routes = [
+    'generate/dynamicroute' => 'generate dynamicroute [<name>] [<newController>] [<newAction>] [<module>]',
+    'generate/extendservice' => 'generate extendservice [<source>] [<target>]',
+    'generate/nontabrecordaction' => 'generate nontabrecordaction [<newAction>] [<module>]',
+    'generate/recordroute' => 'generate recordroute [<base>] [<newController>] [<module>]',
+    'generate/staticroute' => 'generate staticroute [<name>] [<module>]',
+    // harvest/harvest_oai is too complex to represent here; we need to rely on default-route
+    'harvest/merge-marc' => 'harvest merge-marc [<dir>]',
+    'import/import-xsl' => 'import import-xsl [--test-only] [--index=] [<xml>] [<properties>]',
+    'import/webcrawl' => 'import webcrawl [--test-only] [--index=]',
+    'language/addusingtemplate' => 'language addusingtemplate [<target>] [<template>]',
+    'language/copystring' => 'language copystring [<source>] [<target>]',
+    'language/delete' => 'language delete [<target>]',
+    'language/normalize' => 'language normalize [<target>]',
+    'util/cleanup_record_cache' => 'util (cleanuprecordcache|cleanup_record_cache) [--help|-h]',
+    'util/commit' => 'util commit [<core>]',
+    'util/createHierarchyTrees' => 'util createHierarchyTrees [--skip-xml|-sx] [--skip-json|-sj] [--help|-h]',
+    // util/cssBuilder relies on default-route because it has an arbitrary number of parameters
+    'util/deletes' => 'util deletes [--verbose] [<filename>] [<format>] [<index>]',
+    'util/expire_external_sessions' => 'util expire_external_sessions [--help|-h] [--batch=] [--sleep=] [<daysOld>]',
+    'util/expire_searches' => 'util expire_searches [--help|-h] [--batch=] [--sleep=] [<daysOld>]',
+    'util/expire_sessions' => 'util expire_sessions [--help|-h] [--batch=] [--sleep=] [<daysOld>]',
+    'util/index_reserves' => 'util index_reserves [--help|-h] [-d=s] [-t=s] [-f=s]',
+    'util/optimize' => 'util optimize [<core>]',
+    'util/sitemap' => 'util sitemap',
+    'util/suppressed' => 'util suppressed [--help|-h] [--authorities]',
+    'util/switch_db_hash' => 'util switch_db_hash [<newhash>] [<newkey>]',
+];
+
+$routeGenerator = new \VuFindConsole\Route\RouteGenerator();
+$routeGenerator->addRoutes($config, $routes);
+
 return $config;
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php b/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php
index dfc34ed0c95..0d92c43012f 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php
@@ -27,7 +27,7 @@
  * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
  */
 namespace VuFindConsole\Controller;
-use Zend\Console\Console, Zend\Console\Getopt,
+use Zend\Console\Console,
     Zend\Mvc\Controller\AbstractActionController;
 
 /**
@@ -42,13 +42,6 @@ use Zend\Console\Console, Zend\Console\Getopt,
  */
 class AbstractBase extends AbstractActionController
 {
-    /**
-     * Console options
-     *
-     * @var Getopt
-     */
-    protected $consoleOpts;
-
     /**
      * Constructor
      */
@@ -59,11 +52,8 @@ class AbstractBase extends AbstractActionController
             throw new \Exception('Access denied to command line tools.');
         }
 
-        // Get access to information about the CLI request.
-        $this->consoleOpts = new Getopt([]);
-
         // Switch the context back to the original working directory so that
-        // relative paths work as expected.  (This constant is set in
+        // relative paths work as expected. (This constant is set in
         // public/index.php)
         if (defined('ORIGINAL_WORKING_DIRECTORY')) {
             chdir(ORIGINAL_WORKING_DIRECTORY);
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
index 7058b980023..e7b47b05a34 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
@@ -50,11 +50,15 @@ class GenerateController extends AbstractBase
      */
     public function dynamicrouteAction()
     {
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[3])) {
+        $request = $this->getRequest();
+        $route = $request->getParam('name');
+        $controller = $request->getParam('newController');
+        $action = $request->getParam('newAction');
+        $module = $request->getParam('module');
+        if (empty($module)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [route] [controller] [action]"
-                . " [target_module]"
+                'Usage: ' . $request->getScriptName() . ' generate dynamicroute'
+                . ' [route] [controller] [action] [target_module]'
             );
             Console::writeLine(
                 "\troute - the route name (used by router), e.g. customList"
@@ -71,11 +75,6 @@ class GenerateController extends AbstractBase
             return $this->getFailureResponse();
         }
 
-        $route = $argv[0];
-        $controller = $argv[1];
-        $action = $argv[2];
-        $module = $argv[3];
-
         // Create backup of configuration
         $configPath = $this->getModuleConfigPath($module);
         $this->backUpFile($configPath);
@@ -98,10 +97,13 @@ class GenerateController extends AbstractBase
     public function extendserviceAction()
     {
         // Display help message if parameters missing:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[1])) {
+        $request = $this->getRequest();
+        $source = $request->getParam('source');
+        $target = $request->getParam('target');
+        if (empty($source) || empty($target)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [config_path] [target_module]"
+                'Usage: ' . $request->getScriptName() . ' generate extendservice'
+                . ' [config_path] [target_module]'
             );
             Console::writeLine(
                 "\tconfig_path - the path to the service in the framework config"
@@ -113,9 +115,6 @@ class GenerateController extends AbstractBase
             return $this->getFailureResponse();
         }
 
-        $source = $argv[0];
-        $target = $argv[1];
-
         $parts = explode('/', $source);
         $partCount = count($parts);
         if ($partCount < 3) {
@@ -166,10 +165,13 @@ class GenerateController extends AbstractBase
      */
     public function nontabrecordactionAction()
     {
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[1])) {
+        $request = $this->getRequest();
+        $action = $request->getParam('newAction');
+        $module = $request->getParam('module');
+        if (empty($action) || empty($module)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [action] [target_module]"
+                'Usage: ' . $request->getScriptName()
+                . ' generate nontabrecordaction [action] [target_module]'
             );
             Console::writeLine(
                 "\taction - new action to add"
@@ -180,9 +182,6 @@ class GenerateController extends AbstractBase
             return $this->getFailureResponse();
         }
 
-        $action = $argv[0];
-        $module = $argv[1];
-
         // Create backup of configuration
         $configPath = $this->getModuleConfigPath($module);
         $this->backUpFile($configPath);
@@ -221,10 +220,14 @@ class GenerateController extends AbstractBase
      */
     public function recordrouteAction()
     {
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[2])) {
+        $request = $this->getRequest();
+        $base = $request->getParam('base');
+        $controller = $request->getParam('newController');
+        $module = $request->getParam('module');
+        if (empty($module)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [base] [controller] [target_module]"
+                'Usage: ' . $request->getScriptName() . ' generate recordroute'
+                . ' [base] [controller] [target_module]'
             );
             Console::writeLine(
                 "\tbase - the base route name (used by router), e.g. record"
@@ -238,10 +241,6 @@ class GenerateController extends AbstractBase
             return $this->getFailureResponse();
         }
 
-        $base = $argv[0];
-        $controller = $argv[1];
-        $module = $argv[2];
-
         // Create backup of configuration
         $configPath = $this->getModuleConfigPath($module);
         $this->backUpFile($configPath);
@@ -263,10 +262,13 @@ class GenerateController extends AbstractBase
      */
     public function staticrouteAction()
     {
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[1])) {
+        $request = $this->getRequest();
+        $route = $request->getParam('name');
+        $module = $request->getParam('module');
+        if (empty($module)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [route_definition] [target_module]"
+                'Usage: ' . $request->getScriptName() . ' generate staticroute'
+                . ' [route_definition] [target_module]'
             );
             Console::writeLine(
                 "\troute_definition - a Controller/Action string, e.g. Search/Home"
@@ -277,9 +279,6 @@ class GenerateController extends AbstractBase
             return $this->getFailureResponse();
         }
 
-        $route = $argv[0];
-        $module = $argv[1];
-
         // Create backup of configuration
         $configPath = $this->getModuleConfigPath($module);
         $this->backUpFile($configPath);
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php b/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php
index bddc17f9734..bdf4ab3c1d5 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php
@@ -101,10 +101,12 @@ class HarvestController extends AbstractBase
     {
         $this->checkLocalSetting();
 
-        $argv = $this->consoleOpts->getRemainingArgs();
-        $dir = isset($argv[0]) ? rtrim($argv[0], '/') : '';
+        $dir = rtrim($this->getRequest()->getParam('dir', ''), '/');
         if (empty($dir)) {
             $scriptName = $this->getRequest()->getScriptName();
+            if (substr($scriptName, -9) === 'index.php') {
+                $scriptName .= ' harvest merge-marc';
+            }
             Console::writeLine('Merge MARC XML files into a single <collection>;');
             Console::writeLine('writes to stdout.');
             Console::writeLine('');
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php b/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php
index 91a9473e313..eb68fe4152d 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php
@@ -46,22 +46,19 @@ class ImportController extends AbstractBase
      */
     public function importXslAction()
     {
-        // Parse switches:
-        $this->consoleOpts->addRules(
-            ['test-only' => 'Use test mode', 'index-s' => 'Solr index to use']
-        );
-        $testMode = $this->consoleOpts->getOption('test-only') ? true : false;
-        $index = $this->consoleOpts->getOption('index');
-        if (empty($index)) {
-            $index = 'Solr';
-        }
-
-        // Display help message if parameters missing:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[1])) {
+        $request = $this->getRequest();
+        $testMode = $request->getParam('test-only') ? true : false;
+        $index = $request->getParam('index', 'Solr');
+        $xml = $request->getParam('xml');
+        $properties = $request->getParam('properties');
+        if (empty($properties)) {
+            $scriptName = $this->getRequest()->getScriptName();
+            if (substr($scriptName, -9) === 'index.php') {
+                $scriptName .= ' import import-xsl';
+            }
             Console::writeLine(
-                "Usage: import-xsl.php [--test-only] [--index <type>] "
-                . "XML_file properties_file"
+                "Usage: $scriptName [--test-only] [--index <type>] "
+                . 'XML_file properties_file'
             );
             Console::writeLine("\tXML_file - source file to index");
             Console::writeLine("\tproperties_file - import configuration file");
@@ -95,7 +92,7 @@ class ImportController extends AbstractBase
 
         // Try to import the document if successful:
         try {
-            $this->performImport($argv[0], $argv[1], $index, $testMode);
+            $this->performImport($xml, $properties, $index, $testMode);
         } catch (\Exception $e) {
             Console::writeLine("Fatal error: " . $e->getMessage());
             if (is_callable([$e, 'getPrevious']) && $e = $e->getPrevious()) {
@@ -107,7 +104,7 @@ class ImportController extends AbstractBase
             return $this->getFailureResponse();
         }
         if (!$testMode) {
-            Console::writeLine("Successfully imported {$argv[0]}...");
+            Console::writeLine("Successfully imported $xml...");
         }
         return $this->getSuccessResponse();
     }
@@ -137,15 +134,10 @@ class ImportController extends AbstractBase
      */
     public function webcrawlAction()
     {
-        // Parse switches:
-        $this->consoleOpts->addRules(
-            ['test-only' => 'Use test mode', 'index-s' => 'Solr index to use']
-        );
-        $testMode = $this->consoleOpts->getOption('test-only') ? true : false;
-        $index = $this->consoleOpts->getOption('index');
-        if (empty($index)) {
-            $index = 'SolrWeb';
-        }
+        // Get command line parameters:
+        $request = $this->getRequest();
+        $testMode = $request->getParam('test-only') ? true : false;
+        $index = $request->getParam('index', 'SolrWeb');
 
         $configLoader = $this->getServiceLocator()->get('VuFind\Config');
         $crawlConfig = $configLoader->get('webcrawl');
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php b/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php
index ba600720d03..deb3d9f91a1 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php
@@ -49,10 +49,13 @@ class LanguageController extends AbstractBase
     public function copystringAction()
     {
         // Display help message if parameters missing:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[1])) {
+        $request = $this->getRequest();
+        $source = $request->getParam('source');
+        $target = $request->getParam('target');
+        if (empty($source) || empty($target)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [source] [target]"
+                'Usage: ' . $request->getScriptName()
+                . ' language copystring [source] [target]'
             );
             Console::writeLine("\tsource - the source key to read");
             Console::writeLine("\ttarget - the target key to write");
@@ -64,8 +67,8 @@ class LanguageController extends AbstractBase
 
         $reader = new ExtendedIniReader();
         $normalizer = new ExtendedIniNormalizer();
-        list($sourceDomain, $sourceKey) = $this->extractTextDomain($argv[0]);
-        list($targetDomain, $targetKey) = $this->extractTextDomain($argv[1]);
+        list($sourceDomain, $sourceKey) = $this->extractTextDomain($source);
+        list($targetDomain, $targetKey) = $this->extractTextDomain($target);
 
         if (!($sourceDir = $this->getLangDir($sourceDomain))
             || !($targetDir = $this->getLangDir($targetDomain, true))
@@ -114,10 +117,13 @@ class LanguageController extends AbstractBase
     public function addusingtemplateAction()
     {
         // Display help message if parameters missing:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[1])) {
+        $request = $this->getRequest();
+        $target = $request->getParam('target');
+        $template = $request->getParam('template');
+        if (empty($template)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [target] [template]"
+                'Usage: ' . $request->getScriptName()
+                . ' language addusingtemplate [target] [template]'
             );
             Console::writeLine(
                 "\ttarget - the target key to add "
@@ -129,13 +135,12 @@ class LanguageController extends AbstractBase
         }
 
         // Make sure a valid target has been specified:
-        list($targetDomain, $targetKey) = $this->extractTextDomain($argv[0]);
+        list($targetDomain, $targetKey) = $this->extractTextDomain($target);
         if (!($targetDir = $this->getLangDir($targetDomain, true))) {
             return $this->getFailureResponse();
         }
 
         // Extract required source values from template:
-        $template = $argv[1];
         preg_match_all('/\|\|[^|]+\|\|/', $template, $matches);
         $lookups = [];
         foreach ($matches[0] as $current) {
@@ -208,10 +213,11 @@ class LanguageController extends AbstractBase
     public function deleteAction()
     {
         // Display help message if parameters missing:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[0])) {
+        $request = $this->getRequest();
+        $target = $request->getParam('target');
+        if (empty($target)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [target]"
+                'Usage: ' . $request->getScriptName() . ' language delete [target]'
             );
             Console::writeLine(
                 "\ttarget - the target key to remove "
@@ -221,7 +227,7 @@ class LanguageController extends AbstractBase
         }
 
         $normalizer = new ExtendedIniNormalizer();
-        list($domain, $key) = $this->extractTextDomain($argv[0]);
+        list($domain, $key) = $this->extractTextDomain($target);
         $target = $key . ' = "';
 
         if (!($dir = $this->getLangDir($domain))) {
@@ -258,17 +264,18 @@ class LanguageController extends AbstractBase
     public function normalizeAction()
     {
         // Display help message if parameters missing:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (!isset($argv[0])) {
+        $request = $this->getRequest();
+        $target = $request->getParam('target');
+        if (empty($target)) {
             Console::writeLine(
-                "Usage: {$_SERVER['argv'][0]} [target]"
+                'Usage: ' . $request->getScriptName()
+                . ' language normalize [target]'
             );
             Console::writeLine("\ttarget - a file or directory to normalize");
             return $this->getFailureResponse();
         }
 
         $normalizer = new ExtendedIniNormalizer();
-        $target = $argv[0];
         if (is_dir($target)) {
             $normalizer->normalizeDirectory($target);
         } else if (is_file($target)) {
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/RedirectController.php b/module/VuFindConsole/src/VuFindConsole/Controller/RedirectController.php
new file mode 100644
index 00000000000..517c23b57aa
--- /dev/null
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/RedirectController.php
@@ -0,0 +1,101 @@
+<?php
+/**
+ * Redirect Controller
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2016.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Controller
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
+ */
+namespace VuFindConsole\Controller;
+use Zend\Console\Console;
+use Zend\Mvc\Application;
+
+/**
+ * This controller handles various command-line tools
+ *
+ * @category VuFind
+ * @package  Controller
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
+ */
+class RedirectController extends \Zend\Mvc\Controller\AbstractActionController
+{
+    /**
+     * Get a usage message with the help of the RouteNotFoundStrategy.
+     *
+     * @return mixed
+     */
+    protected function getUsage()
+    {
+        $strategy = $this->getServiceLocator()->get('ViewManager')
+            ->getRouteNotFoundStrategy();
+        $event = $this->getEvent();
+        $event->setError(Application::ERROR_ROUTER_NO_MATCH);
+        $strategy->handleRouteNotFoundError($event);
+        return $event->getResult();
+    }
+
+    /**
+     * Use the first two command line parameters to redirect the user to an
+     * appropriate controller.
+     *
+     * @return mixed
+     */
+    public function consoledefaultAction()
+    {
+        // We need to modify the $_SERVER superglobals so that \Zend\Console\GetOpt
+        // will behave correctly after we've manipulated the CLI parameters. Let's
+        // use references for convenience.
+        $argv = & $_SERVER['argv'];
+        $argc = & $_SERVER['argc'];
+
+        // Pull the script name off the front of the argument array:
+        $script = array_shift($argv);
+
+        // Fail if we don't have at least two arguments (controller/action):
+        if ($argc < 2) {
+            return $this->getUsage();
+        }
+
+        // Pull off the controller and action.
+        $controller = array_shift($argv);
+        $action = array_shift($argv);
+
+        // In case later scripts are displaying $argv[0] for the script name,
+        // let's push the full invocation into that position when index.php is
+        // used. We want to eliminate the $controller and $action values as separate
+        // parts of the array since they'll confuse subsequent parameter processing.
+        if (substr($script, -9) === 'index.php') {
+            $script .= " $controller $action";
+        }
+        array_unshift($argv, $script);
+        $argc -= 2;
+
+        try {
+            return $this->forward()->dispatch($controller, compact('action'));
+        } catch (\Exception $e) {
+            Console::writeLine('ERROR: ' . $e->getMessage());
+            return $this->getUsage();
+        }
+    }
+}
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
index 532f0f8ce3f..f0023bd3506 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
@@ -93,7 +93,7 @@ class UtilController extends AbstractBase
         Console::writeLine(
             '     Default template is BIB_ID,COURSE,INSTRUCTOR,DEPARTMENT'
         );
-        Console::writeLine(' -h or -? display this help information.');
+        Console::writeLine(' -h or --help display this help information.');
 
         return $this->getFailureResponse();
     }
@@ -108,26 +108,14 @@ class UtilController extends AbstractBase
         ini_set('memory_limit', '50M');
         ini_set('max_execution_time', '3600');
 
-        $this->consoleOpts->setOptions(
-            [\Zend\Console\Getopt::CONFIG_CUMULATIVE_PARAMETERS => true]
-        );
-        $this->consoleOpts->addRules(
-            [
-                'h|help' => 'Get help',
-                'd-s' => 'Delimiter',
-                't-s' => 'Template',
-                'f-s' => 'File',
-            ]
-        );
+        $request = $this->getRequest();
 
-        if ($this->consoleOpts->getOption('h')
-            || $this->consoleOpts->getOption('help')
-        ) {
+        if ($request->getParam('h') || $request->getParam('help')) {
             return $this->indexReservesHelp();
-        } elseif ($file = $this->consoleOpts->getOption('f')) {
+        } elseif ($file = $request->getParam('f')) {
             try {
-                $delimiter = ($d = $this->consoleOpts->getOption('d')) ? $d : ',';
-                $template = ($t = $this->consoleOpts->getOption('t')) ? $t : null;
+                $delimiter = $request->getParam('d', ',');
+                $template = $request->getParam('t');
                 $reader = new \VuFind\Reserves\CsvReader(
                     $file, $delimiter, $template
                 );
@@ -138,9 +126,9 @@ class UtilController extends AbstractBase
             } catch (\Exception $e) {
                 return $this->indexReservesHelp($e->getMessage());
             }
-        } elseif ($this->consoleOpts->getOption('d')) {
+        } elseif ($request->getParam('d')) {
             return $this->indexReservesHelp('-d is meaningless without -f');
-        } elseif ($this->consoleOpts->getOption('t')) {
+        } elseif ($request->getParam('t')) {
             return $this->indexReservesHelp('-t is meaningless without -f');
         } else {
             try {
@@ -270,10 +258,8 @@ class UtilController extends AbstractBase
         ini_set('memory_limit', '50M');
         ini_set('max_execution_time', '3600');
 
-        // Setup Solr Connection -- Allow core to be specified as first command line
-        // param.
-        $argv = $this->consoleOpts->getRemainingArgs();
-        $core = isset($argv[0]) ? $argv[0] : 'Solr';
+        // Setup Solr Connection -- Allow core to be specified from command line.
+        $core = $this->getRequest()->getParam('core', 'Solr');
 
         // Commit and Optimize the Solr Index
         $solr = $this->getServiceLocator()->get('VuFind\Solr\Writer');
@@ -314,23 +300,22 @@ class UtilController extends AbstractBase
         // Parse the command line parameters -- check verbosity, see if we are in
         // "flat file" mode, find out what file we are reading in, and determine
         // the index we are affecting!
-        $this->consoleOpts->addRules(
-            [
-                'verbose' => 'Verbose mode',
-            ]
-        );
-        $verbose = $this->consoleOpts->getOption('verbose');
-        $argv = $this->consoleOpts->getRemainingArgs();
-        $filename = isset($argv[0]) ? $argv[0] : null;
-        $mode = isset($argv[1]) ? $argv[1] : 'marc';
-        $index = isset($argv[2]) ? $argv[2] : 'Solr';
+        $request = $this->getRequest();
+        $verbose = $request->getParam('verbose');
+        $filename = $request->getParam('filename');
+        $mode = $request->getParam('format', 'marc');
+        $index = $request->getParam('index', 'Solr');
 
         // No filename specified?  Give usage guidelines:
         if (empty($filename)) {
+            $scriptName = $this->getRequest()->getScriptName();
+            if (substr($scriptName, -9) === 'index.php') {
+                $scriptName .= ' util deletes';
+            }
             Console::writeLine("Delete records from VuFind's index.");
             Console::writeLine('');
             Console::writeLine(
-                'Usage: deletes.php [--verbose] FILENAME FORMAT INDEX'
+                'Usage: ' . $scriptName . ' [--verbose] FILENAME FORMAT INDEX'
             );
             Console::writeLine('');
             Console::writeLine(
@@ -431,15 +416,8 @@ class UtilController extends AbstractBase
      */
     public function cleanuprecordcacheAction()
     {
-        $this->consoleOpts->addRules(
-            [
-                'h|help' => 'Get help',
-            ]
-        );
-
-        if ($this->consoleOpts->getOption('h')
-            || $this->consoleOpts->getOption('help')
-        ) {
+        $request = $this->getRequest();
+        if ($request->getParam('help') || $request->getParam('h')) {
             Console::writeLine('Clean up unused cached records from the database.');
             return $this->getFailureResponse();
         }
@@ -494,15 +472,8 @@ class UtilController extends AbstractBase
      */
     public function expiresearchesAction()
     {
-        $this->consoleOpts->addRules(
-            [
-                'h|help' => 'Get help',
-                'batch=i' => 'Batch size',
-                'sleep=i' => 'Sleep interval between batches'
-            ]
-        );
-
-        if ($this->consoleOpts->getOption('h')) {
+        $request = $this->getRequest();
+        if ($request->getParam('help') || $request->getParam('h')) {
             return $this->expirationHelp('searches');
         }
 
@@ -521,15 +492,8 @@ class UtilController extends AbstractBase
      */
     public function expiresessionsAction()
     {
-        $this->consoleOpts->addRules(
-            [
-                'h|help' => 'Get help',
-                'batch=i' => 'Batch size',
-                'sleep=i' => 'Sleep interval between batches'
-            ]
-        );
-
-        if ($this->consoleOpts->getOption('h')) {
+        $request = $this->getRequest();
+        if ($request->getParam('help') || $request->getParam('h')) {
             return $this->expirationHelp('sessions');
         }
 
@@ -548,15 +512,8 @@ class UtilController extends AbstractBase
      */
     public function expireExternalSessionsAction()
     {
-        $this->consoleOpts->addRules(
-            [
-                'h|help' => 'Get help',
-                'batch=i' => 'Batch size',
-                'sleep=i' => 'Sleep interval between batches'
-            ]
-        );
-
-        if ($this->consoleOpts->getOption('h')) {
+        $request = $this->getRequest();
+        if ($request->getParam('help') || $request->getParam('h')) {
             return $this->expirationHelp('external sessions');
         }
 
@@ -574,15 +531,19 @@ class UtilController extends AbstractBase
      */
     public function suppressedAction()
     {
+        $request = $this->getRequest();
+        if ($request->getParam('help') || $request->getParam('h')) {
+            Console::writeLine('Available switches:');
+            Console::writeLine(
+                '--authorities =>'
+                . ' Delete authority records instead of bibliographic records'
+            );
+            Console::writeLine('--help or -h => Show this message');
+            return $this->getFailureResponse();
+        }
+
         // Setup Solr Connection
-        $this->consoleOpts->addRules(
-            [
-                'authorities' =>
-                    'Delete authority records instead of bibliographic records'
-            ]
-        );
-        $backend = $this->consoleOpts->getOption('authorities')
-            ? 'SolrAuth' : 'Solr';
+        $backend = $request->getParam('authorities') ? 'SolrAuth' : 'Solr';
 
         // Make ILS Connection
         try {
@@ -619,17 +580,23 @@ class UtilController extends AbstractBase
      */
     public function createhierarchytreesAction()
     {
+        $request = $this->getRequest();
+        if ($request->getParam('help') || $request->getParam('h')) {
+            Console::writeLine('Available switches:');
+            Console::writeLine('--skip-xml or -sx => Skip the XML cache');
+            Console::writeLine('--skip-json or -sj => Skip the JSON cache');
+            Console::writeLine('--help or -h => Show this message');
+            return $this->getFailureResponse();
+        }
+        $skipJson = $request->getParam('skip-json') || $request->getParam('sj');
+        $skipXml = $request->getParam('skip-xml') || $request->getParam('sx');
         $recordLoader = $this->getServiceLocator()->get('VuFind\RecordLoader');
-        // Parse switches:
-        $this->consoleOpts->addRules(
-            [
-                'skip-xml|sx' => 'Skip the XML cache',
-                'skip-json|sj' => 'Skip the JSON cache'
-            ]
-        );
         $hierarchies = $this->getServiceLocator()
             ->get('VuFind\SearchResultsPluginManager')->get('Solr')
             ->getFullFieldFacets(['hierarchy_top_id']);
+        if (!isset($hierarchies['hierarchy_top_id']['data']['list'])) {
+            $hierarchies['hierarchy_top_id']['data']['list'] = [];
+        }
         foreach ($hierarchies['hierarchy_top_id']['data']['list'] as $hierarchy) {
             $recordid = $hierarchy['value'];
             $count = $hierarchy['count'];
@@ -645,7 +612,7 @@ class UtilController extends AbstractBase
                 // Only do this if the record is actually a hierarchy type record
                 if ($driver->getHierarchyType()) {
                     // JSON
-                    if (!$this->consoleOpts->getOption('skip-json')) {
+                    if (!$skipJson) {
                         Console::writeLine("\t\tJSON cache...");
                         $driver->getHierarchyDriver()->getTreeSource()->getJSON(
                             $recordid, ['refresh' => true]
@@ -654,7 +621,7 @@ class UtilController extends AbstractBase
                         Console::writeLine("\t\tJSON skipped.");
                     }
                     // XML
-                    if (!$this->consoleOpts->getOption('skip-xml')) {
+                    if (!$skipXml) {
                         Console::writeLine("\t\tXML cache...");
                         $driver->getHierarchyDriver()->getTreeSource()->getXML(
                             $recordid, ['refresh' => true]
@@ -683,12 +650,12 @@ class UtilController extends AbstractBase
      */
     public function cssbuilderAction()
     {
-        $argv = $this->consoleOpts->getRemainingArgs();
+        $opts = new \Zend\Console\Getopt([]);
         $compiler = new \VuFindTheme\LessCompiler(true);
         $cacheManager = $this->getServiceLocator()->get('VuFind\CacheManager');
         $cacheDir = $cacheManager->getCacheDir() . 'less/';
         $compiler->setTempPath($cacheDir);
-        $compiler->compile($argv);
+        $compiler->compile(array_unique($opts->getRemainingArgs()));
         return $this->getSuccessResponse();
     }
 
@@ -706,15 +673,14 @@ class UtilController extends AbstractBase
     protected function expire($tableName, $successString, $failString, $minAge = 2)
     {
         // Get command-line arguments
-        $argv = $this->consoleOpts->getRemainingArgs();
+        $request = $this->getRequest();
 
         // Use command line value as expiration age, or default to $minAge.
-        $daysOld = isset($argv[0]) ? intval($argv[0]) : $minAge;
+        $daysOld = intval($request->getParam('daysOld', $minAge));
 
         // Use command line values for batch size and sleep time if specified.
-        $options = $this->consoleOpts->getArguments();
-        $batchSize = isset($options['batch']) ? $options['batch'] : 1000;
-        $sleepTime = isset($options['sleep']) ? $options['sleep'] : 100;
+        $batchSize = $request->getParam('batch', 1000);
+        $sleepTime = $request->getParam('sleep', 100);
 
         // Abort if we have an invalid expiration age.
         if ($daysOld < 2) {
@@ -781,8 +747,9 @@ class UtilController extends AbstractBase
     public function switchdbhashAction()
     {
         // Validate command line arguments:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (count($argv) < 1) {
+        $request = $this->getRequest();
+        $newhash = $request->getParam('newhash');
+        if (empty($newhash)) {
             Console::writeLine(
                 'Expected parameters: newmethod [newkey]'
             );
@@ -804,8 +771,7 @@ class UtilController extends AbstractBase
         }
 
         // Pull new encryption settings from arguments:
-        $newhash = $argv[0];
-        $newkey = isset($argv[1]) ? $argv[1] : $oldkey;
+        $newkey = $request->getParam('newkey', $oldkey);
 
         // No key specified AND no key on file = fatal error:
         if ($newkey === null) {
diff --git a/module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php b/module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php
deleted file mode 100644
index a1503295c2f..00000000000
--- a/module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php
+++ /dev/null
@@ -1,167 +0,0 @@
-<?php
-/**
- * VuFind Console Router
- *
- * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category VuFind
- * @package  Mvc_Router
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org Main Site
- */
-namespace VuFindConsole\Mvc\Router;
-use Zend\Mvc\Router\Http\RouteMatch,
-    Zend\Stdlib\RequestInterface as Request;
-use Zend\Mvc\Router\Console\SimpleRouteStack;
-
-/**
- * VuFind Console Router
- *
- * @category VuFind
- * @package  Mvc_Router
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org Main Site
- */
-class ConsoleRouter extends SimpleRouteStack
-{
-    /**
-     * Present working directory
-     *
-     * @var string
-     */
-    protected $pwd = '';
-
-    /**
-     * Extract controller and action from command line when user directly accesses
-     * the index.php file.
-     *
-     * @return array Array with 0 => controller, 1 => action
-     */
-    protected function extractFromCommandLine()
-    {
-        // We need to modify the $_SERVER superglobals so that \Zend\Console\GetOpt
-        // will behave correctly after we've manipulated the CLI parameters. Let's
-        // use references for convenience.
-        $argv = & $_SERVER['argv'];
-        $argc = & $_SERVER['argc'];
-
-        // Pull the script name off the front of the argument array:
-        $script = array_shift($argv);
-
-        // Pull off the controller and action; if they are missing, we'll fill
-        // in dummy values for consistency's sake, which also requires us to
-        // adjust the argument count.
-        if ($argc > 0) {
-            $controller = array_shift($argv);
-        } else {
-            $controller = "undefined";
-            $argc++;
-        }
-        if ($argc > 1) {
-            $action = array_shift($argv);
-        } else {
-            $action = "undefined";
-            $argc++;
-        }
-
-        // In case later scripts are displaying $argv[0] for the script name,
-        // let's push the full invocation into that position. We want to eliminate
-        // the $controller and $action values as separate parts of the array since
-        // they'll confuse subsequent command line processing logic.
-        array_unshift($argv, "$script $controller $action");
-        $argc -= 2;
-
-        return [$controller, $action];
-    }
-
-    /**
-     * Check CLIDIR
-     *
-     * @return string
-     */
-    public function getCliDir()
-    {
-        if ($this->pwd == '' && defined('CLI_DIR')) {
-            $this->pwd = CLI_DIR;
-        }
-        return $this->pwd;
-    }
-
-    /**
-     * Set CLIDIR (used primarily for testing)
-     *
-     * @param string $pwd Present directory
-     *
-     * @return void
-     */
-    public function setCliDir($pwd)
-    {
-        $this->pwd = $pwd;
-    }
-
-    /**
-     * Legacy handling for scripts: Match a given request.
-     *
-     * @param Request $request Request to match
-     *
-     * @return RouteMatch
-     */
-    public function match(Request $request)
-    {
-        // Get command line arguments and present working directory from
-        // server superglobal:
-        $filename = $request->getScriptName();
-
-        // WARNING: cwd is $VUFIND_HOME, so that throws off realpath!
-        //
-        // Convert base filename (minus .php extension and underscores) and
-        // containing directory name into action and controller, respectively:
-        $base = basename($filename, ".php");
-        $actionName = str_replace('_', '', $base);      // action is the easy part
-
-        $dir = dirname($filename);
-        if ($dir == false || $dir == '' || $dir == '.' || basename($dir) == '.') {
-            // legacy style: cd to subdir, but set CLI_DIR
-            $dir  = $this->getCliDir();
-            $path = $dir . '/' . $filename;
-        } else {
-            // modern style: invoked as, e.g. $base=util/ping.php, already has path
-            $level1 = basename(dirname($filename));
-            // but we need to re-orient relative to VUFIND_HOME
-            $path   = $level1 . '/' . basename($filename);
-        }
-        $controller = basename($dir);       // the last directory part
-
-        // Special case: if user is accessing index.php directly, we expect
-        // controller and action as first two parameters.
-        if ($controller == 'public' && $actionName == 'index') {
-            list($controller, $actionName) = $this->extractFromCommandLine();
-        }
-
-        $routeMatch = new RouteMatch(
-            ['controller' => $controller, 'action' => $actionName], 1
-        );
-
-        // Override standard routing:
-        $routeMatch->setMatchedRouteName('default');
-        return $routeMatch;
-    }
-
-}
diff --git a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php b/module/VuFindConsole/src/VuFindConsole/Route/RouteGenerator.php
similarity index 54%
rename from module/VuFindConsole/tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php
rename to module/VuFindConsole/src/VuFindConsole/Route/RouteGenerator.php
index 34dac835f5d..0ac97026acd 100644
--- a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php
+++ b/module/VuFindConsole/src/VuFindConsole/Route/RouteGenerator.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * ConsoleRouter Test Class
+ * Route Generator Class
  *
  * PHP version 5
  *
@@ -20,39 +20,43 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
- * @package  Tests
+ * @package  Route
  * @author   Demian Katz <demian.katz@villanova.edu>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ * @link     https://vufind.org/wiki/development Wiki
  */
-namespace VuFindTest\Mvc\Router;
-use VuFindConsole\Mvc\Router\ConsoleRouter;
+namespace VuFindConsole\Route;
 
 /**
- * InjectTemplateListener Test Class
+ * Route Generator Class
  *
  * @category VuFind
- * @package  Tests
+ * @package  Route
  * @author   Demian Katz <demian.katz@villanova.edu>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ * @link     https://vufind.org/wiki/development Wiki
  */
-class ConsoleRouterTest extends \PHPUnit_Framework_TestCase
+class RouteGenerator
 {
     /**
-     * Test routing.
+     * Add console routes to the configuration.
+     *
+     * @param array $config Configuration array to update
+     * @param array $routes Array of Controller/Action strings => route values
      *
      * @return void
      */
-    public function testRoute()
+    public function addRoutes(& $config, $routes)
     {
-        $router = ConsoleRouter::factory();
-        $router->setCliDir(__DIR__);
-        $request = $this->getMock('Zend\Console\Request', ['getScriptName']);
-        $request->expects($this->any())->method('getScriptName')
-            ->will($this->returnValue('ConsoleRouterTest.php'));
-        $result = $router->match($request);
-        $this->assertEquals($result->getParam('controller'), 'Router');
-        $this->assertEquals($result->getParam('action'), 'ConsoleRouterTest');
+        foreach ($routes as $key => $route) {
+            list($controller, $action) = explode('/', $key);
+            $name = $controller . '-' . $action;
+            $config['console']['router']['routes'][$name] = [
+                'options' => [
+                    'route' => $route,
+                    'defaults' => compact('controller', 'action'),
+                ]
+            ];
+        }
     }
 }
diff --git a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Route/RouteGeneratorTest.php b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Route/RouteGeneratorTest.php
new file mode 100644
index 00000000000..dfff0982e36
--- /dev/null
+++ b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Route/RouteGeneratorTest.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * Route generator tests.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2016.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+namespace VuFindConsoleTest\Route;
+use VuFindConsole\Route\RouteGenerator;
+
+/**
+ * Route generator tests.
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+class CacheTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Test route generation
+     *
+     * @return void
+     */
+    public function testGeneration()
+    {
+        $config = [];
+        $routes = [
+            'controller1/action1' => 'controller1 action1',
+            'controller2/action2' => 'controller2 action2',
+        ];
+        $generator = new RouteGenerator();
+        $generator->addRoutes($config, $routes);
+        $expected = [
+            'console' => [
+                'router' => [
+                    'routes' => [
+                        'controller1-action1' => [
+                            'options' => [
+                                'route' => 'controller1 action1',
+                                'defaults' => [
+                                    'controller' => 'controller1',
+                                    'action' => 'action1',
+                                ],
+                            ],
+                        ],
+                        'controller2-action2' => [
+                            'options' => [
+                                'route' => 'controller2 action2',
+                                'defaults' => [
+                                    'controller' => 'controller2',
+                                    'action' => 'action2',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        $this->assertEquals($expected, $config);
+    }
+}
diff --git a/util/commit.php b/util/commit.php
index deb839887bc..c5ff187b38b 100644
--- a/util/commit.php
+++ b/util/commit.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki/performance#index_optimization Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'commit');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/createHierarchyTrees.php b/util/createHierarchyTrees.php
index 7b134cb725d..a9fdb0596cc 100644
--- a/util/createHierarchyTrees.php
+++ b/util/createHierarchyTrees.php
@@ -32,7 +32,7 @@
  * @link     https://vufind.org/wiki Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'createHierarchyTrees');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/cssBuilder.php b/util/cssBuilder.php
index 11896591891..b2e0e26a2d2 100644
--- a/util/cssBuilder.php
+++ b/util/cssBuilder.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'cssBuilder');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/deletes.php b/util/deletes.php
index fd13511b0e1..9b68a09479a 100644
--- a/util/deletes.php
+++ b/util/deletes.php
@@ -27,7 +27,7 @@
  * @link     https://vufind.org/wiki/automation Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'deletes');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/expire_searches.php b/util/expire_searches.php
index 098a169e0d1..c1275ff59cb 100644
--- a/util/expire_searches.php
+++ b/util/expire_searches.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/jira/browse/VUFIND-235 JIRA Ticket
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'expire_searches');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/expire_sessions.php b/util/expire_sessions.php
index 06db2c4c6d4..92a46f27a81 100644
--- a/util/expire_sessions.php
+++ b/util/expire_sessions.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/jira/browse/VUFIND-235 JIRA Ticket
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'expire_sessions');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/index_reserves.php b/util/index_reserves.php
index 02dcea582ec..546ad8c5f07 100644
--- a/util/index_reserves.php
+++ b/util/index_reserves.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'index_reserves');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/optimize.php b/util/optimize.php
index 5e8cef74d0a..3436f6d263f 100644
--- a/util/optimize.php
+++ b/util/optimize.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki/performance#index_optimization Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'optimize');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/sitemap.php b/util/sitemap.php
index d54eeaebd59..756192e965e 100644
--- a/util/sitemap.php
+++ b/util/sitemap.php
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki/search_engine_optimization Wiki
  */
  
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'sitemap');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
diff --git a/util/suppressed.php b/util/suppressed.php
index 5ca75afcf16..65239ad84a7 100644
--- a/util/suppressed.php
+++ b/util/suppressed.php
@@ -27,7 +27,7 @@
  * @link     https://vufind.org/wiki/automation Wiki
  */
 
-// Load the Zend framework -- this will automatically trigger the appropriate
-// controller action based on directory and file names
-define('CLI_DIR', __DIR__);     // save directory name of current script
+// Manipulate command line to load correct route, then load Zend Framework:
+array_unshift($_SERVER['argv'], array_shift($_SERVER['argv']), 'util', 'suppressed');
+$_SERVER['argc'] += 2;
 require_once __DIR__ . '/../public/index.php';
-- 
GitLab