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()
asked Apr 17, 2015 at 14:23
\$\endgroup\$

1 Answer 1

2
\$\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.

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
answered Apr 17, 2015 at 15:05
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.