added doc
[afo.git] / afo.py
blob56d8c4f71715036a317e7091dd336f83c1dad3d2
1 #!/usr/bin/python
2 # afo - automatized file opener.
4 # -----------------------------------------------------
5 # Environment Variables are:
7 # $f = the full path to the file name
8 # $F = $f without the file extension
9 # $e = the extension of $f only
10 # $d = dirname($f)
11 # $b = basename($f)
12 # $B = basename($f) without the file extension
13 # $args = the list of positional arguments passed to afo
14 # $a0..$a999 = one specific argument passed to afo
16 # -----------------------------------------------------
17 # Configuration file:
19 # It's a yaml dictionary, in the form of:
20 # <regexp>: <programs>
22 # For example:
23 # py$: python2.7 $f
24 # (png|jpg)$: feh $f $args
26 # -----------------------------------------------------
27 # <regexp> is a regular expression that matches against this string:
28 # <mime-type>///<file-path>. This way you can check for complicated
29 # relationships with a single regexp. For example, this regular expression
30 # matches images (by mime-type) except SVG's: ^image.*(?<!svg)$
32 # The first regular expression that matches is used and its respective program
33 # is run.
35 # -----------------------------------------------------
36 # <program> is either a string, a list or a dictionary. In case of a string,
37 # the command in the string is simply executed. But sometimes it is necessary
38 # to open files in different ways. For example, one way to compile it and one
39 # way to execute it:
41 # tex$:
42 # compile: pdflatex $B
43 # view: epdfview $B.pdf
44 # c$:
45 # - gcc $f -o /tmp/a.out
46 # - /tmp/a.out
47 # /home/me/my_project/:
48 # - cd /home/me/my_project; make
49 # - /home/me/my_project/my_executable
51 # You can then use the --ways option to specify which way to run it.
52 # For example, "afo --ways 0,1 test.c" would compile and run test.c.
54 # -----------------------------------------------------
55 # Sometimes you can't be sure whether a program is installed or not.
56 # In this case, you can specify multiple programs which will be run in succession
57 # until one ends with an exit code which is not 127 (sh's exit code when a
58 # command is not found.) Examples:
60 # avi$:
61 # normal:
62 # - mplayer $f
63 # - totem $f
64 # fullscreen:
65 # - mplayer -fs $f
66 # - totem --fullscreen $f
67 # ^image:
68 # -
69 # - sxiv $f
70 # - feh $f
72 # -----------------------------------------------------
73 # If the first word in the program description starts with a "-", it will
74 # be interpreted as a set of additional afo options:
75 # avi$: -qf mplayer $f
76 # txt$: -p cat $f
78 import mimetypes
79 import optparse
80 import os.path
81 import re
82 import subprocess
83 import sys
84 import time
85 try:
86 import yaml
87 except ImportError:
88 print("afo: error: python-yaml required.")
90 class AFO(object):
91 def __init__(self, file, options=[], ways=[0], list_ways=False, args=[]):
92 self.__dict__.update(locals())
93 self.file = os.path.abspath(file)
94 self.config = self._load_config()
96 mimetypes.knownfiles.append(os.path.expanduser('~/.mime.types'))
97 basename = os.path.basename(self.file)
98 self.mimetype = mimetypes.MimeTypes().guess_type(basename, False)[0] or ""
99 match_string = '%s///%s' % (self.mimetype, self.file)
101 for entry, program in self.config.items():
102 try:
103 regex = re.compile(entry)
104 except:
105 print("afo: warning: Bad regexp: %s" % entry)
106 else:
107 if regex.search(match_string):
108 return self._run(program)
110 try:
111 first_line = open(self.file).readline()
112 if first_line[0:2] == '#!':
113 program = first_line.strip()[2:]
114 if program:
115 return self._run(program + ' $f $@')
116 except:
117 pass
119 print("afo: error: Unknown type")
121 def _normalize_program_entry(self, program):
122 if isinstance(program, str):
123 return {'0': program}
124 elif isinstance(program, dict):
125 return dict(map((lambda v: (str(v[0]), v[1])), program.items()))
126 elif isinstance(program, list):
127 return dict(map((lambda v: (str(v[0]), v[1])), enumerate(program)))
129 def _load_config(self):
130 basedir = os.path.expanduser(os.getenv('XDG_CONFIG_PATH', '~/.config'))
131 confpath = os.path.join(basedir, 'afo', 'config.yaml')
132 try:
133 return yaml.load(open(confpath))
134 except Exception as e:
135 print("afo: error: Failed to read config file:\n", e)
136 return {}
138 def _run(self, program):
139 program = self._normalize_program_entry(program)
140 if self.list_ways:
141 print("\n".join(("%d: %s" % (n, line)) for n, line in program.items()))
142 else:
143 for way in self.ways:
144 if way in program:
145 if isinstance(program[way], list):
146 self._shell(program[way])
147 else:
148 self._shell([program[way]])
149 else:
150 print("afo: warning: Unknown way `%s'" % str(way))
152 def _generate_env(self, program):
153 env = dict(os.environ)
154 env.update({
155 'f': self.file,
156 'F': os.path.splitext(self.file)[0],
157 'e': os.path.splitext(self.file)[1][1:],
158 'd': os.path.dirname(self.file),
159 'b': os.path.basename(self.file),
160 'B': os.path.splitext(os.path.basename(self.file))[0],
161 'm': self.mimetype,
162 'args': " ".join(self.args),
164 for i, arg in enumerate(self.args):
165 env[str(i+1)] = self.args[i]
166 return env
168 def _shell(self, commands):
169 if set('vt') & set(self.options):
170 print(command[0])
172 if 't' not in self.options:
173 for i, command in enumerate(commands):
174 is_last = len(commands) is i + 1
176 if command[0] == '-':
177 options, command = command.split(' ', 1)
178 self.options += options[1:]
180 popen_kws = {'shell': True, 'env': self._generate_env(command)}
181 if set('qf') & set(self.options):
182 for key in ('stdout', 'stderr', 'stdin'):
183 popen_kws[key] = open(os.devnull, 'a')
184 p = subprocess.Popen(command, **popen_kws)
186 if 'f' not in self.options:
187 p.wait()
189 elif not is_last:
190 for i in range(100):
191 if p.poll() is not None:
192 break
193 time.sleep(0.001)
195 if p.poll() is not 127:
196 is_last = True
198 if is_last and 'w' in self.options:
199 print("Press ENTER to continue")
200 try: raw_input()
201 except: input()
203 if is_last:
204 break
206 @staticmethod
207 def get_parameters_from_argv(argv=None):
208 class MoreOptions(optparse.Option):
209 TYPES = optparse.Option.TYPES + ('list', )
210 TYPE_CHECKER = dict(optparse.Option.TYPE_CHECKER,
211 list=lambda _, __, value: value.split(','))
213 p = optparse.OptionParser(option_class=MoreOptions,
214 usage="%prog [options] path [-- args...]")
215 p.add_option('-p', action='store_true', help='pipe output into a pager')
216 p.add_option('-w', action='store_true',
217 help='wait for a key press afterwards')
218 p.add_option('-q', action='store_true', help='discard output')
219 p.add_option('-t', action='store_true', help='test only')
220 p.add_option('-v', action='store_true', help='be verbose')
221 p.add_option('-f', action='store_true', help='fork process')
222 p.add_option('--ways', type='list', default='0', metavar='N,M,..',
223 help="open the file in what way(s)?")
224 p.add_option('--list-ways', action='store_true',
225 help="list all possible ways to run this file")
226 keywords, args = p.parse_args(argv)
227 if not len(args) > 1:
228 p.print_help()
229 raise SystemExit()
230 opts = ''.join(f for f,v in keywords.__dict__.items() if len(f) == 1 and v)
232 return {
233 'options': opts,
234 'file': args[1],
235 'ways': keywords.ways,
236 'list_ways': keywords.list_ways,
237 'args': args[1:] }
239 if __name__ == '__main__':
240 AFO(**AFO.get_parameters_from_argv(sys.argv))