From ff1fb5570b996c7c1080b71032dd37fbda8cc284 Mon Sep 17 00:00:00 2001 From: Patrick Marsee Date: Fri, 8 Feb 2019 13:40:21 -0500 Subject: [PATCH] Added the actual package --- .gitignore | 1 + __init__.py | 1 + __main__.py | 6 ++ shell.py | 284 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 __main__.py create mode 100644 shell.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61f2dc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/__pycache__/ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a0acae5 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .shell import * diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..b522eb0 --- /dev/null +++ b/__main__.py @@ -0,0 +1,6 @@ +from .shell import * +print("""NOTE: this is a base shell with only the default commands.\n +If you want to actually do things with a shell, import the shell package\n +and instantiate a shell.Shell or derive a class from it.""") +s = Shell() +s.run() diff --git a/shell.py b/shell.py new file mode 100644 index 0000000..2f893c5 --- /dev/null +++ b/shell.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# gameshell.py +# +# Copyright 2018 chees +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# Ver. 0.1.0033 + + +import types as _types +import traceback as _tb +import math as _math +import re as _re + +_CONV24TO8 = 6 / 256 + +class Shell(object): + + def __init__(self): + self.__commands = {'exit': self.exitShell, 'batch': self.batch, 'alias': self.makeAlias, 'def': self.defineBatch} + self.__aliases = {} + self.__batches = {} + self.ps1 = '> ' + self.ps2 = '. ' + self.colorMode = 0 # bits of color depth. Supports: 0, 3, 4, 8, 24 + self.prevColor = '\x1b[0m' + self.__exit = False + + def __color24(self, r, g, b, fg = True): + if fg: + return '\x1b[38;2;{0};{1};{2}m'.format(r, g, b) + else: + return '\x1b[48;2;{0};{1};{2}m'.format(r, g, b) + + def __color8(self, r, g, b, fg = True): + r = _math.floor(r * _CONV24TO8) + g = _math.floor(g * _CONV24TO8) + b = _math.floor(b * _CONV24TO8) + ret = 16 + b + 6 * g + 36 * r + if fg: + return '\x1b[38;5;{0}m'.format(ret) + else: + return '\x1b[48;5;{0}m'.format(ret) + + def __color4(self, r, g, b, fg = True): + color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128) + r2, g2, b2 = r % 128, g % 128, b % 128 + if r2 + g2 + b2 >= 192: + color += 60 + if fg: + color += 30 + else: + color += 40 + return '\x1b[{0}m'.format(color) + + def __color3(self, r, g, b, fg = True): + color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128) + if fg: + color += 30 + else: + color += 40 + return '\x1b[{0}m'.format(color) + + def colorFromHex(self, color): + """expects string formmatted like 3377DD""" + return int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16) + + def color(self, r, g = 0, b = 0, fg = True, setPrevColor = True): + if isinstance(r, str): + r, g, b = self.colorFromHex(r) + ret = '' + if self.colorMode == 0: # no color + ret = '' + elif self.colorMode == 3: + ret = self.__color3(r, g, b, fg) + elif self.colorMode == 4: + ret = self.__color4(r, g, b, fg) + elif self.colorMode == 8: + ret = self.__color8(r, g, b, fg) + elif self.colorMode == 24: + ret = self.__color24(r, g, b, fg) + else: + ret = '' + if ret == self.prevColor: + return '' + if setPrevColor: + self.prevColor = ret + return ret + + def setColor(self, r, g = 0, b = 0, fg = True): + """Set the text color.""" + print(color(r, g, b, fg), end = '') + return + + def clearColor(self, setPrevColor = True): + ret = '' + if self.colorMode > 0: + ret = '\x1b[0m' + if ret == self.prevColor: + return '' + if setPrevColor: + self.prevColor = ret + return ret + + def setClearColor(self): + print(clearColor()) + return + + def run(self): + """The main game/shell loop""" + while not self.__exit: + print(self.ps1, end = '') + command = self.scanInput() + # we have to handle shell built-ins first (when we get some) + self.handleCommand(command) + self.update() + self.__exit = False + + def man(self, args): + help(self.__commands[args[0]]) + + def registerCommand(self, commandName: str, command: _types.FunctionType): + """command must be a function that takes one argument: a list of strings, +conventionally called args or argv""" + self.__commands[commandName] = command + + def registerAlias(self, a: str, original: list): + """makes 'a' an alias for original. +'a' must be one token, but original can be multiple.""" + self.__aliases[a] = original + + def registerBatch(self, name: str, commands: list): + self.__batches[name] = commands + + def getAlias(self, a: str): + if a in self.__aliases: + return self.__aliases[a] + else: + return None + + def getBatch(self, name: str): + if name in self.__batches: + return ['batch'] + else: + return None + + def scanLine(self, instr): + """Take a line of text and turn it into an argument list""" + if instr == '': + return [] + inQuotes = False + ret = [] + argStart = 0 + c = 0 + while c < len(instr): + if inQuotes: + if instr[c] == '"': + inQuotes = False + ret.append(instr[argStart:c]) + argStart = c+1 + else: + if instr[c] == '"': + inQuotes = True + if argStart != c: + ret.append(instr[argStart:c]) + argStart = c+1 + elif instr[c] in ' \t\n': + if argStart != c: + ret.append(instr[argStart:c]) + argStart = c + 1 + c += 1 + if argStart != c: + ret.append(instr[argStart:c]) + a = self.getAlias(ret[0]) + if a: + ret = a + ret[1:] + b = self.getBatch(ret[0]) + if b: + ret = b + ret + ret[0] = _re.sub(r'\A\$\((\w+)\)', r'\1', ret[0]) + return ret + + # Default functions + + def exitShell(self, args): + """The default exit command.""" + self.__exit = True + + def makeAlias(self, args): + """Make an alias.""" + self.registerAlias(args[0], args[1:]) + + def defineBatch(self, args): + """Define a batch file.""" + if len(args) < 1: + raise ValueError('def takes at least one argument') + ret = [] + command = input(self.ps2) + while command != 'end': + ret.append(command) + command = input(self.ps2) + for i in args: + self.registerBatch(i, ret) + + def batch(self, args): + """Run commands in batch mode +$0 is the name of the batch +$1 - $n are individual arguments +$* is all the arguments except 0 +$>0 - $>n is all arguments after the nth +$>=0 - $>=n is the nth and later arguments""" + script = self.__batches[args[0]] + var = _re.compile(r'\$((?:[>][=]?)?[0-9]+)') + u = False + for line in script: + if u: + self.update() + else: + u = True + newLine = line.replace(r'$*', ' '.join(args[1:])) + matches = var.finditer(newLine) + for m in matches: + n = m.group(1) + if n[0] == '>': + if n[1] == '=': + num = int(n[2:]) + else: + num = int(n[1:])+1 + newLine = newLine.replace(m.group(), ' '.join(args[num:])) + else: + newLine = newLine.replace(m.group(), args[int(n)]) + newLine = _re.sub(r'\A({0})'.format(args[0]), r'$(\1)', newLine) + #print(newLine) + self.handleCommand(self.scanLine(newLine)) + + # Beyond this point are functions that are called within the main loop. + def scanInput(self): + """Parses input. Override this for custom input parsing, or input source.""" + return self.scanLine(input()) + + def handleCommand(self, command): + if len(command) == 0: + return + if command[0] in self.__commands: + try: + self.__commands[command[0]](command[1:]) + except Exception as e: + _tb.print_exc() + print(e) + else: + self.handleUnknownCommand(command) + + def handleUnknownCommand(self, command): + """Handle commands that aren't registered. Override this if you want to do +something with those commands.""" + print("Bad command.") + + def update(self): + """Runs at the end of each loop. Does nothing by default. Override this if +there is something you want the shell to do between every command.""" + pass + +def main(args): + return 0 + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv))