# Copyright (C) 2009-2013 Roman Zimbelmann # This software is distributed under the terms of the GNU GPL version 3. # TODO: Add an optional "!" to all commands and set a flag if it's there import os import ranger import re from collections import deque from ranger.api import * from ranger.core.shared import FileManagerAware from ranger.ext.lazy_property import lazy_property _SETTINGS_RE = re.compile(r'^\s*([^\s]+?)=(.*)$') DELETE_WARNING = 'delete seriously? ' # COMPAT def alias(*_): pass # COMPAT class CommandContainer(object): def __init__(self): self.commands = {} def __getitem__(self, key): return self.commands[key] def alias(self, name, full_command): try: cmd = type(name, (AliasCommand, ), dict()) cmd._based_function = name cmd._function_name = name cmd._object_name = name cmd._line = full_command self.commands[name] = cmd except: pass def load_commands_from_module(self, module): for var in vars(module).values(): try: if issubclass(var, Command) and var != Command \ and var != FunctionCommand: self.commands[var.get_name()] = var except TypeError: pass def load_commands_from_object(self, obj, filtr): for attribute_name in dir(obj): if attribute_name[0] == '_' or attribute_name not in filtr: continue attribute = getattr(obj, attribute_name) if hasattr(attribute, '__call__'): cmd = type(attribute_name, (FunctionCommand, ), dict()) cmd._based_function = attribute cmd._function_name = attribute.__name__ cmd._object_name = obj.__class__.__name__ self.commands[attribute_name] = cmd def get_command(self, name, abbrev=True): if abbrev: lst = [cls for cmd, cls in self.commands.items() \ if cls.allow_abbrev and cmd.startswith(name) \ or cmd == name] if len(lst) == 0: raise KeyError if len(lst) == 1: return lst[0] if self.commands[name] in lst: return self.commands[name] raise ValueError("Ambiguous command") else: try: return self.commands[name] except KeyError: return None def command_generator(self, start): return (cmd + ' ' for cmd in self.commands if cmd.startswith(start)) class Command(FileManagerAware): """Abstract command class""" name = None allow_abbrev = True resolve_macros = True escape_macros_for_shell = False quantifier = None _shifted = 0 _setting_line = None def __init__(self, line, quantifier=None): self.line = line self.args = line.split() self.quantifier = quantifier self.quickly_executed = False try: self.firstpart = line[:line.rindex(' ') + 1] except ValueError: self.firstpart = '' @classmethod def get_name(self): classdict = self.__mro__[0].__dict__ if 'name' in classdict and classdict['name']: return self.name else: return self.__name__ def execute(self): """Override this""" def tab(self): """Override this""" def quick(self): """Override this""" def cancel(self): """Override this""" # Easy ways to get information def arg(self, n): """Returns the nth space separated word""" try: return self.args[n] except IndexError: return "" def rest(self, n): """Returns everything from and after arg(n)""" got_space = True word_count = 0 for i in range(len(self.line)): if self.line[i] == " ": if not got_space: got_space = True word_count += 1 elif got_space: got_space = False if word_count == n + self._shifted: return self.line[i:] return "" def start(self, n): """Returns everything until (inclusively) arg(n)""" return ' '.join(self.args[:n]) + " " # XXX def shift(self): del self.args[0] self._setting_line = None self._shifted += 1 def tabinsert(self, word): return ''.join([self._tabinsert_left, word, self._tabinsert_right]) def parse_setting_line(self): if self._setting_line is not None: return self._setting_line match = _SETTINGS_RE.match(self.rest(1)) if match: self.firstpart += match.group(1) + '=' result = [match.group(1), match.group(2), True] else: result = [self.arg(1), self.rest(2), ' ' in self.rest(1)] self._setting_line = result return result def parse_flags(self): """Finds and returns flags in the command >>> Command("").parse_flags() ('', '') >>> Command("foo").parse_flags() ('', '') >>> Command("shell test").parse_flags() ('', 'test') >>> Command("shell -t ls -l").parse_flags() ('t', 'ls -l') >>> Command("shell -f -- -q test").parse_flags() ('f', '-q test') >>> Command("shell -foo -bar rest of the command").parse_flags() ('foobar', 'rest of the command') """ flags = "" args = self.line.split() rest = "" if len(args) > 0: rest = self.line[len(args[0]):].lstrip() for arg in args[1:]: if arg == "--": rest = rest[2:].lstrip() break elif len(arg) > 1 and arg[0] == "-": rest = rest[len(arg):].lstrip() flags += arg[1:] else: break return flags, rest # XXX: Lazy properties? Not so smart? self.line can change after all! @lazy_property def _tabinsert_left(self): try: return self.line[:self.line[0:self.pos].rindex(' ') + 1] except ValueError: return '' @lazy_property def _tabinsert_right(self): return self.line[self.pos:] # COMPAT: this is still used in old commands.py configs def _tab_only_directories(self): from os.path import dirname, basename, expanduser, join cwd = self.fm.thisdir.path rel_dest = self.rest(1) # expand the tilde into the user directory if rel_dest.startswith('~'): rel_dest = expanduser(rel_dest) # define some shortcuts abs_dest = join(cwd, rel_dest) abs_dirname = dirname(abs_dest) rel_basename = basename(rel_dest) rel_dirname = dirname(rel_dest) try: # are we at the end of a directory? if rel_dest.endswith('/') or rel_dest == '': _, dirnames, _ = next(os.walk(abs_dest)) # are we in the middle of the filename? else: _, dirnames, _ = next(os.walk(abs_dirname)) dirnames = [dn for dn in dirnames \ if dn.startswith(rel_basename)] except (OSError, StopIteration): # os.walk found nothing pass else: dirnames.sort() # no results, return None if len(dirnames) == 0: return # one result. since it must be a directory, append a slash. if len(dirnames) == 1: return self.start(1) + join(rel_dirname, dirnames[0]) + '/' # more than one result. append no slash, so the user can # manually type in the slash to advance into that directory return (self.start(1) + join(rel_dirname, dirname) for dirname in dirnames) def _tab_directory_content(self): from os.path import dirname, basename, expanduser, join cwd = self.fm.thisdir.path rel_dest = self.rest(1) # expand the tilde into the user directory if rel_dest.startswith('~'): rel_dest = expanduser(rel_dest) # define some shortcuts abs_dest = join(cwd, rel_dest) abs_dirname = dirname(abs_dest) rel_basename = basename(rel_dest) rel_dirname = dirname(rel_dest) try: directory = self.fm.get_directory(abs_dest) # are we at the end of a directory? if rel_dest.endswith('/') or rel_dest == '': if directory.content_loaded: # Take the order from the directory object names = [f.basename for f in directory.files] if self.fm.thisfile.basename in names: i = names.index(self.fm.thisfile.basename) names = names[i:] + names[:i] else: # Fall back to old method with "os.walk" _, dirnames, filenames = next(os.walk(abs_dest)) names = dirnames + filenames names.sort() # are we in the middle of the filename? else: if directory.content_loaded: # Take the order from the directory object names = [f.basename for f in directory.files \ if f.basename.startswith(rel_basename)] if self.fm.thisfile.basename in names: i = names.index(self.fm.thisfile.basename) names = names[i:] + names[:i] else: # Fall back to old method with "os.walk" _, dirnames, filenames = next(os.walk(abs_dirname)) names = [name for name in (dirnames + filenames) \ if name.startswith(rel_basename)] names.sort() except (OSError, StopIteration): # os.walk found nothing pass else: # no results, return None if len(names) == 0: return # one result. append a slash if it's a directory if len(names) == 1: path = join(rel_dirname, names[0]) slash = '/' if os.path.isdir(path) else '' return self.start(1) + path + slash # more than one result. append no slash, so the user can # manually type in the slash to advance into that directory return (self.start(1) + join(rel_dirname, name) for name in names) def _tab_through_executables(self): from ranger.ext.get_executables import get_executables programs = [program for program in get_executables() if \ program.startswith(self.rest(1))] if not programs: return if len(programs) == 1: return self.start(1) + programs[0] programs.sort() return (self.start(1) + program for program in programs) class FunctionCommand(Command): _based_function = None _object_name = "" _function_name = "unknown" def execute(self): if not self._based_function: return if len(self.args) == 1: try: return self._based_function(**{'narg':self.quantifier}) except TypeError: return self._based_function() args, keywords = list(), dict() for arg in self.args[1:]: equal_sign = arg.find("=") value = arg if (equal_sign is -1) else arg[equal_sign + 1:] try: value = int(value) except: if value in ('True', 'False'): value = (value == 'True') else: try: value = float(value) except: pass if equal_sign == -1: args.append(value) else: keywords[arg[:equal_sign]] = value if self.quantifier is not None: keywords['narg'] = self.quantifier try: if self.quantifier is None: return self._based_function(*args, **keywords) else: try: return self._based_function(*args, **keywords) except TypeError: del keywords['narg'] return self._based_function(*args, **keywords) except TypeError: if ranger.arg.debug: raise else: self.fm.notify("Bad arguments for %s.%s: %s, %s" % (self._object_name, self._function_name, repr(args), repr(keywords)), bad=True) class AliasCommand(Command): _based_function = None _object_name = "" _function_name = "unknown" _line = "" def execute(self): return self._make_cmd().execute() def quick(self): return self._make_cmd().quick() def tab(self): return self._make_cmd().tab() def cancel(self): return self._make_cmd().cancel() def _make_cmd(self): cmd_class = self.fm.commands.get_command(self._line.split()[0]) cmd = cmd_class(self._line + ' ' + self.rest(1)) cmd.quickly_executed = self.quickly_executed cmd.quantifier = self.quantifier cmd.escape_macros_for_shell = self.escape_macros_for_shell cmd.resolve_macros = self.resolve_macros cmd.allow_abbrev = self.allow_abbrev return cmd if __name__ == '__main__': import doctest doctest.testmod()