dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

commands_full.py (57039B)


      1 # -*- coding: utf-8 -*-
      2 # This file is part of ranger, the console file manager.
      3 # This configuration file is licensed under the same terms as ranger.
      4 # ===================================================================
      5 #
      6 # NOTE: If you copied this file to /etc/ranger/commands_full.py or
      7 # ~/.config/ranger/commands_full.py, then it will NOT be loaded by ranger,
      8 # and only serve as a reference.
      9 #
     10 # ===================================================================
     11 # This file contains ranger's commands.
     12 # It's all in python; lines beginning with # are comments.
     13 #
     14 # Note that additional commands are automatically generated from the methods
     15 # of the class ranger.core.actions.Actions.
     16 #
     17 # You can customize commands in the files /etc/ranger/commands.py (system-wide)
     18 # and ~/.config/ranger/commands.py (per user).
     19 # They have the same syntax as this file.  In fact, you can just copy this
     20 # file to ~/.config/ranger/commands_full.py with
     21 # `ranger --copy-config=commands_full' and make your modifications, don't
     22 # forget to rename it to commands.py.  You can also use
     23 # `ranger --copy-config=commands' to copy a short sample commands.py that
     24 # has everything you need to get started.
     25 # But make sure you update your configs when you update ranger.
     26 #
     27 # ===================================================================
     28 # Every class defined here which is a subclass of `Command' will be used as a
     29 # command in ranger.  Several methods are defined to interface with ranger:
     30 #   execute():   called when the command is executed.
     31 #   cancel():    called when closing the console.
     32 #   tab(tabnum): called when <TAB> is pressed.
     33 #   quick():     called after each keypress.
     34 #
     35 # tab() argument tabnum is 1 for <TAB> and -1 for <S-TAB> by default
     36 #
     37 # The return values for tab() can be either:
     38 #   None: There is no tab completion
     39 #   A string: Change the console to this string
     40 #   A list/tuple/generator: cycle through every item in it
     41 #
     42 # The return value for quick() can be:
     43 #   False: Nothing happens
     44 #   True: Execute the command afterwards
     45 #
     46 # The return value for execute() and cancel() doesn't matter.
     47 #
     48 # ===================================================================
     49 # Commands have certain attributes and methods that facilitate parsing of
     50 # the arguments:
     51 #
     52 # self.line: The whole line that was written in the console.
     53 # self.args: A list of all (space-separated) arguments to the command.
     54 # self.quantifier: If this command was mapped to the key "X" and
     55 #      the user pressed 6X, self.quantifier will be 6.
     56 # self.arg(n): The n-th argument, or an empty string if it doesn't exist.
     57 # self.rest(n): The n-th argument plus everything that followed.  For example,
     58 #      if the command was "search foo bar a b c", rest(2) will be "bar a b c"
     59 # self.start(n): Anything before the n-th argument.  For example, if the
     60 #      command was "search foo bar a b c", start(2) will be "search foo"
     61 #
     62 # ===================================================================
     63 # And this is a little reference for common ranger functions and objects:
     64 #
     65 # self.fm: A reference to the "fm" object which contains most information
     66 #      about ranger.
     67 # self.fm.notify(string): Print the given string on the screen.
     68 # self.fm.notify(string, bad=True): Print the given string in RED.
     69 # self.fm.reload_cwd(): Reload the current working directory.
     70 # self.fm.thisdir: The current working directory. (A File object.)
     71 # self.fm.thisfile: The current file. (A File object too.)
     72 # self.fm.thistab.get_selection(): A list of all selected files.
     73 # self.fm.execute_console(string): Execute the string as a ranger command.
     74 # self.fm.open_console(string): Open the console with the given string
     75 #      already typed in for you.
     76 # self.fm.move(direction): Moves the cursor in the given direction, which
     77 #      can be something like down=3, up=5, right=1, left=1, to=6, ...
     78 #
     79 # File objects (for example self.fm.thisfile) have these useful attributes and
     80 # methods:
     81 #
     82 # tfile.path: The path to the file.
     83 # tfile.basename: The base name only.
     84 # tfile.load_content(): Force a loading of the directories content (which
     85 #      obviously works with directories only)
     86 # tfile.is_directory: True/False depending on whether it's a directory.
     87 #
     88 # For advanced commands it is unavoidable to dive a bit into the source code
     89 # of ranger.
     90 # ===================================================================
     91 
     92 from __future__ import (absolute_import, division, print_function)
     93 
     94 from collections import deque
     95 import os
     96 import re
     97 
     98 from ranger.api.commands import Command
     99 
    100 
    101 class alias(Command):
    102     """:alias <newcommand> <oldcommand>
    103 
    104     Copies the oldcommand as newcommand.
    105     """
    106 
    107     context = 'browser'
    108     resolve_macros = False
    109 
    110     def execute(self):
    111         if not self.arg(1) or not self.arg(2):
    112             self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True)
    113             return
    114 
    115         self.fm.commands.alias(self.arg(1), self.rest(2))
    116 
    117 
    118 class echo(Command):
    119     """:echo <text>
    120 
    121     Display the text in the statusbar.
    122     """
    123 
    124     def execute(self):
    125         self.fm.notify(self.rest(1))
    126 
    127 
    128 class cd(Command):
    129     """:cd [-r] <path>
    130 
    131     The cd command changes the directory.
    132     If the path is a file, selects that file.
    133     The command 'cd -' is equivalent to typing ``.
    134     Using the option "-r" will get you to the real path.
    135     """
    136 
    137     def execute(self):
    138         if self.arg(1) == '-r':
    139             self.shift()
    140             destination = os.path.realpath(self.rest(1))
    141             if os.path.isfile(destination):
    142                 self.fm.select_file(destination)
    143                 return
    144         else:
    145             destination = self.rest(1)
    146 
    147         if not destination:
    148             destination = '~'
    149 
    150         if destination == '-':
    151             self.fm.enter_bookmark('`')
    152         else:
    153             self.fm.cd(destination)
    154 
    155     def _tab_args(self):
    156         # dest must be rest because path could contain spaces
    157         if self.arg(1) == '-r':
    158             start = self.start(2)
    159             dest = self.rest(2)
    160         else:
    161             start = self.start(1)
    162             dest = self.rest(1)
    163 
    164         if dest:
    165             head, tail = os.path.split(os.path.expanduser(dest))
    166             if head:
    167                 dest_exp = os.path.join(os.path.normpath(head), tail)
    168             else:
    169                 dest_exp = tail
    170         else:
    171             dest_exp = ''
    172         return (start, dest_exp, os.path.join(self.fm.thisdir.path, dest_exp),
    173                 dest.endswith(os.path.sep))
    174 
    175     @staticmethod
    176     def _tab_paths(dest, dest_abs, ends_with_sep):
    177         if not dest:
    178             try:
    179                 return next(os.walk(dest_abs))[1], dest_abs
    180             except (OSError, StopIteration):
    181                 return [], ''
    182 
    183         if ends_with_sep:
    184             try:
    185                 return [os.path.join(dest, path) for path in next(os.walk(dest_abs))[1]], ''
    186             except (OSError, StopIteration):
    187                 return [], ''
    188 
    189         return None, None
    190 
    191     def _tab_match(self, path_user, path_file):
    192         if self.fm.settings.cd_tab_case == 'insensitive':
    193             path_user = path_user.lower()
    194             path_file = path_file.lower()
    195         elif self.fm.settings.cd_tab_case == 'smart' and path_user.islower():
    196             path_file = path_file.lower()
    197         return path_file.startswith(path_user)
    198 
    199     def _tab_normal(self, dest, dest_abs):
    200         dest_dir = os.path.dirname(dest)
    201         dest_base = os.path.basename(dest)
    202 
    203         try:
    204             dirnames = next(os.walk(os.path.dirname(dest_abs)))[1]
    205         except (OSError, StopIteration):
    206             return [], ''
    207 
    208         return [os.path.join(dest_dir, d) for d in dirnames if self._tab_match(dest_base, d)], ''
    209 
    210     def _tab_fuzzy_match(self, basepath, tokens):
    211         """ Find directories matching tokens recursively """
    212         if not tokens:
    213             tokens = ['']
    214         paths = [basepath]
    215         while True:
    216             token = tokens.pop()
    217             matches = []
    218             for path in paths:
    219                 try:
    220                     directories = next(os.walk(path))[1]
    221                 except (OSError, StopIteration):
    222                     continue
    223                 matches += [os.path.join(path, d) for d in directories
    224                             if self._tab_match(token, d)]
    225             if not tokens or not matches:
    226                 return matches
    227             paths = matches
    228 
    229         return None
    230 
    231     def _tab_fuzzy(self, dest, dest_abs):
    232         tokens = []
    233         basepath = dest_abs
    234         while True:
    235             basepath_old = basepath
    236             basepath, token = os.path.split(basepath)
    237             if basepath == basepath_old:
    238                 break
    239             if os.path.isdir(basepath_old) and not token.startswith('.'):
    240                 basepath = basepath_old
    241                 break
    242             tokens.append(token)
    243 
    244         paths = self._tab_fuzzy_match(basepath, tokens)
    245         if not os.path.isabs(dest):
    246             paths_rel = basepath
    247             paths = [os.path.relpath(path, paths_rel) for path in paths]
    248         else:
    249             paths_rel = ''
    250         return paths, paths_rel
    251 
    252     def tab(self, tabnum):
    253         from os.path import sep
    254 
    255         start, dest, dest_abs, ends_with_sep = self._tab_args()
    256 
    257         paths, paths_rel = self._tab_paths(dest, dest_abs, ends_with_sep)
    258         if paths is None:
    259             if self.fm.settings.cd_tab_fuzzy:
    260                 paths, paths_rel = self._tab_fuzzy(dest, dest_abs)
    261             else:
    262                 paths, paths_rel = self._tab_normal(dest, dest_abs)
    263 
    264         paths.sort()
    265 
    266         if self.fm.settings.cd_bookmarks:
    267             paths[0:0] = [
    268                 os.path.relpath(v.path, paths_rel) if paths_rel else v.path
    269                 for v in self.fm.bookmarks.dct.values() for path in paths
    270                 if v.path.startswith(os.path.join(paths_rel, path) + sep)
    271             ]
    272 
    273         if not paths:
    274             return None
    275         if len(paths) == 1:
    276             return start + paths[0] + sep
    277         return [start + dirname for dirname in paths]
    278 
    279 
    280 class chain(Command):
    281     """:chain <command1>; <command2>; ...
    282 
    283     Calls multiple commands at once, separated by semicolons.
    284     """
    285 
    286     def execute(self):
    287         if not self.rest(1).strip():
    288             self.fm.notify('Syntax: chain <command1>; <command2>; ...', bad=True)
    289             return
    290         for command in [s.strip() for s in self.rest(1).split(";")]:
    291             self.fm.execute_console(command)
    292 
    293 
    294 class shell(Command):
    295     escape_macros_for_shell = True
    296 
    297     def execute(self):
    298         if self.arg(1) and self.arg(1)[0] == '-':
    299             flags = self.arg(1)[1:]
    300             command = self.rest(2)
    301         else:
    302             flags = ''
    303             command = self.rest(1)
    304 
    305         if command:
    306             self.fm.execute_command(command, flags=flags)
    307 
    308     def tab(self, tabnum):
    309         from ranger.ext.get_executables import get_executables
    310         if self.arg(1) and self.arg(1)[0] == '-':
    311             command = self.rest(2)
    312         else:
    313             command = self.rest(1)
    314         start = self.line[0:len(self.line) - len(command)]
    315 
    316         try:
    317             position_of_last_space = command.rindex(" ")
    318         except ValueError:
    319             return (start + program + ' ' for program
    320                     in get_executables() if program.startswith(command))
    321         if position_of_last_space == len(command) - 1:
    322             selection = self.fm.thistab.get_selection()
    323             if len(selection) == 1:
    324                 return self.line + selection[0].shell_escaped_basename + ' '
    325             return self.line + '%s '
    326 
    327         before_word, start_of_word = self.line.rsplit(' ', 1)
    328         return (before_word + ' ' + file.shell_escaped_basename
    329                 for file in self.fm.thisdir.files or []
    330                 if file.shell_escaped_basename.startswith(start_of_word))
    331 
    332 
    333 class open_with(Command):
    334 
    335     def execute(self):
    336         app, flags, mode = self._get_app_flags_mode(self.rest(1))
    337         self.fm.execute_file(
    338             files=[f for f in self.fm.thistab.get_selection()],
    339             app=app,
    340             flags=flags,
    341             mode=mode)
    342 
    343     def tab(self, tabnum):
    344         return self._tab_through_executables()
    345 
    346     def _get_app_flags_mode(self, string):  # pylint: disable=too-many-branches,too-many-statements
    347         """Extracts the application, flags and mode from a string.
    348 
    349         examples:
    350         "mplayer f 1" => ("mplayer", "f", 1)
    351         "atool 4" => ("atool", "", 4)
    352         "p" => ("", "p", 0)
    353         "" => None
    354         """
    355 
    356         app = ''
    357         flags = ''
    358         mode = 0
    359         split = string.split()
    360 
    361         if len(split) == 1:
    362             part = split[0]
    363             if self._is_app(part):
    364                 app = part
    365             elif self._is_flags(part):
    366                 flags = part
    367             elif self._is_mode(part):
    368                 mode = part
    369 
    370         elif len(split) == 2:
    371             part0 = split[0]
    372             part1 = split[1]
    373 
    374             if self._is_app(part0):
    375                 app = part0
    376                 if self._is_flags(part1):
    377                     flags = part1
    378                 elif self._is_mode(part1):
    379                     mode = part1
    380             elif self._is_flags(part0):
    381                 flags = part0
    382                 if self._is_mode(part1):
    383                     mode = part1
    384             elif self._is_mode(part0):
    385                 mode = part0
    386                 if self._is_flags(part1):
    387                     flags = part1
    388 
    389         elif len(split) >= 3:
    390             part0 = split[0]
    391             part1 = split[1]
    392             part2 = split[2]
    393 
    394             if self._is_app(part0):
    395                 app = part0
    396                 if self._is_flags(part1):
    397                     flags = part1
    398                     if self._is_mode(part2):
    399                         mode = part2
    400                 elif self._is_mode(part1):
    401                     mode = part1
    402                     if self._is_flags(part2):
    403                         flags = part2
    404             elif self._is_flags(part0):
    405                 flags = part0
    406                 if self._is_mode(part1):
    407                     mode = part1
    408             elif self._is_mode(part0):
    409                 mode = part0
    410                 if self._is_flags(part1):
    411                     flags = part1
    412 
    413         return app, flags, int(mode)
    414 
    415     def _is_app(self, arg):
    416         return not self._is_flags(arg) and not arg.isdigit()
    417 
    418     @staticmethod
    419     def _is_flags(arg):
    420         from ranger.core.runner import ALLOWED_FLAGS
    421         return all(x in ALLOWED_FLAGS for x in arg)
    422 
    423     @staticmethod
    424     def _is_mode(arg):
    425         return all(x in '0123456789' for x in arg)
    426 
    427 
    428 class set_(Command):
    429     """:set <option name>=<python expression>
    430 
    431     Gives an option a new value.
    432 
    433     Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!`
    434     """
    435     name = 'set'  # don't override the builtin set class
    436 
    437     def execute(self):
    438         name = self.arg(1)
    439         name, value, _, toggle = self.parse_setting_line_v2()
    440         if toggle:
    441             self.fm.toggle_option(name)
    442         else:
    443             self.fm.set_option_from_string(name, value)
    444 
    445     def tab(self, tabnum):  # pylint: disable=too-many-return-statements
    446         from ranger.gui.colorscheme import get_all_colorschemes
    447         name, value, name_done = self.parse_setting_line()
    448         settings = self.fm.settings
    449         if not name:
    450             return sorted(self.firstpart + setting for setting in settings)
    451         if not value and not name_done:
    452             return sorted(self.firstpart + setting for setting in settings
    453                           if setting.startswith(name))
    454         if not value:
    455             value_completers = {
    456                 "colorscheme":
    457                 # Cycle through colorschemes when name, but no value is specified
    458                 lambda: sorted(self.firstpart + colorscheme for colorscheme
    459                                in get_all_colorschemes(self.fm)),
    460 
    461                 "column_ratios":
    462                 lambda: self.firstpart + ",".join(map(str, settings[name])),
    463             }
    464 
    465             def default_value_completer():
    466                 return self.firstpart + str(settings[name])
    467 
    468             return value_completers.get(name, default_value_completer)()
    469         if bool in settings.types_of(name):
    470             if 'true'.startswith(value.lower()):
    471                 return self.firstpart + 'True'
    472             if 'false'.startswith(value.lower()):
    473                 return self.firstpart + 'False'
    474         # Tab complete colorscheme values if incomplete value is present
    475         if name == "colorscheme":
    476             return sorted(self.firstpart + colorscheme for colorscheme
    477                           in get_all_colorschemes(self.fm) if colorscheme.startswith(value))
    478         return None
    479 
    480 
    481 class setlocal(set_):
    482     """:setlocal path=<regular expression> <option name>=<python expression>
    483 
    484     Gives an option a new value.
    485     """
    486     PATH_RE_DQUOTED = re.compile(r'^setlocal\s+path="(.*?)"')
    487     PATH_RE_SQUOTED = re.compile(r"^setlocal\s+path='(.*?)'")
    488     PATH_RE_UNQUOTED = re.compile(r'^path=(.*?)$')
    489 
    490     def _re_shift(self, match):
    491         if not match:
    492             return None
    493         path = os.path.expanduser(match.group(1))
    494         for _ in range(len(path.split())):
    495             self.shift()
    496         return path
    497 
    498     def execute(self):
    499         path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
    500         if path is None:
    501             path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
    502         if path is None:
    503             path = self._re_shift(self.PATH_RE_UNQUOTED.match(self.arg(1)))
    504         if path is None and self.fm.thisdir:
    505             path = self.fm.thisdir.path
    506         if not path:
    507             return
    508 
    509         name, value, _ = self.parse_setting_line()
    510         self.fm.set_option_from_string(name, value, localpath=path)
    511 
    512 
    513 class setintag(set_):
    514     """:setintag <tag or tags> <option name>=<option value>
    515 
    516     Sets an option for directories that are tagged with a specific tag.
    517     """
    518 
    519     def execute(self):
    520         tags = self.arg(1)
    521         self.shift()
    522         name, value, _ = self.parse_setting_line()
    523         self.fm.set_option_from_string(name, value, tags=tags)
    524 
    525 
    526 class default_linemode(Command):
    527 
    528     def execute(self):
    529         from ranger.container.fsobject import FileSystemObject
    530 
    531         if len(self.args) < 2:
    532             self.fm.notify(
    533                 "Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
    534 
    535         # Extract options like "path=..." or "tag=..." from the command line
    536         arg1 = self.arg(1)
    537         method = "always"
    538         argument = None
    539         if arg1.startswith("path="):
    540             method = "path"
    541             argument = re.compile(arg1[5:])
    542             self.shift()
    543         elif arg1.startswith("tag="):
    544             method = "tag"
    545             argument = arg1[4:]
    546             self.shift()
    547 
    548         # Extract and validate the line mode from the command line
    549         lmode = self.rest(1)
    550         if lmode not in FileSystemObject.linemode_dict:
    551             self.fm.notify(
    552                 "Invalid linemode: %s; should be %s" % (
    553                     lmode, "/".join(FileSystemObject.linemode_dict)),
    554                 bad=True,
    555             )
    556 
    557         # Add the prepared entry to the fm.default_linemodes
    558         entry = [method, argument, lmode]
    559         self.fm.default_linemodes.appendleft(entry)
    560 
    561         # Redraw the columns
    562         if self.fm.ui.browser:
    563             for col in self.fm.ui.browser.columns:
    564                 col.need_redraw = True
    565 
    566     def tab(self, tabnum):
    567         return (self.arg(0) + " " + lmode
    568                 for lmode in self.fm.thisfile.linemode_dict.keys()
    569                 if lmode.startswith(self.arg(1)))
    570 
    571 
    572 class quit(Command):  # pylint: disable=redefined-builtin
    573     """:quit
    574 
    575     Closes the current tab, if there's only one tab.
    576     Otherwise quits if there are no tasks in progress.
    577     """
    578     def _exit_no_work(self):
    579         if self.fm.loader.has_work():
    580             self.fm.notify('Not quitting: Tasks in progress: Use `quit!` to force quit')
    581         else:
    582             self.fm.exit()
    583 
    584     def execute(self):
    585         if len(self.fm.tabs) >= 2:
    586             self.fm.tab_close()
    587         else:
    588             self._exit_no_work()
    589 
    590 
    591 class quit_bang(Command):
    592     """:quit!
    593 
    594     Closes the current tab, if there's only one tab.
    595     Otherwise force quits immediately.
    596     """
    597     name = 'quit!'
    598     allow_abbrev = False
    599 
    600     def execute(self):
    601         if len(self.fm.tabs) >= 2:
    602             self.fm.tab_close()
    603         else:
    604             self.fm.exit()
    605 
    606 
    607 class quitall(Command):
    608     """:quitall
    609 
    610     Quits if there are no tasks in progress.
    611     """
    612     def _exit_no_work(self):
    613         if self.fm.loader.has_work():
    614             self.fm.notify('Not quitting: Tasks in progress: Use `quitall!` to force quit')
    615         else:
    616             self.fm.exit()
    617 
    618     def execute(self):
    619         self._exit_no_work()
    620 
    621 
    622 class quitall_bang(Command):
    623     """:quitall!
    624 
    625     Force quits immediately.
    626     """
    627     name = 'quitall!'
    628     allow_abbrev = False
    629 
    630     def execute(self):
    631         self.fm.exit()
    632 
    633 
    634 class terminal(Command):
    635     """:terminal
    636 
    637     Spawns an "x-terminal-emulator" starting in the current directory.
    638     """
    639 
    640     def execute(self):
    641         from ranger.ext.get_executables import get_term
    642         self.fm.run(get_term(), flags='f')
    643 
    644 
    645 class delete(Command):
    646     """:delete
    647 
    648     Tries to delete the selection or the files passed in arguments (if any).
    649     The arguments use a shell-like escaping.
    650 
    651     "Selection" is defined as all the "marked files" (by default, you
    652     can mark files with space or v). If there are no marked files,
    653     use the "current file" (where the cursor is)
    654 
    655     When attempting to delete non-empty directories or multiple
    656     marked files, it will require a confirmation.
    657     """
    658 
    659     allow_abbrev = False
    660     escape_macros_for_shell = True
    661 
    662     def execute(self):
    663         import shlex
    664         from functools import partial
    665 
    666         def is_directory_with_files(path):
    667             return os.path.isdir(path) and not os.path.islink(path) and len(os.listdir(path)) > 0
    668 
    669         if self.rest(1):
    670             files = shlex.split(self.rest(1))
    671             many_files = (len(files) > 1 or is_directory_with_files(files[0]))
    672         else:
    673             cwd = self.fm.thisdir
    674             tfile = self.fm.thisfile
    675             if not cwd or not tfile:
    676                 self.fm.notify("Error: no file selected for deletion!", bad=True)
    677                 return
    678 
    679             # relative_path used for a user-friendly output in the confirmation.
    680             files = [f.relative_path for f in self.fm.thistab.get_selection()]
    681             many_files = (cwd.marked_items or is_directory_with_files(tfile.path))
    682 
    683         confirm = self.fm.settings.confirm_on_delete
    684         if confirm != 'never' and (confirm != 'multiple' or many_files):
    685             self.fm.ui.console.ask(
    686                 "Confirm deletion of: %s (y/N)" % ', '.join(files),
    687                 partial(self._question_callback, files),
    688                 ('n', 'N', 'y', 'Y'),
    689             )
    690         else:
    691             # no need for a confirmation, just delete
    692             self.fm.delete(files)
    693 
    694     def tab(self, tabnum):
    695         return self._tab_directory_content()
    696 
    697     def _question_callback(self, files, answer):
    698         if answer == 'y' or answer == 'Y':
    699             self.fm.delete(files)
    700 
    701 
    702 class jump_non(Command):
    703     """:jump_non [-FLAGS...]
    704 
    705     Jumps to first non-directory if highlighted file is a directory and vice versa.
    706 
    707     Flags:
    708      -r    Jump in reverse order
    709      -w    Wrap around if reaching end of filelist
    710     """
    711     def __init__(self, *args, **kwargs):
    712         super(jump_non, self).__init__(*args, **kwargs)
    713 
    714         flags, _ = self.parse_flags()
    715         self._flag_reverse = 'r' in flags
    716         self._flag_wrap = 'w' in flags
    717 
    718     @staticmethod
    719     def _non(fobj, is_directory):
    720         return fobj.is_directory if not is_directory else not fobj.is_directory
    721 
    722     def execute(self):
    723         tfile = self.fm.thisfile
    724         passed = False
    725         found_before = None
    726         found_after = None
    727         for fobj in self.fm.thisdir.files[::-1] if self._flag_reverse else self.fm.thisdir.files:
    728             if fobj.path == tfile.path:
    729                 passed = True
    730                 continue
    731 
    732             if passed:
    733                 if self._non(fobj, tfile.is_directory):
    734                     found_after = fobj.path
    735                     break
    736             elif not found_before and self._non(fobj, tfile.is_directory):
    737                 found_before = fobj.path
    738 
    739         if found_after:
    740             self.fm.select_file(found_after)
    741         elif self._flag_wrap and found_before:
    742             self.fm.select_file(found_before)
    743 
    744 
    745 class mark_tag(Command):
    746     """:mark_tag [<tags>]
    747 
    748     Mark all tags that are tagged with either of the given tags.
    749     When leaving out the tag argument, all tagged files are marked.
    750     """
    751     do_mark = True
    752 
    753     def execute(self):
    754         cwd = self.fm.thisdir
    755         tags = self.rest(1).replace(" ", "")
    756         if not self.fm.tags or not cwd.files:
    757             return
    758         for fileobj in cwd.files:
    759             try:
    760                 tag = self.fm.tags.tags[fileobj.realpath]
    761             except KeyError:
    762                 continue
    763             if not tags or tag in tags:
    764                 cwd.mark_item(fileobj, val=self.do_mark)
    765         self.fm.ui.status.need_redraw = True
    766         self.fm.ui.need_redraw = True
    767 
    768 
    769 class console(Command):
    770     """:console <command>
    771 
    772     Open the console with the given command.
    773     """
    774 
    775     def execute(self):
    776         position = None
    777         if self.arg(1)[0:2] == '-p':
    778             try:
    779                 position = int(self.arg(1)[2:])
    780             except ValueError:
    781                 pass
    782             else:
    783                 self.shift()
    784         self.fm.open_console(self.rest(1), position=position)
    785 
    786 
    787 class load_copy_buffer(Command):
    788     """:load_copy_buffer
    789 
    790     Load the copy buffer from datadir/copy_buffer
    791     """
    792     copy_buffer_filename = 'copy_buffer'
    793 
    794     def execute(self):
    795         import sys
    796         from ranger.container.file import File
    797         from os.path import exists
    798         fname = self.fm.datapath(self.copy_buffer_filename)
    799         unreadable = IOError if sys.version_info[0] < 3 else OSError
    800         try:
    801             fobj = open(fname, 'r')
    802         except unreadable:
    803             return self.fm.notify(
    804                 "Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
    805 
    806         self.fm.copy_buffer = set(File(g)
    807                                   for g in fobj.read().split("\n") if exists(g))
    808         fobj.close()
    809         self.fm.ui.redraw_main_column()
    810         return None
    811 
    812 
    813 class save_copy_buffer(Command):
    814     """:save_copy_buffer
    815 
    816     Save the copy buffer to datadir/copy_buffer
    817     """
    818     copy_buffer_filename = 'copy_buffer'
    819 
    820     def execute(self):
    821         import sys
    822         fname = None
    823         fname = self.fm.datapath(self.copy_buffer_filename)
    824         unwritable = IOError if sys.version_info[0] < 3 else OSError
    825         try:
    826             fobj = open(fname, 'w')
    827         except unwritable:
    828             return self.fm.notify("Cannot open %s" %
    829                                   (fname or self.copy_buffer_filename), bad=True)
    830         fobj.write("\n".join(fobj.path for fobj in self.fm.copy_buffer))
    831         fobj.close()
    832         return None
    833 
    834 
    835 class unmark_tag(mark_tag):
    836     """:unmark_tag [<tags>]
    837 
    838     Unmark all tags that are tagged with either of the given tags.
    839     When leaving out the tag argument, all tagged files are unmarked.
    840     """
    841     do_mark = False
    842 
    843 
    844 class mkdir(Command):
    845     """:mkdir <dirname>
    846 
    847     Creates a directory with the name <dirname>.
    848     """
    849 
    850     def execute(self):
    851         from os.path import join, expanduser, lexists
    852         from os import makedirs
    853 
    854         dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
    855         if not lexists(dirname):
    856             makedirs(dirname)
    857         else:
    858             self.fm.notify("file/directory exists!", bad=True)
    859 
    860     def tab(self, tabnum):
    861         return self._tab_directory_content()
    862 
    863 
    864 class touch(Command):
    865     """:touch <fname>
    866 
    867     Creates a file with the name <fname>.
    868     """
    869 
    870     def execute(self):
    871         from os.path import join, expanduser, lexists
    872 
    873         fname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
    874         if not lexists(fname):
    875             open(fname, 'a').close()
    876         else:
    877             self.fm.notify("file/directory exists!", bad=True)
    878 
    879     def tab(self, tabnum):
    880         return self._tab_directory_content()
    881 
    882 
    883 class edit(Command):
    884     """:edit <filename>
    885 
    886     Opens the specified file in vim
    887     """
    888 
    889     def execute(self):
    890         if not self.arg(1):
    891             self.fm.edit_file(self.fm.thisfile.path)
    892         else:
    893             self.fm.edit_file(self.rest(1))
    894 
    895     def tab(self, tabnum):
    896         return self._tab_directory_content()
    897 
    898 
    899 class eval_(Command):
    900     """:eval [-q] <python code>
    901 
    902     Evaluates the python code.
    903     `fm' is a reference to the FM instance.
    904     To display text, use the function `p'.
    905 
    906     Examples:
    907     :eval fm
    908     :eval len(fm.directories)
    909     :eval p("Hello World!")
    910     """
    911     name = 'eval'
    912     resolve_macros = False
    913 
    914     def execute(self):
    915         # The import is needed so eval() can access the ranger module
    916         import ranger  # NOQA pylint: disable=unused-import,unused-variable
    917         if self.arg(1) == '-q':
    918             code = self.rest(2)
    919             quiet = True
    920         else:
    921             code = self.rest(1)
    922             quiet = False
    923         global cmd, fm, p, quantifier  # pylint: disable=invalid-name,global-variable-undefined
    924         fm = self.fm
    925         cmd = self.fm.execute_console
    926         p = fm.notify
    927         quantifier = self.quantifier
    928         try:
    929             try:
    930                 result = eval(code)  # pylint: disable=eval-used
    931             except SyntaxError:
    932                 exec(code)  # pylint: disable=exec-used
    933             else:
    934                 if result and not quiet:
    935                     p(result)
    936         except Exception as err:  # pylint: disable=broad-except
    937             fm.notify("The error `%s` was caused by evaluating the "
    938                       "following code: `%s`" % (err, code), bad=True)
    939 
    940 
    941 class rename(Command):
    942     """:rename <newname>
    943 
    944     Changes the name of the currently highlighted file to <newname>
    945     """
    946 
    947     def execute(self):
    948         from ranger.container.file import File
    949         from os import access
    950 
    951         new_name = self.rest(1)
    952 
    953         if not new_name:
    954             return self.fm.notify('Syntax: rename <newname>', bad=True)
    955 
    956         if new_name == self.fm.thisfile.relative_path:
    957             return None
    958 
    959         if access(new_name, os.F_OK):
    960             return self.fm.notify("Can't rename: file already exists!", bad=True)
    961 
    962         if self.fm.rename(self.fm.thisfile, new_name):
    963             file_new = File(new_name)
    964             self.fm.bookmarks.update_path(self.fm.thisfile.path, file_new)
    965             self.fm.tags.update_path(self.fm.thisfile.path, file_new.path)
    966             self.fm.thisdir.pointed_obj = file_new
    967             self.fm.thisfile = file_new
    968 
    969         return None
    970 
    971     def tab(self, tabnum):
    972         return self._tab_directory_content()
    973 
    974 
    975 class rename_append(Command):
    976     """:rename_append [-FLAGS...]
    977 
    978     Opens the console with ":rename <current file>" with the cursor positioned
    979     before the file extension.
    980 
    981     Flags:
    982      -a    Position before all extensions
    983      -r    Remove everything before extensions
    984     """
    985     def __init__(self, *args, **kwargs):
    986         super(rename_append, self).__init__(*args, **kwargs)
    987 
    988         flags, _ = self.parse_flags()
    989         self._flag_ext_all = 'a' in flags
    990         self._flag_remove = 'r' in flags
    991 
    992     def execute(self):
    993         from ranger import MACRO_DELIMITER, MACRO_DELIMITER_ESC
    994 
    995         tfile = self.fm.thisfile
    996         relpath = tfile.relative_path.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
    997         basename = tfile.basename.replace(MACRO_DELIMITER, MACRO_DELIMITER_ESC)
    998 
    999         if basename.find('.') <= 0:
   1000             self.fm.open_console('rename ' + relpath)
   1001             return
   1002 
   1003         if self._flag_ext_all:
   1004             pos_ext = re.search(r'[^.]+', basename).end(0)
   1005         else:
   1006             pos_ext = basename.rindex('.')
   1007         pos = len(relpath) - len(basename) + pos_ext
   1008 
   1009         if self._flag_remove:
   1010             relpath = relpath[:-len(basename)] + basename[pos_ext:]
   1011             pos -= pos_ext
   1012 
   1013         self.fm.open_console('rename ' + relpath, position=(7 + pos))
   1014 
   1015 
   1016 class chmod(Command):
   1017     """:chmod <octal number>
   1018 
   1019     Sets the permissions of the selection to the octal number.
   1020 
   1021     The octal number is between 0 and 777. The digits specify the
   1022     permissions for the user, the group and others.
   1023 
   1024     A 1 permits execution, a 2 permits writing, a 4 permits reading.
   1025     Add those numbers to combine them. So a 7 permits everything.
   1026     """
   1027 
   1028     def execute(self):
   1029         mode_str = self.rest(1)
   1030         if not mode_str:
   1031             if not self.quantifier:
   1032                 self.fm.notify("Syntax: chmod <octal number>", bad=True)
   1033                 return
   1034             mode_str = str(self.quantifier)
   1035 
   1036         try:
   1037             mode = int(mode_str, 8)
   1038             if mode < 0 or mode > 0o777:
   1039                 raise ValueError
   1040         except ValueError:
   1041             self.fm.notify("Need an octal number between 0 and 777!", bad=True)
   1042             return
   1043 
   1044         for fobj in self.fm.thistab.get_selection():
   1045             try:
   1046                 os.chmod(fobj.path, mode)
   1047             except OSError as ex:
   1048                 self.fm.notify(ex)
   1049 
   1050         # reloading directory.  maybe its better to reload the selected
   1051         # files only.
   1052         self.fm.thisdir.content_outdated = True
   1053 
   1054 
   1055 class bulkrename(Command):
   1056     """:bulkrename
   1057 
   1058     This command opens a list of selected files in an external editor.
   1059     After you edit and save the file, it will generate a shell script
   1060     which does bulk renaming according to the changes you did in the file.
   1061 
   1062     This shell script is opened in an editor for you to review.
   1063     After you close it, it will be executed.
   1064     """
   1065 
   1066     def execute(self):  # pylint: disable=too-many-locals,too-many-statements
   1067         import sys
   1068         import tempfile
   1069         from ranger.container.file import File
   1070         from ranger.ext.shell_escape import shell_escape as esc
   1071         py3 = sys.version_info[0] >= 3
   1072 
   1073         # Create and edit the file list
   1074         filenames = [f.relative_path for f in self.fm.thistab.get_selection()]
   1075         listfile = tempfile.NamedTemporaryFile(delete=False)
   1076         listpath = listfile.name
   1077 
   1078         if py3:
   1079             listfile.write("\n".join(filenames).encode("utf-8"))
   1080         else:
   1081             listfile.write("\n".join(filenames))
   1082         listfile.close()
   1083         self.fm.execute_file([File(listpath)], app='editor')
   1084         listfile = open(listpath, 'r')
   1085         new_filenames = listfile.read().split("\n")
   1086         listfile.close()
   1087         os.unlink(listpath)
   1088         if all(a == b for a, b in zip(filenames, new_filenames)):
   1089             self.fm.notify("No renaming to be done!")
   1090             return
   1091 
   1092         # Generate script
   1093         cmdfile = tempfile.NamedTemporaryFile()
   1094         script_lines = []
   1095         script_lines.append("# This file will be executed when you close the editor.\n")
   1096         script_lines.append("# Please double-check everything, clear the file to abort.\n")
   1097         script_lines.extend("mv -vi -- %s %s\n" % (esc(old), esc(new))
   1098                             for old, new in zip(filenames, new_filenames) if old != new)
   1099         script_content = "".join(script_lines)
   1100         if py3:
   1101             cmdfile.write(script_content.encode("utf-8"))
   1102         else:
   1103             cmdfile.write(script_content)
   1104         cmdfile.flush()
   1105 
   1106         # Open the script and let the user review it, then check if the script
   1107         # was modified by the user
   1108         self.fm.execute_file([File(cmdfile.name)], app='editor')
   1109         cmdfile.seek(0)
   1110         script_was_edited = (script_content != cmdfile.read())
   1111 
   1112         # Do the renaming
   1113         self.fm.run(['/bin/sh', cmdfile.name], flags='w')
   1114         cmdfile.close()
   1115 
   1116         # Retag the files, but only if the script wasn't changed during review,
   1117         # because only then we know which are the source and destination files.
   1118         if not script_was_edited:
   1119             tags_changed = False
   1120             for old, new in zip(filenames, new_filenames):
   1121                 if old != new:
   1122                     oldpath = self.fm.thisdir.path + '/' + old
   1123                     newpath = self.fm.thisdir.path + '/' + new
   1124                     if oldpath in self.fm.tags:
   1125                         old_tag = self.fm.tags.tags[oldpath]
   1126                         self.fm.tags.remove(oldpath)
   1127                         self.fm.tags.tags[newpath] = old_tag
   1128                         tags_changed = True
   1129             if tags_changed:
   1130                 self.fm.tags.dump()
   1131         else:
   1132             fm.notify("files have not been retagged")
   1133 
   1134 
   1135 class relink(Command):
   1136     """:relink <newpath>
   1137 
   1138     Changes the linked path of the currently highlighted symlink to <newpath>
   1139     """
   1140 
   1141     def execute(self):
   1142         new_path = self.rest(1)
   1143         tfile = self.fm.thisfile
   1144 
   1145         if not new_path:
   1146             return self.fm.notify('Syntax: relink <newpath>', bad=True)
   1147 
   1148         if not tfile.is_link:
   1149             return self.fm.notify('%s is not a symlink!' % tfile.relative_path, bad=True)
   1150 
   1151         if new_path == os.readlink(tfile.path):
   1152             return None
   1153 
   1154         try:
   1155             os.remove(tfile.path)
   1156             os.symlink(new_path, tfile.path)
   1157         except OSError as err:
   1158             self.fm.notify(err)
   1159 
   1160         self.fm.reset()
   1161         self.fm.thisdir.pointed_obj = tfile
   1162         self.fm.thisfile = tfile
   1163 
   1164         return None
   1165 
   1166     def tab(self, tabnum):
   1167         if not self.rest(1):
   1168             return self.line + os.readlink(self.fm.thisfile.path)
   1169         return self._tab_directory_content()
   1170 
   1171 
   1172 class help_(Command):
   1173     """:help
   1174 
   1175     Display ranger's manual page.
   1176     """
   1177     name = 'help'
   1178 
   1179     def execute(self):
   1180         def callback(answer):
   1181             if answer == "q":
   1182                 return
   1183             elif answer == "m":
   1184                 self.fm.display_help()
   1185             elif answer == "c":
   1186                 self.fm.dump_commands()
   1187             elif answer == "k":
   1188                 self.fm.dump_keybindings()
   1189             elif answer == "s":
   1190                 self.fm.dump_settings()
   1191 
   1192         self.fm.ui.console.ask(
   1193             "View [m]an page, [k]ey bindings, [c]ommands or [s]ettings? (press q to abort)",
   1194             callback,
   1195             list("mqkcs")
   1196         )
   1197 
   1198 
   1199 class copymap(Command):
   1200     """:copymap <keys> <newkeys1> [<newkeys2>...]
   1201 
   1202     Copies a "browser" keybinding from <keys> to <newkeys>
   1203     """
   1204     context = 'browser'
   1205 
   1206     def execute(self):
   1207         if not self.arg(1) or not self.arg(2):
   1208             return self.fm.notify("Not enough arguments", bad=True)
   1209 
   1210         for arg in self.args[2:]:
   1211             self.fm.ui.keymaps.copy(self.context, self.arg(1), arg)
   1212 
   1213         return None
   1214 
   1215 
   1216 class copypmap(copymap):
   1217     """:copypmap <keys> <newkeys1> [<newkeys2>...]
   1218 
   1219     Copies a "pager" keybinding from <keys> to <newkeys>
   1220     """
   1221     context = 'pager'
   1222 
   1223 
   1224 class copycmap(copymap):
   1225     """:copycmap <keys> <newkeys1> [<newkeys2>...]
   1226 
   1227     Copies a "console" keybinding from <keys> to <newkeys>
   1228     """
   1229     context = 'console'
   1230 
   1231 
   1232 class copytmap(copymap):
   1233     """:copycmap <keys> <newkeys1> [<newkeys2>...]
   1234 
   1235     Copies a "taskview" keybinding from <keys> to <newkeys>
   1236     """
   1237     context = 'taskview'
   1238 
   1239 
   1240 class unmap(Command):
   1241     """:unmap <keys> [<keys2>, ...]
   1242 
   1243     Remove the given "browser" mappings
   1244     """
   1245     context = 'browser'
   1246 
   1247     def execute(self):
   1248         for arg in self.args[1:]:
   1249             self.fm.ui.keymaps.unbind(self.context, arg)
   1250 
   1251 
   1252 class cunmap(unmap):
   1253     """:cunmap <keys> [<keys2>, ...]
   1254 
   1255     Remove the given "console" mappings
   1256     """
   1257     context = 'browser'
   1258 
   1259 
   1260 class punmap(unmap):
   1261     """:punmap <keys> [<keys2>, ...]
   1262 
   1263     Remove the given "pager" mappings
   1264     """
   1265     context = 'pager'
   1266 
   1267 
   1268 class tunmap(unmap):
   1269     """:tunmap <keys> [<keys2>, ...]
   1270 
   1271     Remove the given "taskview" mappings
   1272     """
   1273     context = 'taskview'
   1274 
   1275 
   1276 class map_(Command):
   1277     """:map <keysequence> <command>
   1278 
   1279     Maps a command to a keysequence in the "browser" context.
   1280 
   1281     Example:
   1282     map j move down
   1283     map J move down 10
   1284     """
   1285     name = 'map'
   1286     context = 'browser'
   1287     resolve_macros = False
   1288 
   1289     def execute(self):
   1290         if not self.arg(1) or not self.arg(2):
   1291             self.fm.notify("Syntax: {0} <keysequence> <command>".format(self.get_name()), bad=True)
   1292             return
   1293 
   1294         self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2))
   1295 
   1296 
   1297 class cmap(map_):
   1298     """:cmap <keysequence> <command>
   1299 
   1300     Maps a command to a keysequence in the "console" context.
   1301 
   1302     Example:
   1303     cmap <ESC> console_close
   1304     cmap <C-x> console_type test
   1305     """
   1306     context = 'console'
   1307 
   1308 
   1309 class tmap(map_):
   1310     """:tmap <keysequence> <command>
   1311 
   1312     Maps a command to a keysequence in the "taskview" context.
   1313     """
   1314     context = 'taskview'
   1315 
   1316 
   1317 class pmap(map_):
   1318     """:pmap <keysequence> <command>
   1319 
   1320     Maps a command to a keysequence in the "pager" context.
   1321     """
   1322     context = 'pager'
   1323 
   1324 
   1325 class scout(Command):
   1326     """:scout [-FLAGS...] <pattern>
   1327 
   1328     Swiss army knife command for searching, traveling and filtering files.
   1329 
   1330     Flags:
   1331      -a    Automatically open a file on unambiguous match
   1332      -e    Open the selected file when pressing enter
   1333      -f    Filter files that match the current search pattern
   1334      -g    Interpret pattern as a glob pattern
   1335      -i    Ignore the letter case of the files
   1336      -k    Keep the console open when changing a directory with the command
   1337      -l    Letter skipping; e.g. allow "rdme" to match the file "readme"
   1338      -m    Mark the matching files after pressing enter
   1339      -M    Unmark the matching files after pressing enter
   1340      -p    Permanent filter: hide non-matching files after pressing enter
   1341      -r    Interpret pattern as a regular expression pattern
   1342      -s    Smart case; like -i unless pattern contains upper case letters
   1343      -t    Apply filter and search pattern as you type
   1344      -v    Inverts the match
   1345 
   1346     Multiple flags can be combined.  For example, ":scout -gpt" would create
   1347     a :filter-like command using globbing.
   1348     """
   1349     # pylint: disable=bad-whitespace
   1350     AUTO_OPEN     = 'a'
   1351     OPEN_ON_ENTER = 'e'
   1352     FILTER        = 'f'
   1353     SM_GLOB       = 'g'
   1354     IGNORE_CASE   = 'i'
   1355     KEEP_OPEN     = 'k'
   1356     SM_LETTERSKIP = 'l'
   1357     MARK          = 'm'
   1358     UNMARK        = 'M'
   1359     PERM_FILTER   = 'p'
   1360     SM_REGEX      = 'r'
   1361     SMART_CASE    = 's'
   1362     AS_YOU_TYPE   = 't'
   1363     INVERT        = 'v'
   1364     # pylint: enable=bad-whitespace
   1365 
   1366     def __init__(self, *args, **kwargs):
   1367         super(scout, self).__init__(*args, **kwargs)
   1368         self._regex = None
   1369         self.flags, self.pattern = self.parse_flags()
   1370 
   1371     def execute(self):  # pylint: disable=too-many-branches
   1372         thisdir = self.fm.thisdir
   1373         flags = self.flags
   1374         pattern = self.pattern
   1375         regex = self._build_regex()
   1376         count = self._count(move=True)
   1377 
   1378         self.fm.thistab.last_search = regex
   1379         self.fm.set_search_method(order="search")
   1380 
   1381         if (self.MARK in flags or self.UNMARK in flags) and thisdir.files:
   1382             value = flags.find(self.MARK) > flags.find(self.UNMARK)
   1383             if self.FILTER in flags:
   1384                 for fobj in thisdir.files:
   1385                     thisdir.mark_item(fobj, value)
   1386             else:
   1387                 for fobj in thisdir.files:
   1388                     if regex.search(fobj.relative_path):
   1389                         thisdir.mark_item(fobj, value)
   1390 
   1391         if self.PERM_FILTER in flags:
   1392             thisdir.filter = regex if pattern else None
   1393 
   1394         # clean up:
   1395         self.cancel()
   1396 
   1397         if self.OPEN_ON_ENTER in flags or \
   1398                 (self.AUTO_OPEN in flags and count == 1):
   1399             if pattern == '..':
   1400                 self.fm.cd(pattern)
   1401             else:
   1402                 self.fm.move(right=1)
   1403                 if self.quickly_executed:
   1404                     self.fm.block_input(0.5)
   1405 
   1406         if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir:
   1407             # reopen the console:
   1408             if not pattern:
   1409                 self.fm.open_console(self.line)
   1410             else:
   1411                 self.fm.open_console(self.line[0:-len(pattern)])
   1412 
   1413         if self.quickly_executed and thisdir != self.fm.thisdir and pattern != "..":
   1414             self.fm.block_input(0.5)
   1415 
   1416     def cancel(self):
   1417         self.fm.thisdir.temporary_filter = None
   1418         self.fm.thisdir.refilter()
   1419 
   1420     def quick(self):
   1421         asyoutype = self.AS_YOU_TYPE in self.flags
   1422         if self.FILTER in self.flags:
   1423             self.fm.thisdir.temporary_filter = self._build_regex()
   1424         if self.PERM_FILTER in self.flags and asyoutype:
   1425             self.fm.thisdir.filter = self._build_regex()
   1426         if self.FILTER in self.flags or self.PERM_FILTER in self.flags:
   1427             self.fm.thisdir.refilter()
   1428         if self._count(move=asyoutype) == 1 and self.AUTO_OPEN in self.flags:
   1429             return True
   1430         return False
   1431 
   1432     def tab(self, tabnum):
   1433         self._count(move=True, offset=tabnum)
   1434 
   1435     def _build_regex(self):
   1436         if self._regex is not None:
   1437             return self._regex
   1438 
   1439         frmat = "%s"
   1440         flags = self.flags
   1441         pattern = self.pattern
   1442 
   1443         if pattern == ".":
   1444             return re.compile("")
   1445 
   1446         # Handle carets at start and dollar signs at end separately
   1447         if pattern.startswith('^'):
   1448             pattern = pattern[1:]
   1449             frmat = "^" + frmat
   1450         if pattern.endswith('$'):
   1451             pattern = pattern[:-1]
   1452             frmat += "$"
   1453 
   1454         # Apply one of the search methods
   1455         if self.SM_REGEX in flags:
   1456             regex = pattern
   1457         elif self.SM_GLOB in flags:
   1458             regex = re.escape(pattern).replace("\\*", ".*").replace("\\?", ".")
   1459         elif self.SM_LETTERSKIP in flags:
   1460             regex = ".*".join(re.escape(c) for c in pattern)
   1461         else:
   1462             regex = re.escape(pattern)
   1463 
   1464         regex = frmat % regex
   1465 
   1466         # Invert regular expression if necessary
   1467         if self.INVERT in flags:
   1468             regex = "^(?:(?!%s).)*$" % regex
   1469 
   1470         # Compile Regular Expression
   1471         # pylint: disable=no-member
   1472         options = re.UNICODE
   1473         if self.IGNORE_CASE in flags or self.SMART_CASE in flags and \
   1474                 pattern.islower():
   1475             options |= re.IGNORECASE
   1476         # pylint: enable=no-member
   1477         try:
   1478             self._regex = re.compile(regex, options)
   1479         except re.error:
   1480             self._regex = re.compile("")
   1481         return self._regex
   1482 
   1483     def _count(self, move=False, offset=0):
   1484         count = 0
   1485         cwd = self.fm.thisdir
   1486         pattern = self.pattern
   1487 
   1488         if not pattern or not cwd.files:
   1489             return 0
   1490         if pattern == '.':
   1491             return 0
   1492         if pattern == '..':
   1493             return 1
   1494 
   1495         deq = deque(cwd.files)
   1496         deq.rotate(-cwd.pointer - offset)
   1497         i = offset
   1498         regex = self._build_regex()
   1499         for fsobj in deq:
   1500             if regex.search(fsobj.relative_path):
   1501                 count += 1
   1502                 if move and count == 1:
   1503                     cwd.move(to=(cwd.pointer + i) % len(cwd.files))
   1504                     self.fm.thisfile = cwd.pointed_obj
   1505             if count > 1:
   1506                 return count
   1507             i += 1
   1508 
   1509         return count == 1
   1510 
   1511 
   1512 class narrow(Command):
   1513     """
   1514     :narrow
   1515 
   1516     Show only the files selected right now. If no files are selected,
   1517     disable narrowing.
   1518     """
   1519     def execute(self):
   1520         if self.fm.thisdir.marked_items:
   1521             selection = [f.basename for f in self.fm.thistab.get_selection()]
   1522             self.fm.thisdir.narrow_filter = selection
   1523         else:
   1524             self.fm.thisdir.narrow_filter = None
   1525         self.fm.thisdir.refilter()
   1526 
   1527 
   1528 class filter_inode_type(Command):
   1529     """
   1530     :filter_inode_type [dfl]
   1531 
   1532     Displays only the files of specified inode type. Parameters
   1533     can be combined.
   1534 
   1535         d display directories
   1536         f display files
   1537         l display links
   1538     """
   1539 
   1540     def execute(self):
   1541         if not self.arg(1):
   1542             self.fm.thisdir.inode_type_filter = ""
   1543         else:
   1544             self.fm.thisdir.inode_type_filter = self.arg(1)
   1545         self.fm.thisdir.refilter()
   1546 
   1547 
   1548 class filter_stack(Command):
   1549     """
   1550     :filter_stack ...
   1551 
   1552     Manages the filter stack.
   1553 
   1554         filter_stack add FILTER_TYPE ARGS...
   1555         filter_stack pop
   1556         filter_stack decompose
   1557         filter_stack rotate [N=1]
   1558         filter_stack clear
   1559         filter_stack show
   1560     """
   1561     def execute(self):
   1562         from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS
   1563 
   1564         subcommand = self.arg(1)
   1565 
   1566         if subcommand == "add":
   1567             try:
   1568                 self.fm.thisdir.filter_stack.append(
   1569                     SIMPLE_FILTERS[self.arg(2)](self.rest(3))
   1570                 )
   1571             except KeyError:
   1572                 FILTER_COMBINATORS[self.arg(2)](self.fm.thisdir.filter_stack)
   1573         elif subcommand == "pop":
   1574             self.fm.thisdir.filter_stack.pop()
   1575         elif subcommand == "decompose":
   1576             inner_filters = self.fm.thisdir.filter_stack.pop().decompose()
   1577             if inner_filters:
   1578                 self.fm.thisdir.filter_stack.extend(inner_filters)
   1579         elif subcommand == "clear":
   1580             self.fm.thisdir.filter_stack = []
   1581         elif subcommand == "rotate":
   1582             rotate_by = int(self.arg(2) or 1)
   1583             self.fm.thisdir.filter_stack = (
   1584                 self.fm.thisdir.filter_stack[-rotate_by:]
   1585                 + self.fm.thisdir.filter_stack[:-rotate_by]
   1586             )
   1587         elif subcommand == "show":
   1588             stack = list(map(str, self.fm.thisdir.filter_stack))
   1589             pager = self.fm.ui.open_pager()
   1590             pager.set_source(["Filter stack: "] + stack)
   1591             pager.move(to=100, percentage=True)
   1592             return
   1593         else:
   1594             self.fm.notify(
   1595                 "Unknown subcommand: {}".format(subcommand),
   1596                 bad=True
   1597             )
   1598             return
   1599 
   1600         self.fm.thisdir.refilter()
   1601 
   1602 
   1603 class grep(Command):
   1604     """:grep <string>
   1605 
   1606     Looks for a string in all marked files or directories
   1607     """
   1608 
   1609     def execute(self):
   1610         if self.rest(1):
   1611             action = ['grep', '--line-number']
   1612             action.extend(['-e', self.rest(1), '-r'])
   1613             action.extend(f.path for f in self.fm.thistab.get_selection())
   1614             self.fm.execute_command(action, flags='p')
   1615 
   1616 
   1617 class flat(Command):
   1618     """
   1619     :flat <level>
   1620 
   1621     Flattens the directory view up to the specified level.
   1622 
   1623         -1 fully flattened
   1624          0 remove flattened view
   1625     """
   1626 
   1627     def execute(self):
   1628         try:
   1629             level_str = self.rest(1)
   1630             level = int(level_str)
   1631         except ValueError:
   1632             level = self.quantifier
   1633         if level is None:
   1634             self.fm.notify("Syntax: flat <level>", bad=True)
   1635             return
   1636         if level < -1:
   1637             self.fm.notify("Need an integer number (-1, 0, 1, ...)", bad=True)
   1638         self.fm.thisdir.unload()
   1639         self.fm.thisdir.flat = level
   1640         self.fm.thisdir.load_content()
   1641 
   1642 # Version control commands
   1643 # --------------------------------
   1644 
   1645 
   1646 class stage(Command):
   1647     """
   1648     :stage
   1649 
   1650     Stage selected files for the corresponding version control system
   1651     """
   1652 
   1653     def execute(self):
   1654         from ranger.ext.vcs import VcsError
   1655 
   1656         if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
   1657             filelist = [f.path for f in self.fm.thistab.get_selection()]
   1658             try:
   1659                 self.fm.thisdir.vcs.action_add(filelist)
   1660             except VcsError as ex:
   1661                 self.fm.notify('Unable to stage files: {0}'.format(ex))
   1662             self.fm.ui.vcsthread.process(self.fm.thisdir)
   1663         else:
   1664             self.fm.notify('Unable to stage files: Not in repository')
   1665 
   1666 
   1667 class unstage(Command):
   1668     """
   1669     :unstage
   1670 
   1671     Unstage selected files for the corresponding version control system
   1672     """
   1673 
   1674     def execute(self):
   1675         from ranger.ext.vcs import VcsError
   1676 
   1677         if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
   1678             filelist = [f.path for f in self.fm.thistab.get_selection()]
   1679             try:
   1680                 self.fm.thisdir.vcs.action_reset(filelist)
   1681             except VcsError as ex:
   1682                 self.fm.notify('Unable to unstage files: {0}'.format(ex))
   1683             self.fm.ui.vcsthread.process(self.fm.thisdir)
   1684         else:
   1685             self.fm.notify('Unable to unstage files: Not in repository')
   1686 
   1687 # Metadata commands
   1688 # --------------------------------
   1689 
   1690 
   1691 class prompt_metadata(Command):
   1692     """
   1693     :prompt_metadata <key1> [<key2> [<key3> ...]]
   1694 
   1695     Prompt the user to input metadata for multiple keys in a row.
   1696     """
   1697 
   1698     _command_name = "meta"
   1699     _console_chain = None
   1700 
   1701     def execute(self):
   1702         prompt_metadata._console_chain = self.args[1:]
   1703         self._process_command_stack()
   1704 
   1705     def _process_command_stack(self):
   1706         if prompt_metadata._console_chain:
   1707             key = prompt_metadata._console_chain.pop()
   1708             self._fill_console(key)
   1709         else:
   1710             for col in self.fm.ui.browser.columns:
   1711                 col.need_redraw = True
   1712 
   1713     def _fill_console(self, key):
   1714         metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
   1715         if key in metadata and metadata[key]:
   1716             existing_value = metadata[key]
   1717         else:
   1718             existing_value = ""
   1719         text = "%s %s %s" % (self._command_name, key, existing_value)
   1720         self.fm.open_console(text, position=len(text))
   1721 
   1722 
   1723 class meta(prompt_metadata):
   1724     """
   1725     :meta <key> [<value>]
   1726 
   1727     Change metadata of a file.  Deletes the key if value is empty.
   1728     """
   1729 
   1730     def execute(self):
   1731         key = self.arg(1)
   1732         update_dict = dict()
   1733         update_dict[key] = self.rest(2)
   1734         selection = self.fm.thistab.get_selection()
   1735         for fobj in selection:
   1736             self.fm.metadata.set_metadata(fobj.path, update_dict)
   1737         self._process_command_stack()
   1738 
   1739     def tab(self, tabnum):
   1740         key = self.arg(1)
   1741         metadata = self.fm.metadata.get_metadata(self.fm.thisfile.path)
   1742         if key in metadata and metadata[key]:
   1743             return [" ".join([self.arg(0), self.arg(1), metadata[key]])]
   1744         return [self.arg(0) + " " + k for k in sorted(metadata)
   1745                 if k.startswith(self.arg(1))]
   1746 
   1747 
   1748 class linemode(default_linemode):
   1749     """
   1750     :linemode <mode>
   1751 
   1752     Change what is displayed as a filename.
   1753 
   1754     - "mode" may be any of the defined linemodes (see: ranger.core.linemode).
   1755       "normal" is mapped to "filename".
   1756     """
   1757 
   1758     def execute(self):
   1759         mode = self.arg(1)
   1760 
   1761         if mode == "normal":
   1762             from ranger.core.linemode import DEFAULT_LINEMODE
   1763             mode = DEFAULT_LINEMODE
   1764 
   1765         if mode not in self.fm.thisfile.linemode_dict:
   1766             self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
   1767             return
   1768 
   1769         self.fm.thisdir.set_linemode_of_children(mode)
   1770 
   1771         # Ask the browsercolumns to redraw
   1772         for col in self.fm.ui.browser.columns:
   1773             col.need_redraw = True
   1774 
   1775 
   1776 class yank(Command):
   1777     """:yank [name|dir|path]
   1778 
   1779     Copies the file's name (default), directory or path into both the primary X
   1780     selection and the clipboard.
   1781     """
   1782 
   1783     modes = {
   1784         '': 'basename',
   1785         'name_without_extension': 'basename_without_extension',
   1786         'name': 'basename',
   1787         'dir': 'dirname',
   1788         'path': 'path',
   1789     }
   1790 
   1791     def execute(self):
   1792         import subprocess
   1793 
   1794         def clipboards():
   1795             from ranger.ext.get_executables import get_executables
   1796             clipboard_managers = {
   1797                 'xclip': [
   1798                     ['xclip'],
   1799                     ['xclip', '-selection', 'clipboard'],
   1800                 ],
   1801                 'xsel': [
   1802                     ['xsel'],
   1803                     ['xsel', '-b'],
   1804                 ],
   1805                 'pbcopy': [
   1806                     ['pbcopy'],
   1807                 ],
   1808             }
   1809             ordered_managers = ['pbcopy', 'xclip', 'xsel']
   1810             executables = get_executables()
   1811             for manager in ordered_managers:
   1812                 if manager in executables:
   1813                     return clipboard_managers[manager]
   1814             return []
   1815 
   1816         clipboard_commands = clipboards()
   1817 
   1818         mode = self.modes[self.arg(1)]
   1819         selection = self.get_selection_attr(mode)
   1820 
   1821         new_clipboard_contents = "\n".join(selection)
   1822         for command in clipboard_commands:
   1823             process = subprocess.Popen(command, universal_newlines=True,
   1824                                        stdin=subprocess.PIPE)
   1825             process.communicate(input=new_clipboard_contents)
   1826 
   1827     def get_selection_attr(self, attr):
   1828         return [getattr(item, attr) for item in
   1829                 self.fm.thistab.get_selection()]
   1830 
   1831     def tab(self, tabnum):
   1832         return (
   1833             self.start(1) + mode for mode
   1834             in sorted(self.modes.keys())
   1835             if mode
   1836         )