about summary refs log blame commit diff stats
path: root/ranger/api/commands.py
blob: ba1715535df6e19c9b661afae2f7ad928f8313ef (plain) (tree)
1
2
3
4
5
6
7
8
9

                                                                 
 

                                                                        
                                                                  
 
         
         





                                                                                     
                                               
                                                  
 

                     
                                                 
                                     


                                         
 






                                        





                                                                                           
                                                                            



                                                
                                                               









                                                                       
                                                                                   


                                             
                                                            
                                                                               
                       












                                                 
                                                                                  


                                









                                              
                            
                                    
                                     



                                





                                                        

                                           
                                                     
                           
                           



                           
                          



















                                                      

                                            










                                                           
                                                   





                                 
                                 




















                                                                              

                                          
                                                







                                                                     


















                                                                              


















                                                                        
                











                                                    




                                                                              

























                                                               
                                                 
                                                           






                                        
                            










                                                                           
                                                                        
















                                                               

                                                       

                                                        








                                                                    
                                                        


                                                   

                                                              
                                                                
                                                                    





                                                                       

                                                                           



                                        
                                     
                         













                                                                              
                                                               
                                                     





                                                                
 
 

                                                                       


                                                                                          

                                
                       
 
 

                                   



                                                               
                    
                                                            
                                 






































                                                                                              


                                            
                          
 
 


                          
# 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
# COMPAT pylint: disable=unused-import
from collections import deque  # NOQA
from ranger.api import LinemodeBase, hook_init, hook_ready, register_linemode  # NOQA
# pylint: enable=unused-import

import ranger
from ranger.core.shared import FileManagerAware
from ranger.ext.lazy_property import lazy_property

__all__ = ['Command']

_SETTINGS_RE = re.compile(r'^\s*([^\s]+?)=(.*)$')
_ALIAS_LINE_RE = re.compile(r'(\s+)')


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_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()] = 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_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):
        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.__doc__ = func.__doc__
    CommandFunction.__name__ = func.__name__
    return CommandFunction


if __name__ == '__main__':
    import doctest
    doctest.testmod()