diff --git a/dotdrop/config.py b/dotdrop/config.py index 85c1510..b6e62ac 100644 --- a/dotdrop/config.py +++ b/dotdrop/config.py @@ -15,6 +15,7 @@ from dotdrop.templategen import Templategen from dotdrop.logger import Logger from dotdrop.action import Action, Transform from dotdrop.utils import * +from dotdrop.linktypes import LinkTypes class Cfg: @@ -67,7 +68,7 @@ class Cfg: default_backup = True default_create = True default_banner = True - default_link = False + default_link = LinkTypes.NOLINK default_longkey = False default_keepdot = False default_showdiff = False @@ -212,10 +213,13 @@ class Cfg: # ensures the dotfiles entry is a dict self.content[self.key_dotfiles] = {} for k, v in self.content[self.key_dotfiles].items(): - src = v[self.key_dotfiles_src] - dst = v[self.key_dotfiles_dst] - link = v[self.key_dotfiles_link] if self.key_dotfiles_link \ - in v else self.default_link + src = os.path.normpath(v[self.key_dotfiles_src]) + dst = os.path.normpath(v[self.key_dotfiles_dst]) + link = LinkTypes.PARENTS \ + if self.key_dotfiles_link in v and v[self.key_dotfiles_link] \ + else LinkTypes.CHILDREN \ + if 'link_children' in v and v['link_children'] \ + else LinkTypes.NOLINK noempty = v[self.key_dotfiles_noempty] if \ self.key_dotfiles_noempty \ in v else self.lnk_settings[self.key_ignoreempty] @@ -264,7 +268,7 @@ class Cfg: return False # disable transformation when link is true - if link and (trans_r or trans_w): + if link != LinkTypes.NOLINK and (trans_r or trans_w): msg = 'transformations disabled for \"{}\"'.format(dst) msg += ' because link is True' self.log.warn(msg) @@ -520,7 +524,7 @@ class Cfg: return False, self._get_long_key(path) return False, self._get_short_key(path, self.dotfiles.keys()) - def new(self, dotfile, profile, link=False, debug=False): + def new(self, dotfile, profile, link=LinkTypes.NOLINK, debug=False): """import new dotfile dotfile key will change and can be empty""" # keep it short @@ -569,9 +573,9 @@ class Cfg: self.key_dotfiles_dst: dotfile.dst, self.key_dotfiles_src: dotfile.src, } - if link: + if link != LinkTypes.NOLINK: # set the link flag - dots[dotfile.key][self.key_dotfiles_link] = True + dots[dotfile.key][self.key_dotfiles_link] = link # link it to this profile in the yaml file pro = self.content[self.key_profiles][profile] diff --git a/dotdrop/dotdrop.py b/dotdrop/dotdrop.py index ea37dd9..ff39eb2 100644 --- a/dotdrop/dotdrop.py +++ b/dotdrop/dotdrop.py @@ -7,7 +7,6 @@ entry point import os import sys -import subprocess import socket from docopt import docopt @@ -20,7 +19,8 @@ from dotdrop.updater import Updater from dotdrop.comparator import Comparator from dotdrop.dotfile import Dotfile from dotdrop.config import Cfg -from dotdrop.utils import * +import dotdrop.utils as dd +from dotdrop.linktypes import LinkTypes LOG = Logger() ENV_PROFILE = 'DOTDROP_PROFILE' @@ -92,7 +92,7 @@ def cmd_install(opts, conf, temporary=False, keys=[]): variables=opts['variables'], debug=opts['debug']) tmpdir = None if temporary: - tmpdir = get_tmpdir() + tmpdir = dd.get_tmpdir() inst = Installer(create=opts['create'], backup=opts['backup'], dry=opts['dry'], safe=opts['safe'], base=opts['dotpath'], workdir=opts['workdir'], @@ -106,8 +106,10 @@ def cmd_install(opts, conf, temporary=False, keys=[]): preactions.append(action) if opts['debug']: LOG.dbg('installing {}'.format(dotfile)) - if hasattr(dotfile, 'link') and dotfile.link: + if hasattr(dotfile, 'link') and dotfile.link == LinkTypes.PARENTS: r = inst.link(t, dotfile.src, dotfile.dst, actions=preactions) + elif hasattr(dotfile, 'link') and dotfile.link == LinkTypes.CHILDREN: + r = inst.linkall(t, dotfile.src, dotfile.dst, actions=preactions) else: src = dotfile.src tmp = None @@ -121,7 +123,7 @@ def cmd_install(opts, conf, temporary=False, keys=[]): if tmp: tmp = os.path.join(opts['dotpath'], tmp) if os.path.exists(tmp): - remove(tmp) + dd.remove(tmp) if len(r) > 0: if Cfg.key_actions_post in dotfile.actions: actions = dotfile.actions[Cfg.key_actions_post] @@ -189,7 +191,7 @@ def cmd_compare(opts, conf, tmp, focus=[], ignore=[]): # clean tmp transformed dotfile if any tmpsrc = os.path.join(opts['dotpath'], tmpsrc) if os.path.exists(tmpsrc): - remove(tmpsrc) + dd.remove(tmpsrc) if diff == '': if opts['debug']: LOG.dbg('diffing \"{}\" VS \"{}\"'.format(dotfile.key, @@ -243,7 +245,7 @@ def cmd_importer(opts, conf, paths): continue dst = path.rstrip(os.sep) dst = os.path.abspath(dst) - src = strip_home(dst) + src = dd.strip_home(dst) strip = '.' + os.sep if opts['keepdot']: strip = os.sep @@ -262,7 +264,7 @@ def cmd_importer(opts, conf, paths): if opts['dry']: LOG.dry('would run: {}'.format(' '.join(cmd))) else: - r, _ = run(cmd, raw=False, debug=opts['debug'], checkerr=True) + r, _ = dd.run(cmd, raw=False, debug=opts['debug'], checkerr=True) if not r: LOG.err('importing \"{}\" failed!'.format(path)) ret = False @@ -273,13 +275,13 @@ def cmd_importer(opts, conf, paths): if linkit: LOG.dry('would symlink {} to {}'.format(srcf, dst)) else: - r, _ = run(cmd, raw=False, debug=opts['debug'], checkerr=True) + r, _ = dd.run(cmd, raw=False, debug=opts['debug'], checkerr=True) if not r: LOG.err('importing \"{}\" failed!'.format(path)) ret = False continue if linkit: - remove(dst) + dd.remove(dst) os.symlink(srcf, dst) retconf, dotfile = conf.new(dotfile, opts['profile'], link=linkit, debug=opts['debug']) @@ -400,7 +402,7 @@ def apply_trans(opts, dotfile): msg = 'transformation \"{}\" failed for {}' LOG.err(msg.format(trans.key, dotfile.key)) if new_src and os.path.exists(new_src): - remove(new_src) + dd.remove(new_src) return None return new_src @@ -469,12 +471,12 @@ def main(): # compare local dotfiles with dotfiles stored in dotdrop if opts['debug']: LOG.dbg('running cmd: compare') - tmp = get_tmpdir() + tmp = dd.get_tmpdir() opts['dopts'] = args['--dopts'] ret = cmd_compare(opts, conf, tmp, focus=args['--file'], ignore=args['--ignore']) # clean tmp directory - remove(tmp) + dd.remove(tmp) elif args['import']: # import dotfile(s) diff --git a/dotdrop/dotfile.py b/dotdrop/dotfile.py index 45e14ee..bcd3329 100644 --- a/dotdrop/dotfile.py +++ b/dotdrop/dotfile.py @@ -5,12 +5,14 @@ Copyright (c) 2017, deadc0de6 represents a dotfile in dotdrop """ +from dotdrop.linktypes import LinkTypes + class Dotfile: def __init__(self, key, dst, src, actions={}, trans_r=None, trans_w=None, - link=False, cmpignore=[], noempty=False): + link=LinkTypes.NOLINK, cmpignore=[], noempty=False): # key of dotfile in the config self.key = key # path where to install this dotfile @@ -32,10 +34,11 @@ class Dotfile: def __str__(self): msg = 'key:\"{}\", src:\"{}\", dst:\"{}\", link:\"{}\"' - return msg.format(self.key, self.src, self.dst, self.link) + return msg.format(self.key, self.src, self.dst, self.link.name) def __eq__(self, other): return self.__dict__ == other.__dict__ def __hash__(self): return hash(self.dst) ^ hash(self.src) ^ hash(self.key) + diff --git a/dotdrop/installer.py b/dotdrop/installer.py index 71bb2d2..4a0a42e 100644 --- a/dotdrop/installer.py +++ b/dotdrop/installer.py @@ -64,7 +64,7 @@ class Installer: if not os.path.exists(src): self.log.err('source dotfile does not exist: {}'.format(src)) return [] - dst = os.path.expanduser(dst) + dst = os.path.normpath(os.path.expanduser(dst)) if self.totemp: # ignore actions return self.install(templater, src, dst, actions=[]) @@ -80,6 +80,42 @@ class Installer: src = tmp return self._link(src, dst, actions=actions) + def linkall(self, templater, src, dst, actions=[]): + """link all dotfiles in a given directory""" + self.action_executed = False + parent = os.path.join(self.base, os.path.expanduser(src)) + if not os.path.exists(parent): + self.log.err('source dotfile does not exist: {}'.format(parent)) + return [] + + dst = os.path.normpath(os.path.expanduser(dst)) + if not os.path.lexists(dst): + self.log.sub('creating directory "{}"'.format(dst)) + os.makedirs(dst) + + if os.path.isfile(dst): + msg = 'Remove regular file "{}" and replace with empty directory?' \ + .format(dst) + if self.safe and not self.log.ask(msg): + msg = 'ignoring "{}", nothing installed' + self.log.warn(msg.format(dst)) + return [] + os.unlink(dst) + os.mkdir(dst) + + children = os.listdir(parent) + srcs = [os.path.join(parent, child) for child in children] + dsts = [os.path.join(dst, child) for child in children] + + results = [] + for i in range(len(children)): + result = self._link(srcs[i], dsts[i], actions) + if len(result): + actions = [] + results.append(result) + + return utils.flatten(results) + def _link(self, src, dst, actions=[]): """set src as a link target of dst""" if os.path.lexists(dst): @@ -308,3 +344,4 @@ class Installer: self.comparing = False self.create = createsaved return ret, tmpdst + diff --git a/dotdrop/linktypes.py b/dotdrop/linktypes.py new file mode 100644 index 0000000..bd943a8 --- /dev/null +++ b/dotdrop/linktypes.py @@ -0,0 +1,8 @@ +from enum import IntEnum + + +class LinkTypes(IntEnum): + NOLINK = 0 + PARENTS = 1 + CHILDREN = 2 + diff --git a/dotdrop/utils.py b/dotdrop/utils.py index b88780b..77588f4 100644 --- a/dotdrop/utils.py +++ b/dotdrop/utils.py @@ -10,11 +10,12 @@ import tempfile import os import uuid import shlex +import functools +import operator from shutil import rmtree # local import from dotdrop.logger import Logger -from dotdrop.version import __version__ as VERSION LOG = Logger() @@ -109,3 +110,9 @@ def strip_home(path): if path.startswith(home): path = path[len(home):] return path + + +def flatten(a): + """flatten list""" + return functools.reduce(operator.iconcat, a, []) +