dotfiles

My collection of dotfiles
git clone git://git.stellar-nexus.ru/dotfiles
Log | Files | Refs

commands_full.py (62106B)


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