Source

dotfiles / install.py

Full commit
#!/usr/bin/env python
"""Install symlinks for configs into HOME.

Example::

    $ ./install.py -af

        Reinstall all symlinks.

    $ ./install.py dot.bashrc dot.profile

        Install the specified dot files only.
"""
import os, re, itertools
from optparse import OptionParser

ROOTDIR = os.path.dirname(__file__)
EXCLUDE = [r'\.orig$', r'~$']

def walk_dotfiles():
    """Iterates dot.xxx or dot.xxx/xxx files."""
    for dirpath, dirnames, filenames in os.walk(ROOTDIR):
        reldirpath = dirpath[len(ROOTDIR) + 1:]
        if reldirpath and not reldirpath.startswith('dot.'):
            continue

        for e in filenames:
            relfilepath = os.path.join(reldirpath, e)
            if relfilepath.startswith('dot.'):
                if not any(re.search(pat, relfilepath) for pat in EXCLUDE):
                    yield relfilepath

def walk_installable_files():
    """Iterates dot.xxx and bin/xxx"""
    def iter_subdir(dir):
        if not os.path.exists(os.path.join(ROOTDIR, dir)):
            return []

        return iter(os.path.join(dir, e) for e in os.listdir(os.path.join(ROOTDIR, dir))
                    if not any(re.search(pat, os.path.join(dir, e)) for pat in EXCLUDE))

    return itertools.chain(walk_dotfiles(), iter_subdir('bin'))

def walk_specified_files(files):
    """Iterates specified files with ROOTDIR elimination."""
    path_prefix = os.path.abspath(ROOTDIR) + os.path.sep

    for e in files:
        e = os.path.abspath(e)

        if not e.startswith(path_prefix):
            raise ValueError('unexpected file: %s' % e)

        if not os.path.exists(e):
            raise ValueError('unknown file: %s' % e)

        yield e[len(path_prefix):]


def relpath(path, start=os.curdir):
    """Generates shortest relative path."""
    path, start = os.path.abspath(path), os.path.abspath(start)
    common = os.path.dirname(os.path.commonprefix([path, start])).rstrip(os.path.sep) + os.path.sep
    elems = ['..'] * start[len(common):].count(os.path.sep) + [path[len(common):]]
    return os.path.join(*elems)


def do_symlink(src, dest, force=False):
    """Actually creates symlinks pointing to src, named dest."""
    if not os.path.exists(os.path.dirname(dest)):
        os.makedirs(os.path.dirname(dest))

    if force and os.path.lexists(dest):
        os.unlink(dest)

    os.symlink(src, dest)


def install(files, targetdir, verbose=False, dryrun=False, force=False):
    """Installs symlinks to files.

    If a filename starts with 'dot.', it will be replaced to '.'.
    """
    # we resolves symlinks, so that we can calculate the nearest relative path
    # between symlinked and normal file.
    srcdir, targetdir = os.path.realpath(ROOTDIR), os.path.realpath(targetdir)

    for e in files:
        dest = os.path.join(targetdir, re.sub('^dot\.', '.', e))
        src = relpath(os.path.join(srcdir, e), start=dest)

        if (not force) and os.path.exists(dest):
            continue

        if verbose:
            print "%s -> %s" % (dest, src)

        if not dryrun:
            do_symlink(src, dest, force=force)


def main():
    parser = OptionParser()
    parser.usage += " [dot.file ...]\n\n" + __doc__
    parser.add_option('-a', '--all', action='store_true', default=False)
    parser.add_option('-q', '--quiet', action='store_false', dest='verbose', default=True)
    parser.add_option('-n', '--dry-run', action='store_true', default=False)
    parser.add_option('-f', '--force', action='store_true', default=False)
    parser.add_option('-t', '--target-directory', default=os.path.expanduser('~'))
    (opts, args) = parser.parse_args()

    if not os.path.isdir(opts.target_directory):
        parser.error('bad target directory: %s' % opts.target_directory)

    if opts.all and args:
        parser.error('arguments not allowed with --all')
    elif not (opts.all or args):
        parser.error('please specify dot.file to install')

    install(opts.all and walk_installable_files() or walk_specified_files(args),
            targetdir=opts.target_directory,
            verbose=opts.verbose, dryrun=opts.dry_run, force=opts.force)


if __name__ == '__main__':
    main()