""" author: deadc0de6 (https://github.com/deadc0de6) Copyright (c) 2017, deadc0de6 stores all options to use across dotdrop """ # attribute-defined-outside-init # pylint: disable=W0201 import os import sys import socket from docopt import docopt # local imports from dotdrop.version import __version__ as VERSION from dotdrop.linktypes import LinkTypes from dotdrop.logger import Logger from dotdrop.cfg_aggregator import CfgAggregator from dotdrop.action import Action from dotdrop.utils import uniq_list, debug_list, debug_dict from dotdrop.exceptions import YamlException, OptionsException ENV_PROFILE = 'DOTDROP_PROFILE' ENV_CONFIG = 'DOTDROP_CONFIG' ENV_NOBANNER = 'DOTDROP_NOBANNER' ENV_DEBUG = 'DOTDROP_DEBUG' ENV_NODEBUG = 'DOTDROP_FORCE_NODEBUG' ENV_XDG = 'XDG_CONFIG_HOME' ENV_WORKERS = 'DOTDROP_WORKERS' BACKUP_SUFFIX = '.dotdropbak' PROFILE = socket.gethostname() if ENV_PROFILE in os.environ: PROFILE = os.environ[ENV_PROFILE] NAME = 'dotdrop' CONFIGFILEYAML = 'config.yaml' CONFIGFILETOML = 'config.toml' HOMECFG = f'~/.config/{NAME}' ETCXDGCFG = f'/etc/xdg/{NAME}' ETCCFG = f'/etc/{NAME}' OPT_LINK = { LinkTypes.NOLINK.name.lower(): LinkTypes.NOLINK, LinkTypes.ABSOLUTE.name.lower(): LinkTypes.ABSOLUTE, LinkTypes.RELATIVE.name.lower(): LinkTypes.RELATIVE, LinkTypes.LINK_CHILDREN.name.lower(): LinkTypes.LINK_CHILDREN} BANNER = fr""" _ _ _ __| | ___ | |_ __| |_ __ ___ _ __ / _` |/ _ \| __/ _` | '__/ _ \| '_ | \__,_|\___/ \__\__,_|_| \___/| .__/ v{VERSION} |_|""" USAGE = f""" {BANNER} Usage: dotdrop install [-VbtfndDaW] [-c ] [-p ] [-w ] [...] dotdrop import [-Vbdfm] [-c ] [-p ] [-i ...] [--transr=] [--transw=] [-l ] [-s ] ... dotdrop compare [-LVbz] [-c ] [-p ] [-w ] [-C ...] [-i ...] dotdrop update [-VbfdkPz] [-c ] [-p ] [-w ] [-i ...] [...] dotdrop remove [-Vbfdk] [-c ] [-p ] [...] dotdrop files [-VbTG] [-c ] [-p ] dotdrop detail [-Vb] [-c ] [-p ] [...] dotdrop profiles [-VbG] [-c ] dotdrop --help dotdrop --version Options: -a --force-actions Execute all actions even if no dotfile is installed. -b --no-banner Do not display the banner. -c --cfg= Path to the config. -C --file= Path of dotfile to compare. -d --dry Dry run. -D --showdiff Show a diff before overwriting. -f --force Do not ask user confirmation for anything. -G --grepable Grepable output. -i --ignore= Pattern to ignore. -k --key Treat as a dotfile key. -l --link= Link option (nolink|absolute|relative|link_children). -L --file-only Do not show diff but only the files that differ. -m --preserve-mode Insert a chmod entry in the dotfile with its mode. -n --nodiff Do not diff when installing. -p --profile= Specify the profile to use [default: {PROFILE}]. -P --show-patch Provide a one-liner to manually patch template. -s --as= Import as a different path from actual path. --transr= Associate trans_read key on import. --transw= Apply trans_write key on import. -t --temp Install to a temporary directory for review. -T --template Only template dotfiles. -V --verbose Be verbose. -w --workers= Number of concurrent workers [default: 1]. -W --workdir-clear Clear the workdir. -z --ignore-missing Ignore files in installed folders that are missing. -v --version Show version. -h --help Show this screen. """ class AttrMonitor: """monitor attribute setter""" _set_attr_err = False # pylint: disable=W0235 def __setattr__(self, key, value): """monitor attribute setting""" super().__setattr__(key, value) # pylint: enable=W0235 def _attr_set(self, attr): """do something when unexistent attr is set""" class Options(AttrMonitor): """dotdrop options manager""" def __init__(self, args=None): """constructor @args: argument dictionary (if None use sys) """ # attributes gotten from self.conf.get_settings() self.banner = None self.showdiff = None self.default_actions = [] self.instignore = None self.force_chmod = None self.cmpignore = None self.impignore = None self.upignore = None self.link_on_import = None self.chmod_on_import = None self.check_version = None self.clear_workdir = None self.key_prefix = None self.key_separator = None # args parsing self.args = {} if not args: self.args = docopt(USAGE, version=VERSION) if args: self.args = args.copy() self.debug = self.args['--verbose'] or ENV_DEBUG in os.environ self.log = Logger(debug=self.debug) self.dry = self.args['--dry'] if ENV_NODEBUG in os.environ: # force disabling debugs self.debug = False # selected profile self.profile = self.args['--profile'] self.confpath = self._get_config_path() self.confpath = os.path.abspath(self.confpath) self.log.dbg(f'config abs path: {self.confpath}') if not self.confpath: raise YamlException('no config file found') if not os.path.exists(self.confpath): err = f'bad config file path: {self.confpath}' raise YamlException(err) self.log.dbg('#################################################') self.log.dbg('#################### DOTDROP ####################') self.log.dbg('#################################################') self.log.dbg(f'version: {VERSION}') args = ' '.join(sys.argv) self.log.dbg(f'command: {args}') self.log.dbg(f'config file: {self.confpath}') self._read_config() self._apply_args() self._fill_attr() if ENV_NOBANNER not in os.environ \ and self.banner \ and not self.args['--no-banner']: self._header() self._debug_attr() # start monitoring for bad attribute self._set_attr_err = True @classmethod def _get_config_from_env(cls, name): # look in XDG_CONFIG_HOME if ENV_XDG in os.environ: cfg = os.path.expanduser(os.environ[ENV_XDG]) path = os.path.join(cfg, NAME, name) if os.path.exists(path): return path return '' @classmethod def _get_config_from_fs(cls, name): """get config from filesystem""" # look in ~/.config/dotdrop cfg = os.path.expanduser(HOMECFG) path = os.path.join(cfg, name) if os.path.exists(path): return path # look in /etc/xdg/dotdrop path = os.path.join(ETCXDGCFG, name) if os.path.exists(path): return path # look in /etc/dotdrop path = os.path.join(ETCCFG, name) if os.path.exists(path): return path return '' def _get_config_path(self): """get the config path""" # cli provided if self.args['--cfg']: self.log.dbg(f'config from --cfg {self.args["--cfg"]}') return os.path.expanduser(self.args['--cfg']) # environment variable provided if ENV_CONFIG in os.environ: self.log.dbg(f'config from env {ENV_CONFIG}') return os.path.expanduser(os.environ[ENV_CONFIG]) # look in current directory if os.path.exists(CONFIGFILEYAML): self.log.dbg(f'config from yaml in current dir {CONFIGFILEYAML}') return CONFIGFILEYAML # look in current directory if os.path.exists(CONFIGFILETOML): self.log.dbg(f'config from toml in current dir {CONFIGFILETOML}') return CONFIGFILETOML path = self._get_config_from_env(CONFIGFILEYAML) if path: self.log.dbg(f'config from env with {CONFIGFILEYAML}') return path path = self._get_config_from_env(CONFIGFILETOML) if path: self.log.dbg(f'config from env with {CONFIGFILETOML}') return path path = self._get_config_from_fs(CONFIGFILEYAML) if path: self.log.dbg(f'config from fs with {CONFIGFILEYAML}') return path path = self._get_config_from_fs(CONFIGFILETOML) if path: self.log.dbg(f'config from fs with {CONFIGFILETOML}') return path self.log.dbg('no config file found') return None def _header(self): """display the header""" self.log.log(BANNER) self.log.log('') def _read_config(self): """read the config file""" self.conf = CfgAggregator(self.confpath, self.profile, debug=self.debug, dry=self.dry) # transform the config settings to self attribute settings = self.conf.get_settings() debug_dict('effective settings', settings, self.debug) for k, val in settings.items(): setattr(self, k, val) def _apply_args_files(self): """files specifics""" self.files_templateonly = self.args['--template'] self.files_grepable = self.args['--grepable'] def _apply_args_install(self): """install specifics""" self.install_force_action = self.args['--force-actions'] self.install_temporary = self.args['--temp'] self.install_keys = self.args[''] self.install_diff = not self.args['--nodiff'] self.install_showdiff = self.showdiff or self.args['--showdiff'] self.install_backup_suffix = BACKUP_SUFFIX self.install_default_actions_pre = [a for a in self.default_actions if a.kind == Action.pre] self.install_default_actions_post = [a for a in self.default_actions if a.kind == Action.post] self.install_ignore = self.instignore self.install_force_chmod = self.force_chmod self.install_clear_workdir = self.args['--workdir-clear'] or \ self.clear_workdir def _apply_args_compare(self): """compare specifics""" self.compare_focus = self.args['--file'] self.compare_ignore = self.args['--ignore'] self.compare_ignore.extend(self.cmpignore) self.compare_ignore.append(f'*{self.install_backup_suffix}') self.compare_ignore = uniq_list(self.compare_ignore) self.compare_fileonly = self.args['--file-only'] self.ignore_missing_in_dotdrop = self.ignore_missing_in_dotdrop or \ self.args['--ignore-missing'] def _apply_args_import(self): """import specifics""" self.import_path = self.args[''] self.import_as = self.args['--as'] self.import_mode = self.args['--preserve-mode'] or self.chmod_on_import self.import_ignore = self.args['--ignore'] self.import_ignore.extend(self.impignore) self.import_ignore.append(f'*{self.install_backup_suffix}') self.import_ignore = uniq_list(self.import_ignore) self.import_transw = self.args['--transw'] self.import_transr = self.args['--transr'] def _apply_args_update(self): """update specifics""" self.update_path = self.args[''] self.update_iskey = self.args['--key'] self.update_ignore = self.args['--ignore'] self.update_ignore.extend(self.upignore) self.update_ignore.append(f'*{self.install_backup_suffix}') self.update_ignore = uniq_list(self.update_ignore) self.update_showpatch = self.args['--show-patch'] def _apply_args_profiles(self): """profiles specifics""" self.profiles_grepable = self.args['--grepable'] def _apply_args_remove(self): """remove specifics""" self.remove_path = self.args[''] self.remove_iskey = self.args['--key'] def _apply_args_detail(self): """detail specifics""" self.detail_keys = self.args[''] def _apply_args(self): """apply cli args as attribute""" # the commands self.cmd_profiles = self.args['profiles'] self.cmd_files = self.args['files'] self.cmd_install = self.args['install'] self.cmd_compare = self.args['compare'] self.cmd_import = self.args['import'] self.cmd_update = self.args['update'] self.cmd_detail = self.args['detail'] self.cmd_remove = self.args['remove'] # adapt attributes based on arguments self.safe = not self.args['--force'] try: if ENV_WORKERS in os.environ: workers = int(os.environ[ENV_WORKERS]) else: workers = int(self.args['--workers']) self.workers = workers except ValueError: self.log.err('bad option for --workers') sys.exit(USAGE) # import link default value self.import_link = self.link_on_import if self.args['--link']: # overwrite default import link with cli switch link = self.args['--link'] if link not in OPT_LINK: self.log.err(f'bad option for --link: {link}') sys.exit(USAGE) self.import_link = OPT_LINK[link] # "files" specifics self._apply_args_files() # "install" specifics self._apply_args_install() # "compare" specifics self._apply_args_compare() # "import" specifics self._apply_args_import() # "update" specifics self._apply_args_update() # "profiles" specifics self._apply_args_profiles() # "detail" specifics self._apply_args_detail() # "remove" specifics self._apply_args_remove() def _fill_attr(self): """create attributes from conf""" # defined variables self.variables = self.conf.get_variables() # dotfiles for this profile self.dotfiles = self.conf.get_dotfiles(profile_key=self.profile) # all defined profiles self.profiles = self.conf.get_profiles() def _debug_attr(self): """debug display all of this class attributes""" if not self.debug: return self.log.dbg('effective options:') for att in dir(self): if att.startswith('_'): continue val = getattr(self, att) if callable(val): continue if isinstance(val, list): debug_list(f'-> {att}', val, self.debug) elif isinstance(val, dict): debug_dict(f'-> {att}', val, self.debug) else: self.log.dbg(f'-> {att}: {val}') def _attr_set(self, attr): """error when some inexistent attr is set""" raise OptionsException(f'bad option: {attr}')