about summary refs log tree commit diff stats
path: root/ranger
diff options
context:
space:
mode:
authortoonn <toonn@toonn.io>2021-09-07 19:20:56 +0200
committertoonn <toonn@toonn.io>2021-09-07 19:20:56 +0200
commit6f3a6006a0cd074a550e645383c7240d2d97f1ef (patch)
treedddf80df892f850736f585703549ed6db1dbc64c /ranger
parent7e6a0aa6a144f6733a8000a2822c107c93e60c1a (diff)
parent99771df4d7ec71b45976bdc85be66c30b725d14e (diff)
downloadranger-6f3a6006a0cd074a550e645383c7240d2d97f1ef.tar.gz
Merge branch 'jakanaka-eva-setlocal'
Diffstat (limited to 'ranger')
-rwxr-xr-xranger/config/commands.py106
-rw-r--r--ranger/container/settings.py2
2 files changed, 89 insertions, 19 deletions
diff --git a/ranger/config/commands.py b/ranger/config/commands.py
index d1d84e91..43c4993f 100755
--- a/ranger/config/commands.py
+++ b/ranger/config/commands.py
@@ -482,36 +482,106 @@ class set_(Command):
         return None
 
 
-class setlocal(set_):
-    """:setlocal path=<regular expression> <option name>=<python expression>
+class setlocal_(set_):
+    """Shared class for setinpath and setinregex
 
-    Gives an option a new value.
+    By implementing the _arg abstract propery you can affect what the name of
+    the pattern/path/regex argument can be, this should be a regular expression
+    without match groups.
+
+    By implementing the _format_arg abstract method you can affect how the
+    argument is formatted as a regular expression.
     """
-    PATH_RE_DQUOTED = re.compile(r'^setlocal\s+path="(.*?)"')
-    PATH_RE_SQUOTED = re.compile(r"^setlocal\s+path='(.*?)'")
-    PATH_RE_UNQUOTED = re.compile(r'^path=(.*?)$')
+    from abc import (ABCMeta, abstractmethod, abstractproperty)
+
+    __metaclass__ = ABCMeta
+
+    @property
+    @abstractmethod
+    def _arg(self):
+        """The name of the option for the path/regex"""
+        raise NotImplementedError
+
+    def __init__(self, *args, **kwargs):
+        super(setlocal_, self).__init__(*args, **kwargs)
+        # We require quoting of paths with whitespace so we have to take care
+        # not to match escaped quotes.
+        self.path_re_dquoted = re.compile(
+            r'^set.+?\s+{arg}="(.*?[^\\])"'.format(arg=self._arg)
+        )
+        self.path_re_squoted = re.compile(
+            r"^set.+?\s+{arg}='(.*?[^\\])'".format(arg=self._arg)
+        )
+        self.path_re_unquoted = re.compile(
+            r'^{arg}=(.+?)$'.format(arg=self._arg)
+        )
 
     def _re_shift(self, match):
         if not match:
             return None
-        path = os.path.expanduser(match.group(1))
-        for _ in range(len(path.split())):
+        path = match.group(1)
+        # Prepend something that behaves like "path=" in case path starts with
+        # whitespace
+        for _ in "={0}".format(path).split():
             self.shift()
-        return path
+        return os.path.expanduser(path)
+
+    @abstractmethod
+    def _format_arg(self, arg):
+        """How to format the argument as a regular expression"""
+        raise NotImplementedError
 
     def execute(self):
-        path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
-        if path is None:
-            path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
-        if path is None:
-            path = self._re_shift(self.PATH_RE_UNQUOTED.match(self.arg(1)))
-        if path is None and self.fm.thisdir:
-            path = self.fm.thisdir.path
-        if not path:
+        arg = self._re_shift(self.path_re_dquoted.match(self.line))
+        if arg is None:
+            arg = self._re_shift(self.path_re_squoted.match(self.line))
+        if arg is None:
+            arg = self._re_shift(self.path_re_unquoted.match(self.arg(1)))
+        if arg is None and self.fm.thisdir:
+            arg = self.fm.thisdir.path
+        if arg is None:
             return
+        else:
+            arg = self._format_arg(arg)
 
         name, value, _ = self.parse_setting_line()
-        self.fm.set_option_from_string(name, value, localpath=path)
+        self.fm.set_option_from_string(name, value, localpath=arg)
+
+
+class setinpath(setlocal_):
+    """:setinpath path=<path> <option name>=<python expression>
+
+    Sets an option when in a directory that matches <path>, relative paths can
+    match multiple directories, for example, ``path=build`` would match a build
+    directory in any directory. If the <path> contains whitespace it needs to
+    be quoted and nested quotes need to be backslash-escaped. The "path"
+    argument can also be named "pattern" to allow for easier switching with
+    ``setinregex``.
+    """
+    _arg = "(?:path|pattern)"
+
+    def _format_arg(self, arg):
+        return "{0}$".format(re.escape(arg))
+
+
+class setlocal(setinpath):
+    """:setlocal is an alias for :setinpath"""
+
+
+class setinregex(setlocal_):
+    """:setinregex re=<regex> <option name>=<python expression>
+
+    Sets an option when in a specific directory. If the <regex> contains
+    whitespace it needs to be quoted and nested quotes need to be
+    backslash-escaped. Special characters need to be escaped if they are
+    intended to match literally as documented in the ``re`` library
+    documentation. The "re" argument can also be named "regex" or "pattern,"
+    which allows for easier switching with ``setinpath``.
+    """
+    _arg = "(?:re(?:gex)?|pattern)"
+
+    def _format_arg(self, arg):
+        return arg
 
 
 class setintag(set_):
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index e083c5ac..80250d4f 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -281,7 +281,7 @@ class Settings(SignalDispatcher, FileManagerAware):
         if path:
             if path not in self._localsettings:
                 try:
-                    regex = re.compile(re.escape(path))
+                    regex = re.compile(path)
                 except re.error:  # Bad regular expression
                     return
                 self._localregexes[path] = regex