commit a51764c907915516e7e17cef971f3c726bab1e4f Author: Avery Pennarun Date: Fri Nov 12 05:24:46 2010 -0800 Extremely basic first crack at implementing djb's redo. And a test program. 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 +