def load_tags(): """Load the tags from disk. Return a dictionary which maps tag names to sub-dictionaries, which in turn maps fully expanded paths to unexpanded paths like this: { "@app" : { "/home/user/app/backend": "~/app/backend", "/home/user/app/frontend": "~/app/frontend", }, "@frontend": { "/home/user/app/frontend": "~/app/frontend", }, "@backend": { "/home/user/app/backend": "~/app/backend", } ... } :return: the tags loaded from the disk """ if not os.path.exists(TAGS_FILE_PATH): try: with open(TAGS_FILE_PATH, 'w') as open_file: json.dump({}, open_file) return {} except (IOError, OSError): halt('Failed to initialize {}'.format(TAGS_FILE_PATH)) else: try: with open(TAGS_FILE_PATH, 'r') as open_file: json_str = open_file.read().strip() if not json_str: return {} tag_data = json.loads(json_str) if not tag_data: return {} else: return { tag: {expand_path(path): path for path in paths} for tag, paths in tag_data.items() } except (ValueError, IOError, OSError): # TODO better error handling and messaging here halt('Failed to load {}'.format(TAGS_FILE_PATH))
def save_tags(tags): """Save the tags to disk. Convert the incoming tags dictionary to JSON and dump the content to the file. For portability, only the unexpanded paths are saved. :param tags: tags to save to disk """ try: with open(TEMP_FILE_PATH, 'w') as open_file: json.dump( {tag: sorted(paths.values()) for tag, paths in tags.items()}, open_file, indent=4, sort_keys=True ) except IOError: # TODO better error handling and messaging here halt('Failed to write to {}'.format(TAGS_FILE_PATH)) else: # Overwrite the old file with the new os.rename(TEMP_FILE_PATH, TAGS_FILE_PATH)
def save_tags(tags): """Save the tags to disk. Convert the incoming tags dictionary to JSON and dump the content to the file. For portability, only the unexpanded paths are saved. :param tags: tags to save to disk """ try: with open(TEMP_FILE_PATH, 'w') as open_file: json.dump( {tag: sorted(paths.values()) for tag, paths in tags.items()}, open_file, indent=4, sort_keys=True) except IOError: # TODO better error handling and messaging here halt('Failed to write to {}'.format(TAGS_FILE_PATH)) else: # Overwrite the old file with the new os.rename(TEMP_FILE_PATH, TAGS_FILE_PATH)
def main(): tag_to_paths = load_tags() path_to_tags = defaultdict(set) for tag, paths in tag_to_paths.items(): for path in paths.keys(): path_to_tags[path].add(tag) parser = ArgumentParser( prog='tags', description=cmd_description, usage='tags [options] [paths] [tags]', formatter_class=HelpFormatter ) parser.add_argument( '-e', '--edit', action='store_true', help='Edit the tags directly using an editor' ) parser.add_argument( '-x', '--expand', action='store_true', help='Expand the directory paths' ) parser.add_argument( '-r', '--reverse', help='display the reverse mapping', action='store_true' ) parser.add_argument( '-j', '--json', help='display the raw JSON', action='store_true' ) parser.add_argument( 'search_terms', type=str, nargs='*', metavar='[paths] [tags]', help='tag and directory paths to filter by' ).completer = ChoicesCompleter(tag_to_paths.keys()) autocomplete(parser) parsed = parser.parse_args() if parsed.edit: if parsed.search_terms: raise parser.error('no arguments allowed with option -e/--edit') edit_file_path = TAGS_FILE_PATH + '.edit' shutil.copy2(TAGS_FILE_PATH, edit_file_path) subprocess.call([os.environ.get('EDITOR', 'vi'), edit_file_path]) new_tag_to_paths = save_error = None with open(edit_file_path, 'r') as edit_file: try: new_tag_to_paths = json.load(edit_file) except ValueError as err: save_error = 'Failed to save tags: {}'.format(msgify(err)) else: try: check_tags(new_tag_to_paths) except ValueError as err: save_error = 'Failed to save tags: {}'.format(err) safe_remove(edit_file_path) if save_error is not None: halt(save_error) save_tags({ tag: {expand_path(p): shrink_path(p) for p in paths} for tag, paths in new_tag_to_paths.items() }) print('New tags saved successfully') sys.exit(0) if len(tag_to_paths) == 0: print('No tags defined') sys.exit(0) # Filter by any given tags and paths # TODO optimize here if possible if not parsed.search_terms: if parsed.expand: filtered = {t: ps.keys() for t, ps in tag_to_paths.items()} else: filtered = {t: ps.values() for t, ps in tag_to_paths.items()} else: filtered = {} for term in parsed.search_terms: if term in tag_to_paths: if parsed.expand: filtered[term] = tag_to_paths[term].keys() else: filtered[term] = tag_to_paths[term].values() elif os.path.isdir(term): term = expand_path(term) if term in path_to_tags: for tag in path_to_tags[term]: if parsed.expand: filtered[tag] = tag_to_paths[tag].keys() else: filtered[tag] = tag_to_paths[tag].values() if parsed.json: formatted = {tag: sorted(paths) for tag, paths in filtered.items()} print(json.dumps(formatted, sort_keys=True, indent=4)) elif parsed.reverse: reverse = defaultdict(set) for tag, paths in filtered.items(): for path in paths: reverse[path].add(tag) for path, tags in reverse.items(): print('{}{}{}'.format(CYAN, path, CLEAR)) print('{}{}{}\n'.format(PINK, ' '.join(sorted(tags)), CLEAR)) else: for tag, paths in sorted(filtered.items()): print('{}{}{}'.format(PINK, tag, CLEAR)) print('{}{}{}\n'.format(CYAN, '\n'.join(sorted(paths)), CLEAR))
def main(): tag_to_paths = load_tags() path_to_tags = defaultdict(set) for tag, paths in tag_to_paths.items(): for path in paths.keys(): path_to_tags[path].add(tag) parser = ArgumentParser(prog='tags', description=cmd_description, usage='tags [options] [paths] [tags]', formatter_class=HelpFormatter) parser.add_argument('-e', '--edit', action='store_true', help='Edit the tags directly using an editor') parser.add_argument('-x', '--expand', action='store_true', help='Expand the directory paths') parser.add_argument('-r', '--reverse', help='display the reverse mapping', action='store_true') parser.add_argument('-j', '--json', help='display the raw JSON', action='store_true') parser.add_argument('search_terms', type=str, nargs='*', metavar='[paths] [tags]', help='tag and directory paths to filter by' ).completer = ChoicesCompleter(tag_to_paths.keys()) autocomplete(parser) parsed = parser.parse_args() if parsed.edit: if parsed.search_terms: raise parser.error('no arguments allowed with option -e/--edit') edit_file_path = TAGS_FILE_PATH + '.edit' shutil.copy2(TAGS_FILE_PATH, edit_file_path) subprocess.call([os.environ.get('EDITOR', 'vi'), edit_file_path]) new_tag_to_paths = save_error = None with open(edit_file_path, 'r') as edit_file: try: new_tag_to_paths = json.load(edit_file) except ValueError as err: save_error = 'Failed to save tags: {}'.format(msgify(err)) else: try: check_tags(new_tag_to_paths) except ValueError as err: save_error = 'Failed to save tags: {}'.format(err) safe_remove(edit_file_path) if save_error is not None: halt(save_error) save_tags({ tag: {expand_path(p): shrink_path(p) for p in paths} for tag, paths in new_tag_to_paths.items() }) print('New tags saved successfully') sys.exit(0) if len(tag_to_paths) == 0: print('No tags defined') sys.exit(0) # Filter by any given tags and paths # TODO optimize here if possible if not parsed.search_terms: if parsed.expand: filtered = {t: ps.keys() for t, ps in tag_to_paths.items()} else: filtered = {t: ps.values() for t, ps in tag_to_paths.items()} else: filtered = {} for term in parsed.search_terms: if term in tag_to_paths: if parsed.expand: filtered[term] = tag_to_paths[term].keys() else: filtered[term] = tag_to_paths[term].values() elif os.path.isdir(term): term = expand_path(term) if term in path_to_tags: for tag in path_to_tags[term]: if parsed.expand: filtered[tag] = tag_to_paths[tag].keys() else: filtered[tag] = tag_to_paths[tag].values() if parsed.json: formatted = {tag: sorted(paths) for tag, paths in filtered.items()} print(json.dumps(formatted, sort_keys=True, indent=4)) elif parsed.reverse: reverse = defaultdict(set) for tag, paths in filtered.items(): for path in paths: reverse[path].add(tag) for path, tags in reverse.items(): print('{}{}{}'.format(CYAN, path, CLEAR)) print('{}{}{}\n'.format(PINK, ' '.join(sorted(tags)), CLEAR)) else: for tag, paths in sorted(filtered.items()): print('{}{}{}'.format(PINK, tag, CLEAR)) print('{}{}{}\n'.format(CYAN, '\n'.join(sorted(paths)), CLEAR))