fixed 'f' option and removed _shell_quote
[afo.git] / afo.py
blobd351cf3d0d355564cf0e5130e5c4db306fd20433
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>: <program entry>
22 # For example:
23 # py$: python2.7 $f
24 # (png|jpg)$: feh $f $args
26 # The first regular expression that matches the given file argument
27 # is used and its respective program is run. Sometimes it is necessary
28 # to open files in different ways. For example, one way to compile it
29 # and one way to execute it:
31 # tex$:
32 # 0: pdflatex $B
33 # 1: epdfview $B.pdf
34 # c$:
35 # - "$w1; $w2"
36 # - gcc $f -o /tmp/a.out
37 # - /tmp/a.out
38 # /home/me/my_project/:
39 # - cd /home/me/my_project; make
40 # - /home/me/my_project/my_executable
42 # You can then use the --way option to specify which way to run it.
43 # For example, "afo --way 1,2 test.c" would compile and run test.c.
44 # "afo test.c" would do the same, because way 0 runs way 1 and 2 in order.
46 # If the first word in the program description starts with a "-", it will
47 # be interpreted as a set of additional afo options:
48 # avi$: -qf mplayer $f
49 # txt$: -p cat $f
51 import mimetypes
52 import optparse
53 import os.path
54 import re
55 import subprocess
56 import sys
57 import time
58 try:
59 import yaml
60 except ImportError:
61 print("afo: error: python-yaml required.")
63 class AFO(object):
64 def __init__(self, file, options=[], ways=[0], list_ways=False, args=[]):
65 self.__dict__.update(locals())
66 self.file = os.path.abspath(file)
67 self.config = self._load_config()
69 mimetypes.knownfiles.append(os.path.expanduser('~/.mime.types'))
70 basename = os.path.basename(self.file)
71 self.mimetype = mimetypes.MimeTypes().guess_type(basename, False)[0] or ""
72 match_string = '%s///%s' % (self.mimetype, self.file)
74 for entry, program in self.config.items():
75 try:
76 regex = re.compile(entry)
77 except:
78 print("afo: warning: Bad regexp: %s" % entry)
79 else:
80 if regex.search(match_string):
81 return self._run(program)
83 try:
84 first_line = open(self.file).readline()
85 if first_line[0:2] == '#!':
86 program = first_line.strip()[2:]
87 if program:
88 return self._run(program + ' $f $@')
89 except:
90 pass
92 print("afo: error: Unknown type")
94 def _normalize_program_entry(self, program):
95 if isinstance(program, str):
96 return {'0': program}
97 elif isinstance(program, dict):
98 return dict(map((lambda v: (str(v[0]), v[1])), program.items()))
99 elif isinstance(program, list):
100 return dict(map((lambda v: (str(v[0]), v[1])), enumerate(program)))
102 def _load_config(self):
103 basedir = os.path.expanduser(os.getenv('XDG_CONFIG_PATH', '~/.config'))
104 confpath = os.path.join(basedir, 'afo', 'config.yaml')
105 try:
106 return yaml.load(open(confpath))
107 except Exception as e:
108 print("afo: error: Failed to read config file:\n", e)
109 return {}
111 def _run(self, program):
112 program = self._normalize_program_entry(program)
113 if self.list_ways:
114 print("\n".join(("%d: %s" % (n, line)) for n, line in program.items()))
115 else:
116 for way in self.ways:
117 if way in program:
118 if isinstance(program[way], list):
119 self._shell(program[way])
120 else:
121 self._shell([program[way]])
122 else:
123 print("afo: warning: Unknown way `%s'" % str(way))
125 def _generate_env(self, program):
126 env = dict(os.environ)
127 env.update({
128 'f': self.file,
129 'F': os.path.splitext(self.file)[0],
130 'e': os.path.splitext(self.file)[1][1:],
131 'd': os.path.dirname(self.file),
132 'b': os.path.basename(self.file),
133 'B': os.path.splitext(os.path.basename(self.file))[0],
134 'm': self.mimetype,
135 'args': " ".join(self.args),
137 for i, arg in enumerate(self.args):
138 env[str(i+1)] = self.args[i]
139 return env
141 def _shell(self, commands):
142 if set('vt') & set(self.options):
143 print(command[0])
145 if 't' not in self.options:
146 for i, command in enumerate(commands):
147 is_last = len(commands) is i + 1
149 if command[0] == '-':
150 options, command = command.split(' ', 1)
151 self.options += options[1:]
153 popen_kws = {'shell': True, 'env': self._generate_env(command)}
154 if set('qf') & set(self.options):
155 for key in ('stdout', 'stderr', 'stdin'):
156 popen_kws[key] = open(os.devnull, 'a')
157 p = subprocess.Popen(command, **popen_kws)
159 if 'f' not in self.options:
160 p.wait()
162 elif not is_last:
163 for i in range(100):
164 if p.poll() is not None:
165 break
166 time.sleep(0.001)
168 if p.poll() is not 127:
169 is_last = True
171 if is_last and 'w' in self.options:
172 print("Press ENTER to continue")
173 try: raw_input()
174 except: input()
176 if is_last:
177 break
179 @staticmethod
180 def get_parameters_from_argv(argv=None):
181 class MoreOptions(optparse.Option):
182 TYPES = optparse.Option.TYPES + ('list', )
183 TYPE_CHECKER = dict(optparse.Option.TYPE_CHECKER,
184 list=lambda _, __, value: value.split(','))
186 p = optparse.OptionParser(option_class=MoreOptions,
187 usage="%prog [options] path [-- args...]")
188 p.add_option('-p', action='store_true', help='pipe output into a pager')
189 p.add_option('-w', action='store_true',
190 help='wait for a key press afterwards')
191 p.add_option('-q', action='store_true', help='discard output')
192 p.add_option('-t', action='store_true', help='test only')
193 p.add_option('-v', action='store_true', help='be verbose')
194 p.add_option('-f', action='store_true', help='fork process')
195 p.add_option('--ways', type='list', default='0', metavar='N,M,..',
196 help="open the file in what way(s)?")
197 p.add_option('--list-ways', action='store_true',
198 help="list all possible ways to run this file")
199 keywords, args = p.parse_args(argv)
200 if not len(args) > 1:
201 p.print_help()
202 raise SystemExit()
203 opts = ''.join(f for f,v in keywords.__dict__.items() if len(f) == 1 and v)
205 return {
206 'options': opts,
207 'file': args[1],
208 'ways': keywords.ways,
209 'list_ways': keywords.list_ways,
210 'args': args[1:] }
212 if __name__ == '__main__':
213 AFO(**AFO.get_parameters_from_argv(sys.argv))