class ExitCommand(Command): ''' Exit the active shell. ''' def __init__(self, name='exit', topic='shell', brief='exit the shell', **kwargs): self.parser = PypsiArgParser( prog=name, description=brief ) self.parser.add_argument( 'code', metavar='CODE', action='store', default=0, type=int, nargs='?', help='exit code to return to parent process' ) super(ExitCommand, self).__init__( name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs ) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code raise SystemExit(ns.code)
class ExitCommand(Command): ''' Exit the active shell. ''' def __init__(self, name='exit', topic='shell', brief='exit the shell', **kwargs): self.parser = PypsiArgParser(prog=name, description=brief) self.parser.add_argument('code', metavar='CODE', action='store', default=0, type=int, nargs='?', help='exit code to return to parent process') super(ExitCommand, self).__init__(name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code raise SystemExit(ns.code)
class EchoCommand(Command): ''' Prints text to the screen. ''' def __init__(self, name='echo', topic='shell', **kwargs): self.parser = PypsiArgParser( prog=name, description='display a line of text', usage=EchoCmdUsage ) subcmd = self.parser.add_argument_group(title='Stream') self.parser.add_argument( 'message', help='message to print', nargs=argparse.REMAINDER, metavar="MESSAGE" ) self.parser.add_argument( '-n', '--nolf', help="don't print newline character", action='store_true' ) super(EchoCommand, self).__init__(name=name, usage=self.parser.format_help(), topic=topic, brief='print a line of text', **kwargs) def run(self, shell, args, ctx): ns = self.parser.parse_args(shell, args) if self.parser.rc is not None: return self.parser.rc tail = '' if ns.nolf else '\n' print(' '.join(ns.message), sep='', end=tail) return 0
class AliasCommand(Command): def __init__(self, name='alias', brief='manage command aliases', topic='shell', **kwargs): super(AliasCommand, self).__init__(name=name, brief=brief, topic=topic, **kwargs) self.parser = PypsiArgParser(prog=name, description=brief) self.parser.add_argument('-l', '--list', help='list registered aliases', action='store_true') self.parser.add_argument('exp', metavar='EXP', help='alias expression', nargs=argparse.REMAINDER) self.parser.add_argument('-d', '--delete', help='delete alias', nargs=1, metavar='ALIAS') def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code rc = None if ns.list: for name in shell.ctx.aliases: print(name, "=", shell.ctx.aliases[name]) rc = 0 elif ns.delete: for name in ns.delete: if name in shell.ctx.aliases: del shell.ctx.aliases[name] rc = 0 else: self.error(shell, "alias does not exist: ", name) rc = 1 break else: (remainder, exp) = Expression.parse(args) if remainder or not exp or exp.operator != '=': self.error(shell, "invalid expression") rc = 1 else: shell.ctx.aliases[exp.operand] = exp.value rc = 0 return rc
class ChdirCommand(Command): ''' Change the current working directory. This accepts one of the following: * <path> - a relative or absolute path * ~ - the current user's home directory * ~<user> - <user>'s home directory * - - the previous directory ''' def __init__(self, name='cd', brief='change current working directory', topic='shell', **kwargs): super(ChdirCommand, self).__init__(name=name, brief=brief, topic=topic, **kwargs) self.parser = PypsiArgParser(prog=name, description=brief) self.parser.add_argument('path', help='path', metavar="PATH") def setup(self, shell): shell.ctx.chdir_last_dir = os.getcwd() def chdir(self, shell, path, print_cwd=False): prev = os.getcwd() try: os.chdir(path) if print_cwd: print(os.getcwd()) except OSError as e: self.error(shell, path, ": ", e.strerror) return -1 except Exception as e: self.error(shell, path, ": ", str(e)) return -1 shell.ctx.chdir_last_dir = prev return 0 def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code if ns.path == '-': return self.chdir(shell, shell.ctx.chdir_last_dir, True) if ns.path.startswith('~'): return self.chdir(shell, os.path.expanduser(ns.path)) return self.chdir(shell, ns.path)
class XArgsCommand(Command): ''' Execute a command for each line of input from :data:`sys.stdin`. ''' def __init__(self, name='xargs', topic='shell', brief='build and execute command lines from stdin', **kwargs): self.parser = PypsiArgParser( prog=name, description=brief, usage=XArgsUsage.format(name=name) ) self.parser.add_argument( '-I', default='{}', action='store', metavar='REPSTR', help='string token to replace', dest='token' ) self.parser.add_argument( 'command', nargs=argparse.REMAINDER, help="command to execute", metavar='COMMAND' ) super(XArgsCommand, self).__init__( name=name, topic=topic, usage=self.parser.format_help(), brief=brief, **kwargs ) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code if not ns.command: self.error(shell, "missing command") return 1 base = ' '.join([ '"{}"'.format(c.replace('"', '\\"')) for c in ns.command ]) child = ctx.fork() for line in sys.stdin: cmd = base.replace(ns.token, line.strip()) shell.execute(cmd, child) return 0
class HelpCommand(Command): ''' Provides access to manpage-esque topics and command usage information. ''' def __init__(self, name='help', topic='shell', brief='print information on a topic or command', topics=None, **kwargs): self.parser = PypsiArgParser(prog=name, description=brief) self.parser.add_argument("topic", metavar="TOPIC", help="command or topic to print", nargs='?') super(HelpCommand, self).__init__(name=name, brief=brief, usage=self.parser.format_help(), topic=topic, **kwargs) self.topics = list(topics or []) self.uncat = Topic('uncat', 'Uncategorized Commands & Features') self.lookup = {t.id: t for t in self.topics} self.dirty = True def reload(self, shell): self.uncat.commands = [] for id in self.lookup: self.lookup[id].commands = [] for (name, cmd) in shell.commands.items(): if cmd.topic: if cmd.topic in self.lookup: self.lookup[cmd.topic].commands.append(cmd) else: self.add_topic(Topic(cmd.topic, commands=[cmd])) else: self.uncat.commands.append(cmd) self.dirty = False def add_topic(self, topic): self.dirty = True self.lookup[topic.id] = topic self.topics.append(topic) def print_topic_commands(self, shell, topic, title=None): print(AnsiStderr.yellow, title_str(title or topic.name or topic.id, shell.width), AnsiStderr.reset, sep='') print(AnsiStderr.yellow, end='') Table(columns=(Column(''), Column('', Column.Grow)), spacing=4, header=False, width=shell.width).extend( *[(' Name', 'Description'), (' ----', '-----------')]).extend( *[(' ' + c.name, c.brief or '') for c in topic.commands]).write(sys.stdout) print(AnsiStderr.reset, end='') def print_topics(self, shell): addl = [] for topic in self.topics: if topic.content or not topic.commands: addl.append(topic) if topic.commands: self.print_topic_commands(shell, topic) print() if self.uncat.commands: self.print_topic_commands(shell, self.uncat) print() if addl: print(AnsiStderr.yellow, title_str("Additional Topics", shell.width), AnsiStderr.reset, sep='') tbl = FixedColumnTable([shell.width // 3] * 3) for topic in addl: tbl.add_cell(sys.stdout, topic.id) tbl.flush(sys.stdout) print() def print_topic(self, shell, id): if id not in self.lookup: if id in shell.commands: cmd = shell.commands[id] print(AnsiStderr.yellow, cmd.usage, AnsiStderr.reset, sep='') return 0 self.error(shell, "unknown topic: ", id) return -1 topic = self.lookup[id] if topic.content: print(title_str(topic.name or topic.id, shell.width)) print(word_wrap(topic.content, shell.width)) print() if topic.commands: self.print_topic_commands(shell, topic, "Commands") return 0 def run(self, shell, args, ctx): if self.dirty: self.reload(shell) ns = self.parser.parse_args(shell, args) if self.parser.rc is not None: return self.parser.rc rc = 0 if not ns.topic: self.print_topics(shell) else: rc = self.print_topic(shell, ns.topic) return rc
class TailCommand(Command): ''' Displays the last N lines of a file to the screen. ''' def __init__(self, name='tail', topic='shell', brief='display the last lines of a file', **kwargs): self.parser = PypsiArgParser(prog=name, description=brief, usage=TailCmdUsage) self.parser.add_argument('input_file', help='file to display', metavar="FILE") self.parser.add_argument('-n', '--lines', metavar="N", type=int, default=10, help="number of lines to display") self.parser.add_argument('-f', '--follow', help="continue to output as file grows", action='store_true') super(TailCommand, self).__init__(name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code # check for valid input file if not os.path.isfile(ns.input_file): self.error(shell, "invalid file path: ", ns.input_file, "\n") return -1 # print the last N lines last_lines = self.tail(ns.input_file, ns.lines) for line in last_lines: print(line) print() # continue to follow the file and display new content if ns.follow: self.follow_file(ns.input_file) return 0 def complete(self, shell, args, prefix): return path_completer(shell, args, prefix) def tail(self, fname, lines=10, block_size=1024): data = [] blocks = -1 num_lines = 0 where = 0 with open(fname) as fp: # seek to the end and get the number of bytes in the file fp.seek(0, 2) num_bytes = fp.tell() bytes_left = num_bytes while num_lines < lines and bytes_left > 0: if bytes_left - block_size > 0: # Seek back a block_size fp.seek(num_bytes - (blocks * block_size)) # read data from file data.insert(0, fp.read(block_size)) else: # jump back to the beginning fp.seek(0, 0) # read data data.insert(0, fp.read(num_bytes)) num_lines = data[0].count('\n') bytes_left -= block_size blocks -= 1 return ''.join(data).splitlines()[-lines:] def follow_file(self, fname): with open(fname) as fp: # jump to the end of the file fp.seek(0, 2) try: while 1: where = fp.tell() line = fp.readline() if not line: time.sleep(1) fp.seek(where) else: print(line, end='', flush=True) except (KeyboardInterrupt): print() return 0 return 0
class MacroCommand(BlockCommand): ''' Record and execute command macros. Macros can consist of one or more command statements. Macros are comparable to Bash functions. Once a macro has been recorded, a new command is registered in the shell as an instance of a :class:`Macro`. This command requires the :class:`pypsi.plugins.block.BlockPlugin` plugin. ''' def __init__(self, name='macro', topic='shell', brief="manage registered macros", macros=None, **kwargs): self.parser = PypsiArgParser( prog=name, description=brief, usage=MacroCmdUsage ) self.parser.add_argument( '-l', '--list', help='list all macros', action='store_true' ) self.parser.add_argument( '-d', '--delete', help='delete macro', action='store_true' ) self.parser.add_argument( '-s', '--show', help='print macro body', action='store_true' ) self.parser.add_argument( 'name', help='macro name', nargs='?', metavar='NAME' ) super(MacroCommand, self).__init__( name=name, usage=self.parser.format_help(), brief=brief, topic=topic, **kwargs ) self.base_macros = macros or {} def setup(self, shell): rc = 0 if 'macros' not in shell.ctx: shell.ctx.macros = {} for name in self.base_macros: rc = self.add_macro(shell, name, shell.ctx.macros[name]) return rc def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code rc = 0 if ns.name: if ns.delete: if ns.name in shell.ctx.macros: del shell.ctx.macros[ns.name] #It gets registered as a command too. See line 202 in this file and register() in shell.py del shell.commands[ns.name] else: self.error(shell, "unknown macro ", ns.name) rc = -1 elif ns.show: if ns.name in shell.ctx.macros: print("macro ", ns.name, sep='') for line in shell.ctx.macros[ns.name]: print(" ", line, sep='') print("end") else: self.error(shell, "unknown macro ", ns.name) rc = -1 elif ns.list: self.usage_error(shell, "list option does not take an argument") else: if ns.name in shell.commands.keys() and ns.name not in shell.ctx.macros: self.error(shell, "A macro cannot have the same name as an existing command.") return -1 self.macro_name = ns.name self.begin_block(shell) if sys.stdin.isatty(): print("Beginning macro, use the '", shell.ctx.block.end_cmd, "' command to save.", sep='') shell.ctx.macro_orig_eof_is_sigint = shell.eof_is_sigint shell.eof_is_sigint = True elif ns.list: tbl = FixedColumnTable(widths=[shell.width//3]*3) print(title_str("Registered Macros", shell.width)) for name in shell.ctx.macros: tbl.add_cell(sys.stdout, name) tbl.flush(sys.stdout) else: self.usage_error(shell, "missing required argument: NAME") rc = 1 return rc def end_block(self, shell, lines): self.add_macro(shell, self.macro_name, lines) self.macro_name = None shell.eof_is_sigint = shell.ctx.macro_orig_eof_is_sigint return 0 def cancel_block(self, shell): self.macro_name = None shell.eof_is_sigint = shell.ctx.macro_orig_eof_is_sigint def add_macro(self, shell, name, lines): shell.register( Macro(lines=lines, name=name, topic='__hidden__') ) shell.ctx.macros[name] = lines return 0
class IncludeCommand(Command): ''' Execute a script file. This entails reading each line of the given file and processing it as if it were typed into the shell. ''' def __init__(self, name='include', topic='shell', brief='execute a script file', **kwargs): self.parser = PypsiArgParser( prog=name, description=brief, #usage=IncludeCmdUsage ) self.parser.add_argument( 'path', metavar='PATH', action='store', help='file to execute' ) super(IncludeCommand, self).__init__( name=name, topic=topic, brief=brief, usage=self.parser.format_help(), **kwargs ) self.stack = [] def complete(self, shell, args, prefix): return path_completer(shell, args, prefix) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code return self.include_file(shell, ns.path, ctx) def include_file(self, shell, path, ctx): fp = None ifile = IncludeFile(path) top = False templ = '' if self.stack: #templ = shell.error.prefix for i in self.stack: if i.abspath == ifile.abspath: self.error(shell, "recursive include for file ", ifile.abspath, '\n') return -1 else: #templ = shell.error.prefix + "error in file {file} on line {line}: " top = True self.stack.append(ifile) try: fp = safe_open(path, 'r') except (OSError, IOError) as e: self.error(shell, "error opening file ", path, ": ", str(e), '\n') return -1 #orig_prefix = shell.error.prefix next = ctx.fork() for line in fp: #shell.error.prefix = templ.format(file=ifile.name, line=ifile.line) shell.execute(line.strip(), next) ifile.line += 1 #if top: # shell.error.prefix = orig_prefix self.stack.pop() fp.close() return 0
class HistoryCommand(Command): ''' Interact with and manage the shell's history. ''' def __init__(self, name='history', brief='manage shell history', topic='shell', **kwargs): self.setup_parser(brief) super(HistoryCommand, self).__init__( name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs ) def complete(self, shell, args, prefix): if len(args) == 1: return [x for x in ('clear', 'delete', 'exec', 'list', 'load', 'save') if x.startswith(prefix)] if len(args) == 2: if args[0] == 'save' or args[0] == 'load': return path_completer(shell, args, prefix) return [] def setup_parser(self, brief): self.parser = PypsiArgParser( prog='history', description=brief, usage=CmdUsage ) subcmd = self.parser.add_subparsers(prog='history', dest='subcmd', metavar='subcmd') subcmd.required = True ls = subcmd.add_parser('list', help='list history events') ls.add_argument( 'count', metavar='N', type=int, help='number of events to display', nargs='?' ) subcmd.add_parser('clear', help='remove all history events') delete = subcmd.add_parser('delete', help='delete single history event') delete.add_argument( 'index', metavar='N', type=int, help='remove item at index N', ) save = subcmd.add_parser('save', help='save history to a file') save.add_argument( 'path', metavar='PATH', help='save history to file located at PATH' ) load = subcmd.add_parser('load', help='load history from a file') load.add_argument( 'path', metavar='PATH', help='load history from file located at PATH' ) exe = subcmd.add_parser('exec', help='execute previous history event') exe.add_argument( 'prefix', metavar='PREFIX', help='find and execute previous history event with give PREFIX' ) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) #(shell, args) except CommandShortCircuit as e: return e.code rc = 0 if ns.subcmd == 'list': start = 0 if ns.count: start = len(shell.ctx.history) - ns.count if start < 0: start = 0 i = start + 1 for event in shell.ctx.history[start:]: print(i, ' ', event, sep='') i += 1 elif ns.subcmd == 'exec': event = None if ns.prefix.isdigit() or (ns.prefix[0] == '-' and ns.prefix[1:].isdigit()): try: index = int(ns.prefix) - 1 event = shell.ctx.history[index] except ValueError: self.error(shell, "invalid event index\n") rc = -1 except IndexError as e: self.error(shell, "invalid event index\n") rc = -1 else: event = shell.ctx.history.search_prefix(ns.prefix) if event is None: self.error(shell, "event not found") rc = -1 if event: print("found event: ", event, sep='') shell.execute(event,None) elif ns.subcmd == 'clear': shell.ctx.history.clear() elif ns.subcmd == 'delete': try: del shell.ctx.history[ns.index - 1] except: self.error(shell, "invalid event index\n") rc = -1 elif ns.subcmd == 'save': try: with open(ns.path, 'w') as fp: for event in shell.ctx.history: fp.write(event) fp.write('\n') except IOError as e: self.error(shell, "error saving history to file: ", os.strerror(e.errno), '\n') rc = -1 elif ns.subcmd == 'load': try: lines = [] with safe_open(ns.path, 'r') as fp: for event in fp: lines.append(str(event)) shell.ctx.history.clear() for line in lines: shell.ctx.history.append(line.strip()) except IOError as e: self.error(shell, "error saving history to file: ", os.strerror(e.errno), '\n') rc = -1 except UnicodeEncodeError: self.error(shell, "error: file contains invalide unicode characters\n") return rc
class VariableCommand(Command): ''' Manage variables. ''' Usage = """usage: var name = value or: var -l or: var -d name Manage local variables.""" def __init__(self, name='var', brief='manage variables', topic='shell', **kwargs): self.parser = PypsiArgParser( prog=name, description=brief ) self.parser.add_argument( '-l', '--list', help='list variables', action='store_true' ) self.parser.add_argument( '-d', '--delete', help='delete variable', metavar='VARIABLE' ) self.parser.add_argument( 'exp', metavar='EXPRESSION', help='expression defining variable', nargs=argparse.REMAINDER ) super(VariableCommand, self).__init__( name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs ) def run(self, shell, args, ctx): ns = self.parser.parse_args(shell, args) if self.parser.rc is not None: return self.parser.rc rc = 0 if ns.list: tbl = Table( columns=(Column("Variable"), Column("Value", Column.Grow)), spacing=4, ) for name in shell.ctx.vars: if name == '_': continue s = shell.ctx.vars[name] if callable(s): s = s() elif isinstance(s, ManagedVariable): s = s.getter(shell) tbl.append(name, obj_str(s)) tbl.write(sys.stdout) elif ns.delete: if ns.delete in shell.ctx.vars: s = shell.ctx.vars[ns.delete] if isinstance(s, ManagedVariable): self.error(shell, "variable is managed and cannot be deleted") rc = -1 else: del shell.ctx.vars[ns.delete] else: self.error(shell, "unknown variable: ", ns.delete) elif ns.exp and '=' in ''.join(args): (remainder, exp) = Expression.parse(args) if remainder or not exp: self.error(shell, "invalid expression") return 1 shell.ctx.vars[exp.operand] = exp.value elif ns.exp: if len(args) == 1: if args[0] in shell.ctx.vars: s = shell.ctx.vars[args[0]] if callable(s): s = s() elif isinstance(s, ManagedVariable): s = s.getter(shell) print(obj_str(s)) else: self.error(shell, "unknown variable: ", args[0]) return 1 else: self.error(shell, "invalid expression") return 1 else: self.usage_error(shell, "missing required EXPRESSION") rc =1 return rc
class IncludeCommand(Command): ''' Execute a script file. This entails reading each line of the given file and processing it as if it were typed into the shell. ''' def __init__(self, name='include', topic='shell', brief='execute a script file', **kwargs): self.parser = PypsiArgParser( prog=name, description=brief, #usage=IncludeCmdUsage ) self.parser.add_argument('path', metavar='PATH', action='store', help='file to execute') super(IncludeCommand, self).__init__(name=name, topic=topic, brief=brief, usage=self.parser.format_help(), **kwargs) self.stack = [] def complete(self, shell, args, prefix): return path_completer(shell, args, prefix) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code return self.include_file(shell, ns.path, ctx) def include_file(self, shell, path, ctx): fp = None ifile = IncludeFile(path) top = False templ = '' if self.stack: #templ = shell.error.prefix for i in self.stack: if i.abspath == ifile.abspath: self.error(shell, "recursive include for file ", ifile.abspath, '\n') return -1 else: #templ = shell.error.prefix + "error in file {file} on line {line}: " top = True self.stack.append(ifile) try: fp = safe_open(path, 'r') except (OSError, IOError) as e: self.error(shell, "error opening file ", path, ": ", str(e), '\n') return -1 #orig_prefix = shell.error.prefix next = ctx.fork() for line in fp: #shell.error.prefix = templ.format(file=ifile.name, line=ifile.line) shell.execute(line.strip(), next) ifile.line += 1 #if top: # shell.error.prefix = orig_prefix self.stack.pop() fp.close() return 0
class HelpCommand(Command): ''' Provides access to manpage-esque topics and command usage information. ''' def __init__(self, name='help', topic='shell', brief='print information on a topic or command', topics=None, **kwargs): self.parser = PypsiArgParser( prog=name, description=brief ) self.parser.add_argument( "topic", metavar="TOPIC", help="command or topic to print", nargs='?' ) super(HelpCommand, self).__init__( name=name, brief=brief, usage=self.parser.format_help(), topic=topic, **kwargs ) self.topics = list(topics or []) self.uncat = Topic('uncat', 'Uncategorized Commands & Features') self.lookup = {t.id: t for t in self.topics} self.dirty = True def reload(self, shell): self.uncat.commands = [] for id in self.lookup: self.lookup[id].commands = [] for (name, cmd) in shell.commands.items(): if cmd.topic: if cmd.topic in self.lookup: self.lookup[cmd.topic].commands.append(cmd) else: self.add_topic(Topic(cmd.topic, commands=[cmd])) else: self.uncat.commands.append(cmd) self.dirty = False def add_topic(self, topic): self.dirty = True self.lookup[topic.id] = topic self.topics.append(topic) def print_topic_commands(self, shell, topic, title=None): print( AnsiStderr.yellow, title_str(title or topic.name or topic.id, shell.width), AnsiStderr.reset, sep='' ) print(AnsiStderr.yellow, end='') Table( columns=(Column(''), Column('', Column.Grow)), spacing=4, header=False, width=shell.width ).extend( *[ (' Name', 'Description'), (' ----', '-----------') ] ).extend( *[(' '+c.name, c.brief or '') for c in topic.commands] ).write(sys.stdout) print(AnsiStderr.reset, end='') def print_topics(self, shell): addl = [] for topic in self.topics: if topic.content or not topic.commands: addl.append(topic) if topic.commands: self.print_topic_commands(shell, topic) print() if self.uncat.commands: self.print_topic_commands(shell, self.uncat) print() if addl: print( AnsiStderr.yellow, title_str("Additional Topics", shell.width), AnsiStderr.reset, sep='' ) tbl = FixedColumnTable([shell.width // 3] * 3) for topic in addl: tbl.add_cell(sys.stdout, topic.id) tbl.flush(sys.stdout) print() def print_topic(self, shell, id): if id not in self.lookup: if id in shell.commands: cmd = shell.commands[id] print(AnsiStderr.yellow, cmd.usage, AnsiStderr.reset, sep='') return 0 self.error(shell, "unknown topic: ", id) return -1 topic = self.lookup[id] if topic.content: print(title_str(topic.name or topic.id, shell.width)) print(word_wrap(topic.content, shell.width)) print() if topic.commands: self.print_topic_commands(shell, topic, "Commands") return 0 def run(self, shell, args, ctx): if self.dirty: self.reload(shell) ns = self.parser.parse_args(shell, args) if self.parser.rc is not None: return self.parser.rc rc = 0 if not ns.topic: self.print_topics(shell) else: rc = self.print_topic(shell, ns.topic) return rc
class MacroCommand(BlockCommand): ''' Record and execute command macros. Macros can consist of one or more command statements. Macros are comparable to Bash functions. Once a macro has been recorded, a new command is registered in the shell as an instance of a :class:`Macro`. This command requires the :class:`pypsi.plugins.block.BlockPlugin` plugin. ''' def __init__(self, name='macro', topic='shell', brief="manage registered macros", macros=None, **kwargs): self.parser = PypsiArgParser(prog=name, description=brief) self.parser.add_argument('-l', '--list', help='list all macros', action='store_true') self.parser.add_argument('-d', '--delete', help='delete macro', action='store_true') self.parser.add_argument('-s', '--show', help='print macro body', action='store_true') self.parser.add_argument('name', help='macro name', nargs='?', metavar='NAME') super(MacroCommand, self).__init__(name=name, usage=self.parser.format_help(), brief=brief, topic=topic, **kwargs) self.base_macros = macros or {} def setup(self, shell): if 'macros' not in shell.ctx: shell.ctx.macros = {} for name in self.base_macros: self.add_macro(shell, name, shell.ctx.macros[name]) return 0 def run(self, shell, args, ctx): ns = self.parser.parse_args(shell, args) if self.parser.rc is not None: return self.parser.rc rc = 0 if ns.name: if ns.delete: if ns.name in shell.ctx.macros: del shell.ctx.macros[ns.name] else: self.error(shell, "unknown macro ", ns.name) rc = -1 elif ns.show: if ns.name in shell.ctx.macros: print("macro ", ns.name, sep='') for line in shell.ctx.macros[ns.name]: print(" ", line, sep='') print("end") else: self.error(shell, "unknown macro ", ns.name) rc = -1 elif ns.list: self.usage_error(shell, "list option does not take an argument") else: self.macro_name = ns.name self.begin_block(shell) elif ns.list: tbl = FixedColumnTable(widths=[shell.width // 3] * 3) print(title_str("Registered Macros", shell.width)) for name in shell.ctx.macros: tbl.add_cell(sys.stdout, name) tbl.flush(sys.stdout) else: self.usage_error(shell, "missing required argument: NAME") rc = 1 return rc def end_block(self, shell, lines): self.add_macro(shell, self.macro_name, lines) self.macro_name = None return 0 def cancel_block(self, shell): self.macro_name = None def add_macro(self, shell, name, lines): shell.register(Macro(lines=lines, name=name)) shell.ctx.macros[name] = lines return 0
class TipCommand(Command): def __init__(self, name='tip', brief='print shell tips', topic='shell', tips=None, motd=None, vars=None, **kwargs): self.tips = tips or [] self.motd = motd or '' self.vars = vars or {} self.rand = random.Random() self.rand.seed() self.parser = PypsiArgParser( prog=name, description=brief ) self.parser.add_argument( '-m', '--motd', action='store_true', help="print the message of the day" ) super(TipCommand, self).__init__( name=name, brief=brief, topic=topic, usage=self.parser.format_help(), **kwargs ) def load_tips(self, path): fp = safe_open(path, 'r') tip = [] for line in fp.readlines(): line = line.rstrip() if line: tip.append(line) elif tip: self.tips.append(' '.join(tip)) tip = [] if tip: self.tips.append(' '.join(tip)) fp.close() def load_motd(self, path): fp = safe_open(path, 'r') self.motd = fp.read().rstrip() fp.close() def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code rc = None if ns.motd: rc = self.print_motd(shell) else: rc = self.print_random_tip(shell) return rc def print_random_tip(self, shell, header=True): if not self.tips: self.error(shell, "no tips available") return -1 i = self.rand.randrange(len(self.tips)) #int(self.rand.random() * len(self.tips) if header: title = "Tip #{}".format(i+1) title += '\n' + ('-'*len(title)) print(AnsiCodes.green, title, AnsiCodes.reset, sep='') try: cnt = self.tips[i].format(**self.vars) except: cnt = self.tips[i] print(word_wrap(cnt, shell.width)) def print_motd(self, shell): if not self.motd: self.error(shell, "no motd available") return -1 try: cnt = self.motd.format(**self.vars) except: cnt = self.motd print( AnsiCodes.green, "Message of the Day".center(shell.width), '\n', '>' * shell.width, "\n", AnsiCodes.reset, word_wrap(cnt, shell.width), '\n', AnsiCodes.green, "<" * shell.width, "\n", AnsiCodes.reset, sep='' )
class VariableCommand(Command): ''' Manage variables. ''' Usage = """var name or: var name = value or: var -l or: var -d name""" def __init__(self, name='var', brief='manage local variables', topic='shell', **kwargs): self.parser = PypsiArgParser( prog=name, description=brief, usage=VariableCommand.Usage ) self.parser.add_argument( '-l', '--list', help='list variables', action='store_true' ) self.parser.add_argument( '-d', '--delete', help='delete variable', metavar='VARIABLE' ) self.parser.add_argument( 'exp', metavar='EXPRESSION', help='expression defining variable', nargs=argparse.REMAINDER ) super(VariableCommand, self).__init__( name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs ) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code rc = 0 if ns.list: tbl = Table( columns=(Column("Variable"), Column("Value", Column.Grow)), width=shell.width, spacing=4, ) for name in shell.ctx.vars: if name == '_': continue s = shell.ctx.vars[name] if callable(s): s = s() elif isinstance(s, ManagedVariable): s = s.get(shell) tbl.append(name, obj_str(s)) tbl.write(sys.stdout) elif ns.delete: if ns.delete in shell.ctx.vars: s = shell.ctx.vars[ns.delete] if isinstance(s, ManagedVariable): self.error(shell, "variable is managed and cannot be deleted") rc = -1 else: del shell.ctx.vars[ns.delete] else: self.error(shell, "unknown variable: ", ns.delete) elif ns.exp and '=' in ''.join(args): (remainder, exp) = Expression.parse(args) if remainder or not exp: self.error(shell, "invalid expression") return 1 if isinstance(shell.ctx.vars[exp.operand], ManagedVariable): try: shell.ctx.vars[exp.operand].set(shell, exp.value) except ValueError as e: self.error(shell, "could not set variable: ", str(e)) else: shell.ctx.vars[exp.operand] = exp.value elif ns.exp: if len(args) == 1: if args[0] in shell.ctx.vars: s = shell.ctx.vars[args[0]] if callable(s): s = s() elif isinstance(s, ManagedVariable): s = s.getter(shell) print(obj_str(s)) else: self.error(shell, "unknown variable: ", args[0]) return 1 else: self.error(shell, "invalid expression") return 1 else: self.usage_error(shell, "missing required EXPRESSION") rc =1 return rc
class HelpCommand(Command): ''' Provides access to manpage-esque topics and command usage information. ''' def __init__(self, name='help', topic='shell', brief='print information on a topic or command', topics=None, vars=None, **kwargs): self.parser = PypsiArgParser( prog=name, description=brief ) self.parser.add_argument( "topic", metavar="TOPIC", help="command or topic to print", nargs='?' ) super(HelpCommand, self).__init__( name=name, brief=brief, usage=self.parser.format_help(), topic=topic, **kwargs ) self.topics = list(topics or []) self.uncat = Topic('uncat', 'Uncategorized Commands & Topics') self.lookup = {t.id: t for t in self.topics} self.dirty = True self.vars = vars or {} def complete(self, shell, args, prefix): ''' args = [arg for arg in args if not arg.startswith('-')] completions = [] base = [] for topic in self.topics: base.append(topic.name or topic.id) base.extend([command.name for command in topic.commands]) if len(args) <= 1: completions.extend([x for x in base if x.startswith(prefix) or not prefix]) return sorted(completions) ''' #pre = args[-1] if args else prefix if self.dirty: self.reload(shell) completions = sorted([x.id for x in self.topics if x.id.startswith(prefix) or not prefix]) return completions def reload(self, shell): self.uncat.commands = [] for id in self.lookup: self.lookup[id].commands = [] for (name, cmd) in shell.commands.items(): if cmd.topic == '__hidden__': continue if cmd.topic: if cmd.topic in self.lookup: self.lookup[cmd.topic].commands.append(cmd) else: self.add_topic(Topic(cmd.topic, commands=[cmd])) else: self.uncat.commands.append(cmd) self.dirty = False for topic in self.topics: if topic.commands: topic.commands = sorted(topic.commands, key=lambda x: x.name) def add_topic(self, topic): self.dirty = True self.lookup[topic.id] = topic self.topics.append(topic) def print_topic_commands(self, shell, topic, title=None, name_col_width=20): print( AnsiCodes.yellow, title_str(title or topic.name or topic.id, shell.width), AnsiCodes.reset, sep='' ) print(AnsiCodes.yellow, end='') Table( columns=(Column(''), Column('', Column.Grow)), spacing=4, header=False, width=shell.width #).extend( # *[ # (' Name', 'Description'), # (' ----', '-----------') # ] ).extend( *[(' '+c.name.ljust(name_col_width - 1), c.brief or '') for c in topic.commands] ).write(sys.stdout) print(AnsiCodes.reset, end='') def print_topics(self, shell): max_name_width = 0 for topic in self.topics: for c in topic.commands: max_name_width = max(len(c.name), max_name_width) for c in self.uncat.commands: max_name_width = max(len(c.name), max_name_width) addl = [] for topic in self.topics: if topic.content or not topic.commands: addl.append(topic) if topic.commands: self.print_topic_commands(shell, topic, name_col_width=max_name_width) print() if self.uncat.commands: self.print_topic_commands(shell, self.uncat, name_col_width=max_name_width) print() if addl: addl = sorted(addl, key=lambda x: x.id) print( AnsiCodes.yellow, title_str("Additional Topics", shell.width), sep='' ) Table( columns=(Column(''), Column('', Column.Grow)), spacing=4, header=False, width=shell.width ).extend( *[(' '+topic.id.ljust(max_name_width - 1), topic.name or '') for topic in addl] ).write(sys.stdout) print(AnsiCodes.reset) def print_topic(self, shell, id): if id not in self.lookup: if id in shell.commands: cmd = shell.commands[id] print(AnsiCodes.yellow, cmd.usage, AnsiCodes.reset, sep='') return 0 self.error(shell, "unknown topic: ", id) return -1 topic = self.lookup[id] if topic.content: print(title_str(topic.name or topic.id, shell.width)) try: cnt = topic.content.format(**self.vars) except: cnt = topic.content print(word_wrap(cnt, shell.width)) print() if topic.commands: self.print_topic_commands(shell, topic, "Commands") return 0 def run(self, shell, args, ctx): if self.dirty: self.reload(shell) try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code rc = 0 if not ns.topic: self.print_topics(shell) else: rc = self.print_topic(shell, ns.topic) return rc
class TipCommand(Command): def __init__(self, name="tip", brief="print shell tips", topic="shell", tips=None, motd=None, vars=None, **kwargs): self.tips = tips or [] self.motd = motd or "" self.vars = vars or {} self.rand = random.Random() self.rand.seed() self.parser = PypsiArgParser(prog=name, description=brief) self.parser.add_argument("-m", "--motd", action="store_true", help="print the message of the day") super(TipCommand, self).__init__(name=name, brief=brief, topic=topic, usage=self.parser.format_help(), **kwargs) def load_tips(self, path): fp = safe_open(path, "r") tip = [] for line in fp.readlines(): line = line.rstrip() if line: tip.append(line) elif tip: self.tips.append(" ".join(tip)) tip = [] if tip: self.tips.append(" ".join(tip)) fp.close() def load_motd(self, path): fp = safe_open(path, "r") self.motd = fp.read().rstrip() fp.close() def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code rc = None if ns.motd: rc = self.print_motd(shell) else: rc = self.print_random_tip(shell) return rc def print_random_tip(self, shell, header=True): if not self.tips: self.error(shell, "no tips available") return -1 i = self.rand.randrange(len(self.tips)) # int(self.rand.random() * len(self.tips) if header: title = "Tip #{}".format(i + 1) title += "\n" + ("-" * len(title)) print(AnsiCodes.green, title, AnsiCodes.reset, sep="") try: cnt = self.tips[i].format(**self.vars) except: cnt = self.tips[i] print(word_wrap(cnt, shell.width)) def print_motd(self, shell): if not self.motd: self.error(shell, "no motd available") return -1 try: cnt = self.motd.format(**self.vars) except: cnt = self.motd print( AnsiCodes.green, "Message of the Day".center(shell.width), "\n", ">" * shell.width, "\n", AnsiCodes.reset, word_wrap(cnt, shell.width), "\n", AnsiCodes.green, "<" * shell.width, "\n", AnsiCodes.reset, sep="", )
class TailCommand(Command): ''' Displays the last N lines of a file to the screen. ''' def __init__(self, name='tail', topic='shell', brief='display the last lines of a file', **kwargs): self.parser = PypsiArgParser( prog=name, description=brief, usage=TailCmdUsage ) self.parser.add_argument( 'input_file', help='file to display', metavar="FILE" ) self.parser.add_argument( '-n', '--lines', metavar="N", type=int, default=10, help="number of lines to display" ) self.parser.add_argument( '-f', '--follow', help="continue to output as file grows", action='store_true' ) super(TailCommand, self).__init__( name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs ) def run(self, shell, args, ctx): try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code # check for valid input file if not os.path.isfile(ns.input_file): self.error(shell, "invalid file path: ", ns.input_file, "\n") return -1 # print the last N lines last_lines = self.tail(ns.input_file, ns.lines) for line in last_lines: print(line) print() # continue to follow the file and display new content if ns.follow: self.follow_file(ns.input_file) return 0 def complete(self, shell, args, prefix): return path_completer(shell, args, prefix) def tail(self, fname, lines=10, block_size=1024): data = [] blocks = -1 num_lines = 0 where = 0 with open(fname) as fp: # seek to the end and get the number of bytes in the file fp.seek(0,2) num_bytes = fp.tell() bytes_left = num_bytes while num_lines < lines and bytes_left > 0: if bytes_left - block_size > 0: # Seek back a block_size fp.seek(num_bytes - (blocks * block_size)) # read data from file data.insert(0, fp.read(block_size)) else: # jump back to the beginning fp.seek(0,0) # read data data.insert(0, fp.read(num_bytes)) num_lines = data[0].count('\n') bytes_left -= block_size blocks -= 1 return ''.join(data).splitlines()[-lines:] def follow_file(self, fname): with open(fname) as fp: # jump to the end of the file fp.seek(0,2) try: while 1: where = fp.tell() line = fp.readline() if not line: time.sleep(1) fp.seek(where) else: print(line, end='', flush=True) except (KeyboardInterrupt): print() return 0 return 0
class HelpCommand(Command): ''' Provides access to manpage-esque topics and command usage information. ''' def __init__(self, name='help', topic='shell', brief='print information on a topic or command', topics=None, vars=None, **kwargs): self.parser = PypsiArgParser(prog=name, description=brief) self.parser.add_argument("topic", metavar="TOPIC", help="command or topic to print", nargs='?') super(HelpCommand, self).__init__(name=name, brief=brief, usage=self.parser.format_help(), topic=topic, **kwargs) self.topics = list(topics or []) self.uncat = Topic('uncat', 'Uncategorized Commands & Topics') self.lookup = {t.id: t for t in self.topics} self.dirty = True self.vars = vars or {} def complete(self, shell, args, prefix): ''' args = [arg for arg in args if not arg.startswith('-')] completions = [] base = [] for topic in self.topics: base.append(topic.name or topic.id) base.extend([command.name for command in topic.commands]) if len(args) <= 1: completions.extend([x for x in base if x.startswith(prefix) or not prefix]) return sorted(completions) ''' #pre = args[-1] if args else prefix if self.dirty: self.reload(shell) completions = sorted([ x.id for x in self.topics if x.id.startswith(prefix) or not prefix ]) return completions def reload(self, shell): self.uncat.commands = [] for id in self.lookup: self.lookup[id].commands = [] for (name, cmd) in shell.commands.items(): if cmd.topic == '__hidden__': continue if cmd.topic: if cmd.topic in self.lookup: self.lookup[cmd.topic].commands.append(cmd) else: self.add_topic(Topic(cmd.topic, commands=[cmd])) else: self.uncat.commands.append(cmd) self.dirty = False for topic in self.topics: if topic.commands: topic.commands = sorted(topic.commands, key=lambda x: x.name) def add_topic(self, topic): self.dirty = True self.lookup[topic.id] = topic self.topics.append(topic) def print_topic_commands(self, shell, topic, title=None, name_col_width=20): print(AnsiCodes.yellow, title_str(title or topic.name or topic.id, shell.width), AnsiCodes.reset, sep='') print(AnsiCodes.yellow, end='') Table( columns=(Column(''), Column('', Column.Grow)), spacing=4, header=False, width=shell.width #).extend( # *[ # (' Name', 'Description'), # (' ----', '-----------') # ] ).extend(*[(' ' + c.name.ljust(name_col_width - 1), c.brief or '') for c in topic.commands]).write(sys.stdout) print(AnsiCodes.reset, end='') def print_topics(self, shell): max_name_width = 0 for topic in self.topics: for c in topic.commands: max_name_width = max(len(c.name), max_name_width) for c in self.uncat.commands: max_name_width = max(len(c.name), max_name_width) addl = [] for topic in self.topics: if topic.content or not topic.commands: addl.append(topic) if topic.commands: self.print_topic_commands(shell, topic, name_col_width=max_name_width) print() if self.uncat.commands: self.print_topic_commands(shell, self.uncat, name_col_width=max_name_width) print() if addl: addl = sorted(addl, key=lambda x: x.id) print(AnsiCodes.yellow, title_str("Additional Topics", shell.width), sep='') Table(columns=(Column(''), Column('', Column.Grow)), spacing=4, header=False, width=shell.width).extend( *[(' ' + topic.id.ljust(max_name_width - 1), topic.name or '') for topic in addl]).write(sys.stdout) print(AnsiCodes.reset) def print_topic(self, shell, id): if id not in self.lookup: if id in shell.commands: cmd = shell.commands[id] print(AnsiCodes.yellow, cmd.usage, AnsiCodes.reset, sep='') return 0 self.error(shell, "unknown topic: ", id) return -1 topic = self.lookup[id] if topic.content: print(title_str(topic.name or topic.id, shell.width)) try: cnt = topic.content.format(**self.vars) except: cnt = topic.content print(word_wrap(cnt, shell.width)) print() if topic.commands: self.print_topic_commands(shell, topic, "Commands") return 0 def run(self, shell, args, ctx): if self.dirty: self.reload(shell) try: ns = self.parser.parse_args(args) except CommandShortCircuit as e: return e.code rc = 0 if not ns.topic: self.print_topics(shell) else: rc = self.print_topic(shell, ns.topic) return rc
class HistoryCommand(Command): ''' Interact with and manage the shell's history. ''' def __init__(self, name='history', brief='manage shell history', topic='shell', **kwargs): self.setup_parser(brief) super(HistoryCommand, self).__init__(name=name, usage=self.parser.format_help(), topic=topic, brief=brief, **kwargs) def complete(self, shell, args, prefix): if len(args) == 1: return [ x for x in ('clear', 'delete', 'exec', 'list', 'load', 'save') if x.startswith(prefix) ] if len(args) == 2: if args[0] == 'save' or args[0] == 'load': return path_completer(shell, args, prefix) return [] def setup_parser(self, brief): self.parser = PypsiArgParser(prog='history', description=brief, usage=CmdUsage) subcmd = self.parser.add_subparsers(prog='history', dest='subcmd') subcmd.required = True ls = subcmd.add_parser('list', help='list history events') ls.add_argument('count', metavar='N', type=int, help='number of events to display', nargs='?') subcmd.add_parser('clear', help='remove all history events') delete = subcmd.add_parser('delete', help='delete single history event') delete.add_argument( 'index', metavar='N', type=int, help='remove item at index N', ) save = subcmd.add_parser('save', help='save history to a file') save.add_argument('path', metavar='PATH', help='save history to file located at PATH') load = subcmd.add_parser('load', help='load history from a file') load.add_argument('path', metavar='PATH', help='load history from file located at PATH') exe = subcmd.add_parser('exec', help='execute previous history event') exe.add_argument( 'prefix', metavar='PREFIX', help='find and execute previous history event with give PREFIX') def run(self, shell, args, ctx): ns = self.parser.parse_args(shell, args) if self.parser.rc is not None: return self.parser.rc rc = 0 if ns.subcmd == 'list': start = 0 if ns.count: start = len(shell.ctx.history) - ns.count if start < 0: start = 0 i = start + 1 for event in shell.ctx.history[start:]: print(i, ' ', event, sep='') i += 1 elif ns.subcmd == 'exec': event = None if ns.prefix.isdigit() or (ns.prefix[0] == '-' and ns.prefix[1:].isdigit()): try: index = int(ns.prefix) - 1 event = shell.ctx.history[index] except ValueError: self.error(shell, "invalid event index\n") rc = -1 except IndexError as e: self.error(shell, "invalid event index\n") rc = -1 else: event = shell.ctx.history.search_prefix(ns.prefix) if event is None: self.error(shell, "event not found") rc = -1 if event: print("found event: ", event, sep='') elif ns.subcmd == 'clear': shell.ctx.history.clear() elif ns.subcmd == 'delete': try: del shell.ctx.history[ns.index - 1] except: self.error(shell, "invalid event index\n") rc = -1 elif ns.subcmd == 'save': try: with open(ns.path, 'w') as fp: for event in shell.ctx.history: fp.write(event) fp.write('\n') except IOError as e: self.error(shell, "error saving history to file: ", os.strerror(e.errno), '\n') rc = -1 elif ns.subcmd == 'load': try: lines = [] with safe_open(ns.path, 'r') as fp: for event in fp: lines.append(str(event)) shell.ctx.history.clear() for line in lines: shell.ctx.history.append(line.strip()) except IOError as e: self.error(shell, "error saving history to file: ", os.strerror(e.errno), '\n') rc = -1 except UnicodeEncodeError: self.error( shell, "error: file contains invalide unicode characters\n") return rc
class TipCommand(Command): def __init__(self, name='tip', brief='print shell tips', topic='shell', tips=None, motd=None, **kwargs): self.tips = tips or [] self.motd = motd or '' self.rand = random.Random() self.rand.seed() self.parser = PypsiArgParser( prog=name, description=brief ) self.parser.add_argument( '-m', '--motd', action='store_true', help="print the message of the day" ) super(TipCommand, self).__init__( name=name, brief=brief, topic=topic, usage=self.parser.format_help(), **kwargs ) def load_tips(self, path): fp = safe_open(path, 'r') tip = [] for line in fp.readlines(): line = line.rstrip() if line: tip.append(line) elif tip: self.tips.append(''.join(tip)) tip = [] if tip: self.tips.append(''.join(tip)) fp.close() def load_motd(self, path): fp = safe_open(path, 'r') self.motd = fp.read().rstrip() fp.close() def run(self, shell, args, ctx): ns = self.parser.parse_args(shell, args) if self.parser.rc is not None: return self.parser.rc if ns.motd: self.print_motd(shell) else: self.print_random_tip(shell) return 0 def print_random_tip(self, shell, header=True): i = int(self.rand.random() * 10) % len(self.tips) if header: title = "Tip #{}".format(i+1) title += '\n' + ('-'*len(title)) print(AnsiStdout.green, title, AnsiStdout.reset, sep='') print(word_wrap(self.tips[i], shell.width)) def print_motd(self, shell): print( AnsiStdout.green, "Message of the Day".center(shell.width), '\n', '>' * shell.width, "\n", AnsiStdout.reset, word_wrap(self.motd, shell.width), '\n', AnsiStdout.green, "<" * shell.width, "\n", AnsiStdout.reset, sep='' )