Add default options
This feature was implemented with feedback from Aleks Kamko <aykamko@gmail.com> and Casey Rodarmor <casey@rodarmor.com>.
This commit is contained in:
parent
daf8d82e02
commit
f52bbd1eec
8 changed files with 126 additions and 26 deletions
35
README.md
35
README.md
|
@ -78,6 +78,10 @@ Here's an example of a complete configuration.
|
||||||
The conventional name for the configuration file is `install.conf.yaml`.
|
The conventional name for the configuration file is `install.conf.yaml`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
- defaults:
|
||||||
|
link:
|
||||||
|
relink: true
|
||||||
|
|
||||||
- clean: ['~']
|
- clean: ['~']
|
||||||
|
|
||||||
- link:
|
- link:
|
||||||
|
@ -97,6 +101,13 @@ The conventional name for this file is `install.conf.json`.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"defaults": {
|
||||||
|
"link": {
|
||||||
|
"relink": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"clean": ["~"]
|
"clean": ["~"]
|
||||||
},
|
},
|
||||||
|
@ -225,6 +236,30 @@ Clean commands are specified as an array of directories to be cleaned.
|
||||||
- clean: ['~']
|
- clean: ['~']
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Defaults
|
||||||
|
|
||||||
|
Default options for plugins can be specified so that options don't have to be
|
||||||
|
repeated many times. This can be very useful to use with the link command, for
|
||||||
|
example.
|
||||||
|
|
||||||
|
Defaults apply to all commands that follow setting the defaults. Defaults can
|
||||||
|
be set multiple times; each change replaces the defaults with a new set of
|
||||||
|
options.
|
||||||
|
|
||||||
|
#### Format
|
||||||
|
|
||||||
|
Defaults are specified as a dictionary mapping action names to settings, which
|
||||||
|
are dictionaries from option names to values.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- defaults:
|
||||||
|
link:
|
||||||
|
create: true
|
||||||
|
relink: true
|
||||||
|
```
|
||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
Dotbot also supports custom directives implemented by plugins. Plugins are
|
Dotbot also supports custom directives implemented by plugins. Plugins are
|
||||||
|
|
23
dotbot/context.py
Normal file
23
dotbot/context.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import copy
|
||||||
|
|
||||||
|
class Context(object):
|
||||||
|
'''
|
||||||
|
Contextual data and information for plugins.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, base_directory):
|
||||||
|
self._base_directory = base_directory
|
||||||
|
self._defaults = {}
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_base_directory(self, base_directory):
|
||||||
|
self._base_directory = base_directory
|
||||||
|
|
||||||
|
def base_directory(self):
|
||||||
|
return self._base_directory
|
||||||
|
|
||||||
|
def set_defaults(self, defaults):
|
||||||
|
self._defaults = defaults
|
||||||
|
|
||||||
|
def defaults(self):
|
||||||
|
return copy.deepcopy(self._defaults)
|
|
@ -1,26 +1,30 @@
|
||||||
import os
|
import os
|
||||||
from .plugin import Plugin
|
from .plugin import Plugin
|
||||||
from .messenger import Messenger
|
from .messenger import Messenger
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
class Dispatcher(object):
|
class Dispatcher(object):
|
||||||
def __init__(self, base_directory):
|
def __init__(self, base_directory):
|
||||||
self._log = Messenger()
|
self._log = Messenger()
|
||||||
self._set_base_directory(base_directory)
|
self._setup_context(base_directory)
|
||||||
self._load_plugins()
|
self._load_plugins()
|
||||||
|
|
||||||
def _set_base_directory(self, base_directory):
|
def _setup_context(self, base_directory):
|
||||||
path = os.path.abspath(os.path.realpath(
|
path = os.path.abspath(os.path.realpath(
|
||||||
os.path.expanduser(base_directory)))
|
os.path.expanduser(base_directory)))
|
||||||
if os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
self._base_directory = path
|
|
||||||
else:
|
|
||||||
raise DispatchError('Nonexistent base directory')
|
raise DispatchError('Nonexistent base directory')
|
||||||
|
self._context = Context(path)
|
||||||
|
|
||||||
def dispatch(self, tasks):
|
def dispatch(self, tasks):
|
||||||
success = True
|
success = True
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
for action in task:
|
for action in task:
|
||||||
handled = False
|
handled = False
|
||||||
|
if action == 'defaults':
|
||||||
|
self._context.set_defaults(task[action]) # replace, not update
|
||||||
|
handled = True
|
||||||
|
# keep going, let other plugins handle this if they want
|
||||||
for plugin in self._plugins:
|
for plugin in self._plugins:
|
||||||
if plugin.can_handle(action):
|
if plugin.can_handle(action):
|
||||||
try:
|
try:
|
||||||
|
@ -36,7 +40,7 @@ class Dispatcher(object):
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def _load_plugins(self):
|
def _load_plugins(self):
|
||||||
self._plugins = [plugin(self._base_directory)
|
self._plugins = [plugin(self._context)
|
||||||
for plugin in Plugin.__subclasses__()]
|
for plugin in Plugin.__subclasses__()]
|
||||||
|
|
||||||
class DispatchError(Exception):
|
class DispatchError(Exception):
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
from .messenger import Messenger
|
from .messenger import Messenger
|
||||||
|
from .context import Context
|
||||||
|
|
||||||
class Plugin(object):
|
class Plugin(object):
|
||||||
'''
|
'''
|
||||||
Abstract base class for commands that process directives.
|
Abstract base class for commands that process directives.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, base_directory):
|
def __init__(self, context):
|
||||||
self._base_directory = base_directory
|
self._context = context
|
||||||
self._log = Messenger()
|
self._log = Messenger()
|
||||||
|
|
||||||
def can_handle(self, directive):
|
def can_handle(self, directive):
|
||||||
|
|
|
@ -36,7 +36,7 @@ class Clean(dotbot.Plugin):
|
||||||
for item in os.listdir(os.path.expanduser(target)):
|
for item in os.listdir(os.path.expanduser(target)):
|
||||||
path = os.path.join(os.path.expanduser(target), item)
|
path = os.path.join(os.path.expanduser(target), item)
|
||||||
if not os.path.exists(path) and os.path.islink(path):
|
if not os.path.exists(path) and os.path.islink(path):
|
||||||
if self._in_directory(path, self._base_directory):
|
if self._in_directory(path, self._context.base_directory()):
|
||||||
self._log.lowinfo('Removing invalid link %s -> %s' %
|
self._log.lowinfo('Removing invalid link %s -> %s' %
|
||||||
(path, os.path.join(os.path.dirname(path), os.readlink(path))))
|
(path, os.path.join(os.path.dirname(path), os.readlink(path))))
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
|
|
|
@ -17,25 +17,27 @@ class Link(dotbot.Plugin):
|
||||||
|
|
||||||
def _process_links(self, links):
|
def _process_links(self, links):
|
||||||
success = True
|
success = True
|
||||||
|
defaults = self._context.defaults().get('link', {})
|
||||||
for destination, source in links.items():
|
for destination, source in links.items():
|
||||||
source = os.path.expandvars(source)
|
source = os.path.expandvars(source)
|
||||||
destination = os.path.expandvars(destination)
|
destination = os.path.expandvars(destination)
|
||||||
|
relative = defaults.get('relative', False)
|
||||||
|
force = defaults.get('force', False)
|
||||||
|
relink = defaults.get('relink', False)
|
||||||
|
create = defaults.get('create', False)
|
||||||
if isinstance(source, dict):
|
if isinstance(source, dict):
|
||||||
# extended config
|
# extended config
|
||||||
|
relative = source.get('relative', relative)
|
||||||
|
force = source.get('force', force)
|
||||||
|
relink = source.get('relink', relink)
|
||||||
|
create = source.get('create', create)
|
||||||
path = source['path']
|
path = source['path']
|
||||||
relative = source.get('relative', False)
|
|
||||||
force = source.get('force', False)
|
|
||||||
relink = source.get('relink', False)
|
|
||||||
create = source.get('create', False)
|
|
||||||
if create:
|
|
||||||
success &= self._create(destination)
|
|
||||||
if force:
|
|
||||||
success &= self._delete(path, destination, force=True)
|
|
||||||
elif relink:
|
|
||||||
success &= self._delete(path, destination, force=False)
|
|
||||||
else:
|
else:
|
||||||
relative = False
|
|
||||||
path = source
|
path = source
|
||||||
|
if create:
|
||||||
|
success &= self._create(destination)
|
||||||
|
if force or relink:
|
||||||
|
success &= self._delete(path, destination, force=force)
|
||||||
success &= self._link(path, destination, relative)
|
success &= self._link(path, destination, relative)
|
||||||
if success:
|
if success:
|
||||||
self._log.info('All links have been set up')
|
self._log.info('All links have been set up')
|
||||||
|
@ -79,7 +81,7 @@ class Link(dotbot.Plugin):
|
||||||
|
|
||||||
def _delete(self, source, path, force):
|
def _delete(self, source, path, force):
|
||||||
success = True
|
success = True
|
||||||
source = os.path.join(self._base_directory, source)
|
source = os.path.join(self._context.base_directory(), source)
|
||||||
if ((self._is_link(path) and self._link_destination(path) != source) or
|
if ((self._is_link(path) and self._link_destination(path) != source) or
|
||||||
(self._exists(path) and not self._is_link(path))):
|
(self._exists(path) and not self._is_link(path))):
|
||||||
fullpath = os.path.expanduser(path)
|
fullpath = os.path.expanduser(path)
|
||||||
|
@ -110,7 +112,7 @@ class Link(dotbot.Plugin):
|
||||||
Returns true if successfully linked files.
|
Returns true if successfully linked files.
|
||||||
'''
|
'''
|
||||||
success = False
|
success = False
|
||||||
source = os.path.join(self._base_directory, source)
|
source = os.path.join(self._context.base_directory(), source)
|
||||||
if (not self._exists(link_name) and self._is_link(link_name) and
|
if (not self._exists(link_name) and self._is_link(link_name) and
|
||||||
self._link_destination(link_name) != source):
|
self._link_destination(link_name) != source):
|
||||||
self._log.warning('Invalid link %s -> %s' %
|
self._log.warning('Invalid link %s -> %s' %
|
||||||
|
|
|
@ -18,17 +18,18 @@ class Shell(dotbot.Plugin):
|
||||||
|
|
||||||
def _process_commands(self, data):
|
def _process_commands(self, data):
|
||||||
success = True
|
success = True
|
||||||
|
defaults = self._context.defaults().get('shell', {})
|
||||||
with open(os.devnull, 'w') as devnull:
|
with open(os.devnull, 'w') as devnull:
|
||||||
for item in data:
|
for item in data:
|
||||||
stdin = stdout = stderr = devnull
|
stdin = stdout = stderr = devnull
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
cmd = item['command']
|
cmd = item['command']
|
||||||
msg = item.get('description', None)
|
msg = item.get('description', None)
|
||||||
if item.get('stdin', False) is True:
|
if item.get('stdin', defaults.get('stdin', False)) is True:
|
||||||
stdin = None
|
stdin = None
|
||||||
if item.get('stdout', False) is True:
|
if item.get('stdout', defaults.get('stdout', False)) is True:
|
||||||
stdout = None
|
stdout = None
|
||||||
if item.get('stderr', False) is True:
|
if item.get('stderr', defaults.get('stderr', False)) is True:
|
||||||
stderr = None
|
stderr = None
|
||||||
elif isinstance(item, list):
|
elif isinstance(item, list):
|
||||||
cmd = item[0]
|
cmd = item[0]
|
||||||
|
@ -41,7 +42,7 @@ class Shell(dotbot.Plugin):
|
||||||
else:
|
else:
|
||||||
self._log.lowinfo('%s [%s]' % (msg, cmd))
|
self._log.lowinfo('%s [%s]' % (msg, cmd))
|
||||||
ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout,
|
ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout,
|
||||||
stderr=stderr, cwd=self._base_directory)
|
stderr=stderr, cwd=self._context.base_directory())
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
success = False
|
success = False
|
||||||
self._log.warning('Command [%s] failed' % cmd)
|
self._log.warning('Command [%s] failed' % cmd)
|
||||||
|
|
34
test/tests/defaults.bash
Normal file
34
test/tests/defaults.bash
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
test_description='defaults setting works'
|
||||||
|
. '../test-lib.bash'
|
||||||
|
|
||||||
|
test_expect_success 'setup' '
|
||||||
|
echo "apple" > ${DOTFILES}/f &&
|
||||||
|
echo "grape" > ~/f &&
|
||||||
|
ln -s ~/f ~/.f
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_failure 'run-fail' '
|
||||||
|
run_dotbot <<EOF
|
||||||
|
- link:
|
||||||
|
~/.f: f
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_failure 'test-fail' '
|
||||||
|
grep "apple" ~/.f
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'run' '
|
||||||
|
run_dotbot <<EOF
|
||||||
|
- defaults:
|
||||||
|
link:
|
||||||
|
relink: true
|
||||||
|
|
||||||
|
- link:
|
||||||
|
~/.f: f
|
||||||
|
EOF
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'test' '
|
||||||
|
grep "apple" ~/.f
|
||||||
|
'
|
Loading…
Reference in a new issue