Extremely basic first crack at implementing djb's redo.

And a test program.
This commit is contained in:
Avery Pennarun 2010-11-12 05:24:46 -08:00
commit a51764c907
13 changed files with 305 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*~
*.pyc
*.tmp

4
CC.do Normal file
View file

@ -0,0 +1,4 @@
cat <<-EOF
gcc -Wall -o /dev/fd/1 -c "\$1"
EOF
chmod a+x $3

6
LD.do Normal file
View file

@ -0,0 +1,6 @@
cat <<-EOF
OUT="\$1"
shift
gcc -Wall -o "\$OUT" "\$@"
EOF
chmod a+x $3

1
clean.do Normal file
View file

@ -0,0 +1 @@
rm -f hello *.o *~ .*~ *.pyc CC LD

7
hello.c Normal file
View file

@ -0,0 +1,7 @@
#include <stdio.h>
int main()
{
printf("hello, world!\n");
return 0;
}

2
hello.do Normal file
View file

@ -0,0 +1,2 @@
redo --ifchange LD hello.o
./LD hello hello.o

2
hello.o.do Normal file
View file

@ -0,0 +1,2 @@
redo --ifchange CC hello.c
./CC hello.c

22
helpers.py Normal file
View file

@ -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

1
it.do Normal file
View file

@ -0,0 +1 @@
redo --ifchange hello

201
options.py Normal file
View file

@ -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)

1
redo Symbolic link
View file

@ -0,0 +1 @@
redo.py

52
redo.py Executable file
View file

@ -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)

3
test.do Normal file
View file

@ -0,0 +1,3 @@
redo --ifchange it
./hello >&2