def pre_run(self, args): # For Windows users, expand any glob patterns as needed. args.conf = list(expand_glob_list(args.conf))
def run(self, args): # Ignores case sensitivity. If you're on Windows, name your files right. conf_file_re = re.compile("([a-z]+\.conf|(default|local)\.meta)$") if args.target is None: self.stderr.write("Must provide the '--target' directory.\n") return EXIT_CODE_MISSING_ARG self.stderr.write("Combining conf files into directory {}\n".format( args.target)) args.source = list(expand_glob_list(args.source)) for src in args.source: self.stderr.write( "Reading conf files from directory {}\n".format(src)) marker_file = os.path.join(args.target, CONTROLLED_DIR_MARKER) if os.path.isdir(args.target): if not args.disable_marker and not os.path.isfile(marker_file): self.stderr.write( "Target directory already exists, but it appears to have been " "created by some other means. Marker file missing.\n") return EXIT_CODE_COMBINE_MARKER_MISSING elif args.dry_run: self.stderr.write( "Skipping creating destination directory {0} (dry-run)\n". format(args.target)) else: self.stderr.write("Creating destination directory {0}\n".format( args.target)) os.mkdir(args.target) if not args.disable_marker: open(marker_file, "w").write( "This directory is managed by KSCONF. Don't touch\n") # Build a common tree of all src files. src_file_index = defaultdict(list) for src_root in args.source: for (root, dirs, files) in relwalk(src_root, followlinks=args.follow_symlink): for fn in files: # Todo: Add blocklist CLI support: defaults to consider: *sw[po], .git*, .bak, .~ if fn.endswith(".swp") or fn.endswith("*.bak"): continue # pragma: no cover (peephole optimization) src_file = os.path.join(root, fn) src_path = os.path.join(src_root, root, fn) src_file_index[src_file].append(src_path) # Find a set of files that exist in the target folder, but in NO source folder (for cleanup) target_extra_files = set() for (root, dirs, files) in relwalk(args.target, followlinks=args.follow_symlink): for fn in files: tgt_file = os.path.join(root, fn) if tgt_file not in src_file_index: # Todo: Add support for additional blocklist wildcards (using fnmatch) if fn == CONTROLLED_DIR_MARKER or fn.endswith(".bak"): continue # pragma: no cover (peephole optimization) target_extra_files.add(tgt_file) for (dest_fn, src_files) in sorted(src_file_index.items()): # Source file must be in sort order (10-x is lower prio and therefore replaced by 90-z) src_files = sorted(src_files) dest_path = os.path.join(args.target, dest_fn) # Make missing destination folder, if missing dest_dir = os.path.dirname(dest_path) if not os.path.isdir(dest_dir) and not args.dry_run: os.makedirs(dest_dir) # Handle conf files and non-conf files separately if not conf_file_re.search(dest_fn): # self.stderr.write("Considering {0:50} NON-CONF Copy from source: {1!r}\n".format(dest_fn, src_files[-1])) # Always use the last file in the list (since last directory always wins) src_file = src_files[-1] if args.dry_run: if os.path.isfile(dest_path): if file_compare(src_file, dest_path): smart_rc = SMART_NOCHANGE else: if (_is_binary_file(src_file) or _is_binary_file(dest_path)): # Binary files. Can't compare... smart_rc = "DRY-RUN (NO-DIFF=BIN)" else: show_text_diff(self.stdout, dest_path, src_file) smart_rc = "DRY-RUN (DIFF)" else: smart_rc = "DRY-RUN (NEW)" else: smart_rc = smart_copy(src_file, dest_path) if smart_rc != SMART_NOCHANGE: self.stderr.write("Copy <{0}> {1:50} from {2}\n".format( smart_rc, dest_path, src_file)) else: try: # Handle merging conf files dest = ConfFileProxy(os.path.join(args.target, dest_fn), "r+", parse_profile=PARSECONF_MID) srcs = [ ConfFileProxy(sf, "r", parse_profile=PARSECONF_STRICT) for sf in src_files ] # self.stderr.write("Considering {0:50} CONF MERGE from source: {1!r}\n".format(dest_fn, src_files[0])) smart_rc = merge_conf_files(dest, srcs, dry_run=args.dry_run, banner_comment=args.banner) if smart_rc != SMART_NOCHANGE: self.stderr.write( "Merge <{0}> {1:50} from {2!r}\n".format( smart_rc, dest_path, src_files)) finally: # Protect against any dangling open files: (ResourceWarning: unclosed file) dest.close() for src in srcs: src.close() if True and target_extra_files: # Todo: Allow for cleanup to be disabled via CLI self.stderr.write( "Cleaning up extra files not part of source tree(s): {0} files.\n" .format(len(target_extra_files))) for dest_fn in target_extra_files: self.stderr.write("Remove unwanted file {0}\n".format(dest_fn)) os.unlink(os.path.join(args.target, dest_fn))