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