Example #1
0
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)
Example #2
0
File: tag.py Project: xexes/dtags
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))
Example #3
0
File: untag.py Project: jean/dtags
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))
Example #4
0
File: tags.py Project: nkhuyu/dtags
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))
Example #5
0
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))
Example #6
0
File: run.py Project: nkhuyu/dtags
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)
Example #7
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))
Example #8
0
File: tag.py Project: jean/dtags
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))