def check_tags(tags): """Check the tags for correctness. :param tags: tags to check for correctness :raise: ValueError """ if not is_dict(tags): raise ValueError('expecting a JSON object') for tag, paths in tags.items(): if not (is_str(tag) and tag.startswith('@')): raise ValueError('invalid tag name {}'.format(tag)) tag_name_has_alphabet = False for ch in tag[1:]: if ch not in TAG_NAME_CHARS: raise ValueError('bad char {} in tag name {}'.format(ch, tag)) tag_name_has_alphabet |= ch.isalpha() if not tag_name_has_alphabet: raise ValueError('no alphabets in tag name {}'.format(tag)) if not (is_list(paths) and len(paths) > 0): raise ValueError('expecting a non-empty list for {}'.format(tag)) for path in paths: if not (is_str(path) and os.path.isdir(expand_path(path))): raise ValueError( 'invalid directory path {} for {}'.format(path, tag) )
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 check_tags(tags): """Check the tags for correctness. :param tags: tags to check for correctness :raise: ValueError """ if not is_dict(tags): raise ValueError('expecting a JSON object') for tag, paths in tags.items(): if not (is_str(tag) and tag.startswith('@')): raise ValueError('invalid tag name {}'.format(tag)) tag_name_has_alphabet = False for ch in tag[1:]: if ch not in TAG_NAME_CHARS: raise ValueError('bad char {} in tag name {}'.format(ch, tag)) tag_name_has_alphabet |= ch.isalpha() if not tag_name_has_alphabet: raise ValueError('no alphabets in tag name {}'.format(tag)) if not (is_list(paths) and len(paths) > 0): raise ValueError('expecting a non-empty list for {}'.format(tag)) for path in paths: if not (is_str(path) and os.path.isdir(expand_path(path))): raise ValueError('invalid directory path {} for {}'.format( path, tag))
def main(): global tmp_file, current_process, child_processes # https://stackoverflow.com/questions/25099895/ signal.signal(signal.SIGTTOU, signal.SIG_IGN) # Ensure that child processes are cleaned up signal.signal(signal.SIGINT, kill_signal_handler) signal.signal(signal.SIGTERM, kill_signal_handler) atexit.register(cleanup_resources) tag_to_paths = load_tags() parser = argparse.ArgumentParser(prog='run', description=cmd_description, usage='run [options] [targets] command', formatter_class=HelpFormatter) parser.add_argument('-p', '--parallel', help='run the command in parallel', action='store_true') parser.add_argument('-e', '--exit-codes', help='display the exit codes', action='store_true') parser.add_argument('arguments', type=str, nargs=argparse.REMAINDER, metavar='[targets]', help='tags or paths to run the command for' ).completer = ChoicesCompleter(tag_to_paths.keys()) autocomplete(parser) parsed = parser.parse_args() # Separate the targets from the command index = 0 targets = OrderedDict() while index < len(parsed.arguments): target = parsed.arguments[index] if target in tag_to_paths: for path in sorted(tag_to_paths[target].keys()): targets[path] = target index += 1 elif target.startswith('@'): parser.error('unknown tag {}'.format(target)) elif os.path.isdir(target): path = expand_path(target) if path not in targets: targets[path] = None index += 1 else: break # beginning of the command # Join the command arguments into a string command_arguments = parsed.arguments[index:] if len(command_arguments) == 0: command = None elif len(command_arguments) == 1: command = ' '.join(command_arguments) else: command = ' '.join("'{}'".format(arg) if ' ' in arg else arg for arg in command_arguments) if not (targets and command): parser.error('too few arguments') if parsed.parallel: # Run the command in parallel for path, tag in targets.items(): tmp_file = tempfile.TemporaryFile(mode='w+t') current_process = subprocess.Popen( '{} -i -c "{}"'.format(shell, command), cwd=path, shell=True, stdout=tmp_file, stderr=tmp_file, preexec_fn=os.setsid, ) child_processes.append((path, tag, current_process, tmp_file)) for path, tag, current_process, tmp_file in child_processes: exit_code = current_process.wait() tail = ' ({}{}{})'.format(PINK, tag, CLEAR) if tag else ':' print('{}>>> {}{}{}{}'.format(YELLOW, CYAN, path, tail, CLEAR)) tmp_file.seek(0) lines = tmp_file.readlines() offset = 0 if len(lines) > 0 and contains_ctrl_error_msg(lines[0]): offset += 1 if len(lines) > 1 and contains_ctrl_error_msg(lines[1]): offset += 1 sys.stdout.write(''.join(lines[offset:])) tmp_file.close() if parsed.exit_codes: print('{}>>> {}exit code: {}{}'.format(YELLOW, RED, exit_code, CLEAR)) sys.stdout.write('\n') else: # Run the command sequentially full_command = [] for path, tag in targets.items(): tag_info = ' ({}{}{})'.format(PINK, tag, CLEAR) if tag else ':' if parsed.exit_codes: tail = 'printf "{}>>> {}exit code: $?{}\n\n"'.format( YELLOW, RED, CLEAR) else: tail = 'printf "\n"' full_command.append( '(printf "{header}"; cd {path} && {cmd};{tail})'.format( header='{}>>> {}{}{}{}\n'.format(YELLOW, CYAN, path, CLEAR, tag_info), path=path, cmd=command, tail=tail)) subprocess.call( [shell, '-i', '-c', '{}'.format(';'.join(full_command))], stderr=sys.stdout.fileno()) # https://stackoverflow.com/questions/25099895/ os.tcsetpgrp(0, os.getpgrp()) sys.exit(0)
def main(): tag_to_paths = load_tags() parser = argparse.ArgumentParser( prog="untag", usage="untag [options] [[paths] [tags]...]", description=cmd_description, formatter_class=HelpFormatter) parser.add_argument('-a', '--all', help='untag all tags or paths', action='store_true') parser.add_argument('arguments', type=str, nargs=argparse.REMAINDER, metavar='[paths] [tags]', help='directory paths and tag names') parsed = parser.parse_args() if parsed.all: # Collect all the paths and tags to remove messages = set() tags_to_remove = set() paths_to_remove = set() for arg in parsed.arguments: if arg in tag_to_paths: tags_to_remove.add(arg) elif os.path.isdir(arg): paths_to_remove.add(expand_path(arg)) # Untag all the paths specified for tag, paths in tag_to_paths.items(): for path in [p for p in paths if p in paths_to_remove]: paths.pop(path) messages.add(msg.format(tag, path)) if len(paths) == 0: tags_to_remove.add(tag) # Remove all the tags specified for tag in tags_to_remove: paths = tag_to_paths.pop(tag) for path in paths: messages.add(msg.format(tag, expand_path(path))) # Save the updated tags and print messages save_tags(tag_to_paths) if messages: print('\n'.join(messages)) else: # Initialize tracking variables and collectors updates = [] arg_index = 0 parsing_paths = True tags_collected = set() paths_collected = set() # Iterate through the arguments and pair up tags with paths while arg_index < len(parsed.arguments): arg = parsed.arguments[arg_index] if parsing_paths and arg.startswith('@'): if len(paths_collected) == 0: parser.error('excepting paths before {}'.format(arg)) parsing_paths = False elif parsing_paths and not arg.startswith('@'): paths_collected.add(arg) arg_index += 1 elif not parsing_paths and arg.startswith('@'): tags_collected.add(arg) arg_index += 1 else: updates.append((tags_collected, paths_collected)) tags_collected, paths_collected = set(), set() parsing_paths = True if parsing_paths: parser.error('expecting a tag name') updates.append((tags_collected, paths_collected)) # Apply updates and collect messages to print messages = set() for tags, paths in updates: for tag in tags: if tag not in tag_to_paths: continue for path in paths: if not os.path.isdir(path): continue full_path = expand_path(path) if full_path in tag_to_paths[tag]: tag_to_paths[tag].pop(full_path) messages.add(msg.format(tag, full_path)) if len(tag_to_paths[tag]) == 0: # Remove the tag completely if it has no paths tag_to_paths.pop(tag) # Save the updated tags and print messages save_tags(tag_to_paths) if messages: print('\n'.join(messages))
def main(): tag_to_paths = load_tags() parser = ArgumentParser( prog="tag", usage="tag [[paths] [tags]...]", description=cmd_description, formatter_class=HelpFormatter ) parser.add_argument( "arguments", type=str, nargs="+", metavar="[paths] [tags]", help="directory paths and tag names" ) parsed = parser.parse_args() # Tracking variables and collectors updates = [] arg_index = 0 parsing_paths = True tags_collected = set() paths_collected = set() # Iterate through the arguments and pair up tags with paths while arg_index < len(parsed.arguments): arg = parsed.arguments[arg_index] if parsing_paths and arg.startswith("@"): if len(paths_collected) == 0: parser.error("expecting paths before {}".format(arg)) parsing_paths = False elif parsing_paths and not arg.startswith("@"): paths_collected.add(arg) arg_index += 1 elif not parsing_paths and arg.startswith("@"): tag_name_has_alphabet = False for ch in arg[1:]: if ch not in TAG_NAME_CHARS: parser.error("bad char {} in tag name {}".format(ch, arg)) tag_name_has_alphabet |= ch.isalpha() if not tag_name_has_alphabet: parser.error("no alphabets in tag name {}".format(arg)) tags_collected.add(arg) arg_index += 1 else: updates.append((tags_collected, paths_collected)) tags_collected, paths_collected = set(), set() parsing_paths = True if parsing_paths: parser.error("expecting a tag name") updates.append((tags_collected, paths_collected)) # Apply updates and message messages = set() for tags, paths in updates: valid_paths = [p for p in paths if os.path.isdir(p)] if len(valid_paths) == 0: continue for tag in tags: if tag not in tag_to_paths: tag_to_paths[tag] = {} for path in valid_paths: full_path = expand_path(path) short_path = shrink_path(path) tag_to_paths[tag][full_path] = short_path messages.add(msg.format(tag, full_path)) save_tags(tag_to_paths) if messages: print("\n".join(messages))
import os import json from dtags.chars import TAG_NAME_CHARS from dtags.utils import halt, expand_path, is_dict, is_list, is_str TAGS_FILE_PATH = expand_path('~/.dtags') TEMP_FILE_PATH = TAGS_FILE_PATH + '.tmp' 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
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))
def main(): global tmp_file, current_process, child_processes # https://stackoverflow.com/questions/25099895/ signal.signal(signal.SIGTTOU, signal.SIG_IGN) # Ensure that child processes are cleaned up signal.signal(signal.SIGINT, kill_signal_handler) signal.signal(signal.SIGTERM, kill_signal_handler) atexit.register(cleanup_resources) tag_to_paths = load_tags() parser = argparse.ArgumentParser( prog='run', description=cmd_description, usage='run [options] [targets] command', formatter_class=HelpFormatter ) parser.add_argument( '-p', '--parallel', help='run the command in parallel', action='store_true' ) parser.add_argument( '-e', '--exit-codes', help='display the exit codes', action='store_true' ) parser.add_argument( 'arguments', type=str, nargs=argparse.REMAINDER, metavar='[targets]', help='tags or paths to run the command for' ).completer = ChoicesCompleter(tag_to_paths.keys()) autocomplete(parser) parsed = parser.parse_args() # Separate the targets from the command index = 0 targets = OrderedDict() while index < len(parsed.arguments): target = parsed.arguments[index] if target in tag_to_paths: for path in sorted(tag_to_paths[target].keys()): targets[path] = target index += 1 elif target.startswith('@'): parser.error('unknown tag {}'.format(target)) elif os.path.isdir(target): path = expand_path(target) if path not in targets: targets[path] = None index += 1 else: break # beginning of the command # Join the command arguments into a string command_arguments = parsed.arguments[index:] if len(command_arguments) == 0: command = None elif len(command_arguments) == 1: command = ' '.join(command_arguments) else: command = ' '.join( "'{}'".format(arg) if ' ' in arg else arg for arg in command_arguments ) if not (targets and command): parser.error('too few arguments') if parsed.parallel: # Run the command in parallel for path, tag in targets.items(): tmp_file = tempfile.TemporaryFile(mode='w+t') current_process = subprocess.Popen( '{} -i -c "{}"'.format(shell, command), cwd=path, shell=True, stdout=tmp_file, stderr=tmp_file, preexec_fn=os.setsid, ) child_processes.append((path, tag, current_process, tmp_file)) for path, tag, current_process, tmp_file in child_processes: exit_code = current_process.wait() tail = ' ({}{}{})'.format(PINK, tag, CLEAR) if tag else ':' print('{}>>> {}{}{}{}'.format( YELLOW, CYAN, path, tail, CLEAR )) tmp_file.seek(0) lines = tmp_file.readlines() offset = 0 if len(lines) > 0 and contains_ctrl_error_msg(lines[0]): offset += 1 if len(lines) > 1 and contains_ctrl_error_msg(lines[1]): offset += 1 sys.stdout.write(''.join(lines[offset:])) tmp_file.close() if parsed.exit_codes: print('{}>>> {}exit code: {}{}'.format( YELLOW, RED, exit_code, CLEAR )) sys.stdout.write('\n') else: # Run the command sequentially full_command = [] for path, tag in targets.items(): tag_info = ' ({}{}{})'.format(PINK, tag, CLEAR) if tag else ':' if parsed.exit_codes: tail = 'printf "{}>>> {}exit code: $?{}\n\n"'.format( YELLOW, RED, CLEAR ) else: tail = 'printf "\n"' full_command.append( '(printf "{header}"; cd {path} && {cmd};{tail})'.format( header='{}>>> {}{}{}{}\n'.format( YELLOW, CYAN, path, CLEAR, tag_info ), path=path, cmd=command, tail=tail ) ) subprocess.call( [shell, '-i', '-c', '{}'.format(';'.join(full_command))], stderr=sys.stdout.fileno() ) # https://stackoverflow.com/questions/25099895/ os.tcsetpgrp(0, os.getpgrp()) sys.exit(0)
def main(): tag_to_paths = load_tags() parser = argparse.ArgumentParser( prog="untag", usage="untag [options] [[paths] [tags]...]", description=cmd_description, formatter_class=HelpFormatter ) parser.add_argument( '-a', '--all', help='untag all tags or paths', action='store_true' ) parser.add_argument( 'arguments', type=str, nargs=argparse.REMAINDER, metavar='[paths] [tags]', help='directory paths and tag names' ) parsed = parser.parse_args() if parsed.all: # Collect all the paths and tags to remove messages = set() tags_to_remove = set() paths_to_remove = set() for arg in parsed.arguments: if arg in tag_to_paths: tags_to_remove.add(arg) elif os.path.isdir(arg): paths_to_remove.add(expand_path(arg)) # Untag all the paths specified for tag, paths in tag_to_paths.items(): for path in [p for p in paths if p in paths_to_remove]: paths.pop(path) messages.add(msg.format(tag, path)) if len(paths) == 0: tags_to_remove.add(tag) # Remove all the tags specified for tag in tags_to_remove: paths = tag_to_paths.pop(tag) for path in paths: messages.add(msg.format(tag, expand_path(path))) # Save the updated tags and print messages save_tags(tag_to_paths) if messages: print('\n'.join(messages)) else: # Initialize tracking variables and collectors updates = [] arg_index = 0 parsing_paths = True tags_collected = set() paths_collected = set() # Iterate through the arguments and pair up tags with paths while arg_index < len(parsed.arguments): arg = parsed.arguments[arg_index] if parsing_paths and arg.startswith('@'): if len(paths_collected) == 0: parser.error('excepting paths before {}'.format(arg)) parsing_paths = False elif parsing_paths and not arg.startswith('@'): paths_collected.add(arg) arg_index += 1 elif not parsing_paths and arg.startswith('@'): tags_collected.add(arg) arg_index += 1 else: updates.append((tags_collected, paths_collected)) tags_collected, paths_collected = set(), set() parsing_paths = True if parsing_paths: parser.error('expecting a tag name') updates.append((tags_collected, paths_collected)) # Apply updates and collect messages to print messages = set() for tags, paths in updates: for tag in tags: if tag not in tag_to_paths: continue for path in paths: if not os.path.isdir(path): continue full_path = expand_path(path) if full_path in tag_to_paths[tag]: tag_to_paths[tag].pop(full_path) messages.add(msg.format(tag, full_path)) if len(tag_to_paths[tag]) == 0: # Remove the tag completely if it has no paths tag_to_paths.pop(tag) # Save the updated tags and print messages save_tags(tag_to_paths) if messages: print('\n'.join(messages))
def main(): tag_to_paths = load_tags() parser = ArgumentParser( prog='tag', usage='tag [[paths] [tags]...]', description=cmd_description, formatter_class=HelpFormatter ) parser.add_argument( 'arguments', type=str, nargs='+', metavar='[paths] [tags]', help='directory paths and tag names' ) parsed = parser.parse_args() # Tracking variables and collectors updates = [] arg_index = 0 parsing_paths = True tags_collected = set() paths_collected = set() # Iterate through the arguments and pair up tags with paths while arg_index < len(parsed.arguments): arg = parsed.arguments[arg_index] if parsing_paths and arg.startswith('@'): if len(paths_collected) == 0: parser.error('expecting paths before {}'.format(arg)) parsing_paths = False elif parsing_paths and not arg.startswith('@'): paths_collected.add(arg) arg_index += 1 elif not parsing_paths and arg.startswith('@'): tag_name_has_alphabet = False for ch in arg[1:]: if ch not in TAG_NAME_CHARS: parser.error('bad char {} in tag name {}'.format(ch, arg)) tag_name_has_alphabet |= ch.isalpha() if not tag_name_has_alphabet: parser.error('no alphabets in tag name {}'.format(arg)) tags_collected.add(arg) arg_index += 1 else: updates.append((tags_collected, paths_collected)) tags_collected, paths_collected = set(), set() parsing_paths = True if parsing_paths: parser.error('expecting a tag name') updates.append((tags_collected, paths_collected)) # Apply updates and message messages = set() for tags, paths in updates: valid_paths = [p for p in paths if os.path.isdir(p)] if len(valid_paths) == 0: continue for tag in tags: if tag not in tag_to_paths: tag_to_paths[tag] = {} for path in valid_paths: full_path = expand_path(path) short_path = shrink_path(path) tag_to_paths[tag][full_path] = short_path messages.add(msg.format(tag, full_path)) save_tags(tag_to_paths) if messages: print('\n'.join(messages))