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()
1 Answer 1
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.