def refresh(): """Write the shell declare variable statements to stdout.""" atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if len(args) >= 2: abort(USAGE + 'dtags: too many arguments') if len(args) == 1: shell_name = args[0] config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('dtags: unsupported shell: ' + style.bad(shell_name)) else: shell_path = os.environ.get('SHELL') if shell_path is None: abort('dtags: undefined environment variable: ' + style.bad('SHELL')) shell_name = None for supported_shell_name in SUPPORTED_SHELLS: if supported_shell_name in shell_path: shell_name = supported_shell_name config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('dtags: unsupported shell: ' + style.bad(shell_path)) assign_string = 'set -g {} "{}"' if shell_name == 'fish' else '{}="{}"' mapping, _ = load_mapping() print('\n'.join( assign_string.format(tag.replace('-', '_'), paths.pop()) for tag, paths in mapping.items() if len(paths) == 1 and tag.replace('-', '_') not in os.environ and not get_invalid_tag_chars(tag) ))
def main(): atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) mapping, excluded = load_mapping() args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head.startswith('-'): abort(USAGE + 'u: invalid argument: ' + style.bad(head)) path = expand(head) tags_removed = set() for tag in tail if tail else mapping.keys(): if path in mapping[tag]: mapping[tag].remove(path) tags_removed.add(tag) if not tags_removed: finish('Nothing to do') save_mapping(mapping) if excluded: print('Cleaned the following invalid entries:\n' + excluded + '\n') finish( style.path(path) + ' ' + ' '.join( style.sign('-') + style.tag(tag) for tag in sorted(tags_removed)))
def main(): atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) mapping, excluded = load_mapping() args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head.startswith('-'): abort(USAGE + 'u: invalid argument: ' + style.bad(head)) path = expand(head) tags_removed = set() for tag in tail if tail else mapping.keys(): if path in mapping[tag]: mapping[tag].remove(path) tags_removed.add(tag) if not tags_removed: finish('Nothing to do') save_mapping(mapping) if excluded: print('Cleaned the following invalid entries:\n' + excluded + '\n') finish(style.path(path) + ' ' + ' '.join( style.sign('-') + style.tag(tag) for tag in sorted(tags_removed) ))
def _edit(args): """Edit the mapping directly using an editor.""" if args: abort(USAGE + 'dtags: too many arguments') try: with TempFile( mode='w+t', delete=False, prefix='mapping.', dir=CFG_DIR ) as tfile: with io.open(MAPPING_FILE, 'rt') as mapping_file: tfile.write(EDIT_HELP_COMMENTS + mapping_file.read()) tfile.flush() except (OSError, IOError) as err: abort('dtags: failed to edit mapping: {}'.format(err), err.errno) else: editor = shlex.split(os.environ.get('EDITOR')) if not editor: abort('dtags: undefined environment variable: ' + style.bad('EDITOR')) try: sp.check_call(editor + [tfile.name]) except sp.CalledProcessError as err: abort('dtags: failed to edit mapping: {}'.format(err.message)) else: mapping, excluded = parse_mapping(tfile.name) save_mapping(mapping) rm_files(tfile.name) if excluded: print('Cleaned the following entries:\n' + excluded + '\n') finish('New entries saved successfully')
def main(): atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == "--help": finish(USAGE + DESCRIPTION) elif head == "--version": finish("Version " + VERSION) path = expand(head) invalid_path_chars = get_invalid_path_chars(path) if invalid_path_chars: abort( "t: directory path {} contains bad characters {}".format( style.bad(path), style.bad_chars(invalid_path_chars) ) ) if not os.path.isdir(path): abort("t: invalid directory: " + style.bad(path)) mapping, excluded = load_mapping() tags_added = set() if not tail: tail.append(os.path.basename(path)) for tag in tail: if not tag[0].isalpha(): abort("t: tag name {} does not start with an alphabet".format(style.bad(tag))) if " " in tag: abort("t: tag name {} contains whitespaces".format(style.bad(tag))) invalid_tag_chars = get_invalid_tag_chars(tag) if invalid_tag_chars: abort( "t: tag name {} contains bad characters {}".format(style.bad(tag), style.bad_chars(invalid_tag_chars)) ) if path not in mapping[tag]: mapping[tag].add(path) tags_added.add(tag) if tags_added or excluded: save_mapping(mapping) if excluded: print("Cleaned the following invalid entries:\n" + excluded + "\n") if not tags_added: finish("Nothing to do") else: finish(style.path(path) + " " + " ".join(style.sign("+") + style.tag(tag) for tag in sorted(tags_added)))
def main(): atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) path = expand(head) invalid_path_chars = get_invalid_path_chars(path) if invalid_path_chars: abort('t: directory path {} contains bad characters {}'.format( style.bad(path), style.bad_chars(invalid_path_chars))) if not os.path.isdir(path): abort('t: invalid directory: ' + style.bad(path)) mapping, excluded = load_mapping() tags_added = set() if not tail: tail.append(os.path.basename(path)) for tag in tail: if not tag[0].isalpha(): abort('t: tag name {} does not start with an alphabet'.format( style.bad(tag))) if ' ' in tag: abort('t: tag name {} contains whitespaces'.format(style.bad(tag))) invalid_tag_chars = get_invalid_tag_chars(tag) if invalid_tag_chars: abort('t: tag name {} contains bad characters {}'.format( style.bad(tag), style.bad_chars(invalid_tag_chars))) if path not in mapping[tag]: mapping[tag].add(path) tags_added.add(tag) if tags_added or excluded: save_mapping(mapping) if excluded: print('Cleaned the following invalid entries:\n' + excluded + '\n') if not tags_added: finish('Nothing to do') else: finish( style.path(path) + ' ' + ' '.join( style.sign('+') + style.tag(tag) for tag in sorted(tags_added)))
def activate(): """Write the shell runtime configuration to stdout.""" atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if len(args) >= 2: abort(USAGE + 'dtags: too many arguments') if len(args) == 1: shell_name = args[0] config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('dtags: unsupported shell: ' + style.bad(shell_name)) else: shell_path = os.environ.get('SHELL') if shell_path is None: abort('dtags: undefined environment variable: ' + style.bad('SHELL')) shell_name = None for supported_shell_name in SUPPORTED_SHELLS: if supported_shell_name in shell_path: shell_name = supported_shell_name config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('dtags: unsupported shell: ' + style.bad(shell_path)) finish( config.format( mapping_file=MAPPING_FILE, tags_file=TAGS_FILE, version=VERSION, usage=d.USAGE, description=d.DESCRIPTION, arg_err_tty=d.ARG_ERR_TTY, dest_err_tty=d.DEST_ERR_TTY, input_err_tty=d.INPUT_ERR_TTY, index_err_tty=d.INDEX_ERR_TTY, prompt_tty=d.PROMPT_TTY, choice_tty=d.CHOICE_TTY, arg_err=d.ARG_ERR, dest_err=d.DEST_ERR, input_err=d.INPUT_ERR, index_err=d.INDEX_ERR, prompt=d.PROMPT, choice=d.CHOICE, ))
def activate(): """Write the shell runtime configuration to stdout.""" atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if len(args) >= 2: abort(USAGE + 'dtags: too many arguments') if len(args) == 1: shell_name = args[0] config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('dtags: unsupported shell: ' + style.bad(shell_name)) else: shell_path = os.environ.get('SHELL') if shell_path is None: abort('dtags: undefined environment variable: ' + style.bad('SHELL')) shell_name = None for supported_shell_name in SUPPORTED_SHELLS: if supported_shell_name in shell_path: shell_name = supported_shell_name config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('dtags: unsupported shell: ' + style.bad(shell_path)) finish(config.format( mapping_file=MAPPING_FILE, tags_file=TAGS_FILE, version=VERSION, usage=d.USAGE, description=d.DESCRIPTION, arg_err_tty=d.ARG_ERR_TTY, dest_err_tty=d.DEST_ERR_TTY, input_err_tty=d.INPUT_ERR_TTY, index_err_tty=d.INDEX_ERR_TTY, prompt_tty=d.PROMPT_TTY, choice_tty=d.CHOICE_TTY, arg_err=d.ARG_ERR, dest_err=d.DEST_ERR, input_err=d.INPUT_ERR, index_err=d.INDEX_ERR, prompt=d.PROMPT, choice=d.CHOICE, ))
def _list(targets=None, reverse=False): """List directories and tags. :param targets: the list of directories or tags to highlight :param reverse: whether to display the reverse mapping or not """ if targets is None: targets = set() else: targets = set(targets) additional_targets = set() for target in targets: if target.startswith('-'): abort(USAGE + 'dtags: invalid argument: ' + style.bad(target)) else: additional_targets.add(expand(target)) targets.update(additional_targets) mapping, excluded = load_mapping() if excluded: print( 'Found invalid entries in the mapping:\n' + excluded + '\nRun ' + style.cmd('dtags clean') + ' to remove them' + '\n' ) if not mapping: finish('Nothing to list') msgs = [] if reverse: if targets: mapping = { tag: paths for tag, paths in mapping.items() if tag in targets or paths & targets } for tag in sorted(mapping): lines = [style.tag(tag, tag in targets)] for path in sorted(mapping[tag]): lines.append(style.path(path, path in targets)) msgs.append('\n'.join(lines)) finish('\n\n'.join(msgs) if msgs else 'Nothing to list') else: rmapping = defaultdict(set) for tag, paths in mapping.items(): for path in paths: rmapping[path].add(tag) if targets: rmapping = { path: tags for path, tags in rmapping.items() if path in targets or tags & targets } for path in sorted(rmapping): tags = ' '.join( style.tag(tag, tag in targets) for tag in sorted(rmapping[path]) ) msgs.append(style.path(path, path in targets) + ' ' + tags) finish('\n'.join(msgs) if msgs else 'Nothing to list')
def _shell(args): """Write the shell runtime configuration to stdout.""" if len(args) >= 2: abort(USAGE + 'Too many arguments') if len(args) == 1: shell_name = args[0] config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('Unsupported shell: ' + style.bad(shell_name)) else: shell_path = os.environ.get('SHELL') if shell_path is None: abort('Undefined environment variable: ' + style.bad('SHELL')) shell_name = None for supported_shell_name in SUPPORTED_SHELLS: if supported_shell_name in shell_path: shell_name = supported_shell_name config = SUPPORTED_SHELLS.get(shell_name) if config is None: abort('Unsupported shell: ' + style.bad(shell_path)) finish(config.format( mapping_file=MAPPING_FILE, tags_file=TAGS_FILE, version=VERSION, usage=directory.USAGE, description=directory.DESCRIPTION, goto_msg_tty=directory.GOTO_MSG_TTY, arg_err_tty=directory.ARG_ERR_TTY, dest_err_tty=directory.DEST_ERR_TTY, input_err_tty=directory.INPUT_ERR_TTY, index_err_tty=directory.INDEX_ERR_TTY, prompt_tty=directory.PROMPT_TTY, choice_tty=directory.CHOICE_TTY, goto_msg=directory.GOTO_MSG, arg_err=directory.ARG_ERR, dest_err=directory.DEST_ERR, input_err=directory.INPUT_ERR, index_err=directory.INDEX_ERR, prompt=directory.PROMPT, choice=directory.CHOICE, ))
def main(): atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head.startswith('-'): abort(USAGE + 'Invalid argument: ' + style.bad(head)) path = expand(head) invalid_path_chars = get_invalid_path_chars(path) if invalid_path_chars: abort('Directory path {} contains bad characters {}'.format( style.bad(path), style.bad_chars(invalid_path_chars) )) if not os.path.isdir(path): abort('Invalid directory ' + style.bad(path)) mapping, excluded = load_mapping() tags_added = set() if not tail: tail.append(os.path.basename(path)) for tag in tail: if not tag[0].isalpha(): abort('Tag name {} does not start with an alphabet' .format(style.bad(tag))) if ' ' in tag: abort('Tag name {} contains whitespaces'.format(style.bad(tag))) invalid_tag_chars = get_invalid_tag_chars(tag) if invalid_tag_chars: abort('Tag name {} contains bad characters {}'.format( style.bad(tag), style.bad_chars(invalid_tag_chars) )) if path not in mapping[tag]: mapping[tag].add(path) tags_added.add(tag) if tags_added or excluded: save_mapping(mapping) if excluded: print('Cleaned the following invalid entries:\n' + excluded + '\n') if not tags_added: finish('Nothing to do') else: finish(style.path(path) + ' ' + ' '.join( style.sign('+') + style.tag(tag) for tag in sorted(tags_added) ))
def main(): atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if not args: _list() head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head == 'edit': _edit(tail) elif head == 'clean': _clean(tail) elif head == 'list': _list(tail, reverse=False) elif head == 'reverse': _list(tail, reverse=True) elif head == 'shell': _shell(tail) else: abort(USAGE + 'Invalid argument: ' + style.bad(head))
def manage(): """Manage directory tags.""" atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) args = sys.argv[1:] if not args: _list() head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head == 'edit': _edit(tail) elif head == 'clean': _clean(tail) elif head == 'list': _list(tail, reverse=False) elif head == 'reverse': _list(tail, reverse=True) elif head == 'commands': _commands(tail) else: abort(USAGE + 'dtags: invalid argument: ' + style.bad(head))
def main(): global temp_file, process, processes atexit.register(_cleanup_resources) signal.signal(signal.SIGPIPE, signal.SIG_DFL) mapping, excluded = load_mapping() if excluded: print( 'Found invalid entries in the mapping:\n' + excluded + '\nRun ' + style.cmd('dtags clean') + ' to remove them' + '\n' ) # Parse the optional arguments interactive = False args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head == '-i' and not tail: abort(USAGE + 'p: missing argument: ' + style.bad('<targets>')) elif head == '-i' and tail: interactive = True head, tail = tail[0], tail[1:] elif head.startswith('-'): abort(USAGE + 'p: invalid argument: ' + style.bad(head)) # Parse the positional arguments if not tail: abort(USAGE + 'p: missing argument: ' + style.bad('<command>')) directories = collections.defaultdict(set) for target in head.split(','): if not target: continue elif target in mapping: for directory in sorted(mapping[target]): directories[directory].add(target) else: path = expand(target) if os.path.isdir(path): directories[path].add(None) else: abort(USAGE + 'p: invalid target: ' + style.bad(target)) # Check which shell is in use (e.g. zsh, bash, fish) shell = os.environ.get('SHELL') if shell is None: abort('p: undefined environment variable: ' + style.bad('SHELL')) if interactive: cmd = [shell, '-i', '-c', sp.list2cmdline(tail)] else: cmd = [shell, '-c', sp.list2cmdline(tail)] # Add signal handlers to terminate child processes gracefully signal.signal(signal.SIGINT, _handle_signal) signal.signal(signal.SIGABRT, _handle_signal) signal.signal(signal.SIGTERM, _handle_signal) # Execute in parallel and pipe the output to temporary files for directory in sorted(directories): tags = directories[directory] temp_file = tempfile.TemporaryFile(mode='w+t') process = sp.Popen( cmd, cwd=directory, stdout=temp_file, stderr=temp_file, preexec_fn=os.setsid ) processes.append((directory, tags, process, temp_file)) # Read from the temporary files back to stdout line by line sys.stdout.write('\n') for directory, tags, process, temp_file in processes: status = process.wait() tags = [style.tag(tag) for tag in tags if tag] sys.stdout.write('{} {}{}\n'.format( style.msg('in'), style.path(directory), ((' ' + ' '.join(tags)) if tags else '') + style.msg(':') )) temp_file.seek(0) for line in temp_file: sys.stdout.write(line) temp_file.close() if status != 0: sys.stdout.write(style.bad('Exit status: {}\n\n'.format(status))) else: sys.stdout.write('\n\n')
def main(): atexit.register(_cleanup_resources) signal.signal(signal.SIGPIPE, signal.SIG_DFL) mapping, excluded = load_mapping() if excluded: print( 'Found invalid entries in the mapping:\n' + excluded + '\nRun ' + style.cmd('dtags clean') + ' to remove them' + '\n' ) # Parse the optional arguments parallel = False args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head == '-p' and not tail: abort(USAGE + 'e: missing argument: ' + style.bad('<targets>')) elif head == '-p' and tail: parallel = True head, tail = tail[0], tail[1:] elif head.startswith('-'): abort(USAGE + 'e: invalid argument: ' + style.bad(head)) # Parse the positional arguments if not tail: abort(USAGE + 'e: missing argument: ' + style.bad('<command>')) directories = collections.defaultdict(set) for target in head.split(','): if not target: continue elif target in mapping: for directory in sorted(mapping[target]): directories[directory].add(target) else: path = expand(target) if os.path.isdir(path): directories[path].add(None) else: abort(USAGE + 'e: invalid target: ' + style.bad(target)) command = sp.list2cmdline(tail) # Check which shell is in use (e.g. zsh, bash, fish) shell = os.environ.get('SHELL') if shell is None: abort('e: undefined environment variable: ' + style.bad('SHELL')) # Execute the command in the targeted directories msg_head = style.msg('Executing command ') + style.cmd(command) if parallel: global temp_file, process, processes # Add signal handlers to terminate child processes gracefully signal.signal(signal.SIGINT, _handle_signal) signal.signal(signal.SIGABRT, _handle_signal) signal.signal(signal.SIGTERM, _handle_signal) # Execute in parallel and pipe the output to temporary files sys.stdout.write(msg_head + style.msg(' in parallel...\n\n')) for directory in sorted(directories): tags = directories[directory] temp_file = tempfile.TemporaryFile(mode='w+t') process = sp.Popen( [shell, '-i', '-c', command], cwd=directory, stdout=temp_file, stderr=temp_file, preexec_fn=os.setsid ) processes.append((directory, tags, process, temp_file)) # Read from the temporary files back to stdout line by line for directory, tags, process, temp_file in processes: status = process.wait() tags = [style.tag(tag) for tag in tags if tag] sys.stdout.write('{} {}{}\n'.format( style.msg('in'), style.path(directory), ((' ' + ' '.join(tags)) if tags else '') + style.msg(':') )) temp_file.seek(0) for line in temp_file: sys.stdout.write(line) temp_file.close() sys.stdout.write(style.msg('Exit status: {}\n\n'.format(status))) else: # Generate the command string and execute all in one subprocess call commands = [] status_printf = 'printf "{}\n\n"'.format(style.msg( 'Exit status: ' + ('$status' if '/fish' in shell else '$?') )) for directory in sorted(directories): tags = [style.tag(tag) for tag in directories[directory] if tag] commands.append('printf "{} {}{}\n"; cd "{}"; {};{}'.format( style.msg('in'), style.path(directory), ((' ' + ' '.join(tags)) if tags else '') + style.msg(':'), directory, command, status_printf )) sys.stdout.write(msg_head + style.msg(' in sequence...\n\n')) sys.stdout.flush() # flush for printing chronologically sp.call([shell, '-i', '-c', ';'.join(commands)], stderr=sp.STDOUT)
def main(): atexit.register(close_stdio) signal.signal(signal.SIGPIPE, signal.SIG_DFL) mapping, excluded = load_mapping() if excluded: print( 'Found invalid entries in the mapping:\n' + excluded + '\nRun ' + style.cmd('dtags clean') + ' to remove them' + '\n' ) # Parse the optional arguments interactive = False args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head == '-i' and not tail: abort(USAGE + 'e: missing argument: ' + style.bad('<targets>')) elif head == '-i' and tail: interactive = True head, tail = tail[0], tail[1:] elif head.startswith('-'): abort(USAGE + 'e: invalid argument: ' + style.bad(head)) # Parse the positional arguments if not tail: abort(USAGE + 'e: missing argument: ' + style.bad('<command>')) directories = collections.defaultdict(set) for target in head.split(','): if not target: continue elif target in mapping: for directory in sorted(mapping[target]): directories[directory].add(target) else: path = expand(target) if os.path.isdir(path): directories[path].add(None) else: abort(USAGE + 'e: invalid target: ' + style.bad(target)) cmd = sp.list2cmdline(tail) # Check which shell is in use (e.g. zsh, bash, fish) shell = os.environ.get('SHELL') if shell is None: abort('e: undefined environment variable: ' + style.bad('SHELL')) # Generate the command string and execute all in one subprocess call cmds = [] if 'fish' in shell: status_printf = ( 'if [ $status = 0 ]; printf "\n\n"; else; printf "{}\n\n"; end' .format(style.bad('Exit status: $status')) ) else: status_printf = ( 'if [ $? -eq 0 ]; then printf "\n\n"; else printf "{}\n\n"; fi' .format(style.bad('Exit status: $?')) ) for directory in sorted(directories): tags = [style.tag(tag) for tag in directories[directory] if tag] cmds.append('printf "{} {}{}\n"; cd "{}"; {}; {}'.format( style.msg('in'), style.path(directory), ((' ' + ' '.join(tags)) if tags else '') + style.msg(':'), directory, cmd, status_printf )) sys.stdout.write('\n') sys.stdout.flush() # flush for printing chronologically if interactive: sp.call([shell, '-i', '-c', ';'.join(cmds)], stderr=sp.STDOUT) else: sp.call([shell, '-c', ';'.join(cmds)], stderr=sp.STDOUT)
def main(): atexit.register(_cleanup_resources) signal.signal(signal.SIGPIPE, signal.SIG_DFL) mapping, excluded = load_mapping() if excluded: print( 'Found invalid entries in the mapping:\n' + excluded + '\nRun ' + style.cmd('dtags clean') + ' to remove them' + '\n' ) # Parse the optional arguments parallel = False args = sys.argv[1:] if not args: finish(USAGE + DESCRIPTION) head, tail = args[0], args[1:] if head == '--help': finish(USAGE + DESCRIPTION) elif head == '--version': finish('Version ' + VERSION) elif head == '-p' and not tail: abort(USAGE + 'Missing argument: ' + style.bad('<targets>')) elif head == '-p' and tail: parallel = True head, tail = tail[0], tail[1:] elif head.startswith('-'): abort(USAGE + 'Invalid argument: ' + style.bad(head)) # Parse the positional arguments if not tail: abort(USAGE + 'Missing argument: ' + style.bad('<command>')) directories = collections.defaultdict(set) for target in head.split(','): if not target: continue elif target in mapping: for directory in sorted(mapping[target]): directories[directory].add(target) else: path = expand(target) if os.path.isdir(path): directories[path].add(None) else: abort(USAGE + 'Invalid target: ' + style.bad(target)) command = sp.list2cmdline(tail) # Check which shell is in use (e.g. zsh, bash, fish) shell = os.environ.get('SHELL') if shell is None: abort('Undefined environment variable: ' + style.bad('SHELL')) # Execute the command in the targeted directories msg_head = style.msg('Executing command ') + style.cmd(command) if parallel: global temp_file, process, processes # Add signal handlers to terminate child processes gracefully signal.signal(signal.SIGINT, _handle_signal) signal.signal(signal.SIGABRT, _handle_signal) signal.signal(signal.SIGTERM, _handle_signal) # Execute in parallel and pipe the output to temporary files sys.stdout.write(msg_head + style.msg(' in parallel...\n\n')) for directory in sorted(directories): tags = directories[directory] temp_file = tempfile.TemporaryFile(mode='w+t') process = sp.Popen( [shell, '-i', '-c', command], cwd=directory, stdout=temp_file, stderr=temp_file, preexec_fn=os.setsid ) processes.append((directory, tags, process, temp_file)) # Read from the temporary files back to stdout line by line for directory, tags, process, temp_file in processes: status = process.wait() tags = [style.tag(tag) for tag in tags if tag] sys.stdout.write('{} {}{}\n'.format( style.msg('in'), style.path(directory), ((' ' + ' '.join(tags)) if tags else '') + style.msg(':') )) temp_file.seek(0) for line in temp_file: sys.stdout.write(line) temp_file.close() sys.stdout.write(style.msg('Exit status: {}\n\n'.format(status))) else: # Generate the command string and execute all in one subprocess call commands = [] status_printf = 'printf "{}\n\n"'.format(style.msg( 'Exit status: ' + ('$status' if '/fish' in shell else '$?') )) for directory in sorted(directories): tags = [style.tag(tag) for tag in directories[directory] if tag] commands.append('printf "{} {}{}\n"; cd "{}"; {};{}'.format( style.msg('in'), style.path(directory), ((' ' + ' '.join(tags)) if tags else '') + style.msg(':'), directory, command, status_printf )) sys.stdout.write(msg_head + style.msg(' in sequence...\n\n')) sys.stdout.flush() # flush for printing chronologically sp.call([shell, '-i', '-c', ';'.join(commands)], stderr=sp.STDOUT)
def parse_mapping(filename): """Parse the mapping file and return a mapping object. The mapping object looks like this (tag to directories): { 'app' : {'/home/user/app/backend', '/home/app/frontend'}, 'frontend': {'/home/user/app/frontend'}, 'backend': {'/home/user/app/backend'}, } Any invalid directories and tags are ignored. :param filename: the name of the mapping file to parse :return: the mapping object and set of ignored paths and tags """ # Collect errors while parsing excluded = set() orphaned_paths = set() # The mapping object to populate mapping = defaultdict(set) # Parse the file line by line with io.open(filename, mode='rt') as mapping_file: for line in mapping_file.readlines(): words = [] for word in line.split(','): word = word.strip() if word: words.append(word) # Skip empty lines or comments if not words or words[0].startswith('#'): continue # Collect orphan directories if len(words) == 1: orphaned_paths.add(words[0]) continue path, tags = expand(words[0]), words[1:] valid_directory = True invalid_path_chars = get_invalid_path_chars(path) if invalid_path_chars: excluded.add( BULLET + style.bad(path) + ' contains bad characters: ' + style.bad_chars(invalid_path_chars) ) valid_directory = False elif not os.path.isdir(path): excluded.add( BULLET + style.bad(path) + ' is not a directory path' ) valid_directory = False for tag in tags: if not tag[0].isalpha(): excluded.add( BULLET + style.bad(tag) + ' does not start with an alphabet ' ) continue if ' ' in tag: excluded.add( BULLET + style.bad(tag) + ' contains whitespaces ' ) continue invalid_tag_chars = get_invalid_tag_chars(tag) if invalid_tag_chars: excluded.add( BULLET + style.bad(tag) + ' contains bad characters: ' + style.bad_chars(invalid_tag_chars) ) elif valid_directory: mapping[tag].add(path) orphaned_paths.discard(path) excluded.update( BULLET + style.bad(orphaned_path) + ' is not mapped to any valid tags' for orphaned_path in orphaned_paths ) # Return the mapping and the ignored entries if any return mapping, '\n'.join(excluded) if excluded else None
DESCRIPTION = """ Arguments: dir The directory path tag The directory tag Options: --help Display the help menu --version Display the version Change directory. If the destination is not specified it defaults to the home directory. If multiple directories are associated with the same tag, a selection menu is displayed. """ ARG_ERR = '%sd: invalid argument: %s\n' CHOICE = '%s: %s\n' DEST_ERR = 'd: invalid destination: %s\n' INDEX_ERR = 'd: index out of range: %s\n' INPUT_ERR = 'd: invalid input: %s\n' PROMPT = '\nSelect directory (1 - %s): ' # Style the messages if the output is going to a terminal tty = True ARG_ERR_TTY = '%sd: invalid argument: ' + style.bad('%s\n', tty=tty) CHOICE_TTY = style.msg('%s: ', tty=tty) + style.path('%s\n', tty=tty) DEST_ERR_TTY = 'd: invalid destination: ' + style.bad('%s\n', tty=tty) INDEX_ERR_TTY = 'd: index out of range: ' + style.bad('%s\n', tty=tty) INPUT_ERR_TTY = 'd: invalid input: ' + style.bad('%s\n', tty=tty) PROMPT_TTY = style.msg('\nSelect directory (1 - %s): ', tty=tty)