summaryrefslogtreecommitdiff
path: root/dotbot
diff options
context:
space:
mode:
authorAnish Athalye <aathalye@me.com>2014-03-19 23:07:30 -0400
committerAnish Athalye <aathalye@me.com>2014-03-20 18:57:56 -0400
commit60a560e97699a1d9a4320b8e787a50b1a9a7734d (patch)
tree573ce63c2a49a278af49c0cc6542bfe3b89cf572 /dotbot
Initial commit
Diffstat (limited to 'dotbot')
-rw-r--r--dotbot/__init__.py1
-rw-r--r--dotbot/cli.py46
-rw-r--r--dotbot/config.py19
-rw-r--r--dotbot/dispatcher.py46
-rw-r--r--dotbot/executor/__init__.py3
-rw-r--r--dotbot/executor/commandrunner.py32
-rw-r--r--dotbot/executor/executor.py24
-rw-r--r--dotbot/executor/linker.py73
-rw-r--r--dotbot/messenger/__init__.py2
-rw-r--r--dotbot/messenger/color.py8
-rw-r--r--dotbot/messenger/level.py7
-rw-r--r--dotbot/messenger/messenger.py60
-rw-r--r--dotbot/util/__init__.py0
-rw-r--r--dotbot/util/singleton.py6
14 files changed, 327 insertions, 0 deletions
diff --git a/dotbot/__init__.py b/dotbot/__init__.py
new file mode 100644
index 0000000..401da57
--- /dev/null
+++ b/dotbot/__init__.py
@@ -0,0 +1 @@
+from .cli import main
diff --git a/dotbot/cli.py b/dotbot/cli.py
new file mode 100644
index 0000000..b9cc528
--- /dev/null
+++ b/dotbot/cli.py
@@ -0,0 +1,46 @@
+from argparse import ArgumentParser
+from .config import ConfigReader, ReadingError
+from .dispatcher import Dispatcher, DispatchError
+from .messenger import Messenger
+from .messenger import Level
+
+def add_options(parser):
+ parser.add_argument('-Q', '--super-quiet', dest = 'super_quiet', action = 'store_true',
+ help = 'suppress almost all output')
+ parser.add_argument('-q', '--quiet', dest = 'quiet', action = 'store_true',
+ help = 'suppress most output')
+ parser.add_argument('-v', '--verbose', dest = 'verbose', action = 'store_true',
+ help = 'enable verbose output')
+ parser.add_argument('-d', '--base-directory', nargs = 1,
+ dest = 'base_directory', help = 'execute commands from within BASEDIR',
+ metavar = 'BASEDIR', required = True)
+ parser.add_argument('-c', '--config-file', nargs = 1, dest = 'config_file',
+ help = 'run commands given in CONFIGFILE', metavar = 'CONFIGFILE',
+ required = True)
+
+def read_config(config_file):
+ reader = ConfigReader(config_file)
+ return reader.get_config()
+
+def main():
+ log = Messenger()
+ try:
+ parser = ArgumentParser()
+ add_options(parser)
+ options = parser.parse_args()
+ if (options.super_quiet):
+ log.set_level(Level.WARNING)
+ if (options.quiet):
+ log.set_level(Level.INFO)
+ if (options.verbose):
+ log.set_level(Level.DEBUG)
+ tasks = read_config(options.config_file[0])
+ dispatcher = Dispatcher(options.base_directory[0])
+ success = dispatcher.dispatch(tasks)
+ if success:
+ log.info('\n==> All tasks executed successfully')
+ else:
+ raise DispatchError('\n==> Some tasks were not executed successfully')
+ except (ReadingError, DispatchError) as e:
+ log.error('%s' % e)
+ exit(1)
diff --git a/dotbot/config.py b/dotbot/config.py
new file mode 100644
index 0000000..79b735c
--- /dev/null
+++ b/dotbot/config.py
@@ -0,0 +1,19 @@
+import json
+
+class ConfigReader(object):
+ def __init__(self, config_file_path):
+ self._config = self._read(config_file_path)
+
+ def _read(self, config_file_path):
+ try:
+ with open(config_file_path) as fin:
+ data = json.load(fin)
+ return data
+ except Exception:
+ raise ReadingError('Could not read config file')
+
+ def get_config(self):
+ return self._config
+
+class ReadingError(Exception):
+ pass
diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py
new file mode 100644
index 0000000..35b0889
--- /dev/null
+++ b/dotbot/dispatcher.py
@@ -0,0 +1,46 @@
+import os
+from .executor import *
+from .messenger import Messenger
+
+class Dispatcher(object):
+ PLUGIN_CLASS = Executor
+ PLUGIN_DIR = 'dotbot/executor'
+
+ def __init__(self, base_directory):
+ self._log = Messenger()
+ self._set_base_directory(base_directory)
+ self._load_plugins()
+
+ def _set_base_directory(self, base_directory):
+ path = os.path.abspath(os.path.realpath(
+ os.path.expanduser(base_directory)))
+ if os.path.exists(path):
+ self._base_directory = path
+ else:
+ raise DispatchError('Nonexistant base directory')
+
+ def dispatch(self, tasks):
+ success = True
+ for task in tasks:
+ for action in task:
+ handled = False
+ for plugin in self._plugins:
+ if plugin.can_handle(action):
+ try:
+ success &= plugin.handle(action, task[action])
+ handled = True
+ except Exception:
+ self._log.error(
+ 'An error was encountered while executing action %s' %
+ action)
+ if not handled:
+ success = False
+ self._log.error('Action %s not handled' % action)
+ return success
+
+ def _load_plugins(self):
+ self._plugins = [plugin(self._base_directory)
+ for plugin in Executor.__subclasses__()]
+
+class DispatchError(Exception):
+ pass
diff --git a/dotbot/executor/__init__.py b/dotbot/executor/__init__.py
new file mode 100644
index 0000000..1762f78
--- /dev/null
+++ b/dotbot/executor/__init__.py
@@ -0,0 +1,3 @@
+from .executor import Executor
+from .linker import Linker
+from .commandrunner import CommandRunner
diff --git a/dotbot/executor/commandrunner.py b/dotbot/executor/commandrunner.py
new file mode 100644
index 0000000..3051ace
--- /dev/null
+++ b/dotbot/executor/commandrunner.py
@@ -0,0 +1,32 @@
+import os, subprocess
+from . import Executor
+
+class CommandRunner(Executor):
+ '''
+ Run arbitrary shell commands.
+ '''
+
+ def can_handle(self, directive):
+ return directive == 'shell'
+
+ def handle(self, directive, data):
+ if directive != 'shell':
+ raise ValueError('CommandRunner cannot handle directive %s' %
+ directive)
+ return self._process_commands(data)
+
+ def _process_commands(self, data):
+ success = True
+ with open(os.devnull, 'w') as devnull:
+ for cmd, msg in data:
+ self._log.lowinfo('%s [%s]' % (msg, cmd))
+ ret = subprocess.call(cmd, shell = True, stdout = devnull,
+ stderr = devnull, cwd = self._base_directory)
+ if ret != 0:
+ success = False
+ self._log.warning('Command [%s] failed' % cmd)
+ if success:
+ self._log.info('All commands have been executed')
+ else:
+ self._log.error('Some commands were not sucessfully executed')
+ return success
diff --git a/dotbot/executor/executor.py b/dotbot/executor/executor.py
new file mode 100644
index 0000000..e6862c0
--- /dev/null
+++ b/dotbot/executor/executor.py
@@ -0,0 +1,24 @@
+from ..messenger import Messenger
+
+class Executor(object):
+ '''
+ Abstract base class for commands that process directives.
+ '''
+
+ def __init__(self, base_directory):
+ self._base_directory = base_directory
+ self._log = Messenger()
+
+ def can_handle(self, directive):
+ '''
+ Returns true if the Executor can handle the directive.
+ '''
+ raise NotImplementedError
+
+ def handle(self, directive, data):
+ '''
+ Executes the directive.
+
+ Returns true if the Executor successfully handled the directive.
+ '''
+ raise NotImplementedError
diff --git a/dotbot/executor/linker.py b/dotbot/executor/linker.py
new file mode 100644
index 0000000..9890ed5
--- /dev/null
+++ b/dotbot/executor/linker.py
@@ -0,0 +1,73 @@
+import os
+from . import Executor
+
+class Linker(Executor):
+ '''
+ Symbolically links dotfiles.
+ '''
+
+ def can_handle(self, directive):
+ return directive == 'link'
+
+ def handle(self, directive, data):
+ if directive != 'link':
+ raise ValueError('Linker cannot handle directive %s' % directive)
+ return self._process_links(data)
+
+ def _process_links(self, links):
+ success = True
+ for destination, source in links.items():
+ success &= self._link(source, destination)
+ if success:
+ self._log.info('All links have been set up')
+ else:
+ self._log.error('Some links were not successfully set up')
+ return success
+
+ def _is_link(self, path):
+ '''
+ Returns true if the path is a symbolic link.
+ '''
+ return os.path.islink(os.path.expanduser(path))
+
+ def _link_destination(self, path):
+ '''
+ Returns the absolute path to the destination of the symbolic link.
+ '''
+ path = os.path.expanduser(path)
+ rel_dest = os.readlink(path)
+ return os.path.join(os.path.dirname(path), rel_dest)
+
+ def _exists(self, path):
+ '''
+ Returns true if the path exists.
+ '''
+ path = os.path.expanduser(path)
+ return os.path.exists(path)
+
+ def _link(self, source, link_name):
+ '''
+ Links link_name to source.
+
+ Returns true if successfully linked files.
+ '''
+ success = False
+ source = os.path.join(self._base_directory, source)
+ if not self._exists(link_name) and self._is_link(link_name):
+ self._log.warning('Invalid link %s -> %s' %
+ (link_name, self._link_destination(link_name)))
+ elif not self._exists(link_name):
+ self._log.lowinfo('Creating link %s -> %s' % (link_name, source))
+ os.symlink(source, os.path.expanduser(link_name))
+ success = True
+ elif self._exists(link_name) and not self._is_link(link_name):
+ self._log.warning(
+ '%s already exists but is a regular file or directory' %
+ link_name)
+ elif self._link_destination(link_name) != source:
+ self._log.warning('Incorrect link %s -> %s' %
+ (link_name, self._link_destination(link_name)))
+ else:
+ self._log.lowinfo('Link exists %s -> %s' % (link_name, source))
+ success = True
+ return success
diff --git a/dotbot/messenger/__init__.py b/dotbot/messenger/__init__.py
new file mode 100644
index 0000000..38fc6bc
--- /dev/null
+++ b/dotbot/messenger/__init__.py
@@ -0,0 +1,2 @@
+from .messenger import Messenger
+from .level import Level
diff --git a/dotbot/messenger/color.py b/dotbot/messenger/color.py
new file mode 100644
index 0000000..193afb7
--- /dev/null
+++ b/dotbot/messenger/color.py
@@ -0,0 +1,8 @@
+class Color(object):
+ NONE = ''
+ RESET = '\033[0m'
+ RED = '\033[91m'
+ GREEN = '\033[92m'
+ YELLOW = '\033[93m'
+ BLUE = '\033[94m'
+ MAGENTA = '\033[95m'
diff --git a/dotbot/messenger/level.py b/dotbot/messenger/level.py
new file mode 100644
index 0000000..2c361f6
--- /dev/null
+++ b/dotbot/messenger/level.py
@@ -0,0 +1,7 @@
+class Level(object):
+ NOTSET = 0
+ DEBUG = 10
+ LOWINFO = 15
+ INFO = 20
+ WARNING = 30
+ ERROR = 40
diff --git a/dotbot/messenger/messenger.py b/dotbot/messenger/messenger.py
new file mode 100644
index 0000000..04081e7
--- /dev/null
+++ b/dotbot/messenger/messenger.py
@@ -0,0 +1,60 @@
+import sys
+from ..util.singleton import Singleton
+from .color import Color
+from .level import Level
+
+class Messenger(object):
+ __metaclass__ = Singleton
+
+ def __init__(self, level = Level.LOWINFO):
+ self.set_level(level)
+
+ def set_level(self, level):
+ self._level = level
+
+ def log(self, level, message):
+ if (level >= self._level):
+ print '%s%s%s' % (self._color(level), message, self._reset())
+
+ def debug(self, message):
+ self.log(Level.DEBUG, message)
+
+ def lowinfo(self, message):
+ self.log(Level.LOWINFO, message)
+
+ def info(self, message):
+ self.log(Level.INFO, message)
+
+ def warning(self, message):
+ self.log(Level.WARNING, message)
+
+ def error(self, message):
+ self.log(Level.ERROR, message)
+
+ def _color(self, level):
+ '''
+ Get a color (terminal escape sequence) according to a level.
+ '''
+ if not sys.stdout.isatty():
+ return ''
+ elif level < Level.DEBUG:
+ return ''
+ elif Level.DEBUG <= level < Level.LOWINFO:
+ return Color.YELLOW
+ elif Level.LOWINFO <= level < Level.INFO:
+ return Color.BLUE
+ elif Level.INFO <= level < Level.WARNING:
+ return Color.GREEN
+ elif Level.WARNING <= level < Level.ERROR:
+ return Color.MAGENTA
+ elif Level.ERROR <= level:
+ return Color.RED
+
+ def _reset(self):
+ '''
+ Get a reset color (terminal escape sequence).
+ '''
+ if not sys.stdout.isatty():
+ return ''
+ else:
+ return Color.RESET
diff --git a/dotbot/util/__init__.py b/dotbot/util/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dotbot/util/__init__.py
diff --git a/dotbot/util/singleton.py b/dotbot/util/singleton.py
new file mode 100644
index 0000000..d6cc857
--- /dev/null
+++ b/dotbot/util/singleton.py
@@ -0,0 +1,6 @@
+class Singleton(type):
+ _instances = {}
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
+ return cls._instances[cls]