1
0
mirror of https://github.com/deadc0de6/dotdrop.git synced 2026-02-16 07:11:10 +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 `dirty` flag as a sign the file needs to be written and its representation in higher
levels in not accurate anymore. 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 # Testing
Dotdrop is tested with the use of the [tests.sh](/tests.sh) script. 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_link = 'link'
key_dotfile_actions = 'actions' key_dotfile_actions = 'actions'
key_dotfile_link_children = 'link_children' key_dotfile_link_children = 'link_children'
key_dotfile_noempty = 'ignoreempty'
# profile # profile
key_profile_dotfiles = 'dotfiles' key_profile_dotfiles = 'dotfiles'
@@ -60,6 +61,7 @@ class CfgYaml:
key_settings_dotpath = 'dotpath' key_settings_dotpath = 'dotpath'
key_settings_workdir = 'workdir' key_settings_workdir = 'workdir'
key_settings_link_dotfile_default = 'link_dotfile_default' key_settings_link_dotfile_default = 'link_dotfile_default'
key_settings_noempty = 'ignoreempty'
key_imp_link = 'link_on_import' key_imp_link = 'link_on_import'
# link values # link values
@@ -81,28 +83,45 @@ class CfgYaml:
self.dirty = False self.dirty = False
self.yaml_dict = self._load_yaml(self.path) self.yaml_dict = self._load_yaml(self.path)
# live patch deprecated entries
self._fix_deprecated(self.yaml_dict) self._fix_deprecated(self.yaml_dict)
# parse to self variables
self._parse_main_yaml(self.yaml_dict) self._parse_main_yaml(self.yaml_dict)
if self.debug: if self.debug:
self.log.dbg('before normalization: {}'.format(self.yaml_dict)) self.log.dbg('before normalization: {}'.format(self.yaml_dict))
# resolve variables # resolve variables
allvars = self._merge_and_apply_variables() self.variables = self._merge_variables()
self.variables.update(allvars)
# process imported configs # 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() self._import_configs()
# process other imports
self._resolve_imports() # process profile include
# process diverse options self._resolve_profile_includes()
self._resolve_rest() # process profile ALL
self._resolve_profile_all()
# patch dotfiles paths # patch dotfiles paths
self._resolve_dotfile_paths() self._resolve_dotfile_paths()
if self.debug: if self.debug:
self.log.dbg('after normalization: {}'.format(self.yaml_dict)) self.log.dbg('after normalization: {}'.format(self.yaml_dict))
def get_variables(self): def get_variables(self):
"""retrieve all variables""" """retrieve all variables"""
return self._merge_dict(self.variables, self.dvariables) return self.variables
########################################################
# parsing
########################################################
def _parse_main_yaml(self, dic): def _parse_main_yaml(self, dic):
"""parse the different blocks""" """parse the different blocks"""
@@ -111,9 +130,9 @@ class CfgYaml:
self.settings.update(self.ori_settings) self.settings.update(self.ori_settings)
# resolve settings paths # 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 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 self.settings[self.key_settings_workdir] = p
if self.debug: if self.debug:
self.log.dbg('settings: {}'.format(self.settings)) self.log.dbg('settings: {}'.format(self.settings))
@@ -133,6 +152,7 @@ class CfgYaml:
# profiles # profiles
self.ori_profiles = self._get_entry(dic, self.key_profiles) self.ori_profiles = self._get_entry(dic, self.key_profiles)
self.profiles = deepcopy(self.ori_profiles) self.profiles = deepcopy(self.ori_profiles)
self.profiles = self._norm_profiles(self.profiles)
if self.debug: if self.debug:
self.log.dbg('profiles: {}'.format(self.profiles)) self.log.dbg('profiles: {}'.format(self.profiles))
@@ -166,152 +186,64 @@ class CfgYaml:
self.ori_variables = self._get_entry(dic, self.ori_variables = self._get_entry(dic,
self.key_variables, self.key_variables,
mandatory=False) mandatory=False)
self.variables = deepcopy(self.ori_variables)
if self.debug: if self.debug:
self.log.dbg('variables: {}'.format(self.variables)) self.log.dbg('variables: {}'.format(self.ori_variables))
# dynvariables # dynvariables
self.ori_dvariables = self._get_entry(dic, self.ori_dvariables = self._get_entry(dic,
self.key_dvariables, self.key_dvariables,
mandatory=False) mandatory=False)
self.dvariables = deepcopy(self.ori_dvariables)
if self.debug: if self.debug:
self.log.dbg('dvariables: {}'.format(self.dvariables)) self.log.dbg('dynvariables: {}'.format(self.ori_dvariables))
def _resolve_dotfile_paths(self): def _resolve_dotfile_paths(self):
"""resolve dotfile paths""" """resolve dotfile paths"""
for dotfile in self.dotfiles.values(): for dotfile in self.dotfiles.values():
src = dotfile[self.key_dotfile_src] src = dotfile[self.key_dotfile_src]
src = os.path.join(self.settings[self.key_settings_dotpath], 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] 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): def _rec_resolve_vars(self, variables):
new = {} """recursive resolve variables"""
for k, v in dvars.items(): t = Templategen(variables=variables)
ret, val = shell(v) for k in variables.keys():
if not ret: val = variables[k]
err = 'command \"{}\" failed: {}'.format(k, val) while Templategen.var_is_template(val):
self.log.err(err) val = t.generate_string(val)
raise YamlException(err) variables[k] = val
new[k] = val t.update_variables(variables)
return new return variables
def _merge_and_apply_variables(self): def _merge_variables(self):
""" """
resolve all variables across the config resolve all variables across the config
apply them to any needed entries apply them to any needed entries
and return the full list of variables 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: if self.debug:
self.log.dbg('all variables: {}'.format(allvars)) self.log.dbg('get local variables')
t = Templategen(variables=allvars) # get all variables from local and resolve
for k in allvars.keys(): var = self._get_variables_dict(self.profile)
val = allvars[k]
while Templategen.var_is_template(val):
val = t.generate_string(val)
allvars[k] = val
t.update_variables(allvars)
# 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 # exec dynvariables
for k in dvar.keys(): self._shell_exec_dvars(dvar.keys(), merged)
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
if self.debug: if self.debug:
self.log.dbg('variables:') self.log.dbg('local variables resolved')
for k, v in allvars.items(): self._debug_vars(merged)
self.log.dbg('\t\"{}\": {}'.format(k, v))
if self.debug: # resolve profile includes
self.log.dbg('resolve all uses of variables in config') t = Templategen(variables=merged)
# 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
for k, v in self.profiles.items(): for k, v in self.profiles.items():
if self.key_profile_include in v: if self.key_profile_include in v:
new = [] new = []
@@ -319,7 +251,70 @@ class CfgYaml:
new.append(t.generate_string(k)) new.append(t.generate_string(k))
v[self.key_profile_include] = new 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): def _norm_actions(self, actions):
""" """
@@ -337,6 +332,18 @@ class CfgYaml:
new[k] = (self.action_post, v) new[k] = (self.action_post, v)
return new 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): def _norm_dotfiles(self, dotfiles):
"""normalize dotfiles entries""" """normalize dotfiles entries"""
if not dotfiles: if not dotfiles:
@@ -360,27 +367,36 @@ class CfgYaml:
if self.key_dotfile_link not in v: if self.key_dotfile_link not in v:
val = self.settings[self.key_settings_link_dotfile_default] val = self.settings[self.key_settings_link_dotfile_default]
v[self.key_dotfile_link] = val 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 return new
def _get_variables_dict(self, profile, seen, sub=False): def _get_variables_dict(self, profile):
"""return enriched variables""" """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 = {} 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(): if not profile or profile not in self.profiles.keys():
return variables return variables
@@ -392,22 +408,15 @@ class CfgYaml:
if inherited_profile == profile or inherited_profile in seen: if inherited_profile == profile or inherited_profile in seen:
raise YamlException('\"include\" loop') raise YamlException('\"include\" loop')
seen.append(inherited_profile) 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) variables.update(new)
return pentry.get(self.key_profile_variables, {})
# overwrite with profile variables def _get_included_dvariables(self, profile, seen):
for k, v in pentry.get(self.key_profile_variables, {}).items(): """return included dynvariables"""
variables[k] = v
return variables
def _get_dvariables_dict(self, profile, seen, sub=False):
"""return dynvariables"""
variables = {} variables = {}
# dynvariables
variables.update(self.dvariables)
if not profile or profile not in self.profiles.keys(): if not profile or profile not in self.profiles.keys():
return variables return variables
@@ -419,114 +428,11 @@ class CfgYaml:
if inherited_profile == profile or inherited_profile in seen: if inherited_profile == profile or inherited_profile in seen:
raise YamlException('\"include loop\"') raise YamlException('\"include loop\"')
seen.append(inherited_profile) 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) variables.update(new)
return pentry.get(self.key_profile_dvariables, {})
# overwrite with profile dynvariables def _resolve_profile_all(self):
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):
"""resolve some other parts of the config""" """resolve some other parts of the config"""
# profile -> ALL # profile -> ALL
for k, v in self.profiles.items(): for k, v in self.profiles.items():
@@ -538,6 +444,7 @@ class CfgYaml:
if self.key_all in dfs: if self.key_all in dfs:
v[self.key_profile_dotfiles] = self.dotfiles.keys() v[self.key_profile_dotfiles] = self.dotfiles.keys()
def _resolve_profile_includes(self):
# profiles -> include other profile # profiles -> include other profile
for k, v in self.profiles.items(): for k, v in self.profiles.items():
self._rec_resolve_profile_include(k) self._rec_resolve_profile_include(k)
@@ -597,75 +504,107 @@ class CfgYaml:
self.profiles[profile][self.key_profile_include] = None self.profiles[profile][self.key_profile_include] = None
return dotfiles, actions return dotfiles, actions
def _resolve_path(self, path): ########################################################
"""resolve a path either absolute or relative to config path""" # handle imported entries
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 _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): mandatory=False, patch_func=None):
""" """
import the block "key" from "path" import the block "key" from "path"
and merge it with "current" patch_func is applied to each element if defined
patch_func is applied before merge if defined
""" """
if self.debug: if self.debug:
self.log.dbg('import \"{}\" from \"{}\"'.format(key, path)) self.log.dbg('import \"{}\" from \"{}\"'.format(key, path))
self.log.dbg('current: {}'.format(current))
extdict = self._load_yaml(path) extdict = self._load_yaml(path)
new = self._get_entry(extdict, key, mandatory=mandatory) new = self._get_entry(extdict, key, mandatory=mandatory)
if patch_func: if patch_func:
new = patch_func(new) new = patch_func(new)
if not new: if not new and mandatory:
self.log.warn('no \"{}\" imported from \"{}\"'.format(key, path)) err = 'no \"{}\" imported from \"{}\"'.format(key, path)
return current self.log.warn(err)
raise YamlException(err)
if self.debug: if self.debug:
self.log.dbg('found: {}'.format(new)) self.log.dbg('new \"{}\": {}'.format(key, new))
if isinstance(current, dict) and isinstance(new, dict): return new
# 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
def _merge_dict(self, high, low): ########################################################
"""merge low into high""" # add/remove entries
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
def _new_profile(self, key): def _new_profile(self, key):
"""add a new profile if it doesn't exist""" """add a new profile if it doesn't exist"""
@@ -746,6 +685,10 @@ class CfgYaml:
self.dirty = True self.dirty = True
return True return True
########################################################
# handle deprecated entries
########################################################
def _fix_deprecated(self, yamldict): def _fix_deprecated(self, yamldict):
"""fix deprecated entries""" """fix deprecated entries"""
self._fix_deprecated_link_by_default(yamldict) self._fix_deprecated_link_by_default(yamldict)
@@ -801,24 +744,9 @@ class CfgYaml:
self.dirty = True self.dirty = True
self.log.warn('deprecated \"link_children\" value') self.log.warn('deprecated \"link_children\" value')
def _clear_none(self, dic): ########################################################
"""recursively delete all none/empty values in a dictionary.""" # yaml utils
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 save(self): def save(self):
"""save this instance and return True if saved""" """save this instance and return True if saved"""
@@ -851,6 +779,18 @@ class CfgYaml:
"""dump the config dictionary""" """dump the config dictionary"""
return self.yaml_dict 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): def _yaml_load(self, path):
"""load from yaml""" """load from yaml"""
with open(path, 'r') as f: with open(path, 'r') as f:
@@ -867,3 +807,106 @@ class CfgYaml:
y.indent = 2 y.indent = 2
y.typ = 'rt' y.typ = 'rt'
y.dump(content, f) 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 return path
def shell(cmd): def shell(cmd, debug=False):
""" """
run a command in the shell (expects a string) run a command in the shell (expects a string)
returns True|False, output returns True|False, output
""" """
if debug:
LOG.dbg('shell exec: {}'.format(cmd))
ret, out = subprocess.getstatusoutput(cmd) ret, out = subprocess.getstatusoutput(cmd)
if debug:
LOG.dbg('shell result ({}): {}'.format(ret, out))
return ret == 0, 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" var3: "{{@@ var2 @@}} var3"
var4: "{{@@ dvar4 @@}}" var4: "{{@@ dvar4 @@}}"
varx: "test" varx: "test"
provar: "local"
dynvariables: dynvariables:
dvar1: "echo dvar1" dvar1: "echo dvar1"
dvar2: "{{@@ dvar1 @@}} dvar2" dvar2: "{{@@ dvar1 @@}} dvar2"
@@ -83,16 +84,15 @@ profiles:
- f_abc - f_abc
variables: variables:
varx: profvarx varx: profvarx
provar: provar
_EOF _EOF
#cat ${cfg} #cat ${cfg}
# create the external variables file # create the external variables file
cat > ${extvars} << _EOF cat > ${extvars} << _EOF
variables: variables:
var1: "extvar1"
varx: "exttest" varx: "exttest"
dynvariables: dynvariables:
dvar1: "echo extdvar1"
evar1: "echo extevar1" evar1: "echo extevar1"
_EOF _EOF
@@ -103,19 +103,22 @@ echo "var4: {{@@ var4 @@}}" >> ${tmps}/dotfiles/abc
echo "dvar4: {{@@ dvar4 @@}}" >> ${tmps}/dotfiles/abc echo "dvar4: {{@@ dvar4 @@}}" >> ${tmps}/dotfiles/abc
echo "varx: {{@@ varx @@}}" >> ${tmps}/dotfiles/abc echo "varx: {{@@ varx @@}}" >> ${tmps}/dotfiles/abc
echo "evar1: {{@@ evar1 @@}}" >> ${tmps}/dotfiles/abc echo "evar1: {{@@ evar1 @@}}" >> ${tmps}/dotfiles/abc
echo "provar: {{@@ provar @@}}" >> ${tmps}/dotfiles/abc
#cat ${tmps}/dotfiles/abc #cat ${tmps}/dotfiles/abc
# install # install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V
echo "check1"
cat ${tmpd}/abc cat ${tmpd}/abc
grep '^var3: extvar1 var2 var3' ${tmpd}/abc >/dev/null grep '^var3: var1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar3: extdvar1 dvar2 dvar3' ${tmpd}/abc >/dev/null grep '^dvar3: dvar1 dvar2 dvar3' ${tmpd}/abc >/dev/null
grep '^var4: echo extvar1 var2 var3' ${tmpd}/abc >/dev/null grep '^var4: echo var1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar4: extvar1 var2 var3' ${tmpd}/abc >/dev/null grep '^dvar4: var1 var2 var3' ${tmpd}/abc >/dev/null
grep '^varx: profvarx' ${tmpd}/abc >/dev/null grep '^varx: exttest' ${tmpd}/abc >/dev/null
grep '^evar1: extevar1' ${tmpd}/abc >/dev/null grep '^evar1: extevar1' ${tmpd}/abc >/dev/null
grep '^provar: provar' ${tmpd}/abc >/dev/null
rm -f ${tmpd}/abc rm -f ${tmpd}/abc
@@ -137,6 +140,7 @@ profiles:
- f_abc - f_abc
variables: variables:
varx: profvarx varx: profvarx
vary: profvary
_EOF _EOF
#cat ${cfg} #cat ${cfg}
@@ -161,19 +165,21 @@ echo "dvar3: {{@@ dvar3 @@}}" >> ${tmps}/dotfiles/abc
echo "var4: {{@@ var4 @@}}" >> ${tmps}/dotfiles/abc echo "var4: {{@@ var4 @@}}" >> ${tmps}/dotfiles/abc
echo "dvar4: {{@@ dvar4 @@}}" >> ${tmps}/dotfiles/abc echo "dvar4: {{@@ dvar4 @@}}" >> ${tmps}/dotfiles/abc
echo "varx: {{@@ varx @@}}" >> ${tmps}/dotfiles/abc echo "varx: {{@@ varx @@}}" >> ${tmps}/dotfiles/abc
echo "vary: {{@@ vary @@}}" >> ${tmps}/dotfiles/abc
#cat ${tmps}/dotfiles/abc #cat ${tmps}/dotfiles/abc
# install # install
cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V cd ${ddpath} | ${bin} install -f -c ${cfg} -p p1 -V
echo "test2"
cat ${tmpd}/abc cat ${tmpd}/abc
grep '^var3: extvar1 var2 var3' ${tmpd}/abc >/dev/null grep '^var3: extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar3: extdvar1 dvar2 dvar3' ${tmpd}/abc >/dev/null grep '^dvar3: extdvar1 dvar2 dvar3' ${tmpd}/abc >/dev/null
grep '^var4: echo extvar1 var2 var3' ${tmpd}/abc >/dev/null grep '^var4: echo extvar1 var2 var3' ${tmpd}/abc >/dev/null
grep '^dvar4: 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 ## CLEANING
rm -rf ${tmps} ${tmpd} rm -rf ${tmps} ${tmpd}

View File

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

View File

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