1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-04 12:46:44 +00:00

fix issues with variables expansions

This commit is contained in:
deadc0de6
2019-06-19 15:39:40 +02:00
parent 7de8e1ef23
commit 44723dc032
7 changed files with 532 additions and 350 deletions

View File

@@ -74,6 +74,35 @@ example) won't be *seen* by the higher layer until the config is reloaded. Consi
`dirty` flag as a sign the file needs to be written and its representation in higher
levels in not accurate anymore.
## Variables resolution
How variables are resolved (pass through jinja2's
templating function) in the config file.
* resolve `include` (the below merge is temporary just to resolve the `includes`)
* `variables` and `dynvariables` are first merged and recursively resolved
* `dynvariables` are executed
* they are all merged and `include` paths are resolved
(allows to use something like `include {{@@ os @@}}.variables.yaml`)
* `variables` and profile's `variables` are merged
* `dynvariables` and profile's `dynvariables` are merged
* `dynvariables` are executed
* they are all merged into the final *local* `variables`
These are then used to resolve different elements in the config file:
see [this](https://github.com/deadc0de6/dotdrop/wiki/config-variables#config-available-variables)
Then additional variables (`import_variables` and `import_configs`) are
then merged and take precedence over local variables.
Note:
* `dynvariables` > `variables`
* profile variables > (`variables` or `dynvariables`)
* imported `variables`/`dynvariables` > any other `variables` or `dynvariables`
* actions using variables are resolved at runtime (when action is executed)
and not when loading the config
# Testing
Dotdrop is tested with the use of the [tests.sh](/tests.sh) script.

View File

@@ -41,6 +41,7 @@ class CfgYaml:
key_dotfile_link = 'link'
key_dotfile_actions = 'actions'
key_dotfile_link_children = 'link_children'
key_dotfile_noempty = 'ignoreempty'
# profile
key_profile_dotfiles = 'dotfiles'
@@ -60,6 +61,7 @@ class CfgYaml:
key_settings_dotpath = 'dotpath'
key_settings_workdir = 'workdir'
key_settings_link_dotfile_default = 'link_dotfile_default'
key_settings_noempty = 'ignoreempty'
key_imp_link = 'link_on_import'
# link values
@@ -81,28 +83,45 @@ class CfgYaml:
self.dirty = False
self.yaml_dict = self._load_yaml(self.path)
# live patch deprecated entries
self._fix_deprecated(self.yaml_dict)
# parse to self variables
self._parse_main_yaml(self.yaml_dict)
if self.debug:
self.log.dbg('before normalization: {}'.format(self.yaml_dict))
# resolve variables
allvars = self._merge_and_apply_variables()
self.variables.update(allvars)
# process imported configs
self.variables = self._merge_variables()
# apply variables
self._apply_variables()
# process imported variables (import_variables)
self._import_variables()
# process imported actions (import_actions)
self._import_actions()
# process imported profile dotfiles (import)
self._import_profiles_dotfiles()
# process imported configs (import_configs)
self._import_configs()
# process other imports
self._resolve_imports()
# process diverse options
self._resolve_rest()
# process profile include
self._resolve_profile_includes()
# process profile ALL
self._resolve_profile_all()
# patch dotfiles paths
self._resolve_dotfile_paths()
if self.debug:
self.log.dbg('after normalization: {}'.format(self.yaml_dict))
def get_variables(self):
"""retrieve all variables"""
return self._merge_dict(self.variables, self.dvariables)
return self.variables
########################################################
# parsing
########################################################
def _parse_main_yaml(self, dic):
"""parse the different blocks"""
@@ -111,9 +130,9 @@ class CfgYaml:
self.settings.update(self.ori_settings)
# resolve settings paths
p = self._resolve_path(self.settings[self.key_settings_dotpath])
p = self._norm_path(self.settings[self.key_settings_dotpath])
self.settings[self.key_settings_dotpath] = p
p = self._resolve_path(self.settings[self.key_settings_workdir])
p = self._norm_path(self.settings[self.key_settings_workdir])
self.settings[self.key_settings_workdir] = p
if self.debug:
self.log.dbg('settings: {}'.format(self.settings))
@@ -133,6 +152,7 @@ class CfgYaml:
# profiles
self.ori_profiles = self._get_entry(dic, self.key_profiles)
self.profiles = deepcopy(self.ori_profiles)
self.profiles = self._norm_profiles(self.profiles)
if self.debug:
self.log.dbg('profiles: {}'.format(self.profiles))
@@ -166,152 +186,64 @@ class CfgYaml:
self.ori_variables = self._get_entry(dic,
self.key_variables,
mandatory=False)
self.variables = deepcopy(self.ori_variables)
if self.debug:
self.log.dbg('variables: {}'.format(self.variables))
self.log.dbg('variables: {}'.format(self.ori_variables))
# dynvariables
self.ori_dvariables = self._get_entry(dic,
self.key_dvariables,
mandatory=False)
self.dvariables = deepcopy(self.ori_dvariables)
if self.debug:
self.log.dbg('dvariables: {}'.format(self.dvariables))
self.log.dbg('dynvariables: {}'.format(self.ori_dvariables))
def _resolve_dotfile_paths(self):
"""resolve dotfile paths"""
for dotfile in self.dotfiles.values():
src = dotfile[self.key_dotfile_src]
src = os.path.join(self.settings[self.key_settings_dotpath], src)
dotfile[self.key_dotfile_src] = self._resolve_path(src)
dotfile[self.key_dotfile_src] = self._norm_path(src)
dst = dotfile[self.key_dotfile_dst]
dotfile[self.key_dotfile_dst] = self._resolve_path(dst)
dotfile[self.key_dotfile_dst] = self._norm_path(dst)
def _shell_dynvars(self, dvars):
new = {}
for k, v in dvars.items():
ret, val = shell(v)
if not ret:
err = 'command \"{}\" failed: {}'.format(k, val)
self.log.err(err)
raise YamlException(err)
new[k] = val
return new
def _rec_resolve_vars(self, variables):
"""recursive resolve variables"""
t = Templategen(variables=variables)
for k in variables.keys():
val = variables[k]
while Templategen.var_is_template(val):
val = t.generate_string(val)
variables[k] = val
t.update_variables(variables)
return variables
def _merge_and_apply_variables(self):
def _merge_variables(self):
"""
resolve all variables across the config
apply them to any needed entries
and return the full list of variables
"""
# first construct the list of variables
var = self._get_variables_dict(self.profile, seen=[self.profile])
dvar = self._get_dvariables_dict(self.profile, seen=[self.profile])
# recursive resolve variables
allvars = var.copy()
allvars.update(dvar)
if self.debug:
self.log.dbg('all variables: {}'.format(allvars))
self.log.dbg('get local variables')
t = Templategen(variables=allvars)
for k in allvars.keys():
val = allvars[k]
while Templategen.var_is_template(val):
val = t.generate_string(val)
allvars[k] = val
t.update_variables(allvars)
# get all variables from local and resolve
var = self._get_variables_dict(self.profile)
# get all dynvariables from local and resolve
dvar = self._get_dvariables_dict()
# temporarly resolve all variables for "include"
merged = self._merge_dict(dvar, var)
merged = self._rec_resolve_vars(merged)
self._debug_vars(merged)
# exec dynvariables
for k in dvar.keys():
ret, out = shell(allvars[k])
if not ret:
err = 'command \"{}\" failed: {}'.format(allvars[k], out)
self.log.err(err)
raise YamlException(err)
allvars[k] = out
self._shell_exec_dvars(dvar.keys(), merged)
if self.debug:
self.log.dbg('variables:')
for k, v in allvars.items():
self.log.dbg('\t\"{}\": {}'.format(k, v))
self.log.dbg('local variables resolved')
self._debug_vars(merged)
if self.debug:
self.log.dbg('resolve all uses of variables in config')
# now resolve blocks
t = Templategen(variables=allvars)
# dotfiles entries
for k, v in self.dotfiles.items():
# src
src = v.get(self.key_dotfile_src)
v[self.key_dotfile_src] = t.generate_string(src)
# dst
dst = v.get(self.key_dotfile_dst)
v[self.key_dotfile_dst] = t.generate_string(dst)
# actions
new = []
for a in v.get(self.key_dotfile_actions, []):
new.append(t.generate_string(a))
if new:
if self.debug:
self.log.dbg('resolved: {}'.format(new))
v[self.key_dotfile_actions] = new
# profile entries
try:
this_profile = self.profiles[self.profile]
# actions
this_profile[self.key_profile_actions] = [
t.generate_string(a)
for a in this_profile.get(self.key_profile_actions, [])
]
this_profile_actions = this_profile[self.key_profile_actions]
if this_profile_actions and self.debug:
self.log.dbg('resolved: {}'.format(this_profile_actions))
except KeyError:
# self.profile is not in the YAML file
pass
# external actions paths
new = []
for p in self.settings.get(self.key_import_actions, []):
new.append(t.generate_string(p))
if new:
if self.debug:
self.log.dbg('resolved: {}'.format(new))
self.settings[self.key_import_actions] = new
# external config paths
new = []
for p in self.settings.get(self.key_import_configs, []):
new.append(t.generate_string(p))
if new:
if self.debug:
self.log.dbg('resolved: {}'.format(new))
self.settings[self.key_import_configs] = new
# external variables paths
new = []
for p in self.settings.get(self.key_import_variables, []):
new.append(t.generate_string(p))
if new:
if self.debug:
self.log.dbg('resolved: {}'.format(new))
self.settings[self.key_import_variables] = new
# external profiles dotfiles
for k, v in self.profiles.items():
new = []
for p in v.get(self.key_import_profile_dfs, []):
new.append(t.generate_string(p))
if new:
if self.debug:
self.log.dbg('resolved: {}'.format(new))
v[self.key_import_profile_dfs] = new
# profile includes
# resolve profile includes
t = Templategen(variables=merged)
for k, v in self.profiles.items():
if self.key_profile_include in v:
new = []
@@ -319,7 +251,70 @@ class CfgYaml:
new.append(t.generate_string(k))
v[self.key_profile_include] = new
return allvars
# now get the included ones
incl_var = self._get_included_variables(self.profile,
seen=[self.profile])
incl_dvar = self._get_included_dvariables(self.profile,
seen=[self.profile])
# exec incl dynvariables
self._shell_exec_dvars(incl_dvar.keys(), incl_dvar)
# merge all and resolve
merged = self._merge_dict(incl_var, merged)
merged = self._merge_dict(incl_dvar, merged)
merged = self._rec_resolve_vars(merged)
if self.debug:
self.log.dbg('with included variables')
self._debug_vars(merged)
if self.debug:
self.log.dbg('with included variables')
self._debug_vars(merged)
if self.debug:
self.log.dbg('resolve all uses of variables in config')
self._debug_vars(merged)
return merged
def _apply_variables(self):
"""template any needed parts of the config"""
t = Templategen(variables=self.variables)
# dotfiles src/dst/actions keys
for k, v in self.dotfiles.items():
# src
src = v.get(self.key_dotfile_src)
v[self.key_dotfile_src] = t.generate_string(src)
# dst
dst = v.get(self.key_dotfile_dst)
v[self.key_dotfile_dst] = t.generate_string(dst)
# import_actions
new = []
entries = self.settings.get(self.key_import_actions, [])
new = self._template_list(t, entries)
if new:
self.settings[self.key_import_actions] = new
# import_configs
entries = self.settings.get(self.key_import_configs, [])
new = self._template_list(t, entries)
if new:
self.settings[self.key_import_configs] = new
# import_variables
entries = self.settings.get(self.key_import_variables, [])
new = self._template_list(t, entries)
if new:
self.settings[self.key_import_variables] = new
# profile's import
for k, v in self.profiles.items():
entries = v.get(self.key_import_profile_dfs, [])
new = self._template_list(t, entries)
if new:
v[self.key_import_profile_dfs] = new
def _norm_actions(self, actions):
"""
@@ -337,6 +332,18 @@ class CfgYaml:
new[k] = (self.action_post, v)
return new
def _norm_profiles(self, profiles):
"""normalize profiles entries"""
if not profiles:
return profiles
new = {}
for k, v in profiles.items():
# add dotfiles entry if not present
if self.key_profile_dotfiles not in v:
v[self.key_profile_dotfiles] = []
new[k] = v
return new
def _norm_dotfiles(self, dotfiles):
"""normalize dotfiles entries"""
if not dotfiles:
@@ -360,27 +367,36 @@ class CfgYaml:
if self.key_dotfile_link not in v:
val = self.settings[self.key_settings_link_dotfile_default]
v[self.key_dotfile_link] = val
# apply noempty if undefined
if self.key_dotfile_noempty not in v:
val = self.settings.get(self.key_settings_noempty, False)
v[self.key_dotfile_noempty] = val
return new
def _get_variables_dict(self, profile, seen, sub=False):
def _get_variables_dict(self, profile):
"""return enriched variables"""
variables = deepcopy(self.ori_variables)
# add profile variable
if profile:
variables['profile'] = profile
# add some more variables
p = self.settings.get(self.key_settings_dotpath)
p = self._norm_path(p)
variables['_dotdrop_dotpath'] = p
variables['_dotdrop_cfgpath'] = self._norm_path(self.path)
p = self.settings.get(self.key_settings_workdir)
p = self._norm_path(p)
variables['_dotdrop_workdir'] = p
return variables
def _get_dvariables_dict(self):
"""return dynvariables"""
variables = deepcopy(self.ori_dvariables)
return variables
def _get_included_variables(self, profile, seen):
"""return included variables"""
variables = {}
if not sub:
# add profile variable
if profile:
variables['profile'] = profile
# add some more variables
p = self.settings.get(self.key_settings_dotpath)
p = self._resolve_path(p)
variables['_dotdrop_dotpath'] = p
variables['_dotdrop_cfgpath'] = self._resolve_path(self.path)
p = self.settings.get(self.key_settings_workdir)
p = self._resolve_path(p)
variables['_dotdrop_workdir'] = p
# variables
variables.update(self.variables)
if not profile or profile not in self.profiles.keys():
return variables
@@ -392,22 +408,15 @@ class CfgYaml:
if inherited_profile == profile or inherited_profile in seen:
raise YamlException('\"include\" loop')
seen.append(inherited_profile)
new = self._get_variables_dict(inherited_profile, seen, sub=True)
new = self._get_included_variables(inherited_profile,
seen)
variables.update(new)
return pentry.get(self.key_profile_variables, {})
# overwrite with profile variables
for k, v in pentry.get(self.key_profile_variables, {}).items():
variables[k] = v
return variables
def _get_dvariables_dict(self, profile, seen, sub=False):
"""return dynvariables"""
def _get_included_dvariables(self, profile, seen):
"""return included dynvariables"""
variables = {}
# dynvariables
variables.update(self.dvariables)
if not profile or profile not in self.profiles.keys():
return variables
@@ -419,114 +428,11 @@ class CfgYaml:
if inherited_profile == profile or inherited_profile in seen:
raise YamlException('\"include loop\"')
seen.append(inherited_profile)
new = self._get_dvariables_dict(inherited_profile, seen, sub=True)
new = self._get_included_dvariables(inherited_profile, seen)
variables.update(new)
return pentry.get(self.key_profile_dvariables, {})
# overwrite with profile dynvariables
for k, v in pentry.get(self.key_profile_dvariables, {}).items():
variables[k] = v
return variables
def _is_glob(self, path):
"""quick test if path is a glob"""
return '*' in path or '?' in path
def _glob_paths(self, paths):
"""glob a list of paths"""
if not isinstance(paths, list):
paths = [paths]
res = []
for p in paths:
if not self._is_glob(p):
res.append(p)
continue
p = os.path.expanduser(p)
new = glob.glob(p)
if not new:
raise YamlException('bad path: {}'.format(p))
res.extend(glob.glob(p))
return res
def _import_variables(self, paths):
"""import external variables from paths"""
if not paths:
return
paths = self._glob_paths(paths)
for p in paths:
path = self._resolve_path(p)
if self.debug:
self.log.dbg('import variables from {}'.format(path))
self.variables = self._import_sub(path, self.key_variables,
self.variables,
mandatory=False)
self.dvariables = self._import_sub(path, self.key_dvariables,
self.dvariables,
mandatory=False,
patch_func=self._shell_dynvars)
def _import_actions(self, paths):
"""import external actions from paths"""
if not paths:
return
paths = self._glob_paths(paths)
for p in paths:
path = self._resolve_path(p)
if self.debug:
self.log.dbg('import actions from {}'.format(path))
self.actions = self._import_sub(path, self.key_actions,
self.actions, mandatory=False,
patch_func=self._norm_actions)
def _resolve_imports(self):
"""handle all the imports"""
# settings -> import_variables
imp = self.settings.get(self.key_import_variables, None)
self._import_variables(imp)
# settings -> import_actions
imp = self.settings.get(self.key_import_actions, None)
self._import_actions(imp)
# profiles -> import
for k, v in self.profiles.items():
imp = v.get(self.key_import_profile_dfs, None)
if not imp:
continue
if self.debug:
self.log.dbg('import dotfiles for profile {}'.format(k))
paths = self._glob_paths(imp)
for p in paths:
current = v.get(self.key_dotfiles, [])
path = self._resolve_path(p)
current = self._import_sub(path, self.key_dotfiles,
current, mandatory=False)
v[self.key_dotfiles] = current
def _import_configs(self):
"""import configs from external file"""
# settings -> import_configs
imp = self.settings.get(self.key_import_configs, None)
if not imp:
return
paths = self._glob_paths(imp)
for path in paths:
path = self._resolve_path(path)
if self.debug:
self.log.dbg('import config from {}'.format(path))
sub = CfgYaml(path, debug=self.debug)
# settings is ignored
self.dotfiles = self._merge_dict(self.dotfiles, sub.dotfiles)
self.profiles = self._merge_dict(self.profiles, sub.profiles)
self.actions = self._merge_dict(self.actions, sub.actions)
self.trans_r = self._merge_dict(self.trans_r, sub.trans_r)
self.trans_w = self._merge_dict(self.trans_w, sub.trans_w)
self.variables = self._merge_dict(self.variables,
sub.variables)
self.dvariables = self._merge_dict(self.dvariables,
sub.dvariables)
def _resolve_rest(self):
def _resolve_profile_all(self):
"""resolve some other parts of the config"""
# profile -> ALL
for k, v in self.profiles.items():
@@ -538,6 +444,7 @@ class CfgYaml:
if self.key_all in dfs:
v[self.key_profile_dotfiles] = self.dotfiles.keys()
def _resolve_profile_includes(self):
# profiles -> include other profile
for k, v in self.profiles.items():
self._rec_resolve_profile_include(k)
@@ -597,75 +504,107 @@ class CfgYaml:
self.profiles[profile][self.key_profile_include] = None
return dotfiles, actions
def _resolve_path(self, path):
"""resolve a path either absolute or relative to config path"""
path = os.path.expanduser(path)
if not os.path.isabs(path):
d = os.path.dirname(self.path)
return os.path.join(d, path)
return os.path.normpath(path)
########################################################
# handle imported entries
########################################################
def _import_sub(self, path, key, current,
def _import_variables(self):
"""import external variables from paths"""
paths = self.settings.get(self.key_import_variables, None)
if not paths:
return
paths = self._glob_paths(paths)
for p in paths:
path = self._norm_path(p)
if self.debug:
self.log.dbg('import variables from {}'.format(path))
var = self._import_sub(path, self.key_variables,
mandatory=False)
if self.debug:
self.log.dbg('import dynvariables from {}'.format(path))
dvar = self._import_sub(path, self.key_dvariables,
mandatory=False)
merged = self._merge_dict(dvar, var)
merged = self._rec_resolve_vars(merged)
# execute dvar
self._shell_exec_dvars(dvar.keys(), merged)
self.variables = self._merge_dict(merged, self.variables)
def _import_actions(self):
"""import external actions from paths"""
paths = self.settings.get(self.key_import_actions, None)
if not paths:
return
paths = self._glob_paths(paths)
for p in paths:
path = self._norm_path(p)
if self.debug:
self.log.dbg('import actions from {}'.format(path))
new = self._import_sub(path, self.key_actions,
mandatory=False,
patch_func=self._norm_actions)
self.actions = self._merge_dict(new, self.actions)
def _import_profiles_dotfiles(self):
"""import profile dotfiles"""
for k, v in self.profiles.items():
imp = v.get(self.key_import_profile_dfs, None)
if not imp:
continue
if self.debug:
self.log.dbg('import dotfiles for profile {}'.format(k))
paths = self._glob_paths(imp)
for p in paths:
current = v.get(self.key_dotfiles, [])
path = self._norm_path(p)
new = self._import_sub(path, self.key_dotfiles,
mandatory=False)
v[self.key_dotfiles] = new + current
def _import_configs(self):
"""import configs from external file"""
# settings -> import_configs
imp = self.settings.get(self.key_import_configs, None)
if not imp:
return
paths = self._glob_paths(imp)
for path in paths:
path = self._norm_path(path)
if self.debug:
self.log.dbg('import config from {}'.format(path))
sub = CfgYaml(path, debug=self.debug)
# settings is ignored
self.dotfiles = self._merge_dict(self.dotfiles, sub.dotfiles)
self.profiles = self._merge_dict(self.profiles, sub.profiles)
self.actions = self._merge_dict(self.actions, sub.actions)
self.trans_r = self._merge_dict(self.trans_r, sub.trans_r)
self.trans_w = self._merge_dict(self.trans_w, sub.trans_w)
self.variables = self._merge_dict(self.variables,
sub.variables)
def _import_sub(self, path, key,
mandatory=False, patch_func=None):
"""
import the block "key" from "path"
and merge it with "current"
patch_func is applied before merge if defined
patch_func is applied to each element if defined
"""
if self.debug:
self.log.dbg('import \"{}\" from \"{}\"'.format(key, path))
self.log.dbg('current: {}'.format(current))
extdict = self._load_yaml(path)
new = self._get_entry(extdict, key, mandatory=mandatory)
if patch_func:
new = patch_func(new)
if not new:
self.log.warn('no \"{}\" imported from \"{}\"'.format(key, path))
return current
if not new and mandatory:
err = 'no \"{}\" imported from \"{}\"'.format(key, path)
self.log.warn(err)
raise YamlException(err)
if self.debug:
self.log.dbg('found: {}'.format(new))
if isinstance(current, dict) and isinstance(new, dict):
# imported entries get more priority than current
current = self._merge_dict(new, current)
elif isinstance(current, list) and isinstance(new, list):
current = current + new
else:
raise YamlException('invalid import {} from {}'.format(key, path))
if self.debug:
self.log.dbg('new \"{}\": {}'.format(key, current))
return current
self.log.dbg('new \"{}\": {}'.format(key, new))
return new
def _merge_dict(self, high, low):
"""merge low into high"""
if not high:
high = {}
if not low:
low = {}
return {**low, **high}
def _get_entry(self, dic, key, mandatory=True):
"""return entry from yaml dictionary"""
if key not in dic:
if mandatory:
raise YamlException('invalid config: no {} found'.format(key))
dic[key] = {}
return dic[key]
if mandatory and not dic[key]:
# ensure is not none
dic[key] = {}
return dic[key]
def _load_yaml(self, path):
"""load a yaml file to a dict"""
content = {}
if not os.path.exists(path):
raise YamlException('config path not found: {}'.format(path))
try:
content = self._yaml_load(path)
except Exception as e:
self.log.err(e)
raise YamlException('invalid config: {}'.format(path))
return content
########################################################
# add/remove entries
########################################################
def _new_profile(self, key):
"""add a new profile if it doesn't exist"""
@@ -746,6 +685,10 @@ class CfgYaml:
self.dirty = True
return True
########################################################
# handle deprecated entries
########################################################
def _fix_deprecated(self, yamldict):
"""fix deprecated entries"""
self._fix_deprecated_link_by_default(yamldict)
@@ -801,24 +744,9 @@ class CfgYaml:
self.dirty = True
self.log.warn('deprecated \"link_children\" value')
def _clear_none(self, dic):
"""recursively delete all none/empty values in a dictionary."""
new = {}
for k, v in dic.items():
newv = v
if isinstance(v, dict):
newv = self._clear_none(v)
if not newv:
# no empty dict
continue
if newv is None:
# no None value
continue
if isinstance(newv, list) and not newv:
# no empty list
continue
new[k] = newv
return new
########################################################
# yaml utils
########################################################
def save(self):
"""save this instance and return True if saved"""
@@ -851,6 +779,18 @@ class CfgYaml:
"""dump the config dictionary"""
return self.yaml_dict
def _load_yaml(self, path):
"""load a yaml file to a dict"""
content = {}
if not os.path.exists(path):
raise YamlException('config path not found: {}'.format(path))
try:
content = self._yaml_load(path)
except Exception as e:
self.log.err(e)
raise YamlException('invalid config: {}'.format(path))
return content
def _yaml_load(self, path):
"""load from yaml"""
with open(path, 'r') as f:
@@ -867,3 +807,106 @@ class CfgYaml:
y.indent = 2
y.typ = 'rt'
y.dump(content, f)
########################################################
# helpers
########################################################
def _merge_dict(self, high, low):
"""merge high and low dict"""
if not high:
high = {}
if not low:
low = {}
return {**low, **high}
def _get_entry(self, dic, key, mandatory=True):
"""return entry from yaml dictionary"""
if key not in dic:
if mandatory:
raise YamlException('invalid config: no {} found'.format(key))
dic[key] = {}
return dic[key]
if mandatory and not dic[key]:
# ensure is not none
dic[key] = {}
return dic[key]
def _clear_none(self, dic):
"""recursively delete all none/empty values in a dictionary."""
new = {}
for k, v in dic.items():
newv = v
if isinstance(v, dict):
newv = self._clear_none(v)
if not newv:
# no empty dict
continue
if newv is None:
# no None value
continue
if isinstance(newv, list) and not newv:
# no empty list
continue
new[k] = newv
return new
def _is_glob(self, path):
"""quick test if path is a glob"""
return '*' in path or '?' in path
def _glob_paths(self, paths):
"""glob a list of paths"""
if not isinstance(paths, list):
paths = [paths]
res = []
for p in paths:
if not self._is_glob(p):
res.append(p)
continue
p = os.path.expanduser(p)
new = glob.glob(p)
if not new:
raise YamlException('bad path: {}'.format(p))
res.extend(glob.glob(p))
return res
def _debug_vars(self, variables):
"""pretty print variables"""
if not self.debug:
return
self.log.dbg('variables:')
for k, v in variables.items():
self.log.dbg('\t\"{}\": {}'.format(k, v))
def _norm_path(self, path):
"""resolve a path either absolute or relative to config path"""
path = os.path.expanduser(path)
if not os.path.isabs(path):
d = os.path.dirname(self.path)
return os.path.join(d, path)
return os.path.normpath(path)
def _shell_exec_dvars(self, keys, variables):
"""shell execute dynvariables"""
for k in list(keys):
ret, out = shell(variables[k], debug=self.debug)
if not ret:
err = 'var \"{}: {}\" failed: {}'.format(k, variables[k], out)
self.log.err(err)
raise YamlException(err)
if self.debug:
self.log.dbg('\"{}\": {} -> {}'.format(k, variables[k], out))
variables[k] = out
def _template_list(self, t, entries):
"""template a list of entries"""
new = []
if not entries:
return new
for e in entries:
et = t.generate_string(e)
if self.debug and e != et:
self.log.dbg('resolved: {} -> {}'.format(e, et))
new.append(et)
return new

View File

@@ -48,12 +48,16 @@ def write_to_tmpfile(content):
return path
def shell(cmd):
def shell(cmd, debug=False):
"""
run a command in the shell (expects a string)
returns True|False, output
"""
if debug:
LOG.dbg('shell exec: {}'.format(cmd))
ret, out = subprocess.getstatusoutput(cmd)
if debug:
LOG.dbg('shell result ({}): {}'.format(ret, out))
return ret == 0, out

98
tests-ng/dotfiles-dyn-paths.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# author: deadc0de6 (https://github.com/deadc0de6)
# Copyright (c) 2019, deadc0de6
#
# test dotfile dynamic paths
# returns 1 in case of error
#
# exit on first error
set -e
#set -v
# 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 --suffix='-dotdrop-tests'`
mkdir -p ${tmps}/dotfiles
echo "dotfiles source (dotpath): ${tmps}"
# the dotfile destination
tmpd=`mktemp -d --suffix='-dotdrop-tests'`
echo "dotfiles destination: ${tmpd}"
# create the config file
cfg="${tmps}/config.yaml"
cat > ${cfg} << _EOF
config:
backup: true
create: true
dotpath: dotfiles
variables:
dst: "${tmpd}/abc"
dynvariables:
src: "echo abc"
dotfiles:
f_abc:
dst: "{{@@ dst @@}}"
src: "{{@@ src @@}}"
profiles:
p1:
dotfiles:
- f_abc
_EOF
#cat ${cfg}
# create the dotfiles
echo "abc" > ${tmps}/dotfiles/abc
###########################
# test install and compare
###########################
# install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -b -V
[ "$?" != "0" ] && exit 1
# checks
[ ! -e ${tmpd}/abc ] && exit 1
## CLEANING
rm -rf ${tmps} ${tmpd}
echo "OK"
exit 0

View File

@@ -68,6 +68,7 @@ variables:
var3: "{{@@ var2 @@}} var3"
var4: "{{@@ dvar4 @@}}"
varx: "test"
provar: "local"
dynvariables:
dvar1: "echo dvar1"
dvar2: "{{@@ dvar1 @@}} dvar2"
@@ -83,16 +84,15 @@ profiles:
- f_abc
variables:
varx: profvarx
provar: provar
_EOF
#cat ${cfg}
# create the external variables file
cat > ${extvars} << _EOF
variables:
var1: "extvar1"
varx: "exttest"
dynvariables:
dvar1: "echo extdvar1"
evar1: "echo extevar1"
_EOF
@@ -103,19 +103,22 @@ echo "var4: {{@@ var4 @@}}" >> ${tmps}/dotfiles/abc
echo "dvar4: {{@@ dvar4 @@}}" >> ${tmps}/dotfiles/abc
echo "varx: {{@@ varx @@}}" >> ${tmps}/dotfiles/abc
echo "evar1: {{@@ evar1 @@}}" >> ${tmps}/dotfiles/abc
echo "provar: {{@@ provar @@}}" >> ${tmps}/dotfiles/abc
#cat ${tmps}/dotfiles/abc
# install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V
echo "check1"
cat ${tmpd}/abc
grep '^var3: extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar3: extdvar1 dvar2 dvar3' ${tmpd}/abc >/dev/null
grep '^var4: echo extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar4: extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^varx: profvarx' ${tmpd}/abc >/dev/null
grep '^var3: var1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar3: dvar1 dvar2 dvar3' ${tmpd}/abc >/dev/null
grep '^var4: echo var1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar4: var1 var2 var3' ${tmpd}/abc >/dev/null
grep '^varx: exttest' ${tmpd}/abc >/dev/null
grep '^evar1: extevar1' ${tmpd}/abc >/dev/null
grep '^provar: provar' ${tmpd}/abc >/dev/null
rm -f ${tmpd}/abc
@@ -137,6 +140,7 @@ profiles:
- f_abc
variables:
varx: profvarx
vary: profvary
_EOF
#cat ${cfg}
@@ -161,19 +165,21 @@ echo "dvar3: {{@@ dvar3 @@}}" >> ${tmps}/dotfiles/abc
echo "var4: {{@@ var4 @@}}" >> ${tmps}/dotfiles/abc
echo "dvar4: {{@@ dvar4 @@}}" >> ${tmps}/dotfiles/abc
echo "varx: {{@@ varx @@}}" >> ${tmps}/dotfiles/abc
echo "vary: {{@@ vary @@}}" >> ${tmps}/dotfiles/abc
#cat ${tmps}/dotfiles/abc
# install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V
echo "test2"
cat ${tmpd}/abc
grep '^var3: extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar3: extdvar1 dvar2 dvar3' ${tmpd}/abc >/dev/null
grep '^var4: echo extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar4: extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^varx: profvarx' ${tmpd}/abc >/dev/null
grep '^varx: exttest' ${tmpd}/abc >/dev/null
grep '^vary: profvary' ${tmpd}/abc >/dev/null
## CLEANING
rm -rf ${tmps} ${tmpd}

View File

@@ -109,7 +109,7 @@ echo "===================" >> ${tmps}/dotfiles/abc
# install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V
#cat ${tmpd}/abc
cat ${tmpd}/abc
# test variables
grep '^local1' ${tmpd}/abc >/dev/null

View File

@@ -92,6 +92,7 @@ echo "test" >> ${tmps}/dotfiles/abc
# install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1
cat ${tmpd}/abc
grep '^this is some sub-test' ${tmpd}/abc >/dev/null
grep '^12' ${tmpd}/abc >/dev/null
grep '^another test' ${tmpd}/abc >/dev/null
@@ -99,7 +100,8 @@ grep '^another test' ${tmpd}/abc >/dev/null
# install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p2
grep '^this is some sub-test' ${tmpd}/abc >/dev/null
cat ${tmpd}/abc
grep '^this is some test' ${tmpd}/abc >/dev/null
grep '^42' ${tmpd}/abc >/dev/null
grep '^another test' ${tmpd}/abc >/dev/null