|  | #!/usr/bin/env python2.7 | 
|  | # Copyright 2017 The Fuchsia Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  | """fd.py is a fascinating directory changer to save your time in typing. | 
|  |  | 
|  | Use shell function "fd()" to enable autocompletion. | 
|  | Use "fd.py" directly for without autocompletion. | 
|  |  | 
|  | See examples by | 
|  | $ fd.py --help | 
|  |  | 
|  | fd stores two helper files, fd.txt and fd.pickle in $FUCHSIA_DIR/out/. | 
|  | If that directory does not exists, fd will create one. | 
|  | """ | 
|  |  | 
|  | from __future__ import print_function | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import pickle | 
|  | import sys | 
|  | import termios | 
|  | import tty | 
|  |  | 
|  | SEARCH_BASE = os.environ['FUCHSIA_DIR']  # or 'HOME' | 
|  | STORE_DIR = SEARCH_BASE + '/out/' | 
|  | DIRS_FILE = STORE_DIR + 'fd.txt' | 
|  | PICKLE_FILE = STORE_DIR + 'fd.pickle' | 
|  |  | 
|  | EXCLUDE_DIRS = [ | 
|  | '"*/.git"', './build', './out', './prebuilt', './third_party', | 
|  | './zircon/build', './zircon/prebuilt', './cmake-build-debug', './zircon/third_party', | 
|  | ] | 
|  |  | 
|  |  | 
|  | def eprint(*args, **kwargs): | 
|  | print(*args, file=sys.stderr, **kwargs) | 
|  |  | 
|  |  | 
|  | class Trie(object): | 
|  | """Class Trie. | 
|  | """ | 
|  |  | 
|  | def __init__(self): | 
|  | self.name = ''  # != path up to here from the root. Key is a valid | 
|  | # complete one. | 
|  | self.vals = [] | 
|  | self.kids = {} | 
|  |  | 
|  | def __getitem__(self, name, idx=0): | 
|  | if self.name == name: | 
|  | return self.vals | 
|  | if idx == name.__len__() or name[idx] not in self.kids: | 
|  | return None | 
|  | return self.kids[name[idx]].__getitem__(name, idx + 1) | 
|  |  | 
|  | def __setitem__(self, name, val, idx=0): | 
|  | if idx < name.__len__(): | 
|  | self.kids.setdefault(name[idx], Trie()).__setitem__(name, val, idx + 1) | 
|  | return | 
|  | self.name = name | 
|  | self.vals.append(val) | 
|  |  | 
|  | def __contains__(self, name): | 
|  | return self[name] is not None | 
|  |  | 
|  | def walk(self): | 
|  | descendants = [] | 
|  | if self.name: | 
|  | descendants.append(self.name) | 
|  | for k in self.kids: | 
|  | descendants.extend(self.kids[k].walk()) | 
|  | return descendants | 
|  |  | 
|  | def prefixed(self, name, idx=0): | 
|  | if idx < name.__len__(): | 
|  | if name[idx] in self.kids: | 
|  | return self.kids[name[idx]].prefixed(name, idx + 1) | 
|  | return [] | 
|  |  | 
|  | return self.walk() | 
|  |  | 
|  |  | 
|  | def build_trie(): | 
|  | """build trie. | 
|  |  | 
|  | Returns: | 
|  | Trie | 
|  | """ | 
|  |  | 
|  | def build_find_cmd(): | 
|  | paths = [] | 
|  | for path in EXCLUDE_DIRS: | 
|  | paths.append('{} {}'.format('-path', path)) | 
|  | return (r'cd {}; find . \( {} \) -prune -o -type d -print > ' | 
|  | '{}').format(SEARCH_BASE, ' -o '.join(paths), DIRS_FILE) | 
|  |  | 
|  | if not os.path.exists(STORE_DIR): | 
|  | os.makedirs(STORE_DIR) | 
|  |  | 
|  | cmd_str = build_find_cmd() | 
|  | os.system(cmd_str) | 
|  |  | 
|  | t = Trie() | 
|  | with open(DIRS_FILE, 'r') as f: | 
|  | for line in f: | 
|  | line = line[2:][:-1] | 
|  | tokens = line.split('/') | 
|  | if tokens.__len__() == 0: | 
|  | continue | 
|  | target = tokens[-1] | 
|  |  | 
|  | t[target] = line | 
|  |  | 
|  | return t | 
|  |  | 
|  |  | 
|  | def get_trie(): | 
|  | """get_trie. | 
|  |  | 
|  | Returns: | 
|  | trie | 
|  | """ | 
|  |  | 
|  | def save_pickle(obj): | 
|  | with open(PICKLE_FILE, 'wb+') as f: | 
|  | pickle.dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL) | 
|  |  | 
|  | def load_pickle(): | 
|  | with open(PICKLE_FILE, 'rb') as f: | 
|  | return pickle.load(f) | 
|  |  | 
|  | if os.path.exists(PICKLE_FILE): | 
|  | return load_pickle() | 
|  |  | 
|  | t = build_trie() | 
|  | save_pickle(t) | 
|  | return t | 
|  |  | 
|  |  | 
|  | def button(idx): | 
|  | """button maps idx to an ascii value. | 
|  | """ | 
|  | ascii = 0 | 
|  | if 0 <= idx <= 8: | 
|  | ascii = ord('1') + idx | 
|  | elif 9 <= idx <= 34: | 
|  | ascii = ord('a') + idx - 9 | 
|  | elif 35 <= idx <= 60: | 
|  | ascii = ord('A') + idx - 35 | 
|  | elif 61 <= idx <= 75: | 
|  | ascii = ord('!') + idx - 61 | 
|  | return str(unichr(ascii)) | 
|  |  | 
|  |  | 
|  | def get_button():  # Unix way | 
|  | fd = sys.stdin.fileno() | 
|  | old_settings = termios.tcgetattr(fd) | 
|  | try: | 
|  | tty.setraw(sys.stdin.fileno()) | 
|  | ch = sys.stdin.read(1) | 
|  | finally: | 
|  | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) | 
|  | return ch | 
|  |  | 
|  |  | 
|  | def choose_options(t, key, choice): | 
|  | # Build options by the given key | 
|  | if key in t: | 
|  | options = t[key] | 
|  | else: | 
|  | prefixed_keys = t.prefixed(key) | 
|  | options = [] | 
|  | for pk in prefixed_keys: | 
|  | options.extend(t[pk]) | 
|  |  | 
|  | options = sorted(options) | 
|  | if options.__len__() == 0: | 
|  | eprint('No such directory: {}'.format(key)) | 
|  | return None | 
|  | elif options.__len__() == 1: | 
|  | return options[0] | 
|  | elif options.__len__() > 75:  # See def button() for the limit. | 
|  | eprint('Too many ({}) results for "{}". ' | 
|  | 'Refine your prefix or time to buy 4K ' | 
|  | 'monitor\n'.format(options.__len__(), key)) | 
|  | return None | 
|  |  | 
|  | def list_choices(l): | 
|  | for i in range(l.__len__()): | 
|  | eprint('[{}] {}'.format(button(i), l[i])) | 
|  | eprint() | 
|  |  | 
|  | choice_dic = {} | 
|  | for idx, val in enumerate(options): | 
|  | choice_dic[button(idx)] = val | 
|  |  | 
|  | if choice is not None and choice not in choice_dic: | 
|  | # Invalid pre-choice | 
|  | eprint('Choice "{}" not available\n'.format(choice)) | 
|  |  | 
|  | if choice not in choice_dic: | 
|  | list_choices(options) | 
|  | choice = get_button() | 
|  | if choice not in choice_dic: | 
|  | return None | 
|  |  | 
|  | return choice_dic[choice] | 
|  |  | 
|  |  | 
|  | def main(): | 
|  |  | 
|  | def parse_cmdline(): | 
|  | example_commands = """ | 
|  |  | 
|  | [eg] # Use "fd" for autocompletion (See //scripts/fx-env.sh) | 
|  | $ fd ral        # change directory to an only option: ralink | 
|  | $ fd wlan       # shows all "wlan" directories and ask to choose | 
|  | $ fd wlan 3     # change directory matching to option 3 of "fd wlan" | 
|  | $ fd [TAB]      # Autocomplete subdirectories from the current directory | 
|  | $ fd //[TAB]    # Autocomplete subdirectories from ${FUCHSIA_DIR} | 
|  | $ fd --rebuild  # rebuilds the directory structure cache | 
|  | """ | 
|  | p = argparse.ArgumentParser( | 
|  | description='A fascinating directory changer', | 
|  | epilog=example_commands, | 
|  | formatter_class=argparse.RawDescriptionHelpFormatter) | 
|  |  | 
|  | p.add_argument( | 
|  | '--rebuild', action='store_true', help='rebuild the directory DB') | 
|  | p.add_argument('--base', type=str, default=None) | 
|  | p.add_argument('target', nargs='?', default='') | 
|  | p.add_argument('choice', nargs='?', default=None) | 
|  |  | 
|  | # Redirect help messages to stderr | 
|  | if len(sys.argv) == 2: | 
|  | if sys.argv[1] in ['-h', '--help']: | 
|  | eprint(p.format_help()) | 
|  | print('.')  # Stay at the current directory | 
|  | sys.exit(0) | 
|  |  | 
|  | return p.parse_args() | 
|  |  | 
|  | def get_abs_path(relative_dir): | 
|  | if relative_dir is not None: | 
|  | return os.path.join(SEARCH_BASE, relative_dir) | 
|  | return os.getcwd() | 
|  |  | 
|  | def derive_dest(target): | 
|  | if not target: | 
|  | # To test if this command was invoked just to rebuild | 
|  | return get_abs_path('.') if args.rebuild is False else os.getcwd() | 
|  |  | 
|  | if target[:2] == '//': | 
|  | target = target[2:] | 
|  |  | 
|  | candidate = target | 
|  |  | 
|  | # Do not guess-work when the user specifies an option to intend to use. | 
|  | # Do guess work otherwise. | 
|  | if not args.choice: | 
|  | if os.path.exists(candidate): | 
|  | return candidate | 
|  |  | 
|  | candidate = get_abs_path(target) | 
|  | if os.path.exists(candidate): | 
|  | return candidate | 
|  |  | 
|  | candidate = os.path.abspath(target) | 
|  | if os.path.exists(candidate): | 
|  | return candidate | 
|  |  | 
|  | t = get_trie() | 
|  | return get_abs_path(choose_options(t, target, args.choice)) | 
|  |  | 
|  | args = parse_cmdline() | 
|  | if args.base: | 
|  | global SEARCH_BASE | 
|  | SEARCH_BASE = args.base | 
|  |  | 
|  | if args.rebuild: | 
|  | os.remove(PICKLE_FILE) | 
|  |  | 
|  | dest = derive_dest(args.target) | 
|  | dest = os.path.normpath(dest) | 
|  | print(dest) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | try: | 
|  | main() | 
|  | except Exception as e:  # Catch all | 
|  | eprint(e.message, e.args) | 
|  | print('.')  # Stay at the current directory upon exception |