#!/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(self.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 unRegisterCommand(self, commandName: str): """Remove a command. May be useful for inheriting classes and the like.""" del self.__commands[commandName] 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 unRegisterAlias(self, commandName: str): """Remove an alias. May be useful for inheriting classes and the like.""" del self.__aliases[commandName] def registerBatch(self, name: str, commands: list): self.__batches[name] = commands def unRegisterBatch(self, commandName: str): """Remove a command. May be useful for inheriting classes and the like.""" del self.__batches[commandName] 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 [] literalStr = list(instr) inQuotes = False ret = [] argStart = 0 c = 0 l = 0 while c < len(instr): if inQuotes: if instr[c] == '"': if instr[c-1] == '\\': del literalStr[l-1] l -= 1 else: inQuotes = False del literalStr[l] l -= 1 else: if instr[c] == '"': if l > 0 and instr[c-1] == '\\': del literalStr[l-1] l -= 1 else: inQuotes = True del literalStr[l] l -= 1 elif instr[c] in ' \t\n': if argStart != l: ret.append(''.join(literalStr[argStart:l])) argStart = l + 1 c += 1 l += 1 if argStart != l: ret.append(''.join(literalStr[argStart:l])) 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))