Add a command to the list of commands being watched.  "name"
        should be a unique value that can be used as a dictionary
        index.  It is typically, but not necessarily, the command
        name.  The value is returned when the command or modules for
        the command change and is used with the remove() method.

        "command_path" is a path to the command executable, which
        must be a python program.  If it includes no directory elements,
        the "path" param or the system PATH will be used to find the
        command's full path.

        If "command_path" is not specified, "name" will be used.  An
        exception is raised if the program file cannot be found.

def add(self, name, command_path=None, **params):
        """
        Add a command to the list of commands being watched.  "name"
        should be a unique value that can be used as a dictionary
        index.  It is typically, but not necessarily, the command
        name.  The value is returned when the command or modules for
        the command change and is used with the remove() method.

        "command_path" is a path to the command executable, which
        must be a python program.  If it includes no directory elements,
        the "path" param or the system PATH will be used to find the
        command's full path.

        If "command_path" is not specified, "name" will be used.  An
        exception is raised if the program file cannot be found.
    """
        log = self._getparam('log', self._discard, **params)
        module_path = self._getparam('module_path', sys.path, **params)

        if module_path:
            if type(module_path) is not list:
                if module_path.find(os.pathsep) >= 0:
                    module_path = module_path.split(os.pathsep)
                else:
                    module_path = [module_path]
        else:
            module_path = None
        log.debug("module_path: %s", str(module_path))

        if not command_path:
            command_path = name

        command = None
        if os.path.basename(command_path) == command_path:
            #  When the command has no directory elements (ie, the base name is the same as the name)
            #  search the system path.  Prefer a file that is both readable and executable but failing
            #  that, accept one that is only readable.
            #
            path_list = self._getparam('path', os.environ['PATH'], **params).split(os.pathsep)
            for dir in path_list:
                path = os.path.join(dir, command_path)
                try:
                    if os.access(path, os.R_OK|os.X_OK):
                        command = path
                        break
                except:
                    continue
            if command is None:
                for dir in path_list:
                    path = os.path.join(dir, command_path)
                    try:
                        if os.access(path, os.R_OK):
                            command = path
                            break
                    except:
                        continue
        if command is None and os.access(command_path, os.R_OK):
            command = command_path
        if command is None:
            log.error("failed to find %r", command_path)
            raise Exception("Could not locate command %r" % (command_path,))
        command = os.path.realpath(command)

        #  It would be nice if ModuleFinder() would retain state between runs so
        #  that it does not have to descend the tree for each command.  Unfortunately
        #  all state appears to be held in its "modules" attribute which then accumulates
        #  module references on each run_script() call.  That would wrongly attribute
        #  modules to subsequent scripts so we have to re-instantiate the class for
        #  each script.
        #
        finder = modulefinder.ModuleFinder(path=module_path)
        finder.run_script(command)

        #  If the name was used previously, remove all references
        #
        if name in self.names:
            log.debug("Removing existing entries for %r", name)
            self.remove(name)
        self.names[name] = command

        #  Build an inverted list of files and associated command names, starting
        #  with the command path itself.
        #
        rebuild = False
        if command in self.modules:
            if name in self.modules[command]:
                log.debug("Name %r already present in %r", name, command)
            else:
                log.debug("Command for %r added to %r", name, command)
                self.modules[command].append(name)
        else:
            log.debug("Command %r added for %r", command, name)
            self.modules[command] = [name]
            rebuild = True
        for modname, mod in list(finder.modules.items()):
            path = mod.__file__
            if not path:
                log.debug("Skipping module %r -- no __file__ in module", modname)
                continue
            path = os.path.realpath(path)
            if path in self.modules:
                if name in  self.modules[path]:
                    log.debug("Name %r already present in %r", name, command)
                else:
                    log.debug("%r added to %r", name, path)
                    self.modules[path].append(name)
            else:
                log.debug("%r added for %r", path, name)
                self.modules[path] = [name]
                rebuild = True
        if rebuild:
            self._build(name, **params)