| # 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. |
| |
| fd.py is intended to be used through shell function "fd()" in fx-env.sh. |
| |
| See examples by |
| $ fd --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. |
| """ |
| |
| 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', './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(chr(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 |