blob: cb27257679e5d62bfa5e23b30d91529fe0a8e087 [file] [log] [blame]
#!/usr/bin/env python
# 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.
""" is a fascinating directory changer to save your time in typing.
Use shell function "fd()" to enable autocompletion.
Use "" directly for without autocompletion.
See examples by
$ --help
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'
TMP_BASE = '/tmp/'
DIRS_FILE = TMP_BASE + 'fd.txt'
PICKLE_FILE = TMP_BASE + 'fd.pickle'
'"*/.git"', './build', './buildtools', './out', './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): = '' # != path up to here from the root. Key is a valid
# complete one.
self.vals = [] = {}
def __getitem__(self, name, idx=0):
if == name:
return self.vals
if idx == name.__len__() or name[idx] not in
return None
return[name[idx]].__getitem__(name, idx + 1)
def __setitem__(self, name, val, idx=0):
if idx < name.__len__():[idx], Trie()).__setitem__(name, val, idx + 1)
return = name
def __contains__(self, name):
return self[name] is not None
def walk(self):
descendants = []
for k in
return descendants
def prefixed(self, name, idx=0):
if idx < name.__len__():
if name[idx] in
return[name[idx]].prefixed(name, idx + 1)
return []
return self.walk()
def build_trie():
"""build 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)
cmd_str = build_find_cmd()
t = Trie()
with open(DIRS_FILE, 'r') as f:
for line in f:
line = line[2:][:-1]
tokens = line.split('/')
if tokens.__len__() == 0:
target = tokens[-1]
t[target] = line
return t
def get_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()
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)
ch =
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]
prefixed_keys = t.prefixed(key)
options = []
for pk in prefixed_keys:
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]))
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:
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/
$ 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',
'--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']:
print('.') # Stay at the current directory
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:
SEARCH_BASE = args.base
if args.rebuild:
dest = derive_dest(
dest = os.path.normpath(dest)
if __name__ == '__main__':
except Exception as e: # Catch all
eprint(e.message, e.args)
print('.') # Stay at the current directory upon exception