示例#1
0
文件: aliascmd.py 项目: bport/yokadi
 def __init__(self):
     try:
         self.aliases = eval(Config.byName("ALIASES").value)
     except SQLObjectNotFound:
         self.aliases = {}
     except Exception:
         tui.error("Aliases syntax error. Ignored")
         self.aliases = {}
示例#2
0
文件: aliascmd.py 项目: hswick/yokadi
 def __init__(self):
     try:
         self.aliases = eval(db.getConfigKey("ALIASES", environ=False))
     except NoResultFound:
         self.aliases = {}
     except Exception:
         tui.error("Aliases syntax error. Ignored")
         self.aliases = {}
示例#3
0
文件: aliascmd.py 项目: bport/yokadi
 def do_a_remove(self, line):
     """Remove an alias"""
     if line in self.aliases:
         del self.aliases[line]
         aliases = Config.selectBy(name="ALIASES")[0]
         aliases.value = repr(self.aliases)
     else:
         tui.error(
             "No alias with that name. Use a_list to display all aliases")
示例#4
0
def warnIfKeywordDoesNotExist(keywordFilters):
    """Warn user is keyword does not exist
    @return: True if at least one keyword does not exist, else False"""
    doesNotExist = False
    for keyword in [k.name for k in keywordFilters]:
        if Keyword.select(LIKE(Keyword.q.name, keyword)).count() == 0:
            tui.error("Keyword %s is unknown." % keyword)
            doesNotExist = True
    return doesNotExist
示例#5
0
def processDbPathArg(dbPath, dataDir):
    if not dbPath:
        return basepaths.getDbPath(dataDir)
    dbPath = os.path.abspath(dbPath)
    dbDir = os.path.dirname(dbPath)
    tui.warning('--db option is deprecated and will be removed in the next version, use --datadir instead')
    if not os.path.isdir(dbDir):
        tui.error("Directory '{}' does not exist".format(dbDir))
        sys.exit(1)
    return dbPath
示例#6
0
文件: aliascmd.py 项目: semtle/yokadi
 def do_a_remove(self, line):
     """Remove an alias"""
     if line in self.aliases:
         session = db.getSession()
         del self.aliases[line]
         alias = session.query(db.Alias).filter_by(name=line).one()
         session.delete(alias)
         session.commit()
     else:
         tui.error("No alias with that name. Use a_list to display all aliases")
示例#7
0
def processDataDirArg(dataDir):
    if dataDir:
        dataDir = os.path.abspath(dataDir)
        if not os.path.isdir(dataDir):
            tui.error("Directory '{}' does not exist".format(dataDir))
            sys.exit(1)
    else:
        dataDir = basepaths.getDataDir()
        os.makedirs(dataDir, exist_ok=True)
    return dataDir
示例#8
0
def processDataDirArg(dataDir):
    if dataDir:
        dataDir = os.path.abspath(dataDir)
        if not os.path.isdir(dataDir):
            tui.error("Directory '{}' does not exist".format(dataDir))
            sys.exit(1)
    else:
        dataDir = basepaths.getDataDir()
        os.makedirs(dataDir, exist_ok=True)
    return dataDir
示例#9
0
 def do_a_remove(self, line):
     """Remove an alias"""
     if line in self.aliases:
         session = db.getSession()
         del self.aliases[line]
         alias = session.query(db.Alias).filter_by(name=line).one()
         session.delete(alias)
         session.commit()
     else:
         tui.error("No alias with that name. Use a_list to display all aliases")
示例#10
0
def warnIfKeywordDoesNotExist(keywordFilters):
    """Warn user is keyword does not exist
    @return: True if at least one keyword does not exist, else False"""
    session = db.getSession()
    doesNotExist = False
    for keyword in [k.name for k in keywordFilters]:
        if session.query(Keyword).filter(Keyword.name.like(keyword)).count() == 0:
            tui.error("Keyword %s is unknown." % keyword)
            doesNotExist = True
    return doesNotExist
示例#11
0
def warnIfKeywordDoesNotExist(keywordFilters):
    """Warn user is keyword does not exist
    @return: True if at least one keyword does not exist, else False"""
    session = db.getSession()
    doesNotExist = False
    for keyword in [k.name for k in keywordFilters]:
        if session.query(Keyword).filter(
                Keyword.name.like(keyword)).count() == 0:
            tui.error("Keyword %s is unknown." % keyword)
            doesNotExist = True
    return doesNotExist
示例#12
0
    def do_k_edit(self, line):
        """Edit a keyword
        k_edit @<keyword>"""
        session = db.getSession()
        keyword = dbutils.getKeywordFromName(line)
        oldName = keyword.name
        newName = tui.editLine(oldName)
        if newName == "":
            print("Cancelled")
            return

        lst = session.query(Keyword).filter_by(name=newName).all()
        if len(lst) == 0:
            # Simple case: newName does not exist, just rename the existing keyword
            keyword.name = newName
            session.merge(keyword)
            session.commit()
            print("Keyword %s has been renamed to %s" % (oldName, newName))
            return

        # We already have a keyword with this name, we need to merge
        print("Keyword %s already exists" % newName)
        if not tui.confirm("Do you want to merge %s and %s" %
                           (oldName, newName)):
            return

        # Check we can merge
        conflictingTasks = []
        for task in keyword.tasks:
            kwDict = task.getKeywordDict()
            if oldName in kwDict and newName in kwDict and kwDict[
                    oldName] != kwDict[newName]:
                conflictingTasks.append(task)

        if len(conflictingTasks) > 0:
            # We cannot merge
            tui.error("Cannot merge keywords %s and %s because they are both"
                      " used with different values in these tasks:" %
                      (oldName, newName))
            for task in conflictingTasks:
                print("- %d, %s" % (task.id, task.title))
            print("Edit these tasks and try again")
            return

        # Merge
        for task in keyword.tasks:
            kwDict = task.getKeywordDict()
            if newName not in kwDict:
                kwDict[newName] = kwDict[oldName]
            del kwDict[oldName]
            task.setKeywordDict(kwDict)
        session.delete(keyword)
        session.commit()
        print("Keyword %s has been merged with %s" % (oldName, newName))
示例#13
0
文件: db.py 项目: hswick/yokadi
 def checkVersion(self):
     """Check version and exit if it is not suitable"""
     version = self.getVersion()
     if version != DB_VERSION:
         sharePath = os.path.abspath(utils.shareDirPath())
         tui.error("Your database version is %d but Yokadi wants version %d." \
             % (version, DB_VERSION))
         print("Please, run the %s/update/update.py script to migrate your database prior to running Yokadi" % \
                 sharePath)
         print("See %s/doc/update.md for details" % sharePath)
         sys.exit(1)
示例#14
0
文件: aliascmd.py 项目: hswick/yokadi
 def do_a_remove(self, line):
     """Remove an alias"""
     if line in self.aliases:
         session = db.getSession()
         del self.aliases[line]
         aliases = session.query(db.Config).filter_by(name="ALIASES").one()
         aliases.value = str(repr(self.aliases))
         session.add(aliases)
         session.commit()
     else:
         tui.error("No alias with that name. Use a_list to display all aliases")
示例#15
0
文件: main.py 项目: bport/yokadi
 def onecmd(self, line):
     """This method is subclassed just to be
     able to encapsulate it with a try/except bloc"""
     try:
         # Decode user input
         line = line.decode(tui.ENCODING)
         return Cmd.onecmd(self, line)
     except YokadiOptionParserNormalExitException:
         pass
     except UnicodeDecodeError, e:
         tui.error("Unicode decoding error. Please check you locale and terminal settings (%s)." % e)
示例#16
0
def processDbPathArg(dbPath, dataDir):
    if not dbPath:
        return basepaths.getDbPath(dataDir)
    dbPath = os.path.abspath(dbPath)
    dbDir = os.path.dirname(dbPath)
    tui.warning(
        '--db option is deprecated and will be removed in the next version, use --datadir instead'
    )
    if not os.path.isdir(dbDir):
        tui.error("Directory '{}' does not exist".format(dbDir))
        sys.exit(1)
    return dbPath
示例#17
0
文件: main.py 项目: bport/yokadi
 def onecmd(self, line):
     """This method is subclassed just to be
     able to encapsulate it with a try/except bloc"""
     try:
         # Decode user input
         line = line.decode(tui.ENCODING)
         return Cmd.onecmd(self, line)
     except YokadiOptionParserNormalExitException:
         pass
     except UnicodeDecodeError, e:
         tui.error(
             "Unicode decoding error. Please check you locale and terminal settings (%s)."
             % e)
示例#18
0
    def do_k_edit(self, line):
        """Edit a keyword
        k_edit @<keyword>"""
        session = db.getSession()
        keyword = dbutils.getKeywordFromName(line)
        oldName = keyword.name
        newName = tui.editLine(oldName)
        if newName == "":
            print("Cancelled")
            return

        lst = session.query(Keyword).filter_by(name=newName).all()
        if len(lst) == 0:
            # Simple case: newName does not exist, just rename the existing keyword
            keyword.name = newName
            session.merge(keyword)
            session.commit()
            print("Keyword %s has been renamed to %s" % (oldName, newName))
            return

        # We already have a keyword with this name, we need to merge
        print("Keyword %s already exists" % newName)
        if not tui.confirm("Do you want to merge %s and %s" % (oldName, newName)):
            return

        # Check we can merge
        conflictingTasks = []
        for task in keyword.tasks:
            kwDict = task.getKeywordDict()
            if oldName in kwDict and newName in kwDict and kwDict[oldName] != kwDict[newName]:
                conflictingTasks.append(task)

        if len(conflictingTasks) > 0:
            # We cannot merge
            tui.error("Cannot merge keywords %s and %s because they are both"
                      " used with different values in these tasks:" % (oldName, newName))
            for task in conflictingTasks:
                print("- %d, %s" % (task.id, task.title))
            print("Edit these tasks and try again")
            return

        # Merge
        for task in keyword.tasks:
            kwDict = task.getKeywordDict()
            if newName not in kwDict:
                kwDict[newName] = kwDict[oldName]
            del kwDict[oldName]
            task.setKeywordDict(kwDict)
        session.delete(keyword)
        session.commit()
        print("Keyword %s has been merged with %s" % (oldName, newName))
示例#19
0
文件: confcmd.py 项目: bport/yokadi
 def do_c_set(self, line):
     """Set a configuration key to value : c_set <key> <value>"""
     line = line.split()
     if len(line) < 2:
         raise BadUsageException("You should provide two arguments : the parameter key and the value")
     name = line[0]
     value = " ".join(line[1:])
     p = Config.select(AND(Config.q.name == name, Config.q.system == False))
     if p.count() == 0:
         tui.error("Sorry, no parameter match")
     else:
         if self.checkParameterValue(name, value):
             p[0].value = value
             tui.info("Parameter updated")
         else:
             tui.error("Parameter value is incorrect")
示例#20
0
def connectDatabase(dbFileName, createIfNeeded=True, memoryDatabase=False):
    """Connect to database and create it if needed
    @param dbFileName: path to database file
    @type dbFileName: str
    @param createIfNeeded: Indicate if database must be created if it does not exists (default True)
    @type createIfNeeded: bool
    @param memoryDatabase: create db in memory. Only usefull for unit test. Default is false.
    @type memoryDatabase: bool
    """

    dbFileName = os.path.abspath(dbFileName)

    if sys.platform == 'win32':
        connectionString = 'sqlite:/' + dbFileName[0] + '|' + dbFileName[2:]
    else:
        connectionString = 'sqlite:' + dbFileName

    if memoryDatabase:
        connectionString = "sqlite:/:memory:"

    connection = connectionForURI(connectionString)
    sqlhub.processConnection = connection

    if not os.path.exists(dbFileName) or memoryDatabase:
        if createIfNeeded:
            print "Creating database"
            createTables()
            # Set database version according to current yokadi release
            Config(name=DB_VERSION_KEY,
                   value=str(DB_VERSION),
                   system=True,
                   desc="Database schema release number")
        else:
            print "Database file (%s) does not exist or is not readable. Exiting" % dbFileName
            sys.exit(1)

    # Check version
    version = getVersion()
    if version != DB_VERSION:
        sharePath = os.path.abspath(utils.shareDirPath())
        tui.error("Your database version is %d but Yokadi wants version %d." \
            % (version, DB_VERSION))
        print "Please, run the %s/update/update.py script to migrate your database prior to running Yokadi" % \
                sharePath
        print "See %s/update/README.markdown for details" % sharePath
        sys.exit(1)
示例#21
0
文件: main.py 项目: kotenev/yokadi
 def onecmd(self, line):
     """This method is subclassed just to be
     able to encapsulate it with a try/except bloc"""
     try:
         # Decode user input
         line = line
         return Cmd.onecmd(self, line)
     except YokadiOptionParserNormalExitException:
         pass
     except UnicodeDecodeError as e:
         tui.error(
             "Unicode decoding error. Please check you locale and terminal settings (%s)."
             % e)
     except UnicodeEncodeError as e:
         tui.error(
             "Unicode encoding error. Please check you locale and terminal settings (%s)."
             % e)
     except BadUsageException as e:
         tui.error("*** Bad usage ***\n\t%s" % e)
         cmd = line.split(' ')[0]
         self.do_help(cmd)
     except YokadiException as e:
         tui.error("*** Yokadi error ***\n\t%s" % e)
     except IOError as e:
         # We can get I/O errors when yokadi is piped onto another shell commands
         # that breaks.
         print("*** I/O error ***\n\t%s" % e, file=sys.stderr)
     except KeyboardInterrupt:
         print("*** Break ***")
     except Exception as e:
         tui.error("Unhandled exception (oups)\n\t%s" % e)
         print("This is a bug of Yokadi, sorry.")
         print(
             "Send the above message by email to Yokadi developers ([email protected]) to help them make"
             " Yokadi better.")
         cut = "---------------------8<----------------------------------------------"
         print(cut)
         traceback.print_exc()
         print("--")
         print("Python: %s" % sys.version.replace("\n", " "))
         print("SQL Alchemy: %s" % sqlalchemy.__version__)
         print("OS: %s (%s)" % os.uname()[0:3:2])
         print("Yokadi: %s" % yokadi.__version__)
         print(cut)
         print()
示例#22
0
文件: confcmd.py 项目: bport/yokadi
 def do_c_set(self, line):
     """Set a configuration key to value : c_set <key> <value>"""
     line = line.split()
     if len(line) < 2:
         raise BadUsageException(
             "You should provide two arguments : the parameter key and the value"
         )
     name = line[0]
     value = " ".join(line[1:])
     p = Config.select(AND(Config.q.name == name, Config.q.system == False))
     if p.count() == 0:
         tui.error("Sorry, no parameter match")
     else:
         if self.checkParameterValue(name, value):
             p[0].value = value
             tui.info("Parameter updated")
         else:
             tui.error("Parameter value is incorrect")
示例#23
0
文件: db.py 项目: bugzy/yokadi
def connectDatabase(dbFileName, createIfNeeded=True, memoryDatabase=False):
    """Connect to database and create it if needed
    @param dbFileName: path to database file
    @type dbFileName: str
    @param createIfNeeded: Indicate if database must be created if it does not exists (default True)
    @type createIfNeeded: bool
    @param memoryDatabase: create db in memory. Only usefull for unit test. Default is false.
    @type memoryDatabase: bool
    """

    dbFileName = os.path.abspath(dbFileName)

    if sys.platform == 'win32':
        connectionString = 'sqlite:/' + dbFileName[0] + '|' + dbFileName[2:]
    else:
        connectionString = 'sqlite:' + dbFileName

    if memoryDatabase:
        connectionString = "sqlite:/:memory:"

    connection = connectionForURI(connectionString)
    sqlhub.processConnection = connection

    if not os.path.exists(dbFileName) or memoryDatabase:
        if createIfNeeded:
            print "Creating database"
            createTables()
            # Set database version according to current yokadi release
            Config(name=DB_VERSION_KEY, value=str(DB_VERSION), system=True, desc="Database schema release number")
        else:
            print "Database file (%s) does not exist or is not readable. Exiting" % dbFileName
            sys.exit(1)

    # Check version
    version = getVersion()
    if version != DB_VERSION:
        sharePath = os.path.abspath(utils.shareDirPath())
        tui.error("Your database version is %d but Yokadi wants version %d." \
            % (version, DB_VERSION))
        print "Please, run the %s/update/update.py script to migrate your database prior to running Yokadi" % \
                sharePath
        print "See %s/update/README.markdown for details" % sharePath
        sys.exit(1)
示例#24
0
 def parse(self, line):
     """Parse given line to create a keyword filter"""
     operators = ("=<", ">=", "!=", "<", ">", "=")
     if " " in line:
         tui.error("No space in keyword filter !")
         return
     if line.startswith("!"):
         self.negative = True
         line = line[1:]
     if not line.startswith("@"):
         tui.error("Keyword name must be be prefixed with a @")
         return
     line = line[1:]  # Squash @
     line = line.replace("==", "=")  # Tolerate == syntax
     for operator in operators:
         if operator in line:
             self.name, self.value = line.split(operator, 1)
             self.valueOperator = operator
             try:
                 self.value = int(self.value)
             except ValueError:
                 tui.error("Keyword value must be an integer (got %s)" %
                           (self.value, self.name))
                 return
             break  # Exit operator loop
     else:
         # No operator found, only keyword name has been provided
         self.name, self.value = line, None
示例#25
0
文件: main.py 项目: agateau/yokadi
 def onecmd(self, line):
     """This method is subclassed just to be
     able to encapsulate it with a try/except bloc"""
     try:
         # Decode user input
         line = line
         return Cmd.onecmd(self, line)
     except YokadiOptionParserNormalExitException:
         pass
     except UnicodeDecodeError as e:
         tui.error("Unicode decoding error. Please check you locale and terminal settings (%s)." % e)
     except UnicodeEncodeError as e:
         tui.error("Unicode encoding error. Please check you locale and terminal settings (%s)." % e)
     except BadUsageException as e:
         tui.error("*** Bad usage ***\n\t%s" % e)
         cmd = line.split(' ')[0]
         self.do_help(cmd)
     except YokadiException as e:
         tui.error("*** Yokadi error ***\n\t%s" % e)
     except IOError as e:
         # We can get I/O errors when yokadi is piped onto another shell commands
         # that breaks.
         print("*** I/O error ***\n\t%s" % e, file=sys.stderr)
     except KeyboardInterrupt:
         print("*** Break ***")
     except Exception as e:
         tui.error("Unhandled exception (oups)\n\t%s" % e)
         print("This is a bug of Yokadi, sorry.")
         print("Send the above message by email to Yokadi developers ([email protected]) to help them make"
               " Yokadi better.")
         cut = "---------------------8<----------------------------------------------"
         print(cut)
         traceback.print_exc()
         print("--")
         print("Python: %s" % sys.version.replace("\n", " "))
         print("SQL Alchemy: %s" % sqlalchemy.__version__)
         print("OS: %s (%s)" % os.uname()[0:3:2])
         print("Yokadi: %s" % yokadi.__version__)
         print(cut)
         print()
示例#26
0
 def parse(self, line):
     """Parse given line to create a keyword filter"""
     operators = ("=<", ">=", "!=", "<", ">", "=")
     if " " in line:
         tui.error("No space in keyword filter !")
         return
     if line.startswith("!"):
         self.negative = True
         line = line[1:]
     if not line.startswith("@"):
         tui.error("Keyword name must be be prefixed with a @")
         return
     line = line[1:]  # Squash @
     line = line.replace("==", "=")  # Tolerate == syntax
     for operator in operators:
         if operator in line:
             self.name, self.value = line.split(operator, 1)
             self.valueOperator = operator
             try:
                 self.value = int(self.value)
             except ValueError:
                 tui.error("Keyword value must be an integer (got %s)" %
                           (self.value, self.name))
                 return
             break  # Exit operator loop
     else:
         # No operator found, only keyword name has been provided
         self.name, self.value = line, None
示例#27
0
文件: main.py 项目: bport/yokadi
    def onecmd(self, line):
        """This method is subclassed just to be
        able to encapsulate it with a try/except bloc"""
        try:
            # Decode user input
            line = line.decode(tui.ENCODING)
            return Cmd.onecmd(self, line)
        except YokadiOptionParserNormalExitException:
            pass
        except UnicodeDecodeError, e:
            tui.error("Unicode decoding error. Please check you locale and terminal settings (%s)." % e)
        except UnicodeEncodeError, e:
            tui.error("Unicode encoding error. Please check you locale and terminal settings (%s)." % e)
        except BadUsageException, e:
            tui.error("*** Bad usage ***\n\t%s" % e)
            cmd = line.split(' ')[0]
            self.do_help(cmd)
        except YokadiException, e:
            tui.error("*** Yokadi error ***\n\t%s" % e)
        except IOError, e:
            # We can get I/O errors when yokadi is piped onto another shell commands
            # that breaks.
            print >> sys.stderr, "*** I/O error ***\n\t%s" % e
        except KeyboardInterrupt:
            print "*** Break ***"
        except Exception, e:
            tui.error("Unhandled exception (oups)\n\t%s" % e)
            print "This is a bug of Yokadi, sorry."
            print "Send the above message by email to Yokadi developers ([email protected]) to help them make Yokadi better."
            cut = "---------------------8<----------------------------------------------"
示例#28
0
文件: main.py 项目: bport/yokadi
 try:
     # Decode user input
     line = line.decode(tui.ENCODING)
     return Cmd.onecmd(self, line)
 except YokadiOptionParserNormalExitException:
     pass
 except UnicodeDecodeError, e:
     tui.error(
         "Unicode decoding error. Please check you locale and terminal settings (%s)."
         % e)
 except UnicodeEncodeError, e:
     tui.error(
         "Unicode encoding error. Please check you locale and terminal settings (%s)."
         % e)
 except BadUsageException, e:
     tui.error("*** Bad usage ***\n\t%s" % e)
     cmd = line.split(' ')[0]
     self.do_help(cmd)
 except YokadiException, e:
     tui.error("*** Yokadi error ***\n\t%s" % e)
 except IOError, e:
     # We can get I/O errors when yokadi is piped onto another shell commands
     # that breaks.
     print >> sys.stderr, "*** I/O error ***\n\t%s" % e
 except KeyboardInterrupt:
     print "*** Break ***"
 except Exception, e:
     tui.error("Unhandled exception (oups)\n\t%s" % e)
     print "This is a bug of Yokadi, sorry."
     print "Send the above message by email to Yokadi developers ([email protected]) to help them make Yokadi better."
     cut = "---------------------8<----------------------------------------------"
示例#29
0
文件: main.py 项目: bport/yokadi
class YokadiCmd(TaskCmd, ProjectCmd, KeywordCmd, ConfCmd, AliasCmd, Cmd):
    def __init__(self):
        Cmd.__init__(self)
        TaskCmd.__init__(self)
        ProjectCmd.__init__(self)
        KeywordCmd.__init__(self)
        AliasCmd.__init__(self)
        ConfCmd.__init__(self)
        self.prompt = "yokadi> "
        self.historyPath = os.getenv("YOKADI_HISTORY")
        if not self.historyPath:
            if os.name == "posix":
                self.historyPath = os.path.join(os.path.expandvars("$HOME"),
                                                ".yokadi_history")
            else:
                # Windows location
                self.historyPath = os.path.join(os.path.expandvars("$APPDATA"),
                                                ".yokadi_history")
        self.loadHistory()
        self.cryptoMgr = cryptutils.YokadiCryptoManager(
        )  # Load shared cryptographic manager

    def emptyline(self):
        """Executed when input is empty. Reimplemented to do nothing."""
        return

    def default(self, line):
        nline = resolveAlias(line, self.aliases)
        if nline != line:
            return self.onecmd(nline)
        elif nline.isdigit():
            self.do_t_show(nline)
        elif nline == "_":
            self.do_t_show(nline)
        else:
            raise YokadiException(
                "Unknown command. Use 'help' to see all available commands")

    def completedefault(self, text, line, begidx, endidx):
        """Default completion command.
        Try to see if command is an alias and find the
        appropriate complete function if it exists"""
        nline = resolveAlias(line, self.aliases)
        compfunc = getattr(self, 'complete_' + nline.split()[0])
        matches = compfunc(text, line, begidx, endidx)
        return matches

    def do_EOF(self, line):
        """Quit."""
        print
        return True

    # Some standard alias
    do_quit = do_EOF
    do_q = do_EOF
    do_exit = do_EOF

    def onecmd(self, line):
        """This method is subclassed just to be
        able to encapsulate it with a try/except bloc"""
        try:
            # Decode user input
            line = line.decode(tui.ENCODING)
            return Cmd.onecmd(self, line)
        except YokadiOptionParserNormalExitException:
            pass
        except UnicodeDecodeError, e:
            tui.error(
                "Unicode decoding error. Please check you locale and terminal settings (%s)."
                % e)
        except UnicodeEncodeError, e:
            tui.error(
                "Unicode encoding error. Please check you locale and terminal settings (%s)."
                % e)