Merge remote-tracking branch 'upstream/master' into HEAD
This commit is contained in:
commit
07c17833f5
13 changed files with 213 additions and 59 deletions
21
README.md
21
README.md
|
@ -8,6 +8,7 @@ Dotbot makes installing your dotfiles as easy as `git clone $url && cd dotfiles
|
|||
- [Configuration](#configuration)
|
||||
- [Directives](#directives) ([Link](#link), [Create](#create), [Shell](#shell), [Clean](#clean), [Defaults](#defaults))
|
||||
- [Plugins](#plugins)
|
||||
- [Command-line Arguments](#command-line-arguments)
|
||||
- [Wiki][wiki]
|
||||
|
||||
---
|
||||
|
@ -380,8 +381,24 @@ plugins:
|
|||
|
||||
```
|
||||
|
||||
Wiki
|
||||
----
|
||||
## Command-line Arguments
|
||||
|
||||
Dotbot takes a number of command-line arguments; you can run Dotbot with
|
||||
`--help`, e.g. by running `./install --help`, to see the full list of options.
|
||||
Here, we highlight a couple that are particularly interesting.
|
||||
|
||||
### `--only`
|
||||
|
||||
You can call `./install --only [list of directives]`, such as `./install --only
|
||||
link`, and Dotbot will only run those sections of the config file.
|
||||
|
||||
### `--except`
|
||||
|
||||
You can call `./install --except [list of directives]`, such as `./install
|
||||
--except shell`, and Dotbot will run all the sections of the config file except
|
||||
the ones listed.
|
||||
|
||||
## Wiki
|
||||
|
||||
Check out the [Dotbot wiki][wiki] for more information, tips and tricks,
|
||||
user-contributed plugins, and more.
|
||||
|
|
|
@ -28,6 +28,10 @@ def add_options(parser):
|
|||
action='store_true', help='disable built-in plugins')
|
||||
parser.add_argument('--plugin-dir', action='append', dest='plugin_dirs', default=[],
|
||||
metavar='PLUGIN_DIR', help='load all plugins in PLUGIN_DIR')
|
||||
parser.add_argument('--only', nargs='+',
|
||||
help='only run specified directives', metavar='DIRECTIVE')
|
||||
parser.add_argument('--except', nargs='+', dest='skip',
|
||||
help='skip specified directives', metavar='DIRECTIVE')
|
||||
parser.add_argument('--no-color', dest='no_color', action='store_true',
|
||||
help='disable color output')
|
||||
parser.add_argument('--version', action='store_true',
|
||||
|
@ -78,7 +82,7 @@ def main():
|
|||
# default to directory of config file
|
||||
base_directory = os.path.dirname(os.path.abspath(options.config_file))
|
||||
os.chdir(base_directory)
|
||||
dispatcher = Dispatcher(base_directory)
|
||||
dispatcher = Dispatcher(base_directory, only=options.only, skip=options.skip)
|
||||
success = dispatcher.dispatch(tasks)
|
||||
if success:
|
||||
log.info('\n==> All tasks executed successfully')
|
||||
|
|
|
@ -4,10 +4,12 @@ from .messenger import Messenger
|
|||
from .context import Context
|
||||
|
||||
class Dispatcher(object):
|
||||
def __init__(self, base_directory):
|
||||
def __init__(self, base_directory, only=None, skip=None):
|
||||
self._log = Messenger()
|
||||
self._setup_context(base_directory)
|
||||
self._load_plugins()
|
||||
self._only = only
|
||||
self._skip = skip
|
||||
|
||||
def _setup_context(self, base_directory):
|
||||
path = os.path.abspath(
|
||||
|
@ -20,6 +22,10 @@ class Dispatcher(object):
|
|||
success = True
|
||||
for task in tasks:
|
||||
for action in task:
|
||||
if self._only is not None and action not in self._only \
|
||||
or self._skip is not None and action in self._skip:
|
||||
self._log.info('Skipping action %s' % action)
|
||||
continue
|
||||
handled = False
|
||||
if action == 'defaults':
|
||||
self._context.set_defaults(task[action]) # replace, not update
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import os, dotbot
|
||||
import os
|
||||
import dotbot
|
||||
|
||||
|
||||
class Clean(dotbot.Plugin):
|
||||
'''
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import os
|
||||
import glob
|
||||
import shutil
|
||||
import dotbot
|
||||
import subprocess
|
||||
|
||||
|
||||
class Create(dotbot.Plugin):
|
||||
|
|
|
@ -2,6 +2,7 @@ import os
|
|||
import glob
|
||||
import shutil
|
||||
import dotbot
|
||||
import dotbot.util
|
||||
import subprocess
|
||||
|
||||
|
||||
|
@ -105,14 +106,7 @@ class Link(dotbot.Plugin):
|
|||
return success
|
||||
|
||||
def _test_success(self, command):
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
ret = subprocess.call(
|
||||
command,
|
||||
shell=True,
|
||||
stdout=devnull,
|
||||
stderr=devnull,
|
||||
executable=os.environ.get('SHELL'),
|
||||
)
|
||||
ret = dotbot.util.shell_command(command, cwd=self._context.base_directory())
|
||||
if ret != 0:
|
||||
self._log.debug('Test \'%s\' returned false' % command)
|
||||
return ret == 0
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import os, subprocess, dotbot
|
||||
import os
|
||||
import subprocess
|
||||
import dotbot
|
||||
import dotbot.util
|
||||
|
||||
|
||||
class Shell(dotbot.Plugin):
|
||||
'''
|
||||
|
@ -19,48 +23,40 @@ class Shell(dotbot.Plugin):
|
|||
def _process_commands(self, data):
|
||||
success = True
|
||||
defaults = self._context.defaults().get('shell', {})
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
for item in data:
|
||||
stdin = stdout = stderr = devnull
|
||||
quiet = False
|
||||
if defaults.get('stdin', False) == True:
|
||||
stdin = None
|
||||
if defaults.get('stdout', False) == True:
|
||||
stdout = None
|
||||
if defaults.get('stderr', False) == True:
|
||||
stderr = None
|
||||
if defaults.get('quiet', False) == True:
|
||||
quiet = True
|
||||
if isinstance(item, dict):
|
||||
cmd = item['command']
|
||||
msg = item.get('description', None)
|
||||
if 'stdin' in item:
|
||||
stdin = None if item['stdin'] == True else devnull
|
||||
if 'stdout' in item:
|
||||
stdout = None if item['stdout'] == True else devnull
|
||||
if 'stderr' in item:
|
||||
stderr = None if item['stderr'] == True else devnull
|
||||
if 'quiet' in item:
|
||||
quiet = True if item['quiet'] == True else False
|
||||
elif isinstance(item, list):
|
||||
cmd = item[0]
|
||||
msg = item[1] if len(item) > 1 else None
|
||||
else:
|
||||
cmd = item
|
||||
msg = None
|
||||
if msg is None:
|
||||
self._log.lowinfo(cmd)
|
||||
elif quiet:
|
||||
self._log.lowinfo('%s' % msg)
|
||||
else:
|
||||
self._log.lowinfo('%s [%s]' % (msg, cmd))
|
||||
executable = os.environ.get('SHELL')
|
||||
ret = subprocess.call(cmd, shell=True, stdin=stdin, stdout=stdout,
|
||||
stderr=stderr, cwd=self._context.base_directory(),
|
||||
executable=executable)
|
||||
if ret != 0:
|
||||
success = False
|
||||
self._log.warning('Command [%s] failed' % cmd)
|
||||
for item in data:
|
||||
stdin = defaults.get('stdin', False)
|
||||
stdout = defaults.get('stdout', False)
|
||||
stderr = defaults.get('stderr', False)
|
||||
quiet = defaults.get('quiet', False)
|
||||
if isinstance(item, dict):
|
||||
cmd = item['command']
|
||||
msg = item.get('description', None)
|
||||
stdin = item.get('stdin', stdin)
|
||||
stdout = item.get('stdout', stdout)
|
||||
stderr = item.get('stderr', stderr)
|
||||
quiet = item.get('quiet', quiet)
|
||||
elif isinstance(item, list):
|
||||
cmd = item[0]
|
||||
msg = item[1] if len(item) > 1 else None
|
||||
else:
|
||||
cmd = item
|
||||
msg = None
|
||||
if msg is None:
|
||||
self._log.lowinfo(cmd)
|
||||
elif quiet:
|
||||
self._log.lowinfo('%s' % msg)
|
||||
else:
|
||||
self._log.lowinfo('%s [%s]' % (msg, cmd))
|
||||
ret = dotbot.util.shell_command(
|
||||
cmd,
|
||||
cwd=self._context.base_directory(),
|
||||
enable_stdin=stdin,
|
||||
enable_stdout=stdout,
|
||||
enable_stderr=stderr
|
||||
)
|
||||
if ret != 0:
|
||||
success = False
|
||||
self._log.warning('Command [%s] failed' % cmd)
|
||||
if success:
|
||||
self._log.info('All commands have been executed')
|
||||
else:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .common import shell_command
|
34
dotbot/util/common.py
Normal file
34
dotbot/util/common.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import os
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
|
||||
def shell_command(command, cwd=None, enable_stdin=False, enable_stdout=False, enable_stderr=False):
|
||||
with open(os.devnull, 'w') as devnull_w, open(os.devnull, 'r') as devnull_r:
|
||||
stdin = None if enable_stdin else devnull_r
|
||||
stdout = None if enable_stdout else devnull_w
|
||||
stderr = None if enable_stderr else devnull_w
|
||||
executable = os.environ.get('SHELL')
|
||||
if platform.system() == 'Windows':
|
||||
# We avoid setting the executable kwarg on Windows because it does
|
||||
# not have the desired effect when combined with shell=True. It
|
||||
# will result in the correct program being run (e.g. bash), but it
|
||||
# will be invoked with a '/c' argument instead of a '-c' argument,
|
||||
# which it won't understand.
|
||||
#
|
||||
# See https://github.com/anishathalye/dotbot/issues/219 and
|
||||
# https://bugs.python.org/issue40467.
|
||||
#
|
||||
# This means that complex commands that require Bash's parsing
|
||||
# won't work; a workaround for this is to write the command as
|
||||
# `bash -c "..."`.
|
||||
executable = None
|
||||
return subprocess.call(
|
||||
command,
|
||||
shell=True,
|
||||
executable=executable,
|
||||
stdin=stdin,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
cwd=cwd
|
||||
)
|
21
test/tests/except-multi.bash
Normal file
21
test/tests/except-multi.bash
Normal file
|
@ -0,0 +1,21 @@
|
|||
test_description='--except with multiple arguments'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
ln -s ${DOTFILES}/nonexistent ~/bad && touch ${DOTFILES}/y
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot --except clean shell <<EOF
|
||||
- clean: ["~"]
|
||||
- shell:
|
||||
- echo "x" > ~/x
|
||||
- link:
|
||||
~/y: y
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
[ "$(readlink ~/bad | cut -d/ -f4-)" = "dotfiles/nonexistent" ] &&
|
||||
! test -f ~/x && test -f ~/y
|
||||
'
|
31
test/tests/except.bash
Normal file
31
test/tests/except.bash
Normal file
|
@ -0,0 +1,31 @@
|
|||
test_description='--except'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ${DOTFILES}/x
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot --except link <<EOF
|
||||
- shell:
|
||||
- echo "pear" > ~/y
|
||||
- link:
|
||||
~/x: x
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "pear" ~/y && ! test -f ~/x
|
||||
'
|
||||
|
||||
test_expect_success 'run 2' '
|
||||
run_dotbot --except shell <<EOF
|
||||
- shell:
|
||||
- echo "pear" > ~/z
|
||||
- link:
|
||||
~/x: x
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "apple" ~/x && ! test -f ~/z
|
||||
'
|
20
test/tests/only-multi.bash
Normal file
20
test/tests/only-multi.bash
Normal file
|
@ -0,0 +1,20 @@
|
|||
test_description='--only with multiple arguments'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
ln -s ${DOTFILES}/nonexistent ~/bad && touch ${DOTFILES}/y
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot --only clean shell <<EOF
|
||||
- clean: ["~"]
|
||||
- shell:
|
||||
- echo "x" > ~/x
|
||||
- link:
|
||||
~/y: y
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
! test -f ~/bad && grep "x" ~/x && ! test -f ~/y
|
||||
'
|
31
test/tests/only.bash
Normal file
31
test/tests/only.bash
Normal file
|
@ -0,0 +1,31 @@
|
|||
test_description='--only'
|
||||
. '../test-lib.bash'
|
||||
|
||||
test_expect_success 'setup' '
|
||||
echo "apple" > ${DOTFILES}/x
|
||||
'
|
||||
|
||||
test_expect_success 'run' '
|
||||
run_dotbot --only shell <<EOF
|
||||
- shell:
|
||||
- echo "pear" > ~/y
|
||||
- link:
|
||||
~/x: x
|
||||
EOF
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "pear" ~/y && ! test -f ~/x
|
||||
'
|
||||
|
||||
test_expect_success 'run 2' '
|
||||
run_dotbot --only link <<EOF
|
||||
- shell:
|
||||
- echo "pear" > ~/z
|
||||
- link:
|
||||
~/x: x
|
||||
'
|
||||
|
||||
test_expect_success 'test' '
|
||||
grep "apple" ~/x && ! test -f ~/z
|
||||
'
|
Loading…
Reference in a new issue