From a51764c907915516e7e17cef971f3c726bab1e4f Mon Sep 17 00:00:00 2001 From: Avery Pennarun Date: Fri, 12 Nov 2010 05:24:46 -0800 Subject: [PATCH] Extremely basic first crack at implementing djb's redo. And a test program. --- .gitignore | 3 + CC.do | 4 ++ LD.do | 6 ++ clean.do | 1 + hello.c | 7 ++ hello.do | 2 + hello.o.do | 2 + helpers.py | 22 ++++++ it.do | 1 + options.py | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++ redo | 1 + redo.py | 52 ++++++++++++++ test.do | 3 + 13 files changed, 305 insertions(+) create mode 100644 .gitignore create mode 100644 CC.do create mode 100644 LD.do create mode 100644 clean.do create mode 100644 hello.c create mode 100644 hello.do create mode 100644 hello.o.do create mode 100644 helpers.py create mode 100644 it.do create mode 100644 options.py create mode 120000 redo create mode 100755 redo.py create mode 100644 test.do diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..158244d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.pyc +*.tmp diff --git a/CC.do b/CC.do new file mode 100644 index 0000000..e015c46 --- /dev/null +++ b/CC.do @@ -0,0 +1,4 @@ +cat <<-EOF + gcc -Wall -o /dev/fd/1 -c "\$1" +EOF +chmod a+x $3 diff --git a/LD.do b/LD.do new file mode 100644 index 0000000..87a9314 --- /dev/null +++ b/LD.do @@ -0,0 +1,6 @@ +cat <<-EOF + OUT="\$1" + shift + gcc -Wall -o "\$OUT" "\$@" +EOF +chmod a+x $3 diff --git a/clean.do b/clean.do new file mode 100644 index 0000000..01bf423 --- /dev/null +++ b/clean.do @@ -0,0 +1 @@ +rm -f hello *.o *~ .*~ *.pyc CC LD diff --git a/hello.c b/hello.c new file mode 100644 index 0000000..76bb8fa --- /dev/null +++ b/hello.c @@ -0,0 +1,7 @@ +#include + +int main() +{ + printf("hello, world!\n"); + return 0; +} diff --git a/hello.do b/hello.do new file mode 100644 index 0000000..8398122 --- /dev/null +++ b/hello.do @@ -0,0 +1,2 @@ +redo --ifchange LD hello.o +./LD hello hello.o diff --git a/hello.o.do b/hello.o.do new file mode 100644 index 0000000..5a6ac05 --- /dev/null +++ b/hello.o.do @@ -0,0 +1,2 @@ +redo --ifchange CC hello.c +./CC hello.c diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..ea9c09f --- /dev/null +++ b/helpers.py @@ -0,0 +1,22 @@ +import sys, os, errno + + +def log(s): + sys.stdout.flush() + sys.stderr.write(s) + sys.stderr.flush() + + +def unlink(f): + """Delete a file at path 'f' if it currently exists. + + Unlike os.unlink(), does not throw an exception if the file didn't already + exist. + """ + try: + os.unlink(f) + except OSError, e: + if e.errno == errno.ENOENT: + pass # it doesn't exist, that's what you asked for + + diff --git a/it.do b/it.do new file mode 100644 index 0000000..22e2266 --- /dev/null +++ b/it.do @@ -0,0 +1 @@ +redo --ifchange hello diff --git a/options.py b/options.py new file mode 100644 index 0000000..5ff2a85 --- /dev/null +++ b/options.py @@ -0,0 +1,201 @@ +"""Command-line options parser. +With the help of an options spec string, easily parse command-line options. +""" +import sys, os, textwrap, getopt, re, struct + +class OptDict: + def __init__(self): + self._opts = {} + + def __setitem__(self, k, v): + if k.startswith('no-') or k.startswith('no_'): + k = k[3:] + v = not v + self._opts[k] = v + + def __getitem__(self, k): + if k.startswith('no-') or k.startswith('no_'): + return not self._opts[k[3:]] + return self._opts[k] + + def __getattr__(self, k): + return self[k] + + +def _default_onabort(msg): + sys.exit(97) + + +def _intify(v): + try: + vv = int(v or '') + if str(vv) == v: + return vv + except ValueError: + pass + return v + + +def _atoi(v): + try: + return int(v or 0) + except ValueError: + return 0 + + +def _remove_negative_kv(k, v): + if k.startswith('no-') or k.startswith('no_'): + return k[3:], not v + return k,v + +def _remove_negative_k(k): + return _remove_negative_kv(k, None)[0] + + +def _tty_width(): + s = struct.pack("HHHH", 0, 0, 0, 0) + try: + import fcntl, termios + s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s) + except (IOError, ImportError): + return _atoi(os.environ.get('WIDTH')) or 70 + (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s) + return xsize + + +class Options: + """Option parser. + When constructed, two strings are mandatory. The first one is the command + name showed before error messages. The second one is a string called an + optspec that specifies the synopsis and option flags and their description. + For more information about optspecs, consult the bup-options(1) man page. + + Two optional arguments specify an alternative parsing function and an + alternative behaviour on abort (after having output the usage string). + + By default, the parser function is getopt.gnu_getopt, and the abort + behaviour is to exit the program. + """ + def __init__(self, exe, optspec, optfunc=getopt.gnu_getopt, + onabort=_default_onabort): + self.exe = exe + self.optspec = optspec + self._onabort = onabort + self.optfunc = optfunc + self._aliases = {} + self._shortopts = 'h?' + self._longopts = ['help'] + self._hasparms = {} + self._defaults = {} + self._usagestr = self._gen_usage() + + def _gen_usage(self): + out = [] + lines = self.optspec.strip().split('\n') + lines.reverse() + first_syn = True + while lines: + l = lines.pop() + if l == '--': break + out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l)) + first_syn = False + out.append('\n') + last_was_option = False + while lines: + l = lines.pop() + if l.startswith(' '): + out.append('%s%s\n' % (last_was_option and '\n' or '', + l.lstrip())) + last_was_option = False + elif l: + (flags, extra) = l.split(' ', 1) + extra = extra.strip() + if flags.endswith('='): + flags = flags[:-1] + has_parm = 1 + else: + has_parm = 0 + g = re.search(r'\[([^\]]*)\]$', extra) + if g: + defval = g.group(1) + else: + defval = None + flagl = flags.split(',') + flagl_nice = [] + for f in flagl: + f,dvi = _remove_negative_kv(f, _intify(defval)) + self._aliases[f] = _remove_negative_k(flagl[0]) + self._hasparms[f] = has_parm + self._defaults[f] = dvi + if len(f) == 1: + self._shortopts += f + (has_parm and ':' or '') + flagl_nice.append('-' + f) + else: + f_nice = re.sub(r'\W', '_', f) + self._aliases[f_nice] = _remove_negative_k(flagl[0]) + self._longopts.append(f + (has_parm and '=' or '')) + self._longopts.append('no-' + f) + flagl_nice.append('--' + f) + flags_nice = ', '.join(flagl_nice) + if has_parm: + flags_nice += ' ...' + prefix = ' %-20s ' % flags_nice + argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(), + initial_indent=prefix, + subsequent_indent=' '*28)) + out.append(argtext + '\n') + last_was_option = True + else: + out.append('\n') + last_was_option = False + return ''.join(out).rstrip() + '\n' + + def usage(self, msg=""): + """Print usage string to stderr and abort.""" + sys.stderr.write(self._usagestr) + e = self._onabort and self._onabort(msg) or None + if e: + raise e + + def fatal(self, s): + """Print an error message to stderr and abort with usage string.""" + msg = 'error: %s\n' % s + sys.stderr.write(msg) + return self.usage(msg) + + def parse(self, args): + """Parse a list of arguments and return (options, flags, extra). + + In the returned tuple, "options" is an OptDict with known options, + "flags" is a list of option flags that were used on the command-line, + and "extra" is a list of positional arguments. + """ + try: + (flags,extra) = self.optfunc(args, self._shortopts, self._longopts) + except getopt.GetoptError, e: + self.fatal(e) + + opt = OptDict() + + for k,v in self._defaults.iteritems(): + k = self._aliases[k] + opt[k] = v + + for (k,v) in flags: + k = k.lstrip('-') + if k in ('h', '?', 'help'): + self.usage() + if k.startswith('no-'): + k = self._aliases[k[3:]] + v = 0 + else: + k = self._aliases[k] + if not self._hasparms[k]: + assert(v == '') + v = (opt._opts.get(k) or 0) + 1 + else: + v = _intify(v) + opt[k] = v + for (f1,f2) in self._aliases.iteritems(): + opt[f1] = opt._opts.get(f2) + return (opt,flags,extra) diff --git a/redo b/redo new file mode 120000 index 0000000..d9204f9 --- /dev/null +++ b/redo @@ -0,0 +1 @@ +redo.py \ No newline at end of file diff --git a/redo.py b/redo.py new file mode 100755 index 0000000..47cec7b --- /dev/null +++ b/redo.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +import sys, os, subprocess +import options +from helpers import * + +optspec = """ +redo [targets...] +-- +ifchange something something +""" +o = options.Options('redo', optspec) +(opt, flags, extra) = o.parse(sys.argv[1:]) + +targets = extra or ['it'] + + +def find_do_file(t): + p = '%s.do' % t + if os.path.exists(p): + return p + else: + return None + + +def build(t): + dofile = find_do_file(t) + if not dofile: + if os.path.exists(t): # an existing source file + return # success + else: + raise Exception('no rule to make %r' % t) + unlink(t) + os.putenv('REDO_TARGET', t) + tmpname = '%s.redo.tmp' % t + unlink(tmpname) + f = open(tmpname, 'w+') + log('running: %r\n' % dofile) + rv = subprocess.call(['sh', '-e', dofile, t , 'FIXME', tmpname], + stdout=f.fileno()) + st = os.stat(tmpname) + log('rv: %d (%d bytes) (%r)\n' % (rv, st.st_size, dofile)) + if rv==0 and st.st_size: + os.rename(tmpname, t) + log('made %r\n' % t) + else: + unlink(tmpname) + f.close() + if rv != 0: + raise Exception('non-zero return code building %r' % t) + +for t in targets: + build(t) diff --git a/test.do b/test.do new file mode 100644 index 0000000..53195c6 --- /dev/null +++ b/test.do @@ -0,0 +1,3 @@ +redo --ifchange it +./hello >&2 +