2 # This file is part of ranger, the console file manager.
3 # License: GNU GPL version 3, see the file "AUTHORS" for details.
5 """rifle, the file executor/opener of ranger
7 This can be used as a standalone program or can be embedded in python code.
8 When used together with ranger, it doesn't have to be installed to $PATH.
12 rifle = Rifle("rifle.conf")
14 rifle.execute(["file1", "file2"])
19 from subprocess
import Popen
, PIPE
22 __version__
= 'rifle 1.7.0'
24 # Options and constants that a user might want to change:
25 DEFAULT_PAGER
= 'less'
26 DEFAULT_EDITOR
= 'vim'
30 # Imports from ranger library, plus reimplementations in case ranger is not
31 # installed so rifle can be run as a standalone program.
33 from ranger
.ext
.get_executables
import get_executables
35 _cached_executables
= None
37 def get_executables():
38 """Return all executable files in $PATH + Cache them."""
39 global _cached_executables
40 if _cached_executables
is not None:
41 return _cached_executables
43 if 'PATH' in os
.environ
:
44 paths
= os
.environ
['PATH'].split(':')
46 paths
= ['/usr/bin', '/bin']
48 from stat
import S_IXOTH
, S_IFREG
50 _cached_executables
= set()
52 if path
in paths_seen
:
56 content
= os
.listdir(path
)
60 abspath
= path
+ '/' + item
62 filestat
= os
.stat(abspath
)
65 if filestat
.st_mode
& (S_IXOTH | S_IFREG
):
66 _cached_executables
.add(item
)
67 return _cached_executables
71 from ranger
.ext
.popen_forked
import Popen_forked
73 def Popen_forked(*args
, **kwargs
):
74 """Forks process and runs Popen with the given args and kwargs."""
81 kwargs
['stdin'] = open(os
.devnull
, 'r')
82 kwargs
['stdout'] = kwargs
['stderr'] = open(os
.devnull
, 'w')
83 Popen(*args
, **kwargs
)
89 # Check if stdin (file descriptor 0), stdout (fd 1) and
90 # stderr (fd 2) are connected to a terminal
100 def squash_flags(flags
):
101 """Remove lowercase flags if the respective uppercase flag exists
103 >>> squash_flags('abc')
105 >>> squash_flags('abcC')
107 >>> squash_flags('CabcAd')
110 exclude
= ''.join(f
.upper() + f
.lower() for f
in flags
if f
== f
.upper())
111 return ''.join(f
for f
in flags
if f
not in exclude
)
118 # TODO: Test all of the hooks properly
119 def hook_before_executing(self
, command
, mimetype
, flags
):
122 def hook_after_executing(self
, command
, mimetype
, flags
):
125 def hook_command_preprocessing(self
, command
):
128 def hook_command_postprocessing(self
, command
):
131 def hook_environment(self
, env
):
134 def hook_logger(self
, string
):
135 sys
.stderr
.write(string
+ "\n")
137 def __init__(self
, config_file
):
138 self
.config_file
= config_file
140 self
._app
_label
= None
141 self
._initialized
_mimetypes
= False
143 # get paths for mimetype files
144 self
._mimetype
_known
_files
= [
145 os
.path
.expanduser("~/.mime.types")]
146 if __file__
.endswith("ranger/ext/rifle.py"):
147 # Add ranger's default mimetypes when run from ranger directory
148 self
._mimetype
_known
_files
.append(
149 __file__
.replace("ext/rifle.py", "data/mime.types"))
151 def reload_config(self
, config_file
=None):
152 """Replace the current configuration with the one in config_file"""
153 if config_file
is None:
154 config_file
= self
.config_file
155 f
= open(config_file
, 'r')
159 if line
.startswith('#') or line
== '\n':
163 if self
.delimiter1
not in line
:
164 raise Exception("Line without delimiter")
165 tests
, command
= line
.split(self
.delimiter1
, 1)
166 tests
= tests
.split(self
.delimiter2
)
167 tests
= tuple(tuple(f
.strip().split(None, 1)) for f
in tests
)
168 command
= command
.strip()
169 self
.rules
.append((command
, tests
))
170 except Exception as e
:
171 self
.hook_logger("Syntax error in %s line %d (%s)" % \
172 (config_file
, lineno
, str(e
)))
176 def _eval_condition(self
, condition
, files
, label
):
177 # Handle the negation of conditions starting with an exclamation mark,
178 # then pass on the arguments to _eval_condition2().
182 if condition
[0].startswith('!'):
183 new_condition
= tuple([condition
[0][1:]]) + tuple(condition
[1:])
184 return not self
._eval
_condition
2(new_condition
, files
, label
)
185 return self
._eval
_condition
2(condition
, files
, label
)
187 def _eval_condition2(self
, rule
, files
, label
):
188 # This function evaluates the condition, after _eval_condition() handled
189 # negation of conditions starting with a "!".
195 argument
= rule
[1] if len(rule
) > 1 else ''
197 if function
== 'ext':
198 extension
= os
.path
.basename(files
[0]).rsplit('.', 1)[-1].lower()
199 return bool(re
.search('^(' + argument
+ ')$', extension
))
200 elif function
== 'name':
201 return bool(re
.search(argument
, os
.path
.basename(files
[0])))
202 elif function
== 'match':
203 return bool(re
.search(argument
, files
[0]))
204 elif function
== 'file':
205 return os
.path
.isfile(files
[0])
206 elif function
== 'directory':
207 return os
.path
.isdir(files
[0])
208 elif function
== 'path':
209 return bool(re
.search(argument
, os
.path
.abspath(files
[0])))
210 elif function
== 'mime':
211 return bool(re
.search(argument
, self
._get
_mimetype
(files
[0])))
212 elif function
== 'has':
213 if argument
.startswith("$"):
214 if argument
[1:] in os
.environ
:
215 return os
.environ
[argument
[1:]] in get_executables()
218 return argument
in get_executables()
219 elif function
== 'terminal':
220 return _is_terminal()
221 elif function
== 'number':
222 if argument
.isdigit():
223 self
._skip
= int(argument
)
225 elif function
== 'label':
226 self
._app
_label
= argument
228 return argument
== label
230 elif function
== 'flag':
231 self
._app
_flags
= argument
233 elif function
== 'X':
234 return 'DISPLAY' in os
.environ
235 elif function
== 'else':
238 def _get_mimetype(self
, fname
):
239 # Spawn "file" to determine the mime-type of the given file.
241 return self
._mimetype
244 for path
in self
._mimetype
_known
_files
:
245 if path
not in mimetypes
.knownfiles
:
246 mimetypes
.knownfiles
.append(path
)
247 self
._mimetype
, encoding
= mimetypes
.guess_type(fname
)
249 if not self
._mimetype
:
250 process
= Popen(["file", "--mime-type", "-Lb", fname
],
251 stdout
=PIPE
, stderr
=PIPE
)
252 mimetype
, _
= process
.communicate()
253 self
._mimetype
= mimetype
.decode(ENCODING
).strip()
254 return self
._mimetype
256 def _build_command(self
, files
, action
, flags
):
258 if isinstance(flags
, str):
259 self
._app
_flags
+= flags
260 self
._app
_flags
= squash_flags(self
._app
_flags
)
261 filenames
= "' '".join(f
.replace("'", "'\\\''") for f
in files
263 return "set -- '%s'; %s" % (filenames
, action
)
265 def list_commands(self
, files
, mimetype
=None):
266 """List all commands that are applicable for the given files
268 Returns one 4-tuple for all currently applicable commands
269 The 4-tuple contains (count, command, label, flags).
270 count is the index, counted from 0 upwards,
271 command is the command that will be executed.
272 label and flags are the label and flags specified in the rule.
274 self
._mimetype
= mimetype
276 for cmd
, tests
in self
.rules
:
279 self
._app
_label
= None
281 if not self
._eval
_condition
(test
, files
, None):
284 if self
._skip
is None:
288 yield (count
, cmd
, self
._app
_label
, self
._app
_flags
)
290 def execute(self
, files
, number
=0, label
=None, flags
="", mimetype
=None):
291 """Executes the given list of files.
293 By default, this executes the first command where all conditions apply,
294 but by specifying number=N you can run the 1+Nth command.
296 If a label is specified, only rules with this label will be considered.
298 If you specify the mimetype, rifle will not try to determine it itself.
300 By specifying a flag, you extend the flag that is defined in the rule.
301 Uppercase flags negate the respective lowercase flags.
302 For example: if the flag in the rule is "pw" and you specify "Pf", then
303 the "p" flag is negated and the "f" flag is added, resulting in "wf".
306 found_at_least_one
= None
309 for count
, cmd
, lbl
, flgs
in self
.list_commands(files
, mimetype
):
310 if label
and label
== lbl
or not label
and count
== number
:
311 cmd
= self
.hook_command_preprocessing(cmd
)
312 if cmd
== ASK_COMMAND
:
314 command
= self
._build
_command
(files
, cmd
, flags
+ flgs
)
315 flags
= self
._app
_flags
318 found_at_least_one
= True
320 if label
and label
in get_executables():
321 cmd
= '%s "$@"' % label
322 command
= self
._build
_command
(files
, cmd
, flags
)
326 if found_at_least_one
:
328 self
.hook_logger("Label '%s' is undefined" % label
)
330 self
.hook_logger("Method number %d is undefined." % number
)
332 self
.hook_logger("No action found.")
334 if 'PAGER' not in os
.environ
:
335 os
.environ
['PAGER'] = DEFAULT_PAGER
336 if 'EDITOR' not in os
.environ
:
337 os
.environ
['EDITOR'] = DEFAULT_EDITOR
338 command
= self
.hook_command_postprocessing(command
)
339 self
.hook_before_executing(command
, self
._mimetype
, self
._app
_flags
)
342 prefix
= ['sudo', '-E', 'su', '-mc']
344 prefix
= ['/bin/sh', '-c']
346 cmd
= prefix
+ [command
]
348 if 'TERMCMD' not in os
.environ
:
349 term
= os
.environ
['TERM']
350 if term
.startswith('rxvt-unicode'):
352 if term
not in get_executables():
353 self
.hook_logger("Can not determine terminal command. "
354 "Please set $TERMCMD manually.")
355 # A fallback terminal that is likely installed:
357 os
.environ
['TERMCMD'] = term
358 cmd
= [os
.environ
['TERMCMD'], '-e'] + cmd
359 if 'f' in flags
or 't' in flags
:
360 Popen_forked(cmd
, env
=self
.hook_environment(os
.environ
))
362 p
= Popen(cmd
, env
=self
.hook_environment(os
.environ
))
365 self
.hook_after_executing(command
, self
._mimetype
, self
._app
_flags
)
369 """The main function which is run when you start this program direectly."""
372 # Find configuration file path
373 if 'XDG_CONFIG_HOME' in os
.environ
and os
.environ
['XDG_CONFIG_HOME']:
374 conf_path
= os
.environ
['XDG_CONFIG_HOME'] + '/ranger/rifle.conf'
376 conf_path
= os
.path
.expanduser('~/.config/ranger/rifle.conf')
377 default_conf_path
= conf_path
378 if not os
.path
.isfile(conf_path
):
379 conf_path
= os
.path
.normpath(os
.path
.join(os
.path
.dirname(__file__
),
380 '../config/rifle.conf'))
381 if not os
.path
.isfile(conf_path
):
383 # if ranger is installed, get the configuration from ranger
388 conf_path
= os
.path
.join(ranger
.__path
__[0], "config", "rifle.conf")
392 from optparse
import OptionParser
393 parser
= OptionParser(usage
="%prog [-fhlpw] [files]", version
=__version__
)
394 parser
.add_option('-f', type="string", default
="", metavar
="FLAGS",
395 help="use additional flags: f=fork, r=root, t=terminal. "
396 "Uppercase flag negates respective lowercase flags.")
397 parser
.add_option('-l', action
="store_true",
398 help="list possible ways to open the files (id:label:flags:command)")
399 parser
.add_option('-p', type='string', default
='0', metavar
="KEYWORD",
400 help="pick a method to open the files. KEYWORD is either the "
401 "number listed by 'rifle -l' or a string that matches a label in "
402 "the configuration file")
403 parser
.add_option('-w', type='string', default
=None, metavar
="PROGRAM",
404 help="open the files with PROGRAM")
405 options
, positional
= parser
.parse_args()
410 if not os
.path
.isfile(conf_path
):
411 sys
.stderr
.write("Could not find a configuration file.\n"
412 "Please create one at %s.\n" % default_conf_path
)
415 if options
.p
.isdigit():
416 number
= int(options
.p
)
422 if options
.w
is not None and not options
.l
:
423 p
= Popen([options
.w
] + list(positional
))
427 rifle
= Rifle(conf_path
)
428 rifle
.reload_config()
429 #print(rifle.list_commands(sys.argv[1:]))
431 for count
, cmd
, label
, flags
in rifle
.list_commands(positional
):
432 print("%d:%s:%s:%s" % (count
, label
or '', flags
, cmd
))
434 result
= rifle
.execute(positional
, number
=number
, label
=label
,
436 if result
== ASK_COMMAND
:
437 # TODO: implement interactive asking for file type?
438 print("Unknown file type: %s" % rifle
._get
_mimetype
(positional
[0]))
442 if __name__
== '__main__':
443 if 'RANGER_DOCTEST' in os
.environ
: