blob: 8190d27da0bb1d14d84a20dfdc4b40775af11466 [file] [log] [blame] [edit]
# 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