diff options
33 files changed, 561 insertions, 77 deletions
@@ -1 +1,4 @@ +*.egg-info *.pyc +build/ +dist/ diff --git a/.gitmodules b/.gitmodules index 111c39c..ffb9af9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "lib/pyyaml"] path = lib/pyyaml - url = https://github.com/anishathalye/pyyaml + url = https://github.com/yaml/pyyaml ignore = dirty @@ -1,7 +1,7 @@ The MIT License (MIT) ===================== -**Copyright (c) 2014-2017 Anish Athalye (me@anishathalye.com)** +**Copyright (c) 2014-2018 Anish Athalye (me@anishathalye.com)** Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -74,6 +74,17 @@ submodule; be sure to commit your changes before running `./install`, otherwise the old version of Dotbot will be checked out by the install script. If using a subrepo, run `git fetch && git checkout origin/master` in the Dotbot directory. +If you prefer, you can install Dotbot from [PyPI] and call it as a command-line +program: + +```bash +pip install dotbot +touch install.conf.yaml +``` + +In this case, rather than running `./install`, you can invoke Dotbot with +`dotbot -c <path to configuration file>`. + ### Full Example Here's an example of a complete configuration. @@ -165,17 +176,23 @@ files if necessary. Environment variables in paths are automatically expanded. Link commands are specified as a dictionary mapping targets to source locations. Source locations are specified relative to the base directory (that -is specified when running the installer). Directory names should *not* contain -a trailing "/" character. +is specified when running the installer). If linking directories, *do not* include a trailing slash. Link commands support an (optional) extended configuration. In this type of configuration, instead of specifying source locations directly, targets are -mapped to extended configuration dictionaries. These dictionaries map `path` to -the source path, specify `create` as `true` if the parent directory should be -created if necessary, specify `relink` as `true` if incorrect symbolic links -should be automatically overwritten, specify `force` as `true` if the file or -directory should be forcibly linked, and specify `relative` as `true` if the -symbolic link should have a relative path. +mapped to extended configuration dictionaries. + +Available extended configuration parameters: + +| Link Option | Explanation | +| -- | -- | +| `path` | The target for the symlink, the same as in the shortcut syntax (default:null, automatic (see below)) | +| `create` | When true, create parent directories to the link as needed. (default:false) | +| `relink` | Removes the old target if it's a symlink (default:false) | +| `force` | Force removes the old target, file or folder, and forces a new link (default:false) | +| `relative` | Use a relative path when creating the symlink (default:false, absolute links) | +| `glob` | Treat a `*` character as a wildcard, and perform link operations on all of those matches (default:false) | +| `if` | Execute this in your `$SHELL` and only link if it is successful. | #### Example @@ -207,6 +224,10 @@ the following three config files equivalent: ~/.zshrc: force: true path: zshrc + ~/.config/: + glob: true + path: config/* + relink: true ``` ```yaml @@ -217,6 +238,10 @@ the following three config files equivalent: relink: true ~/.zshrc: force: true + ~/.config/: + glob: true + path: config/* + relink: true ``` ```json @@ -230,6 +255,11 @@ the following three config files equivalent: }, "~/.zshrc": { "force": true + }, + "~/.config/": { + "glob": true, + "path": "config/*", + "relink": true } } } @@ -350,12 +380,22 @@ Contributing Do you have a feature request, bug report, or patch? Great! See [CONTRIBUTING.md][contributing] for information on what you can do about that. +Packaging +--------- + +1. Update version information. + +2. Build the package using ``python setup.py sdist bdist_wheel``. + +3. Sign and upload the package using ``twine upload -s dist/*``. + License ------- -Copyright (c) 2014-2017 Anish Athalye. Released under the MIT License. See +Copyright (c) 2014-2018 Anish Athalye. Released under the MIT License. See [LICENSE.md][license] for details. +[PyPI]: https://pypi.org/project/dotbot/ [init-dotfiles]: https://github.com/Vaelatern/init-dotfiles [dotfiles-template]: https://github.com/anishathalye/dotfiles_template [inspiration]: https://github.com/anishathalye/dotbot/wiki/Users @@ -11,7 +11,7 @@ which python3 >/dev/null 2>&1 && exec python3 "$0" "$@" which python >/dev/null 2>&1 && exec python "$0" "$@" which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" >&2 echo "error: cannot find python" -return 1 +exit 1 ''' # python code diff --git a/dotbot/__init__.py b/dotbot/__init__.py index 1d03464..525e3cc 100644 --- a/dotbot/__init__.py +++ b/dotbot/__init__.py @@ -1,2 +1,4 @@ from .cli import main from .plugin import Plugin + +__version__ = '1.14.1' diff --git a/dotbot/cli.py b/dotbot/cli.py index d77ab42..fdc2a13 100644 --- a/dotbot/cli.py +++ b/dotbot/cli.py @@ -7,25 +7,31 @@ from .messenger import Messenger from .messenger import Level from .util import module +import dotbot +import yaml + def add_options(parser): - parser.add_argument('-Q', '--super-quiet', dest='super_quiet', action='store_true', + parser.add_argument('-Q', '--super-quiet', action='store_true', help='suppress almost all output') - parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', + parser.add_argument('-q', '--quiet', action='store_true', help='suppress most output') - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', + parser.add_argument('-v', '--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) + parser.add_argument('-d', '--base-directory', + help='execute commands from within BASEDIR', + metavar='BASEDIR') + parser.add_argument('-c', '--config-file', + help='run commands given in CONFIGFILE', metavar='CONFIGFILE') parser.add_argument('-p', '--plugin', action='append', dest='plugins', default=[], help='load PLUGIN as a plugin', metavar='PLUGIN') - parser.add_argument('--disable-built-in-plugins', dest='disable_built_in_plugins', + parser.add_argument('--disable-built-in-plugins', 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('--no-color', dest='no_color', action='store_true', + help='disable color output') + parser.add_argument('--version', action='store_true', + help='show program\'s version number and exit') def read_config(config_file): reader = ConfigReader(config_file) @@ -37,15 +43,20 @@ def main(): parser = ArgumentParser() add_options(parser) options = parser.parse_args() + if options.version: + print('Dotbot version %s (yaml: %s)' % (dotbot.__version__, yaml.__version__)) + exit(0) 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) + if options.no_color: + log.use_color(False) plugin_directories = list(options.plugin_dirs) if not options.disable_built_in_plugins: - plugin_directories.append(os.path.join(os.path.dirname(__file__), '..', 'plugins')) + from .plugins import Clean, Link, Shell plugin_paths = [] for directory in plugin_directories: for plugin_path in glob.glob(os.path.join(directory, '*.py')): @@ -55,10 +66,19 @@ def main(): for plugin_path in plugin_paths: abspath = os.path.abspath(plugin_path) module.load(abspath) - tasks = read_config(options.config_file[0]) + if not options.config_file: + log.error('No configuration file specified') + exit(1) + tasks = read_config(options.config_file) if not isinstance(tasks, list): raise ReadingError('Configuration file must be a list of tasks') - dispatcher = Dispatcher(options.base_directory[0]) + if options.base_directory: + base_directory = options.base_directory + else: + # default to directory of config file + base_directory = os.path.dirname(os.path.realpath(options.config_file)) + os.chdir(base_directory) + dispatcher = Dispatcher(base_directory) success = dispatcher.dispatch(tasks) if success: log.info('\n==> All tasks executed successfully') diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index cc07435..d1a4f95 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -30,10 +30,11 @@ class Dispatcher(object): try: success &= plugin.handle(action, task[action]) handled = True - except Exception: + except Exception as err: self._log.error( 'An error was encountered while executing action %s' % action) + self._log.debug(err) if not handled: success = False self._log.error('Action %s not handled' % action) diff --git a/dotbot/messenger/messenger.py b/dotbot/messenger/messenger.py index f87a367..b83e3f2 100644 --- a/dotbot/messenger/messenger.py +++ b/dotbot/messenger/messenger.py @@ -7,10 +7,14 @@ from .level import Level class Messenger(with_metaclass(Singleton, object)): def __init__(self, level = Level.LOWINFO): self.set_level(level) + self.use_color(True) def set_level(self, level): self._level = level + def use_color(self, yesno): + self._use_color = yesno + def log(self, level, message): if (level >= self._level): print('%s%s%s' % (self._color(level), message, self._reset())) @@ -30,11 +34,14 @@ class Messenger(with_metaclass(Singleton, object)): def error(self, message): self.log(Level.ERROR, message) + def _should_use_color(self): + return self._use_color and sys.stdout.isatty() + def _color(self, level): ''' Get a color (terminal escape sequence) according to a level. ''' - if not sys.stdout.isatty(): + if not self._should_use_color(): return '' elif level < Level.DEBUG: return '' @@ -53,7 +60,7 @@ class Messenger(with_metaclass(Singleton, object)): ''' Get a reset color (terminal escape sequence). ''' - if not sys.stdout.isatty(): + if not self._should_use_color(): return '' else: return Color.RESET diff --git a/dotbot/plugins/__init__.py b/dotbot/plugins/__init__.py new file mode 100644 index 0000000..93bd981 --- /dev/null +++ b/dotbot/plugins/__init__.py @@ -0,0 +1,3 @@ +from .clean import Clean +from .link import Link +from .shell import Shell diff --git a/plugins/clean.py b/dotbot/plugins/clean.py index 7e6cba1..22c975e 100644 --- a/plugins/clean.py +++ b/dotbot/plugins/clean.py @@ -34,11 +34,11 @@ class Clean(dotbot.Plugin): Cleans all the broken symbolic links in target if they point to a subdirectory of the base directory or if forced to clean. ''' - if not os.path.isdir(os.path.expanduser(target)): + if not os.path.isdir(os.path.expandvars(os.path.expanduser(target))): self._log.debug('Ignoring nonexistent directory %s' % target) return True - for item in os.listdir(os.path.expanduser(target)): - path = os.path.join(os.path.expanduser(target), item) + for item in os.listdir(os.path.expandvars(os.path.expanduser(target))): + path = os.path.join(os.path.expandvars(os.path.expanduser(target)), item) if not os.path.exists(path) and os.path.islink(path): points_at = os.path.join(os.path.dirname(path), os.readlink(path)) if self._in_directory(path, self._context.base_directory()) or force: diff --git a/plugins/link.py b/dotbot/plugins/link.py index 4b50320..2506237 100644 --- a/plugins/link.py +++ b/dotbot/plugins/link.py @@ -1,6 +1,8 @@ import os +import glob import shutil import dotbot +import subprocess class Link(dotbot.Plugin): @@ -27,32 +29,86 @@ class Link(dotbot.Plugin): force = defaults.get('force', False) relink = defaults.get('relink', False) create = defaults.get('create', False) + use_glob = defaults.get('glob', False) + test = defaults.get('if', None) if isinstance(source, dict): # extended config + test = source.get('if', test) relative = source.get('relative', relative) force = source.get('force', force) relink = source.get('relink', relink) create = source.get('create', create) + use_glob = source.get('glob', use_glob) path = self._default_source(destination, source.get('path')) else: path = self._default_source(destination, source) - path = os.path.expandvars(os.path.expanduser(path)) - if not self._exists(os.path.join(self._context.base_directory(), path)): - success = False - self._log.warning('Nonexistent target %s -> %s' % - (destination, path)) + if test is not None and not self._test_success(test): + self._log.lowinfo('Skipping %s' % destination) continue - if create: - success &= self._create(destination) - if force or relink: - success &= self._delete(path, destination, relative, force) - success &= self._link(path, destination, relative) + path = os.path.expandvars(os.path.expanduser(path)) + if use_glob: + self._log.debug("Globbing with path: " + str(path)) + glob_results = glob.glob(path) + if len(glob_results) is 0: + self._log.warning("Globbing couldn't find anything matching " + str(path)) + success = False + continue + glob_star_loc = path.find('*') + if glob_star_loc is -1 and destination[-1] is '/': + self._log.error("Ambiguous action requested.") + self._log.error("No wildcard in glob, directory use undefined: " + + destination + " -> " + str(glob_results)) + self._log.warning("Did you want to link the directory or into it?") + success = False + continue + elif glob_star_loc is -1 and len(glob_results) is 1: + # perform a normal link operation + if create: + success &= self._create(destination) + if force or relink: + success &= self._delete(path, destination, relative, force) + success &= self._link(path, destination, relative) + else: + self._log.lowinfo("Globs from '" + path + "': " + str(glob_results)) + glob_base = path[:glob_star_loc] + for glob_full_item in glob_results: + glob_item = glob_full_item[len(glob_base):] + glob_link_destination = os.path.join(destination, glob_item) + if create: + success &= self._create(glob_link_destination) + if force or relink: + success &= self._delete(glob_full_item, glob_link_destination, relative, force) + success &= self._link(glob_full_item, glob_link_destination, relative) + else: + if create: + success &= self._create(destination) + if not self._exists(os.path.join(self._context.base_directory(), path)): + success = False + self._log.warning('Nonexistent target %s -> %s' % + (destination, path)) + continue + if force or relink: + success &= self._delete(path, destination, relative, force) + success &= self._link(path, destination, relative) 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 _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'), + ) + if ret != 0: + self._log.debug('Test \'%s\' returned false' % command) + return ret == 0 + def _default_source(self, destination, source): if source is None: basename = os.path.basename(destination) @@ -87,6 +143,7 @@ class Link(dotbot.Plugin): success = True parent = os.path.abspath(os.path.join(os.path.expanduser(path), os.pardir)) if not self._exists(parent): + self._log.debug("Try to create parent: " + str(parent)) try: os.makedirs(parent) except OSError: diff --git a/plugins/shell.py b/dotbot/plugins/shell.py index 06a9a89..06a9a89 100644 --- a/plugins/shell.py +++ b/dotbot/plugins/shell.py diff --git a/lib/pyyaml b/lib/pyyaml -Subproject f30c956c11aa6b5e7827fe5840cc9ed40b938d1 +Subproject 7e026bfee9cc0bddeb1bbca0c4a0bcd826c2bfd diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3c6e79c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..975deb3 --- /dev/null +++ b/setup.py @@ -0,0 +1,87 @@ +from setuptools import setup, find_packages +from codecs import open # For a consistent encoding +from os import path +import re + + +here = path.dirname(__file__) + + +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + + +def read(*names, **kwargs): + with open( + path.join(here, *names), + encoding=kwargs.get("encoding", "utf8") + ) as fp: + return fp.read() + + +def find_version(*file_paths): + version_file = read(*file_paths) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + + +setup( + name='dotbot', + + version=find_version('dotbot', '__init__.py'), + + description='A tool that bootstraps your dotfiles', + long_description=long_description, + long_description_content_type='text/markdown', + + url='https://github.com/anishathalye/dotbot', + + author='Anish Athalye', + author_email='me@anishathalye.com', + + license='MIT', + + classifiers=[ + 'Development Status :: 5 - Production/Stable', + + 'Intended Audience :: Developers', + + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + + 'Topic :: Utilities', + ], + + keywords='dotfiles', + + packages=find_packages(), + + setup_requires=[ + 'setuptools>=38.6.0', + 'wheel>=0.31.0', + ], + + install_requires=[ + 'PyYAML>=3.12,<4', + ], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={ + 'console_scripts': [ + 'dotbot=dotbot:main', + ], + }, +) diff --git a/test/README.md b/test/README.md index c4abddc..993a753 100644 --- a/test/README.md +++ b/test/README.md @@ -1,12 +1,37 @@ Testing ======= -Dotbot testing code uses [Vagrant][vagrant] to run all tests inside a virtual -machine to have tests be completely isolated from the host machine. The test -driver relies on the [Sahara][sahara] plugin to snapshot and roll back virtual -machine state. The tests are deterministic, and each test is run in a virtual -machine with fresh state, ensuring that tests that modify system state are -easily repeatable. +Dotbot testing code uses [Vagrant] to run all tests inside a virtual machine to +have tests be completely isolated from the host machine. + +Installing the Test environnement +--------------------------------- + +### Debian-based distributions + +- Install the test requirements + +```bash +sudo apt install vagrant virtualbox +``` + +- Install Dotbot dependencies + +```bash +git submodule update --init --recursive +``` + +### macOS + +- Install the test requirements + - [VirtualBox] + - [Vagrant] + +- Install Dotbot dependencies + +```bash +git submodule update --init --recursive +``` Running the Tests ----------------- @@ -23,5 +48,5 @@ Tests can be run with a specific Python version by running `./test --version When finished with testing, it is good to shut down the virtual machine by running `vagrant halt`. -[vagrant]: https://www.vagrantup.com/ -[sahara]: https://github.com/jedi4ever/sahara +[VirtualBox]: https://www.virtualbox.org/wiki/Downloads +[Vagrant]: https://www.vagrantup.com/ diff --git a/test/Vagrantfile b/test/Vagrantfile index 6d3feb0..05d6747 100644 --- a/test/Vagrantfile +++ b/test/Vagrantfile @@ -1,9 +1,8 @@ Vagrant.configure(2) do |config| - config.vm.box = 'debian/jessie64' + config.vm.box = 'debian/stretch64' # sync by copying for isolation - config.vm.synced_folder "..", "/dotbot", type: "rsync", - rsync__exclude: ".git/" + config.vm.synced_folder "..", "/dotbot", type: "rsync" # disable default synced folder config.vm.synced_folder ".", "/vagrant", disabled: true diff --git a/test/driver-lib.bash b/test/driver-lib.bash index 56a0740..02a71a5 100644 --- a/test/driver-lib.bash +++ b/test/driver-lib.bash @@ -31,10 +31,6 @@ check_prereqs() { >&2 echo "vagrant vm must be running." return 1 fi - if ! (vagrant plugin list | grep '^sahara\s\+') >/dev/null 2>&1; then - >&2 echo "vagrant plugin 'sahara' is not installed." - return 1 - fi } until_success() { @@ -56,23 +52,26 @@ wait_for_vagrant() { until_success vagrant ssh -c 'exit' } -rollback() { - vagrant sandbox rollback >/dev/null 2>&1 && - wait_for_vagrant && - vagrant rsync >/dev/null 2>&1 +cleanup() { + vagrant ssh -c " + find . -not \\( \ + -path './.pyenv' -o \ + -path './.pyenv/*' -o \ + -path './.bashrc' -o \ + -path './.profile' -o \ + -path './.ssh' -o \ + -path './.ssh/*' \ + \\) -delete" >/dev/null 2>&1 } initialize() { echo "initializing." - vagrant sandbox on >/dev/null 2>&1 if ! vagrant ssh -c "pyenv local ${2}" >/dev/null 2>&1; then - wait_for_vagrant && vagrant sandbox rollback >/dev/null 2>&1 - wait_for_vagrant if ! vagrant ssh -c "pyenv install -s ${2} && pyenv local ${2}" >/dev/null 2>&1; then die "could not install python ${2}" fi - vagrant sandbox commit >/dev/null 2>&1 fi + vagrant rsync >/dev/null 2>&1 tests_run=0 tests_passed=0 tests_failed=0 @@ -96,7 +95,7 @@ fail() { run_test() { tests_run=$((tests_run + 1)) printf '[%d/%d] (%s)\n' "${tests_run}" "${tests_total}" "${1}" - rollback || die "unable to rollback vm." # start with a clean slate + cleanup vagrant ssh -c "pyenv local ${2}" >/dev/null 2>&1 if vagrant ssh -c "cd /dotbot/test/tests && bash ${1}" 2>/dev/null; then pass @@ -24,7 +24,7 @@ do ;; esac done -VERSION="${VERSION:-2.7.9}" +VERSION="${VERSION:-3.6.4}" declare -a tests=() diff --git a/test/test-lib.bash b/test/test-lib.bash index affb5c9..e4d9a4e 100644 --- a/test/test-lib.bash +++ b/test/test-lib.bash @@ -1,6 +1,6 @@ DEBUG=${DEBUG:-false} USE_VAGRANT=${USE_VAGRANT:-true} -DOTBOT_EXEC=${DOTBOT_EXEC:-"/dotbot/bin/dotbot"} +DOTBOT_EXEC=${DOTBOT_EXEC:-"python /dotbot/bin/dotbot"} DOTFILES="/home/$(whoami)/dotfiles" INSTALL_CONF='install.conf.yaml' INSTALL_CONF_JSON='install.conf.json' @@ -47,17 +47,15 @@ initialize() { run_dotbot() { ( - cd "${DOTFILES}" - cat > "${INSTALL_CONF}" - ${DOTBOT_EXEC} -d . -c "${INSTALL_CONF}" "${@}" + cat > "${DOTFILES}/${INSTALL_CONF}" + ${DOTBOT_EXEC} -c "${DOTFILES}/${INSTALL_CONF}" "${@}" ) } run_dotbot_json() { ( - cd "${DOTFILES}" - cat > "${INSTALL_CONF_JSON}" - ${DOTBOT_EXEC} -d . -c "${INSTALL_CONF_JSON}" "${@}" + cat > "${DOTFILES}/${INSTALL_CONF_JSON}" + ${DOTBOT_EXEC} -c "${DOTFILES}/${INSTALL_CONF_JSON}" "${@}" ) } diff --git a/test/test_travis b/test/test_travis index 20ec1ae..79439e1 100755 --- a/test/test_travis +++ b/test/test_travis @@ -6,7 +6,7 @@ set -e # set -x # set -v -BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Prevent execution outside of Travis CI builds if [[ "${TRAVIS}" != true || "${CI}" != true ]]; then diff --git a/test/tests/clean-environment-variable-expansion.bash b/test/tests/clean-environment-variable-expansion.bash new file mode 100644 index 0000000..fedab45 --- /dev/null +++ b/test/tests/clean-environment-variable-expansion.bash @@ -0,0 +1,16 @@ +test_description='clean expands environment variables' +. '../test-lib.bash' + +test_expect_success 'setup' ' +ln -s ${DOTFILES}/f ~/.f +' + +test_expect_success 'run' ' +run_dotbot <<EOF +- clean: ["\$HOME"] +EOF +' + +test_expect_success 'test' ' +! test -h ~/.f +' diff --git a/test/tests/find-python-executable.bash b/test/tests/find-python-executable.bash index cc46724..d4fa7eb 100644 --- a/test/tests/find-python-executable.bash +++ b/test/tests/find-python-executable.bash @@ -1,6 +1,10 @@ test_description='can find python executable with different names' . '../test-lib.bash' +if ${USE_VAGRANT}; then + DOTBOT_EXEC="/dotbot/bin/dotbot" # revert to calling it as a shell script +fi + # the test machine needs to have a binary named `python` test_expect_success 'setup' ' mkdir ~/tmp_bin && @@ -26,7 +30,7 @@ test_expect_success 'setup 2' ' touch ~/tmp_bin/python && chmod +x ~/tmp_bin/python && cat >> ~/tmp_bin/python <<EOF -#!$HOME/tmp_bin/sh +#!$HOME/tmp_bin/bash exec $(which python) EOF ' diff --git a/test/tests/link-glob-ambiguous.bash b/test/tests/link-glob-ambiguous.bash new file mode 100644 index 0000000..7e23348 --- /dev/null +++ b/test/tests/link-glob-ambiguous.bash @@ -0,0 +1,45 @@ +test_description='link glob ambiguous' +. '../test-lib.bash' + +test_expect_success 'setup' ' +mkdir ${DOTFILES}/foo +' + +test_expect_failure 'run 1' ' +run_dotbot <<EOF +- link: + ~/foo/: + path: foo + glob: true +EOF +' + +test_expect_failure 'test 1' ' +test -d ~/foo +' + +test_expect_failure 'run 2' ' +run_dotbot <<EOF +- link: + ~/foo/: + path: foo/ + glob: true +EOF +' + +test_expect_failure 'test 2' ' +test -d ~/foo +' + +test_expect_success 'run 3' ' +run_dotbot <<EOF +- link: + ~/foo: + path: foo + glob: true +EOF +' + +test_expect_success 'test 3' ' +test -d ~/foo +' diff --git a/test/tests/link-glob-multi-star.bash b/test/tests/link-glob-multi-star.bash new file mode 100644 index 0000000..11ae740 --- /dev/null +++ b/test/tests/link-glob-multi-star.bash @@ -0,0 +1,31 @@ +test_description='link glob' +. '../test-lib.bash' + +test_expect_success 'setup' ' +mkdir ${DOTFILES}/config && +mkdir ${DOTFILES}/config/foo && +mkdir ${DOTFILES}/config/bar && +echo "apple" > ${DOTFILES}/config/foo/a && +echo "banana" > ${DOTFILES}/config/bar/b && +echo "cherry" > ${DOTFILES}/config/bar/c +' + +test_expect_success 'run' ' +run_dotbot -v <<EOF +- defaults: + link: + glob: true + create: true +- link: + ~/.config/: config/*/* +EOF +' + +test_expect_success 'test' ' +! readlink ~/.config/ && +! readlink ~/.config/foo && +readlink ~/.config/foo/a && +grep "apple" ~/.config/foo/a && +grep "banana" ~/.config/bar/b && +grep "cherry" ~/.config/bar/c +' diff --git a/test/tests/link-glob.bash b/test/tests/link-glob.bash new file mode 100644 index 0000000..f1c813d --- /dev/null +++ b/test/tests/link-glob.bash @@ -0,0 +1,47 @@ +test_description='link glob' +. '../test-lib.bash' + +test_expect_success 'setup 1' ' +mkdir ${DOTFILES}/bin && +echo "apple" > ${DOTFILES}/bin/a && +echo "banana" > ${DOTFILES}/bin/b && +echo "cherry" > ${DOTFILES}/bin/c +' + +test_expect_success 'run 1' ' +run_dotbot -v <<EOF +- defaults: + link: + glob: true + create: true +- link: + ~/bin: bin/* +EOF +' + +test_expect_success 'test 1' ' +grep "apple" ~/bin/a && +grep "banana" ~/bin/b && +grep "cherry" ~/bin/c +' + +test_expect_success 'setup 2' ' +rm -rf ~/bin +' + +test_expect_success 'run 2' ' +run_dotbot -v <<EOF +- defaults: + link: + glob: true + create: true +- link: + ~/bin/: bin/* +EOF +' + +test_expect_success 'test 2' ' +grep "apple" ~/bin/a && +grep "banana" ~/bin/b && +grep "cherry" ~/bin/c +' diff --git a/test/tests/link-if.bash b/test/tests/link-if.bash new file mode 100644 index 0000000..1ea7709 --- /dev/null +++ b/test/tests/link-if.bash @@ -0,0 +1,51 @@ +test_description='link if' +. '../test-lib.bash' + +test_expect_success 'setup' ' +mkdir ~/d +echo "apple" > ${DOTFILES}/f +' + +test_expect_success 'run' ' +run_dotbot <<EOF +- link: + ~/.f: + path: f + if: "true" + ~/.g: + path: f + if: "false" + ~/.h: + path: f + if: "[[ -d ~/d ]]" + ~/.i: + path: f + if: "badcommand" +EOF +' + +test_expect_success 'test' ' +grep "apple" ~/.f && +! test -f ~/.g && +grep "apple" ~/.h && +! test -f ~/.i +' + +test_expect_success 'run 2' ' +run_dotbot <<EOF +- defaults: + link: + if: "false" +- link: + ~/.j: + path: f + if: "true" + ~/.k: + path: f +EOF +' + +test_expect_success 'test 2' ' +grep "apple" ~/.j && +! test -f ~/.k +' diff --git a/test/tests/plugin-dir.bash b/test/tests/plugin-dir.bash index 299f144..f3a5e94 100644 --- a/test/tests/plugin-dir.bash +++ b/test/tests/plugin-dir.bash @@ -19,7 +19,7 @@ EOF ' test_expect_success 'run' ' -run_dotbot --plugin-dir plugins <<EOF +run_dotbot --plugin-dir ${DOTFILES}/plugins <<EOF - test: ~ EOF ' diff --git a/test/tests/plugin-disable-builtin.bash b/test/tests/plugin-disable-builtin.bash new file mode 100644 index 0000000..f469b0f --- /dev/null +++ b/test/tests/plugin-disable-builtin.bash @@ -0,0 +1,17 @@ +test_description='can disable built-in plugins' +. '../test-lib.bash' + +test_expect_success 'setup' ' +echo "apple" > ${DOTFILES}/f +' + +test_expect_failure 'run' ' +run_dotbot --disable-built-in-plugins <<EOF +- link: + ~/.f: f +EOF +' + +test_expect_failure 'test' ' +test -f ~/.f +' diff --git a/test/tests/plugin.bash b/test/tests/plugin.bash index 960e9ce..bdf0c7f 100644 --- a/test/tests/plugin.bash +++ b/test/tests/plugin.bash @@ -18,7 +18,7 @@ EOF ' test_expect_success 'run' ' -run_dotbot --plugin test.py <<EOF +run_dotbot --plugin ${DOTFILES}/test.py <<EOF - test: ~ EOF ' diff --git a/test/tests/shim.bash b/test/tests/shim.bash new file mode 100644 index 0000000..2ed7d54 --- /dev/null +++ b/test/tests/shim.bash @@ -0,0 +1,29 @@ +test_description='install shim works' +. '../test-lib.bash' + +test_expect_success 'setup' ' +cd ${DOTFILES} +git init +if ${USE_VAGRANT}; then + git submodule add /dotbot dotbot +else + git submodule add ${BASEDIR} dotbot +fi +cp ./dotbot/tools/git-submodule/install . +echo "pear" > ${DOTFILES}/foo +' + +test_expect_success 'run' ' +cat > ${DOTFILES}/install.conf.yaml <<EOF +- link: + ~/.foo: foo +EOF +if ! ${USE_VAGRANT}; then + sed -i "" "1 s/sh$/python/" ${DOTFILES}/dotbot/bin/dotbot +fi +${DOTFILES}/install +' + +test_expect_success 'test' ' +grep "pear" ~/.foo +' diff --git a/tools/git-submodule/install b/tools/git-submodule/install index b1baa33..5a7e72c 100755 --- a/tools/git-submodule/install +++ b/tools/git-submodule/install @@ -9,6 +9,7 @@ DOTBOT_BIN="bin/dotbot" BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "${BASEDIR}" +git -C "${DOTBOT_DIR}" submodule sync --quiet --recursive git submodule update --init --recursive "${DOTBOT_DIR}" "${BASEDIR}/${DOTBOT_DIR}/${DOTBOT_BIN}" -d "${BASEDIR}" -c "${CONFIG}" "${@}" |