From c45c17184e1651e63bd01c0583e499c3934bad7c Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Tue, 27 Nov 2018 13:15:35 +0100 Subject: [PATCH] adding ability to use dynamic variables in dotfile src/dst --- README.md | 29 ++++++++++-- dotdrop/config.py | 10 ++++ dotdrop/dotdrop.py | 7 ++- dotdrop/templategen.py | 12 ++++- tests-ng/dyndotfilepaths.sh | 94 +++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 7 deletions(-) create mode 100755 tests-ng/dyndotfilepaths.sh diff --git a/README.md b/README.md index 9116130..f09494d 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ why [dotdrop](https://github.com/deadc0de6/dotdrop) rocks. * [Available variables](#available-variables) * [Available methods](#available-methods) + * [Dynamic dotfile paths](#dynamic-dotfile-paths) * [Dotdrop header](#dotdrop-header) * [Example](#example) @@ -579,6 +580,8 @@ the following entries: * `ignoreempty`: do not deploy template if empty (default *false*) * **dotfiles** entry: a list of dotfiles + * `dst`: where this dotfile needs to be deployed (can use `variables` and `dynvariables`, make sure to quote). + * `src`: dotfile path within the `dotpath` (can use `variables` and `dynvariables`, make sure to quote). * `link`: if true dotdrop will create a symlink instead of copying (default *false*). * `cmpignore`: list of pattern to ignore when comparing (enclose in quotes when using wildcards). * `actions`: list of action keys that need to be defined in the **actions** entry below. @@ -625,9 +628,7 @@ the following entries: * **trans** entry (optional): a list of transformations (see [Use transformations](#use-transformations)) -``` - : -``` +``` : ``` * **variables** entry (optional): a list of template variables (see [Variables](#variables)) @@ -798,6 +799,28 @@ it does exist If you'd like a specific function to be available, either open an issue or do a PR. +## Dynamic dotfile paths + +Dotfile source (`src`) and destination (`dst`) can be dynamically constructed using +defined variables (`variables` or `dynvariables`). + +For example to have a dotfile deployed on the unique firefox profile where the +profile path is dynamically found using a shell oneliner stored in a dynvariable: +```yaml +dynvariables: + mozpath: find ~/.mozilla/firefox -name '*.default' +dotfiles: + f_somefile: + dst: "{{@@ mozpath @@}}/somefile" + src: firefox/somefile +profiles: + home: + dotfiles: + - f_somefile +``` + +Make sure to quote the path in the config file. + ## Dotdrop header Dotdrop is able to insert a header in the generated dotfiles. This allows diff --git a/dotdrop/config.py b/dotdrop/config.py index 3c759b5..ca56bc3 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -11,6 +11,7 @@ import shlex # local import from dotdrop.dotfile import Dotfile +from dotdrop.templategen import Templategen from dotdrop.logger import Logger from dotdrop.action import Action, Transform from dotdrop.utils import * @@ -110,6 +111,15 @@ class Cfg: if not self._load_file(): raise ValueError('config is not valid') + def eval_dotfiles(self, profile, debug=False): + """resolve dotfiles src/dst templates""" + t = Templategen(profile=profile, + variables=self.get_variables(), + debug=debug) + for d in self.get_dotfiles(profile): + d.src = t.generate_string(d.src) + d.dst = t.generate_string(d.dst) + def _load_file(self): """load the yaml file""" with open(self.cfgpath, 'r') as f: diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index e285fb1..b7ff39f 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -90,7 +90,7 @@ def cmd_install(opts, conf, temporary=False, keys=[]): LOG.warn(msg.format(opts['profile'])) return False - t = Templategen(opts['profile'], base=opts['dotpath'], + t = Templategen(profile=opts['profile'], base=opts['dotpath'], variables=opts['variables'], debug=opts['debug']) tmpdir = None if temporary: @@ -158,7 +158,7 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): if len(selected) < 1: return False - t = Templategen(opts['profile'], base=opts['dotpath'], + t = Templategen(profile=opts['profile'], base=opts['dotpath'], variables=opts['variables'], debug=opts['debug']) inst = Installer(create=opts['create'], backup=opts['backup'], dry=opts['dry'], base=opts['dotpath'], @@ -439,6 +439,9 @@ def main(): LOG.dbg('config file: {}'.format(args['--cfg'])) LOG.dbg('opts: {}'.format(opts)) + # resolve dynamic paths + conf.eval_dotfiles(opts['profile'], debug=opts['debug']) + if ENV_NOBANNER not in os.environ \ and opts['banner'] \ and not args['--no-banner']: diff --git a/dotdrop/templategen.py b/dotdrop/templategen.py index 99bf970..d41bdc9 100644 --- a/dotdrop/templategen.py +++ b/dotdrop/templategen.py @@ -23,7 +23,7 @@ COMMENT_END = '@@#}' class Templategen: - def __init__(self, profile, base='.', variables={}, debug=False): + def __init__(self, profile='', base='.', variables={}, debug=False): self.base = base.rstrip(os.sep) self.debug = debug self.log = Logger() @@ -39,7 +39,8 @@ class Templategen: comment_end_string=COMMENT_END) # adding variables self.env.globals['env'] = os.environ - self.env.globals['profile'] = profile + if profile: + self.env.globals['profile'] = profile self.env.globals.update(variables) # adding header method self.env.globals['header'] = self._header @@ -47,10 +48,17 @@ class Templategen: self.env.globals['exists'] = jhelpers.exists def generate(self, src): + """render template from path""" if not os.path.exists(src): return '' return self._handle_file(src) + def generate_string(self, string): + """render template from string""" + if not string: + return '' + return self.env.from_string(string).render() + def _header(self, prepend=''): """add a comment usually in the header of a dotfile""" return '{}{}'.format(prepend, utils.header()) diff --git a/tests-ng/dyndotfilepaths.sh b/tests-ng/dyndotfilepaths.sh new file mode 100755 index 0000000..f768545 --- /dev/null +++ b/tests-ng/dyndotfilepaths.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# author: deadc0de6 (https://github.com/deadc0de6) +# Copyright (c) 2017, deadc0de6 +# +# test dynamic variables in dotfile src/dst +# returns 1 in case of error +# + +# exit on first error +set -e + +# all this crap to get current path +rl="readlink -f" +if ! ${rl} "${0}" >/dev/null 2>&1; then + rl="realpath" + + if ! hash ${rl}; then + echo "\"${rl}\" not found !" && exit 1 + fi +fi +cur=$(dirname "$(${rl} "${0}")") + +#hash dotdrop >/dev/null 2>&1 +#[ "$?" != "0" ] && echo "install dotdrop to run tests" && exit 1 + +#echo "called with ${1}" + +# dotdrop path can be pass as argument +ddpath="${cur}/../" +[ "${1}" != "" ] && ddpath="${1}" +[ ! -d ${ddpath} ] && echo "ddpath \"${ddpath}\" is not a directory" && exit 1 + +export PYTHONPATH="${ddpath}:${PYTHONPATH}" +bin="python3 -m dotdrop.dotdrop" + +echo "dotdrop path: ${ddpath}" +echo "pythonpath: ${PYTHONPATH}" + +# get the helpers +source ${cur}/helpers + +echo -e "\e[96m\e[1m==> RUNNING $(basename $BASH_SOURCE) <==\e[0m" + +################################################################ +# this is the test +################################################################ + +# the dotfile source +tmps=`mktemp -d` +mkdir -p ${tmps}/dotfiles +# the dotfile destination +tmpd=`mktemp -d` + +# create the config file +cfg="${tmps}/config.yaml" + +dst=`echo ${tmpd} | rev` + +cat > ${cfg} << _EOF +config: + backup: true + create: true + dotpath: dotfiles +dynvariables: + dpath: echo ${dst} | rev +dotfiles: + f_abc: + dst: "{{@@ dpath @@}}/abc" + src: abc +profiles: + p1: + dotfiles: + - f_abc +_EOF +cat ${cfg} + +# create the dotfile +echo "{{@@ dpath @@}}" > ${tmps}/dotfiles/abc + +# install +cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V + +#cat ${tmpd}/abc + +[ ! -e ${tmpd}/abc ] && echo "abc not installed dynamically" && exit 1 +grep "^${tmpd}" ${tmpd}/abc >/dev/null + +#cat ${tmpd}/abc + +## CLEANING +rm -rf ${tmps} ${tmpd} + +echo "OK" +exit 0