4
\$\begingroup\$

I wanted to create an example of developer-friendly, maintainable, expandable, reusable understandable module that could be used by other programmers even though it won't. Thus, I started developing a simple terminal program engine.

This terminal engine reads a file, where each line is a separate command with its arguments, and when run prompts user's input until it matches the requirements for any of those commands and calls the method associated with that command. It get's clearer in the code, I suppose.

You can create multiple terminal engines within other terminal engines.

I would be glad if you would point out my mistakes. Do you understand this module? Would you use it if you were to create a terminal program? Would it be hard for you to maintain and expand it?

""" A simple terminal program engine. """

import os.path

class Terminal_Engine (object):
    """ A class that holds the terminal engine. """

    def __init__(self, filename, associations, welcome_str, prompt_str = ">> "):
        """ Initializes the terminal engine. """
        self.is_running = True
        self.err_msg = ""
        self.prompt = prompt_str
        self.methods = associations # Dict {"full command string" : method}
        self.parent_strs = [] # Look at Command class constructor
        # Check if file exists
        if not os.path.isfile(filename):
            self.err_msg = "error: '{}' doesn't exist.".format(filename)
            return
        # For each non-empty line try to append a command
        f = open(filename) # Why did closing a file w/out a ref become optional?
        self.commands = [Command(self, line.lower(), i) for i, line \
                         in enumerate(f) if len( line.split() ) > 0 ]
        f.close()
        # Each command has to initialize perfectly, it's safer for both of us
        for i, command in enumerate(self.commands):
            if command.err_msg != "":
                self.err_msg = "CMD on line {} failed to init.".format(i+1)
                self.err_msg += '\n' + command.err_msg
                return
        print (welcome_str)

    def run(self):
        """ Runs the terminal engine. """
        cmd = ""
        if self.err_msg == "":
            while self.is_running:
                try:
                    cmd = self.get_valid_input()
                    self.methods[cmd](self)
                except TypeError:
                    self.err_msg = "'{}' not assigned to method.".format(cmd)
                    return

    def get_valid_input(self):
        """ Prompts input until user enters valid one. """
        while True:
            raw_user_input = input(self.prompt)
            for command in self.commands:
                if command.is_valid(raw_user_input):
                    self.last_input = raw_user_input
                    return ' '.join(command.full_str)
                else:
                    print ("Invalid input.")

class Command (object):
    """ Contains command class. """

    def __init__(self, engine, line, index):
        """ Initializes the command. """
        self.err_msg = ""
        self.full_str = ""

        # N = leading_ws. This command is the Nth subcommand of main command.
        leading_ws = len(line) - len(line.lstrip())
        # If tries to attach a subcommand to a command that doesn't exist
        # MAIN
        #     BAR
        #           PIE <- trying to attach 3rd subcommand to 1st one
        if leading_ws > len(engine.parent_strs):
            engine.err_msg = "Too much indentation at line {}.".format(index+1)
            return
        # Attaching a subcommand
        # MAIN
        #     FOO      <- appending FOO
        elif leading_ws == len(engine.parent_strs):
            engine.parent_strs.append(line.split()[0])
        # Delete previous command leftovers and attach itself
        # MAIN
        #     FOO
        #         BAR
        #             PIE
        #     SUB      <- deleting FOO, BAR, PIE; appending SUB in place of FOO
        elif leading_ws < len(engine.parent_strs):
            engine.parent_strs = engine.parent_strs[:leading_ws]
            engine.parent_strs.append(line.split()[0])
        # End of the complicated part. Do you understand?

        self.full_str = list(engine.parent_strs) # Took me a while to find out
        # why 'self.full_str = engine.parent_strs' does not work.

        # Now the rest of the line is values
        self.values = [Value(str_slice) for str_slice in line.split()[1:]]
        for value in self.values:
            if value.err_msg != "":
                self.err_msg = "Wrong value at line {}.".format(index+1)
                return

    def is_valid(self, raw_user_input):
        """ Determines if user's input is valid for this command. """
        # First of all we format raw user input to be easier to manipulate
        formatted = raw_user_input.lower().split()
        # User input has to have an exact amount of slices
        if len(formatted) != len(self.full_str) + len(self.values):
            return False
        # It has to match command strings...
        for i in range(len(self.full_str)):
             if formatted[i] != self.full_str[i]:
                   return False
        # ...and values
        for i in range(len(self.full_str), len(formatted)):
            if not self.values[i-len(self.full_str)].is_valid(formatted[i]):
                return False
        # And if it passes the tests, we return True
        return True

class Value (object):
    """ Contains value class. """

    VALUES = { "string" : str, "float" : float, "int" : int }

    def __init__(self, str_segment):
        """ Initializes the value. """
        self.type_str = ""
        self.err_msg = ""
        if str_segment not in self.VALUES:
            self.err_msg = "Failed."
            return
        else:
            self.type_str = str_segment

    def is_valid(self, str_segment):
        """ Determines if specified string is valid for this value. """
        try:
            self.VALUES[self.type_str](str_segment)
            print (str_segment)
            return True
        except ValueError:
            return False

And, for a test. data.txt

exit string string
birch float int
hello
    subcommand
command
    subcommand
        subcommand string float int
function float
method
subroutine
procedure

test code, no quality, a quickie

def stuff(app):
    print ("foo")

def tree(app):
    print ("I like trees and bees. I hope I'll buy I tree someday.")

def one(app):
    app.is_running = False
    print ("goodbye")

dictionary = {
    "exit" : stuff,
    "birch" : stuff,
    "hello" : stuff,
    "hello subcommand" : stuff,
    "command" : stuff,
    "command subcommand" : stuff,
    "command subcommand subcommand" : tree,
    "function" : stuff,
    "method" : stuff,
    "subroutine" : stuff,
    "procedure" : one }
t = Terminal_Engine("data.txt", dictionary, "hello")
# You should always test for t.err_msg before running, though
t.run()
\$\endgroup\$
3
\$\begingroup\$

If the code runs it looks pretty good. Other than that the only thing is there is no support for arguments. If you were to implement that this I would use a class with a run(String[] args) function and have a dict of those.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.