# This file is part of ranger, the console file manager. # License: GNU GPL version 3, see the file "AUTHORS" for details. # TODO: Add an optional "!" to all commands and set a flag if it's there from __future__ import (absolute_import, division, print_function) import os import re import ranger from ranger import MACRO_DELIMITER, MACRO_DELIMITER_ESC from ranger.core.shared import FileManagerAware from ranger.ext.lazy_property import lazy_property from ranger.api import LinemodeBase, hook_init, hook_ready, register_linemode # COMPAT __all__ = ['Command', 'LinemodeBase', 'hook_init', 'hook_ready', 'register_linemode'] # COMPAT _SETTINGS_RE = re.compile(r'^\s*([^\s]+?)=(.*)$') _ALIAS_LINE_RE = re.compile(r'(\s+)') def _command_init(cls): # Escape macros for tab completion if cls.resolve_macros: tab_old = cls.tab def tab(self, tabnum): results = tab_old(self, tabnum) if results is None: return None elif isinstance(results, str): return results.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC) elif hasattr(results, '__iter__'): return (result.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC) for result in results) return None setattr(cls, 'tab', tab) return cls class CommandContainer(FileManagerAware): def __init__(self): self.commands = {} def __getitem__(self, key): return self.commands[key] def alias(self, name, full_command): cmd_name = full_command.split()[0] try: cmd = self.get_command(cmd_name) except KeyError: self.fm.notify('alias failed: No such command: {0}'.format(cmd_name), bad=True) return None self.commands[name] = _command_init(command_alias_factory(name, cmd, full_command)) def load_commands_from_module(self, module): for var in vars(module).values(): try: if issubclass(var, Command) and var != Command: self.commands[var.get_name()] = _command_init(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__'): self.commands[attribute_name] = _command_init(command_function_factory(attribute)) 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 not lst: 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 sorted(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.init_line(line) self.quantifier = quantifier self.quickly_executed = False def init_line(self, line): self.line = line self.args = line.split() try: self.firstpart = line[:line.rindex(' ') + 1] except ValueError: self.firstpart = '' @classmethod def get_name(cls): classdict = cls.__mro__[0].__dict__ if 'name' in classdict and classdict['name']: return cls.name return cls.__name__ def execute(self): """Override this""" def tab(self, tabnum): """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, char in enumerate(self.line): if char.isspace(): 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 parse_setting_line(self): """ Parses the command line argument that is passed to the `:set` command. Returns [option, value, name_complete]. Can parse incomplete lines too, and `name_complete` is a boolean indicating whether the option name looks like it's completed or unfinished. This is useful for generating tab completions. >>> Command("set foo=bar").parse_setting_line() ['foo', 'bar', True] >>> Command("set foo").parse_setting_line() ['foo', '', False] >>> Command("set foo=").parse_setting_line() ['foo', '', True] >>> Command("set foo ").parse_setting_line() ['foo', '', True] >>> Command("set myoption myvalue").parse_setting_line() ['myoption', 'myvalue', True] >>> Command("set").parse_setting_line() ['', '', False] """ 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_setting_line_v2(self): """ Parses the command line argument that is passed to the `:set` command. Returns [option, value, name_complete, toggle]. >>> Command("set foo=bar").parse_setting_line_v2() ['foo', 'bar', True, False] >>> Command("set foo!").parse_setting_line_v2() ['foo', '', True, True] """ option, value, name_complete = self.parse_setting_line() if len(option) >= 2 and option[-1] == '!': toggle = True option = option[:-1] name_complete = True else: toggle = False return [option, value, name_complete, toggle] 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 args: 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 @lazy_property def log(self): import logging return logging.getLogger('ranger.commands.' + self.__class__.__name__) # 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 not dirnames: 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): # pylint: disable=too-many-locals 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 = sorted(dirnames + filenames) # 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 = sorted([name for name in (dirnames + filenames) if name.startswith(rel_basename)]) except (OSError, StopIteration): # os.walk found nothing pass else: # no results, return None if not names: 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) def command_alias_factory(name, cls, full_command): class CommandAlias(cls): # pylint: disable=too-few-public-methods def __init__(self, line, *args, **kwargs): super(CommandAlias, self).__init__( (full_command + ''.join(_ALIAS_LINE_RE.split(line)[1:])), *args, **kwargs) CommandAlias.__name__ = name return CommandAlias def command_function_factory(func): class CommandFunction(Command): __doc__ = func.__doc__ def execute(self): # pylint: disable=too-many-branches if not func: return if len(self.args) == 1: try: return func(**{'narg': self.quantifier}) except TypeError: return func() args, kwargs = list(), dict() for arg in self.args[1:]: equal_sign = arg.find("=") value = arg if equal_sign == -1 else arg[equal_sign + 1:] try: value = int(value) except ValueError: if value in ('True', 'False'): value = (value == 'True') else: try: value = float(value) except ValueError: pass if equal_sign == -1: args.append(value) else: kwargs[arg[:equal_sign]] = value if self.quantifier is not None: kwargs['narg'] = self.quantifier try: if self.quantifier is None: return func(*args, **kwargs) else: try: return func(*args, **kwargs) except TypeError: del kwargs['narg'] return func(*args, **kwargs) except TypeError: if ranger.args.debug: raise self.fm.notify("Bad arguments for %s: %s, %s" % (func.__name__, args, kwargs), bad=True) CommandFunction.__name__ = func.__name__ return CommandFunction if __name__ == '__main__': import doctest doctest.testmod()