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 # ===================================================================
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.
10 # ===================================================================
11 # This file contains ranger's commands.
12 # It's all in python; lines beginning with # are comments.
14 # Note that additional commands are automatically generated from the methods
15 # of the class ranger.core.actions.Actions.
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.
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.
35 # tab() argument tabnum is 1 for <TAB> and -1 for <S-TAB> by default
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
42 # The return value for quick() can be:
43 # False: Nothing happens
44 # True: Execute the command afterwards
46 # The return value for execute() and cancel() doesn't matter.
48 # ===================================================================
49 # Commands have certain attributes and methods that facilitate parsing of
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"
62 # ===================================================================
63 # And this is a little reference for common ranger functions and objects:
65 # self.fm: A reference to the "fm" object which contains most information
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, ...
79 # File objects (for example self.fm.thisfile) have these useful attributes and
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.
88 # For advanced commands it is unavoidable to dive a bit into the source code
90 # ===================================================================
92 from __future__ import (absolute_import, division, print_function)
94 from collections import deque
98 from ranger.api.commands import Command
101 class alias(Command):
102 """:alias <newcommand> <oldcommand>
104 Copies the oldcommand as newcommand.
108 resolve_macros = False
111 if not self.arg(1) or not self.arg(2):
112 self.fm.notify('Syntax: alias <newcommand> <oldcommand>', bad=True)
115 self.fm.commands.alias(self.arg(1), self.rest(2))
121 Display the text in the statusbar.
125 self.fm.notify(self.rest(1))
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.
138 if self.arg(1) == '-r':
140 destination = os.path.realpath(self.rest(1))
141 if os.path.isfile(destination):
142 self.fm.select_file(destination)
145 destination = self.rest(1)
150 if destination == '-':
151 self.fm.enter_bookmark('`')
153 self.fm.cd(destination)
156 # dest must be rest because path could contain spaces
157 if self.arg(1) == '-r':
158 start = self.start(2)
161 start = self.start(1)
165 head, tail = os.path.split(os.path.expanduser(dest))
167 dest_exp = os.path.join(os.path.normpath(head), tail)
172 return (start, dest_exp, os.path.join(self.fm.thisdir.path, dest_exp),
173 dest.endswith(os.path.sep))
176 def _tab_paths(dest, dest_abs, ends_with_sep):
179 return next(os.walk(dest_abs))[1], dest_abs
180 except (OSError, StopIteration):
185 return [os.path.join(dest, path) for path in next(os.walk(dest_abs))[1]], ''
186 except (OSError, StopIteration):
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)
199 def _tab_normal(self, dest, dest_abs):
200 dest_dir = os.path.dirname(dest)
201 dest_base = os.path.basename(dest)
204 dirnames = next(os.walk(os.path.dirname(dest_abs)))[1]
205 except (OSError, StopIteration):
208 return [os.path.join(dest_dir, d) for d in dirnames if self._tab_match(dest_base, d)], ''
210 def _tab_fuzzy_match(self, basepath, tokens):
211 """ Find directories matching tokens recursively """
220 directories = next(os.walk(path))[1]
221 except (OSError, StopIteration):
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:
231 def _tab_fuzzy(self, dest, dest_abs):
235 basepath_old = basepath
236 basepath, token = os.path.split(basepath)
237 if basepath == basepath_old:
239 if os.path.isdir(basepath_old) and not token.startswith('.'):
240 basepath = basepath_old
244 paths = self._tab_fuzzy_match(basepath, tokens)
245 if not os.path.isabs(dest):
247 paths = [os.path.relpath(path, paths_rel) for path in paths]
250 return paths, paths_rel
252 def tab(self, tabnum):
253 from os.path import sep
255 start, dest, dest_abs, ends_with_sep = self._tab_args()
257 paths, paths_rel = self._tab_paths(dest, dest_abs, ends_with_sep)
259 if self.fm.settings.cd_tab_fuzzy:
260 paths, paths_rel = self._tab_fuzzy(dest, dest_abs)
262 paths, paths_rel = self._tab_normal(dest, dest_abs)
266 if self.fm.settings.cd_bookmarks:
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)
276 return start + paths[0] + sep
277 return [start + dirname for dirname in paths]
280 class chain(Command):
281 """:chain <command1>; <command2>; ...
283 Calls multiple commands at once, separated by semicolons.
287 if not self.rest(1).strip():
288 self.fm.notify('Syntax: chain <command1>; <command2>; ...', bad=True)
290 for command in [s.strip() for s in self.rest(1).split(";")]:
291 self.fm.execute_console(command)
294 class shell(Command):
295 escape_macros_for_shell = True
298 if self.arg(1) and self.arg(1)[0] == '-':
299 flags = self.arg(1)[1:]
300 command = self.rest(2)
303 command = self.rest(1)
306 self.fm.execute_command(command, flags=flags)
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)
313 command = self.rest(1)
314 start = self.line[0:len(self.line) - len(command)]
317 position_of_last_space = command.rindex(" ")
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 '
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))
333 class open_with(Command):
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()],
343 def tab(self, tabnum):
344 return self._tab_through_executables()
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.
350 "mplayer f 1" => ("mplayer", "f", 1)
351 "atool 4" => ("atool", "", 4)
359 split = string.split()
363 if self._is_app(part):
365 elif self._is_flags(part):
367 elif self._is_mode(part):
370 elif len(split) == 2:
374 if self._is_app(part0):
376 if self._is_flags(part1):
378 elif self._is_mode(part1):
380 elif self._is_flags(part0):
382 if self._is_mode(part1):
384 elif self._is_mode(part0):
386 if self._is_flags(part1):
389 elif len(split) >= 3:
394 if self._is_app(part0):
396 if self._is_flags(part1):
398 if self._is_mode(part2):
400 elif self._is_mode(part1):
402 if self._is_flags(part2):
404 elif self._is_flags(part0):
406 if self._is_mode(part1):
408 elif self._is_mode(part0):
410 if self._is_flags(part1):
413 return app, flags, int(mode)
415 def _is_app(self, arg):
416 return not self._is_flags(arg) and not arg.isdigit()
420 from ranger.core.runner import ALLOWED_FLAGS
421 return all(x in ALLOWED_FLAGS for x in arg)
425 return all(x in '0123456789' for x in arg)
429 """:set <option name>=<python expression>
431 Gives an option a new value.
433 Use `:set <option>!` to toggle or cycle it, e.g. `:set flush_input!`
435 name = 'set' # don't override the builtin set class
439 name, value, _, toggle = self.parse_setting_line_v2()
441 self.fm.toggle_option(name)
443 self.fm.set_option_from_string(name, value)
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
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))
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)),
462 lambda: self.firstpart + ",".join(map(str, settings[name])),
465 def default_value_completer():
466 return self.firstpart + str(settings[name])
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))
481 class setlocal(set_):
482 """:setlocal path=<regular expression> <option name>=<python expression>
484 Gives an option a new value.
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=(.*?)$')
490 def _re_shift(self, match):
493 path = os.path.expanduser(match.group(1))
494 for _ in range(len(path.split())):
499 path = self._re_shift(self.PATH_RE_DQUOTED.match(self.line))
501 path = self._re_shift(self.PATH_RE_SQUOTED.match(self.line))
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
509 name, value, _ = self.parse_setting_line()
510 self.fm.set_option_from_string(name, value, localpath=path)
513 class setintag(set_):
514 """:setintag <tag or tags> <option name>=<option value>
516 Sets an option for directories that are tagged with a specific tag.
522 name, value, _ = self.parse_setting_line()
523 self.fm.set_option_from_string(name, value, tags=tags)
526 class default_linemode(Command):
529 from ranger.container.fsobject import FileSystemObject
531 if len(self.args) < 2:
533 "Usage: default_linemode [path=<regexp> | tag=<tag(s)>] <linemode>", bad=True)
535 # Extract options like "path=..." or "tag=..." from the command line
539 if arg1.startswith("path="):
541 argument = re.compile(arg1[5:])
543 elif arg1.startswith("tag="):
548 # Extract and validate the line mode from the command line
550 if lmode not in FileSystemObject.linemode_dict:
552 "Invalid linemode: %s; should be %s" % (
553 lmode, "/".join(FileSystemObject.linemode_dict)),
557 # Add the prepared entry to the fm.default_linemodes
558 entry = [method, argument, lmode]
559 self.fm.default_linemodes.appendleft(entry)
562 if self.fm.ui.browser:
563 for col in self.fm.ui.browser.columns:
564 col.need_redraw = True
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)))
572 class quit(Command): # pylint: disable=redefined-builtin
575 Closes the current tab, if there's only one tab.
576 Otherwise quits if there are no tasks in progress.
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')
585 if len(self.fm.tabs) >= 2:
591 class quit_bang(Command):
594 Closes the current tab, if there's only one tab.
595 Otherwise force quits immediately.
601 if len(self.fm.tabs) >= 2:
607 class quitall(Command):
610 Quits if there are no tasks in progress.
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')
622 class quitall_bang(Command):
625 Force quits immediately.
634 class terminal(Command):
637 Spawns an "x-terminal-emulator" starting in the current directory.
641 from ranger.ext.get_executables import get_term
642 self.fm.run(get_term(), flags='f')
645 class delete(Command):
648 Tries to delete the selection or the files passed in arguments (if any).
649 The arguments use a shell-like escaping.
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)
655 When attempting to delete non-empty directories or multiple
656 marked files, it will require a confirmation.
660 escape_macros_for_shell = True
664 from functools import partial
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
670 files = shlex.split(self.rest(1))
671 many_files = (len(files) > 1 or is_directory_with_files(files[0]))
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)
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))
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'),
691 # no need for a confirmation, just delete
692 self.fm.delete(files)
694 def tab(self, tabnum):
695 return self._tab_directory_content()
697 def _question_callback(self, files, answer):
698 if answer == 'y' or answer == 'Y':
699 self.fm.delete(files)
702 class jump_non(Command):
703 """:jump_non [-FLAGS...]
705 Jumps to first non-directory if highlighted file is a directory and vice versa.
708 -r Jump in reverse order
709 -w Wrap around if reaching end of filelist
711 def __init__(self, *args, **kwargs):
712 super(jump_non, self).__init__(*args, **kwargs)
714 flags, _ = self.parse_flags()
715 self._flag_reverse = 'r' in flags
716 self._flag_wrap = 'w' in flags
719 def _non(fobj, is_directory):
720 return fobj.is_directory if not is_directory else not fobj.is_directory
723 tfile = self.fm.thisfile
727 for fobj in self.fm.thisdir.files[::-1] if self._flag_reverse else self.fm.thisdir.files:
728 if fobj.path == tfile.path:
733 if self._non(fobj, tfile.is_directory):
734 found_after = fobj.path
736 elif not found_before and self._non(fobj, tfile.is_directory):
737 found_before = fobj.path
740 self.fm.select_file(found_after)
741 elif self._flag_wrap and found_before:
742 self.fm.select_file(found_before)
745 class mark_tag(Command):
746 """:mark_tag [<tags>]
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.
754 cwd = self.fm.thisdir
755 tags = self.rest(1).replace(" ", "")
756 if not self.fm.tags or not cwd.files:
758 for fileobj in cwd.files:
760 tag = self.fm.tags.tags[fileobj.realpath]
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
769 class console(Command):
770 """:console <command>
772 Open the console with the given command.
777 if self.arg(1)[0:2] == '-p':
779 position = int(self.arg(1)[2:])
784 self.fm.open_console(self.rest(1), position=position)
787 class load_copy_buffer(Command):
790 Load the copy buffer from datadir/copy_buffer
792 copy_buffer_filename = 'copy_buffer'
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
801 fobj = open(fname, 'r')
803 return self.fm.notify(
804 "Cannot open %s" % (fname or self.copy_buffer_filename), bad=True)
806 self.fm.copy_buffer = set(File(g)
807 for g in fobj.read().split("\n") if exists(g))
809 self.fm.ui.redraw_main_column()
813 class save_copy_buffer(Command):
816 Save the copy buffer to datadir/copy_buffer
818 copy_buffer_filename = 'copy_buffer'
823 fname = self.fm.datapath(self.copy_buffer_filename)
824 unwritable = IOError if sys.version_info[0] < 3 else OSError
826 fobj = open(fname, 'w')
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))
835 class unmark_tag(mark_tag):
836 """:unmark_tag [<tags>]
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.
844 class mkdir(Command):
847 Creates a directory with the name <dirname>.
851 from os.path import join, expanduser, lexists
852 from os import makedirs
854 dirname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
855 if not lexists(dirname):
858 self.fm.notify("file/directory exists!", bad=True)
860 def tab(self, tabnum):
861 return self._tab_directory_content()
864 class touch(Command):
867 Creates a file with the name <fname>.
871 from os.path import join, expanduser, lexists
873 fname = join(self.fm.thisdir.path, expanduser(self.rest(1)))
874 if not lexists(fname):
875 open(fname, 'a').close()
877 self.fm.notify("file/directory exists!", bad=True)
879 def tab(self, tabnum):
880 return self._tab_directory_content()
886 Opens the specified file in vim
891 self.fm.edit_file(self.fm.thisfile.path)
893 self.fm.edit_file(self.rest(1))
895 def tab(self, tabnum):
896 return self._tab_directory_content()
899 class eval_(Command):
900 """:eval [-q] <python code>
902 Evaluates the python code.
903 `fm' is a reference to the FM instance.
904 To display text, use the function `p'.
908 :eval len(fm.directories)
909 :eval p("Hello World!")
912 resolve_macros = False
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':
923 global cmd, fm, p, quantifier # pylint: disable=invalid-name,global-variable-undefined
925 cmd = self.fm.execute_console
927 quantifier = self.quantifier
930 result = eval(code) # pylint: disable=eval-used
932 exec(code) # pylint: disable=exec-used
934 if result and not quiet:
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)
941 class rename(Command):
944 Changes the name of the currently highlighted file to <newname>
948 from ranger.container.file import File
949 from os import access
951 new_name = self.rest(1)
954 return self.fm.notify('Syntax: rename <newname>', bad=True)
956 if new_name == self.fm.thisfile.relative_path:
959 if access(new_name, os.F_OK):
960 return self.fm.notify("Can't rename: file already exists!", bad=True)
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
971 def tab(self, tabnum):
972 return self._tab_directory_content()
975 class rename_append(Command):
976 """:rename_append [-FLAGS...]
978 Opens the console with ":rename <current file>" with the cursor positioned
979 before the file extension.
982 -a Position before all extensions
983 -r Remove everything before extensions
985 def __init__(self, *args, **kwargs):
986 super(rename_append, self).__init__(*args, **kwargs)
988 flags, _ = self.parse_flags()
989 self._flag_ext_all = 'a' in flags
990 self._flag_remove = 'r' in flags
993 from ranger import MACRO_DELIMITER, MACRO_DELIMITER_ESC
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)
999 if basename.find('.') <= 0:
1000 self.fm.open_console('rename ' + relpath)
1003 if self._flag_ext_all:
1004 pos_ext = re.search(r'[^.]+', basename).end(0)
1006 pos_ext = basename.rindex('.')
1007 pos = len(relpath) - len(basename) + pos_ext
1009 if self._flag_remove:
1010 relpath = relpath[:-len(basename)] + basename[pos_ext:]
1013 self.fm.open_console('rename ' + relpath, position=(7 + pos))
1016 class chmod(Command):
1017 """:chmod <octal number>
1019 Sets the permissions of the selection to the octal number.
1021 The octal number is between 0 and 777. The digits specify the
1022 permissions for the user, the group and others.
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.
1029 mode_str = self.rest(1)
1031 if not self.quantifier:
1032 self.fm.notify("Syntax: chmod <octal number>", bad=True)
1034 mode_str = str(self.quantifier)
1037 mode = int(mode_str, 8)
1038 if mode < 0 or mode > 0o777:
1041 self.fm.notify("Need an octal number between 0 and 777!", bad=True)
1044 for fobj in self.fm.thistab.get_selection():
1046 os.chmod(fobj.path, mode)
1047 except OSError as ex:
1050 # reloading directory. maybe its better to reload the selected
1052 self.fm.thisdir.content_outdated = True
1055 class bulkrename(Command):
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.
1062 This shell script is opened in an editor for you to review.
1063 After you close it, it will be executed.
1066 def execute(self): # pylint: disable=too-many-locals,too-many-statements
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
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
1079 listfile.write("\n".join(filenames).encode("utf-8"))
1081 listfile.write("\n".join(filenames))
1083 self.fm.execute_file([File(listpath)], app='editor')
1084 listfile = open(listpath, 'r')
1085 new_filenames = listfile.read().split("\n")
1088 if all(a == b for a, b in zip(filenames, new_filenames)):
1089 self.fm.notify("No renaming to be done!")
1093 cmdfile = tempfile.NamedTemporaryFile()
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)
1101 cmdfile.write(script_content.encode("utf-8"))
1103 cmdfile.write(script_content)
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')
1110 script_was_edited = (script_content != cmdfile.read())
1113 self.fm.run(['/bin/sh', cmdfile.name], flags='w')
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):
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
1132 fm.notify("files have not been retagged")
1135 class relink(Command):
1136 """:relink <newpath>
1138 Changes the linked path of the currently highlighted symlink to <newpath>
1142 new_path = self.rest(1)
1143 tfile = self.fm.thisfile
1146 return self.fm.notify('Syntax: relink <newpath>', bad=True)
1148 if not tfile.is_link:
1149 return self.fm.notify('%s is not a symlink!' % tfile.relative_path, bad=True)
1151 if new_path == os.readlink(tfile.path):
1155 os.remove(tfile.path)
1156 os.symlink(new_path, tfile.path)
1157 except OSError as err:
1161 self.fm.thisdir.pointed_obj = tfile
1162 self.fm.thisfile = tfile
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()
1172 class help_(Command):
1175 Display ranger's manual page.
1180 def callback(answer):
1184 self.fm.display_help()
1186 self.fm.dump_commands()
1188 self.fm.dump_keybindings()
1190 self.fm.dump_settings()
1192 self.fm.ui.console.ask(
1193 "View [m]an page, [k]ey bindings, [c]ommands or [s]ettings? (press q to abort)",
1199 class copymap(Command):
1200 """:copymap <keys> <newkeys1> [<newkeys2>...]
1202 Copies a "browser" keybinding from <keys> to <newkeys>
1207 if not self.arg(1) or not self.arg(2):
1208 return self.fm.notify("Not enough arguments", bad=True)
1210 for arg in self.args[2:]:
1211 self.fm.ui.keymaps.copy(self.context, self.arg(1), arg)
1216 class copypmap(copymap):
1217 """:copypmap <keys> <newkeys1> [<newkeys2>...]
1219 Copies a "pager" keybinding from <keys> to <newkeys>
1224 class copycmap(copymap):
1225 """:copycmap <keys> <newkeys1> [<newkeys2>...]
1227 Copies a "console" keybinding from <keys> to <newkeys>
1232 class copytmap(copymap):
1233 """:copycmap <keys> <newkeys1> [<newkeys2>...]
1235 Copies a "taskview" keybinding from <keys> to <newkeys>
1237 context = 'taskview'
1240 class unmap(Command):
1241 """:unmap <keys> [<keys2>, ...]
1243 Remove the given "browser" mappings
1248 for arg in self.args[1:]:
1249 self.fm.ui.keymaps.unbind(self.context, arg)
1252 class cunmap(unmap):
1253 """:cunmap <keys> [<keys2>, ...]
1255 Remove the given "console" mappings
1260 class punmap(unmap):
1261 """:punmap <keys> [<keys2>, ...]
1263 Remove the given "pager" mappings
1268 class tunmap(unmap):
1269 """:tunmap <keys> [<keys2>, ...]
1271 Remove the given "taskview" mappings
1273 context = 'taskview'
1276 class map_(Command):
1277 """:map <keysequence> <command>
1279 Maps a command to a keysequence in the "browser" context.
1287 resolve_macros = False
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)
1294 self.fm.ui.keymaps.bind(self.context, self.arg(1), self.rest(2))
1298 """:cmap <keysequence> <command>
1300 Maps a command to a keysequence in the "console" context.
1303 cmap <ESC> console_close
1304 cmap <C-x> console_type test
1310 """:tmap <keysequence> <command>
1312 Maps a command to a keysequence in the "taskview" context.
1314 context = 'taskview'
1318 """:pmap <keysequence> <command>
1320 Maps a command to a keysequence in the "pager" context.
1325 class scout(Command):
1326 """:scout [-FLAGS...] <pattern>
1328 Swiss army knife command for searching, traveling and filtering files.
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
1346 Multiple flags can be combined. For example, ":scout -gpt" would create
1347 a :filter-like command using globbing.
1349 # pylint: disable=bad-whitespace
1364 # pylint: enable=bad-whitespace
1366 def __init__(self, *args, **kwargs):
1367 super(scout, self).__init__(*args, **kwargs)
1369 self.flags, self.pattern = self.parse_flags()
1371 def execute(self): # pylint: disable=too-many-branches
1372 thisdir = self.fm.thisdir
1374 pattern = self.pattern
1375 regex = self._build_regex()
1376 count = self._count(move=True)
1378 self.fm.thistab.last_search = regex
1379 self.fm.set_search_method(order="search")
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)
1387 for fobj in thisdir.files:
1388 if regex.search(fobj.relative_path):
1389 thisdir.mark_item(fobj, value)
1391 if self.PERM_FILTER in flags:
1392 thisdir.filter = regex if pattern else None
1397 if self.OPEN_ON_ENTER in flags or \
1398 (self.AUTO_OPEN in flags and count == 1):
1402 self.fm.move(right=1)
1403 if self.quickly_executed:
1404 self.fm.block_input(0.5)
1406 if self.KEEP_OPEN in flags and thisdir != self.fm.thisdir:
1407 # reopen the console:
1409 self.fm.open_console(self.line)
1411 self.fm.open_console(self.line[0:-len(pattern)])
1413 if self.quickly_executed and thisdir != self.fm.thisdir and pattern != "..":
1414 self.fm.block_input(0.5)
1417 self.fm.thisdir.temporary_filter = None
1418 self.fm.thisdir.refilter()
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:
1432 def tab(self, tabnum):
1433 self._count(move=True, offset=tabnum)
1435 def _build_regex(self):
1436 if self._regex is not None:
1441 pattern = self.pattern
1444 return re.compile("")
1446 # Handle carets at start and dollar signs at end separately
1447 if pattern.startswith('^'):
1448 pattern = pattern[1:]
1450 if pattern.endswith('$'):
1451 pattern = pattern[:-1]
1454 # Apply one of the search methods
1455 if self.SM_REGEX in flags:
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)
1462 regex = re.escape(pattern)
1464 regex = frmat % regex
1466 # Invert regular expression if necessary
1467 if self.INVERT in flags:
1468 regex = "^(?:(?!%s).)*$" % regex
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 \
1475 options |= re.IGNORECASE
1476 # pylint: enable=no-member
1478 self._regex = re.compile(regex, options)
1480 self._regex = re.compile("")
1483 def _count(self, move=False, offset=0):
1485 cwd = self.fm.thisdir
1486 pattern = self.pattern
1488 if not pattern or not cwd.files:
1495 deq = deque(cwd.files)
1496 deq.rotate(-cwd.pointer - offset)
1498 regex = self._build_regex()
1500 if regex.search(fsobj.relative_path):
1502 if move and count == 1:
1503 cwd.move(to=(cwd.pointer + i) % len(cwd.files))
1504 self.fm.thisfile = cwd.pointed_obj
1512 class narrow(Command):
1516 Show only the files selected right now. If no files are selected,
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
1524 self.fm.thisdir.narrow_filter = None
1525 self.fm.thisdir.refilter()
1528 class filter_inode_type(Command):
1530 :filter_inode_type [dfl]
1532 Displays only the files of specified inode type. Parameters
1535 d display directories
1542 self.fm.thisdir.inode_type_filter = ""
1544 self.fm.thisdir.inode_type_filter = self.arg(1)
1545 self.fm.thisdir.refilter()
1548 class filter_stack(Command):
1552 Manages the filter stack.
1554 filter_stack add FILTER_TYPE ARGS...
1556 filter_stack decompose
1557 filter_stack rotate [N=1]
1562 from ranger.core.filter_stack import SIMPLE_FILTERS, FILTER_COMBINATORS
1564 subcommand = self.arg(1)
1566 if subcommand == "add":
1568 self.fm.thisdir.filter_stack.append(
1569 SIMPLE_FILTERS[self.arg(2)](self.rest(3))
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()
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]
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)
1595 "Unknown subcommand: {}".format(subcommand),
1600 self.fm.thisdir.refilter()
1603 class grep(Command):
1606 Looks for a string in all marked files or directories
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')
1617 class flat(Command):
1621 Flattens the directory view up to the specified level.
1624 0 remove flattened view
1629 level_str = self.rest(1)
1630 level = int(level_str)
1632 level = self.quantifier
1634 self.fm.notify("Syntax: flat <level>", bad=True)
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()
1642 # Version control commands
1643 # --------------------------------
1646 class stage(Command):
1650 Stage selected files for the corresponding version control system
1654 from ranger.ext.vcs import VcsError
1656 if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1657 filelist = [f.path for f in self.fm.thistab.get_selection()]
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)
1664 self.fm.notify('Unable to stage files: Not in repository')
1667 class unstage(Command):
1671 Unstage selected files for the corresponding version control system
1675 from ranger.ext.vcs import VcsError
1677 if self.fm.thisdir.vcs and self.fm.thisdir.vcs.track:
1678 filelist = [f.path for f in self.fm.thistab.get_selection()]
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)
1685 self.fm.notify('Unable to unstage files: Not in repository')
1688 # --------------------------------
1691 class prompt_metadata(Command):
1693 :prompt_metadata <key1> [<key2> [<key3> ...]]
1695 Prompt the user to input metadata for multiple keys in a row.
1698 _command_name = "meta"
1699 _console_chain = None
1702 prompt_metadata._console_chain = self.args[1:]
1703 self._process_command_stack()
1705 def _process_command_stack(self):
1706 if prompt_metadata._console_chain:
1707 key = prompt_metadata._console_chain.pop()
1708 self._fill_console(key)
1710 for col in self.fm.ui.browser.columns:
1711 col.need_redraw = True
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]
1719 text = "%s %s %s" % (self._command_name, key, existing_value)
1720 self.fm.open_console(text, position=len(text))
1723 class meta(prompt_metadata):
1725 :meta <key> [<value>]
1727 Change metadata of a file. Deletes the key if value is empty.
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()
1739 def tab(self, tabnum):
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))]
1748 class linemode(default_linemode):
1752 Change what is displayed as a filename.
1754 - "mode" may be any of the defined linemodes (see: ranger.core.linemode).
1755 "normal" is mapped to "filename".
1761 if mode == "normal":
1762 from ranger.core.linemode import DEFAULT_LINEMODE
1763 mode = DEFAULT_LINEMODE
1765 if mode not in self.fm.thisfile.linemode_dict:
1766 self.fm.notify("Unhandled linemode: `%s'" % mode, bad=True)
1769 self.fm.thisdir.set_linemode_of_children(mode)
1771 # Ask the browsercolumns to redraw
1772 for col in self.fm.ui.browser.columns:
1773 col.need_redraw = True
1776 class yank(Command):
1777 """:yank [name|dir|path]
1779 Copies the file's name (default), directory or path into both the primary X
1780 selection and the clipboard.
1785 'name_without_extension': 'basename_without_extension',
1795 from ranger.ext.get_executables import get_executables
1796 clipboard_managers = {
1799 ['xclip', '-selection', 'clipboard'],
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]
1816 clipboard_commands = clipboards()
1818 mode = self.modes[self.arg(1)]
1819 selection = self.get_selection_attr(mode)
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)
1827 def get_selection_attr(self, attr):
1828 return [getattr(item, attr) for item in
1829 self.fm.thistab.get_selection()]
1831 def tab(self, tabnum):
1833 self.start(1) + mode for mode
1834 in sorted(self.modes.keys())