diff options
-rw-r--r-- | README.rst | 69 | ||||
-rwxr-xr-x | bin/dotfiles | 2 | ||||
-rw-r--r-- | dotfiles/__init__.py | 5 | ||||
-rw-r--r-- | dotfiles/cli.py | 97 | ||||
-rw-r--r-- | dotfiles/core.py | 41 | ||||
-rw-r--r-- | setup.py | 14 |
6 files changed, 184 insertions, 44 deletions
@@ -2,14 +2,33 @@ Dotfile management made easy ============================ ``dotfiles`` is a tool to make managing your dotfile symlinks in ``$HOME`` -easy, allowing you to keep all your dotfiles in a single directory. +easy, allowing you to keep all your dotfiles in a single directory. Hosting is left to you. Yes, I've seen `<http://dotfiles.org>`_ but I don't -like that model. If you're advanced enough to need dotfile management, then you -probably already know how you want to host them. Using whatever VCS you +like that model. If you're advanced enough to need dotfile management, then +you probably already know how you want to host them. Using whatever VCS you prefer, or even rsync, you can easily distribute your dotfiles repository across multiple hosts. +Interface +--------- + +``-a, --add <file...>`` + Add dotfile(s) to the repository. + +``-c, --check`` + Check for missing or unmanged dotfiles. + +``-l, --list`` + List currently managed dotfiles, one per line. + +``-r, --remove <file...>`` + Remove dotfile(s) from the repository. + +``-s, --sync`` + Update dotfile symlinks. You can overwrite unmanaged files with ``-f`` or + ``--force``. + Installation ------------ @@ -49,7 +68,7 @@ To make it available to all your hosts: :: $ git commit -m "Added vimrc, welcome aboard!" $ git push -You get the idea. +You get the idea. Type ``dotfiles --help`` to see the available options. Configuration ------------- @@ -67,6 +86,48 @@ example configuration file might look like: :: '.bzr.log': '/dev/null', '.uml': '/tmp'} +Prefixes +-------- + +Dotfiles are stored in the repository with no prefix by default. So, +``~/.bashrc`` will link to ``~/Dotfiles/bashrc``. If your files already have a +prefix, ``.`` is common, but I've also seen ``_``, then you can specify this +in the configuration file and ``dotfiles`` will do the right thing. An example +configuration in ``~/.dotfilesrc`` might look like: :: + + [dotfiles] + prefix = . + +Externals +--------- + +You may want to link some dotfiles to external locations. For example, ``bzr`` +writes debug information to ``~/.bzr.log`` and there is no easy way to disable +it. For that, I link ``~/.bzr.log`` to ``/dev/null``. Since ``/dev/null`` is +not within the repository, this is called an external. You can have as many of +these as you like. The list of externals is specified in the configuration +file: :: + + [dotfiles] + externals = { + '.bzr.log': '/dev/null', + '.adobe': '/tmp', + '.macromedia': '/tmp'} + +Ignores +------- + +If you're using a VCS to manage your repository of dotfiles, you'll want to +tell ``dotfiles`` to ignore VCS-related files. For example, I use ``git``, so +I have the following in my ``~/.dotfilesrc``: :: + + [dotfiles] + ignore = [ + '.git', + '.gitignore'] + +Any file you list in ``ignore`` will be skipped. + License ------- diff --git a/bin/dotfiles b/bin/dotfiles index 1cd5264..5f4e1c9 100755 --- a/bin/dotfiles +++ b/bin/dotfiles @@ -13,7 +13,7 @@ if (os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'dotfiles')) sys.path.insert(0, PROJECT_ROOT_DIRECTORY) os.putenv('PYTHONPATH', PROJECT_ROOT_DIRECTORY) -import dotfiles +import dotfiles.cli if __name__ == '__main__': dotfiles.cli.main() diff --git a/dotfiles/__init__.py b/dotfiles/__init__.py index 0390200..fcb882b 100644 --- a/dotfiles/__init__.py +++ b/dotfiles/__init__.py @@ -1,8 +1,3 @@ # -*- coding: utf-8 -*- -import cli from core import * - -__version__ = '0.2.0' - -__all__ = ['cli', 'core'] diff --git a/dotfiles/cli.py b/dotfiles/cli.py index fadca25..6618719 100644 --- a/dotfiles/cli.py +++ b/dotfiles/cli.py @@ -1,31 +1,67 @@ # -*- coding: utf-8 -*- +""" +dotfiles.cli + +This module provides the CLI interface to dotfiles. +""" + import os from . import core import ConfigParser from optparse import OptionParser, OptionGroup +USAGE = "Usage: %prog ACTION [OPTION...] [FILE...]" + +NO_REPO_MESSAGE = """Could not find dotfiles repository \"%s\" + +If this is your first time running dotfiles, you must first create a +repository. By default, dotfiles will look for '~/Dotfiles'. Something like: + + $ mkdir ~/Dotfiles + +is all you need to do. If you don't like the default, you can put your +repository wherever you like. You have two choices once you've created your +repository. You can specify the path to the repository on the command line +using the '-R' flag. Alternatively, you can create a configuration file at +'~/.dotfilesrc' and place the path to your repository in there. The contents +would look like: + + [dotfiles] + repository = ~/.my-dotfiles-repo + +You can see more information by typing 'dotfiles -h'""" + + def method_list(object): return [method for method in dir(object) if callable(getattr(object, method))] def parse_args(): - parser = OptionParser(usage="Usage: %prog ACTION [OPTION...] [FILE...]") + parser = OptionParser(usage=USAGE) parser.set_defaults(config=os.path.expanduser("~/.dotfilesrc")) - parser.set_defaults(repo=os.path.expanduser("~/Dotfiles")) - parser.set_defaults(prefix='') parser.set_defaults(ignore=[]) parser.set_defaults(externals={}) - parser.add_option("-C", "--config", type="string", dest="config", - help="set configuration file location (default is ~/.dotfilesrc)") + parser.add_option("-v", "--version", action="store_true", + dest="show_version", default=False, + help="show version number and exit") + + parser.add_option("-f", "--force", action="store_true", dest="force", + default=False, help="ignore unmanaged dotfiles (use with --sync)") parser.add_option("-R", "--repo", type="string", dest="repo", help="set repository location (default is ~/Dotfiles)") + parser.add_option("-p", "--prefix", type="string", dest="prefix", + help="set prefix character (default is None)") + + parser.add_option("-C", "--config", type="string", dest="config", + help="set configuration file location (default is ~/.dotfilesrc)") + action_group = OptionGroup(parser, "Actions") action_group.add_option("-a", "--add", action="store_const", dest="action", @@ -49,14 +85,9 @@ def parse_args(): (opts, args) = parser.parse_args() - if not os.path.exists(opts.repo): - parser.error("Could not find dotfiles repository \"%s\"" % opts.repo) - - if not opts.action: - parser.error("An action is required.") - - if opts.action not in method_list(core.Dotfiles): - parser.error("No such action \"%s\"" % opts.action) + # Skip checking if the repository exists here. The user may have specified + # a command line argument or a configuration file, which will be examined + # next. return (opts, args) @@ -65,6 +96,10 @@ def main(): (opts, args) = parse_args() + if opts.show_version: + print 'dotfiles v%s' % core.__version__ + exit(0) + config_defaults = { 'repository': opts.repo, 'prefix': opts.prefix, @@ -74,16 +109,40 @@ def main(): parser = ConfigParser.SafeConfigParser(config_defaults) if opts.config: - parser.read(opts.config) if 'dotfiles' in parser.sections(): - opts.repo = os.path.expanduser(parser.get('dotfiles', 'repository')) - opts.prefix = parser.get('dotfiles', 'prefix') - opts.ignore = eval(parser.get('dotfiles', 'ignore')) - opts.externals = eval(parser.get('dotfiles', 'externals')) + + if not opts.repo: + if parser.get('dotfiles', 'repository'): + opts.repo = os.path.expanduser(parser.get('dotfiles', 'repository')) + else: + opts.repo = os.path.expanduser("~/Dotfiles") + + if not opts.prefix: + if parser.get('dotfiles', 'prefix'): + opts.prefix = parser.get('dotfiles', 'prefix') + else: + opts.prefix = '' + + if not opts.ignore and parser.get('dotfiles', 'ignore'): + opts.ignore = eval(parser.get('dotfiles', 'ignore')) + + if not opts.externals and parser.get('dotfiles', 'externals'): + opts.externals = eval(parser.get('dotfiles', 'externals')) + + if not os.path.exists(opts.repo): + print "%s\n" % USAGE + print NO_REPO_MESSAGE % opts.repo + exit(-1) + + if not opts.action: + print "%s\n" % USAGE + print "Error: An action is required." + exit(-1) getattr(core.Dotfiles(location=opts.repo, prefix=opts.prefix, ignore=opts.ignore, - externals=opts.externals), opts.action)(files=args) + externals=opts.externals, + force=opts.force), opts.action)(files=args) diff --git a/dotfiles/core.py b/dotfiles/core.py index b04892f..c2e368a 100644 --- a/dotfiles/core.py +++ b/dotfiles/core.py @@ -1,9 +1,21 @@ # -*- coding: utf-8 -*- +""" +dotfiles.core +~~~~~~~~~~~~~ + +This module provides the basic functionality of dotfiles. +""" + import os import shutil +__version__ = '0.3.0' +__author__ = "Jon Bernard" +__license__ = "GPL" + + class Dotfile(object): def __init__(self, name, target): @@ -17,11 +29,17 @@ class Dotfile(object): if not os.path.lexists(self.name): self.status = 'missing' elif os.path.realpath(self.name) != self.target: - self.status = 'unmanged' + self.status = 'unmanaged' - def sync(self): + def sync(self, force): if self.status == 'missing': os.symlink(self.target, self.name) + elif self.status == 'unmanaged': + if not force: + print "Skipping \"%s\", use --force to override" % self.basename + return + os.remove(self.name) + os.symlink(self.target, self.name) def add(self): if self.status == 'missing': @@ -30,7 +48,6 @@ class Dotfile(object): if self.status == '': print "Skipping \"%s\", already managed" % self.basename return - print "Adding \"%s\"" % self.basename shutil.move(self.name, self.target) os.symlink(self.target, self.name) @@ -47,20 +64,20 @@ class Dotfile(object): class Dotfiles(object): - def __init__(self, location, prefix, ignore, externals): + def __init__(self, location, prefix, ignore, externals, force): self.location = location + self.prefix = prefix + self.force = force self.dotfiles = [] - contents = [x for x in os.listdir(self.location) - if x not in ignore] + contents = [x for x in os.listdir(self.location) if x not in ignore] for file in contents: - self.dotfiles.append(Dotfile(file, + self.dotfiles.append(Dotfile(file[len(prefix):], os.path.join(self.location, file))) for file in externals.keys(): self.dotfiles.append(Dotfile(file, externals[file])) def list(self, **kwargs): - for dotfile in sorted(self.dotfiles, - key=lambda dotfile: dotfile.name): + for dotfile in sorted(self.dotfiles, key=lambda dotfile: dotfile.name): if dotfile.status or kwargs.get('verbose', True): print dotfile @@ -69,14 +86,14 @@ class Dotfiles(object): def sync(self, **kwargs): for dotfile in self.dotfiles: - dotfile.sync() + dotfile.sync(self.force) def add(self, **kwargs): for file in kwargs.get('files', None): if os.path.basename(file).startswith('.'): Dotfile(file, os.path.join(self.location, - os.path.basename(file).strip('.'))).add() + self.prefix + os.path.basename(file).strip('.'))).add() else: print "Skipping \"%s\", not a dotfile" % file @@ -85,6 +102,6 @@ class Dotfiles(object): if os.path.basename(file).startswith('.'): Dotfile(file, os.path.join(self.location, - os.path.basename(file).strip('.'))).remove() + self.prefix + os.path.basename(file).strip('.'))).remove() else: print "Skipping \"%s\", not a dotfile" % file @@ -1,8 +1,16 @@ -from distutils.core import setup -import dotfiles +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +from dotfiles.core import __version__ + setup(name='dotfiles', - version=dotfiles.__version__, + version=__version__, description='Easily manage your dotfiles', long_description=open('README.rst').read(), author='Jon Bernard', |