diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c8e24db..d74ab80 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -files = kd.py +files = cd.py commit = True tag = True current_version = 0.4.3 diff --git a/kd.py b/cd.py similarity index 97% rename from kd.py rename to cd.py index abbca1b..90391a2 100644 --- a/kd.py +++ b/cd.py @@ -1,7 +1,7 @@ #! /usr/bin/env python3 -"""kd is a better cd +"""cd.py is a better cd -Usage: kd [options] [items] +Usage: cd.py [options] [items] Arguments: items are strings which indicate a path to a directory or file @@ -26,28 +26,28 @@ It gets a close match to a directory from command line arguments Then prints that to stdout which allows a usage in bash like - $ cd $(python kd.py /usr local bin) + $ cd $(python cd.py /usr local bin) or - $ cd $(python kd.py /bin/ls) + $ cd $(python cd.py /bin/ls) First argument is a directory subsequent arguments are prefixes of sub-directories For example: - $ python kd.py /usr/local bi + $ python cd.py /usr/local bi /usr/local/bin Or first argument is a file - $ python kd.py /bin/ls + $ python cd.py /bin/ls /bin Or first argument is a stem of a directory/file - kd.py will add * on to such a stem, + cd.py will add * on to such a stem, and will always find directories first, looking for files only if there are no such directories - $ python kd.py /bin/l + $ python cd.py /bin/l /bin -Or first argument is part of a directory that kd has seen before +Or first argument is part of a directory that cd.py has seen before "part of" means the name or the start of the name or the name of a parent or @@ -55,7 +55,7 @@ (or any part of the full path if you include a "/") If no matches then give directories in $PATH which have matching executables - $ python kd.py ls + $ python cd.py ls /bin """ @@ -219,8 +219,8 @@ def find_python_root_dir(possibles): If all dirs have the same name, and one of them has setup.py then it is probably common Python project tree, like - /path/to/projects/kd - /path/to/projects/kd/kd + /path/to/projects/cd + /path/to/projects/cd/cd Or, if all dirs are the same, except that one has an egg suffix, like /path/to/dotsite/dotsite @@ -236,6 +236,7 @@ def find_python_root_dir(possibles): eggless = {paths.path(p.replace('.egg-info', '')) for p in possibles} if len(eggless) == 1: return eggless.pop() + return None def too_many_possibles(possibles): @@ -389,7 +390,7 @@ def run_args(args, methods): def version(_args): """Show version of the script""" - print('kd %s' % __version__) + print('cd.py %s' % __version__) raise SystemExit(os.EX_OK) @@ -408,7 +409,7 @@ def parse_my_args(methods): """Get the arguments from the command line. Insist on at least one empty string""" - usage = '''usage: kd directory prefix ... + usage = '''usage: cd.py directory prefix ... %s''' % __doc__ parser = argparse.ArgumentParser( @@ -449,7 +450,7 @@ def test(_args): Tell any bash-runners not to use any output by saying "Error" first - >>> 'kd' in __file__ + >>> 'cd.py' in __file__ True """ stem = paths.path(__file__).namebase @@ -738,7 +739,7 @@ def delete_found_item(path_to_item): rewrite_history_without_path(path_to_item) -def kd(string): +def cd(string): def chdir_found_item(path_to_item): os.chdir(path_to_item) diff --git a/cd.sh b/cd.sh new file mode 100644 index 0000000..babe03f --- /dev/null +++ b/cd.sh @@ -0,0 +1,382 @@ +#! /bin/cat + +[[ -n $WELCOME_BYE ]] && echo Welcome to $(basename "$BASH_SOURCE") in $(dirname $(readlink -f "$BASH_SOURCE")) on $(hostname -f) + +# This script is intended to be sourced, not run +if [[ $0 == $BASH_SOURCE ]] +then + echo "This file should be run as" + echo " source $0" + echo "and should not be run as" + echo " sh $0" +fi + +CD_PATH_ONLY=0 +export CDE_SOURCE=$(basename $BASH_SOURCE) + +# x + +c () { + local __doc__="""https://old.reddit.com/r/linux/comments/2k1qz5/post_something_that_makes_your_linux_life_easier/clhjky9/?context=1""" + cde "$@" +} + +# _ +# xx +# _x +# xxx + +cdd () { + local __doc__"""call cde() here""" + cde . +} + +# rule 1: Always leave system commands alone +# So this is called "cde", not "cd" + +cde () { + local __doc__="""cd to a dir and react to it""" + [[ $1 == "-h" ]] && cde_help && return 0 + _pre_cd + CD_QUIET=1 py_cd "$@" || return 1 + [[ -d . ]] || return 1 + _post_cd +} + +cdl () { + local __doc__="""cde and ls""" + cde "$@" + ls +} + +cls () { + local __doc__="clean, clear, ls" + clean + clear + if [[ -n "$@" ]]; then + ls "$@" + else + ls . + echo + fi +} + +cpp () { + local __doc__="""Show where any args would cde to""" + if [[ -n "$1" ]]; then + py_pp "$@" + else + py_pp . + fi | grep -v -- '->' +} + +# _xx +# xxxx + +cdup () { + local __doc__="""cde up a few levels, 'cdup' goes up 1 level, 'cdup 2' goes up 2""" + local _level=1 + if [[ $1 =~ [1-9] ]]; then + _level=$1 + shift + fi + while true; do + _level=$( $level - 1 ) + [[ $_level -le 0 ]] && break + cd .. + done + cde .. "$@" +} +# xxxxx + +cdupp () { + local """cd up 2 levels""" + cdup 2 "$@" +} + +py_cd () { + local __doc__="""Ask cd.py for a destination""" + local _debug= + [[ $1 == "-U" ]] && _debug=1 + [[ $_debug == 1 ]] && set -x + local _cd_dir=$(dirname $(readlink -f $BASH_SOURCE)) + local _cd_script=$_cd_dir/cd.py + local _cd_result=1 + local _cd_options= + [[ $CD_PATH_ONLY == 1 ]] && _cd_options=--one + local _python=$(head -n 1 $_cd_script | cut -d' ' -f3) + local _interpreter=$_python + [[ $_debug == 1 ]] && _interpreter=pudb + if ! destination=$(PYTHONPATH=$_cd_dir $_interpreter $_cd_script $_cd_options "$@" 2>&1) + then + echo "$destination" + elif [[ "$@" =~ -[lp] ]]; then + echo "$destination" + elif [[ $destination =~ ^[uU]sage ]]; then + PYTHONPATH=$_cd_dir $_python $_cd_script "$@" + else + local real_destination=$(PYTHONPATH=$_cd_dir $_python -c "import os; print(os.path.realpath('$destination'))") + if [[ "$destination" != "$real_destination" ]] + then + echo "cd ($destination ->) $real_destination" + destination="$real_destination" + else + [[ $1 =~ '--' ]] && shift + if [[ "$destination" != $(readlink -f "$1") && $1 != "-" ]] + then + [[ -n $CD_QUIET ]] || echo "cd $destination" + fi + fi + if [[ $CD_PATH_ONLY == 1 ]]; then + echo "$destination" + else + same_path . "$destination" || pushd "$destination" >/dev/null 2>&1 + fi + _cd_result=0 + fi + unset destination + [[ $_debug == 1 ]] && set +x + return $_cd_result +} + +py_cg () { + local __doc__="Debug the py_cd function and script" + py_cd -U "$@" +} + +py_pp () { + local __doc__="Show the path that py_cd would go to" + CD_QUIET=1 CD_PATH_ONLY=1 py_cd "$@"; + local _result=$? + CD_PATH_ONLY=0 + return $_result +} +# xxxxxx + +cduppp () { + local """cd up ... etc, you get the idea""" + cdup 3 "$@" +} +# xxxxxxx +# _xxxxxx + +_active () { + local __doc__"""Whether the $ACTIVATE script is in same dir as current python or virtualenv""" + local _activate_dir=$(dirname_ $ACTIVATE) + local _python_dir=$(dirname_ $(readlink -f $(command -v python))) + local _venv_dir="$VIRTUAL_ENV/bin" + [[ $_activate_dir == $_python_dir ]] || [[ $_activate_dir == $_venv_dir ]] +} + +_pre_cd () { + [[ -z $CDE_header ]] && return + echo $CDE_header +} +# xxxxxxxx + +cde_help () { + echo "cd to a dir and react to it" + echo + echo "cde [dirname [subdirname ...]]" +} +# _xxxxxxx +_here_ls () { + ls 2> ~/bash/null +} + + +_post_cd () { + local _path=$(~/jab/bin/short_dir $PWD) + [[ $_path == "~" ]] && _path=HOME + [[ $_path =~ "wwts" ]] && _path="${_path/wwts/dub dub t s}" + # set -x + # echo $_path + local _tildless=${_path/~/home} # ; echo t $_tildless + local _homele=$(echo $_tildless | sed -e "s:$HOME:home:") + local _homeles=${_homele/home\//} # ; echo 1 $_homeles + local _homeless=${_homeles/home/} # ; echo 2 $_homeless + local _rootless=$(echo $_homeless | sed -e "s:^/:root :" ) + local _said=$(echo $_rootless | sed -e "s:/: :g") + sai $_said + # echo "said $_said" + # set +x + _here_show_todo && echo + # set -x + _here_bash + _here_bin + _here_git + # set -x + _here_python && _here_venv + # set +x + [[ -n $1 ]] && + _here_ls && _here_clean + # set +x +} +# xxxxxxxxx + +same_path () { + [[ $(readlink -f "$1") == $(readlink -f "$2") ]] +} +# _xxxxxxxx + +_activate () { + # Thanks to @nxnev at https://unix.stackexchange.com/a/443256/32775 + hash -d python ipython pip pudb >/dev/null 2>&1 + . $ACTIVATE +} + +_here_bin () { + [[ -d ./bin ]] && add_to_a_path PATH ./bin +} + +_here_git () { + [[ -d ./.git ]] || return 0 + show_git_time . | head -n $LOG_LINES_ON_CD_GIT_DIR + local _branch=$(git rev-parse --abbrev-ref HEAD) + echo $_branch + git_simple_status . + show_version_here + gl11 + return 0 +} + +# _xxxxxxxxx + +_here_bash () { + local __doc__="""LOok for __init__.sh here or below and source it if found""" + local _init=./__init__.sh + [[ -f $_init ]] || _init=bash/__init__.sh + [[ -f $_init ]] || _init=src/bash/__init__.sh + [[ -f $_init ]] && . $_init + return 0 +} + +_here_venv () { + local _active_venv="$VIRTUAL_ENV" + local _active_bin="$_active_venv/bin" + if [[ -e $_active_bin/ipython ]]; then + alias ipy="$_active_bin/ipython" + else + unalias ipy >/dev/null + fi > /dev/null 2>&1 + activate_python_here && return 0 + # set -x + local _venvs=$HOME/.virtualenvs + [[ -d $_venvs ]] || return 0 + local _here=$(realpath $(pwd)) + local _name="not_a_name" + [[ -e $_here ]] && _name=$(basename_ $_here) + local _venv_path= + for _venv_path in $_venvs/*; do + local _venv_dir="$_venv_path" + [[ -d "$_venv_dir" ]] || continue + local _venv_name=$(basename_ $_venv_dir) + if [[ $_venv_name == $_name ]]; then + local _venv_activate="$_venv_dir/bin/activate" + [[ -f $_venv_activate ]] || return 1 + . $_venv_activate + return 0 + fi + done + return 0 + # set +x +} + +# _xxxxxxxxxx + +_here_clean () { + for path in $(find . -type f -name '*.sw*'); do + ls -l $path 2> ~/bash/fd/2 + rri $path && continue + [[ $? == 1 ]] && break + done +} + +# _xxxxxxxxxxx + +_here_python () { + any_python_scripts_here || return 0 + local _dir=$(realpath .) + local _dir_name=$(basename_ $_dir) + python_project_here $_dir_name || return 0 + activate_python_here + local egg_info=${_dir_name}.egg-info + if [[ -d $egg_info ]]; then + ri $egg_info + fi + return 0 +} + +# _xxxxxxxxxxxxxx + +_here_show_todo () { + if [[ -f todo.txt ]]; then + todo_show + return 0 + fi + return 1 +} + +# xxxxxxxxxxxxxxxxx + +show_version_here () { + local config=./.bumpversion.cfg + if [[ -f $config ]]; then + bump show + return + fi + echo "[bumpversion]" > $config + echo "commit = True" >> $config + echo "tag = True" >> $config + echo "current_version = 0.0.0" >> $config + git add $config + echo "git commit -m\"v0.0.0\"" + echo bump +} + +# xxxxxxxxxxxxxxxxxxx + +python_project_here () { + local __doc__="""Recognise python project dir by presence of known files/dirs""" + local _dirname="$1" + [[ -f setup.py || -f requirements.txt || -f bin/activate || -d ./$_dir_name || -d .venv || -d .idea ]] +} + +# xxxxxxxxxxxxxxxxxxxxxxx + +activate_python_here () { + find_activate_script || return 1 + _active || _activate + PYTHON_VERSION=$(python --version 2>&1 | head -n 1 | cut -d' ' -f 2) +} + +any_python_scripts_here () { + local _found=$(find . -type f -name "*.py" -exec echo 1 \; -quit) + [[ $_found == 1 ]] && rf -qpr +} + +find_activate_script () { + local _pwd=$(pwd) + local _project_activate= + local _local_activate= + for _activate_dir in .venv/bin bin .; do + local _activate=$_activate_dir/activate + local _project_root=$(git rev-parse --git-dir . 2>/dev/null) + [[ -n $_project_root ]] && _project_activate="$_project_root/$_activate" + if [[ -f $_project_activate ]]; then + ACTIVATE=$_project_activate + else: + _local_activate="$(readlink -f $_activate)" + [[ -f "$_local_activate" ]] && ACTIVATE=$_local_activate + fi + [[ -f $ACTIVATE ]] || continue + export ACTIVATE + return 0 + done + ACTIVATE= + export ACTIVATE + return 1 +} + +[[ -n $WELCOME_BYE ]] && echo Bye from $(basename "$BASH_SOURCE") in $(dirname $(readlink -f "$BASH_SOURCE")) on $(hostname -f) + diff --git a/cd.test b/cd.test new file mode 100644 index 0000000..04a03e7 --- /dev/null +++ b/cd.test @@ -0,0 +1,126 @@ +cd is a cd replacer +=================== + + >>> import cd + >>> assert cd.__doc__.splitlines()[0] == 'cd.py is a better cd' + +More modules for testing +------------------------ + + >>> import os + >>> import sys + +Command Line +------------ + +cd is intended to be used from the command line + +On the command line we expect two arguments: + The first is a possible directory + The rest are prefixes of possible sub-directories + + >>> sys.argv = ['cd.py', 'start', 'here'] + >>> args = cd.parse_args() + >>> assert args.directory == 'start' and args.prefixes == ['here'] + +No prefixes is fine too + + >>> sys.argv = ['cd.py', 'start'] + >>> args = cd.parse_args() + >>> assert args.directory == 'start' and args.prefixes == [] + + +Finding sub-directories +----------------------- + +The parsed arguments are sent on to the cd.find_directory method + Give it a directory to start on + >>> assert cd.find_directory('/usr/lib', []) == '/usr/lib' + +Or a file + >>> assert cd.find_directory('/bin/ls', []) == '/bin' + +Give it the name of a (possible) sub directory + and it will combine them to one path + >>> assert cd.find_directory('/usr/local', ['sbin']) == '/usr/local/sbin' + +The sub-dir can be a prefix + >>> assert cd.find_directory('/usr/local', ['sbi']) == '/usr/local/sbin' + +It can handle many prefixes (albeit more slowly) + >>> assert cd.find_directory('/', ['us', 'loc', 'sbi']) == '/usr/local/sbin' + +In case of ambiguous sub-directories a number can choose one + >>> assert cd.find_directory('/usr', ['li', '0']).startswith('/usr/li') + +Fallbacks +--------- + +If directory is not found directly then try in PATH + >>> assert cd.find_directory('grep', []) in { '/bin', '/usr/bin' } + +If that fails, then try in home directory + >>> assert cd.find_directory('.bashrc', []) == os.path.expanduser('~') + +Running from bash +----------------- + +If run from the command line the python process cannot change dir for calling process (bash) + Hence the script just prints out the result + bash can then capture that and do the actual cd + +Hence the main method will print the found directory + and then return 0 for success, 1 for fail + >>> sys.argv = ['cd', '/usr', 'local', 'lib'] + >>> cd.main() + /usr/local/lib + 0 + +Remembering history +------------------- + +cd can also remember a list of paths + + >>> assert cd._path_to_history().endswith('cd/history') + >>> class Args: + ... directory = cd.__file__ + >>> cd.add(Args()) + >>> stored = False + >>> for _rank, path, _time in cd.read_history(): + ... if path == os.path.dirname(cd.__file__): + ... stored = True + ... break + >>> assert stored + >>> assert os.path.isfile(cd._path_to_history()) + + +Finding in history +------------------ + +Assuming we have remembered some paths + >>> history = [ + ... '/usr/bin', + ... '/usr/local/bin', + ... '/usr/tin/local', + ... '/usr/bin/vocal', + ... '/usr/local/bib', + ... ] + +Then we should be able to search that history for a remembered item + which should be found first is exact same as a history item + >>> expected = sought = history[2] + >>> assert expected == cd._find_in_paths(sought, [], history) + +Or even if it is just the name of one + >>> sought = os.path.basename(sought) + >>> history_paths = [cd.paths.path(_) for _ in history] + >>> assert expected == cd._find_in_paths(sought, [], history_paths) + +Or even just the stem of the name + >>> sought = sought[:3] + >>> assert expected == cd._find_in_paths(sought, [], history_paths) + + +Cleanup +======= + >>> sys.argv = [] diff --git a/kd.tests b/cd.tests similarity index 53% rename from kd.tests rename to cd.tests index 82ce46e..fbde998 100644 --- a/kd.tests +++ b/cd.tests @@ -1,7 +1,7 @@ -Edge cases for kd +Edge cases for cd ================= - >>> import kd + >>> import cd More modules for testing ------------------------ @@ -14,15 +14,15 @@ Command lines We allow more than two args on command line - >>> sys.argv = ['kd.py', 'start', 'here', 'or', 'there'] - >>> args = kd.parse_args() + >>> sys.argv = ['cd.py', 'start', 'here', 'or', 'there'] + >>> args = cd.parse_args() >>> args.directory == 'start' and args.prefixes == ['here', 'or', 'there'] True And no args at all (use home directory) - >>> sys.argv = ['kd.py'] - >>> args = kd.parse_args() + >>> sys.argv = ['cd.py'] + >>> args = cd.parse_args() >>> args.directory == os.path.expanduser('~') and not args.prefixes True @@ -31,71 +31,71 @@ find_directory() Directories sought must be real - >>> kd.find_directory('rubbish', []) + >>> cd.find_directory('rubbish', []) Traceback (most recent call last): ... - ToDo: ... + cd.ToDo: ... >>> a_known_directory = os.environ['HOME'] Can use empty prefixes - gives path to the directory - >>> kd.find_directory(a_known_directory, []) == a_known_directory + >>> cd.find_directory(a_known_directory, []) == a_known_directory True If sub-dir is not found, get only the directory - >>> kd.find_directory(a_known_directory, ['this_is_not_real']) == a_known_directory + >>> cd.find_directory(a_known_directory, ['this_is_not_real']) == a_known_directory True If the sub-dir matches too many directories then an exception is raised - >>> kd.find_directory('/usr', ['li']) + >>> cd.find_directory('/usr', ['li']) Traceback (most recent call last): ... - TryAgain: Too many possiblities + cd.TryAgain: Too many possiblities 0: /usr/li... 1: /usr/li... Which can be suppressed by a numeric arg - >>> kd.find_directory('/usr', ['li', '1']).startswith('/usr/li') + >>> cd.find_directory('/usr', ['li', '1']).startswith('/usr/li') True - >>> kd.find_directory('/usr', ['li', '99']) + >>> cd.find_directory('/usr', ['li', '99']) Traceback (most recent call last): ... - ToDo: Your choice of "99" is not in range: + cd.ToDo: Your choice of "99" is not in range: 0: /usr/li... 1: /usr/li... A number out of context is ignored - >>> kd.find_directory('/usr', ['1']) == '/usr' + >>> cd.find_directory('/usr', ['1']) == '/usr' True find a file ----------- - >>> directory, filename = os.path.split(kd.__file__) - >>> kd.find_directory(kd.__file__, []) == directory + >>> directory, filename = os.path.split(cd.__file__) + >>> cd.find_directory(cd.__file__, []) == directory True - >>> kd.find_directory(directory, [filename]) == directory + >>> cd.find_directory(directory, [filename]) == directory True Find in home directory ---------------------- - >>> my_bin = kd.find_at_home('bi', []) + >>> my_bin = cd.find_at_home('bi', []) >>> os.path.dirname(my_bin) == os.path.expanduser('~') and os.path.basename(my_bin) == 'bin' True - >>> kd.find_at_home('.bashrc', []) == os.path.expanduser('~') + >>> cd.find_at_home('.bashrc', []) == os.path.expanduser('~') True - >>> kd.find_at_home('rubbush', []) is None + >>> cd.find_at_home('rubbush', []) is None True main() ------ When main() fails it prints out an error - >>> sys.argv = [ 'kd', 'no_such_directory' ] - >>> kd.main() + >>> sys.argv = [ 'cd', 'no_such_directory' ] + >>> cd.main() Error: could not use 'no_such_directory' as a directory 1 @@ -110,41 +110,41 @@ Assuming we have remembered some paths ... '/usr/bin/vocal', ... '/usr/local/bib', ... ] - >>> history_paths = [kd.paths.path(_) for _ in history] + >>> history_paths = [cd.paths.path(_) for _ in history] - >>> kd._find_in_paths('bin', [], history_paths) + >>> cd._find_in_paths('bin', [], history_paths) Traceback (most recent call last): ... - TryAgain: Too many possiblities + cd.TryAgain: Too many possiblities 0: /usr/bin 1: /usr/local/bin - >>> kd._find_in_paths('bin', ['0'], history_paths) == '/usr/bin' + >>> cd._find_in_paths('bin', ['0'], history_paths) == '/usr/bin' True - >>> kd._find_in_paths('bin', ['1'], history_paths) == '/usr/local/bin' + >>> cd._find_in_paths('bin', ['1'], history_paths) == '/usr/local/bin' True - >>> kd._find_in_paths('bi', ['0'], history_paths) == '/usr/bin' + >>> cd._find_in_paths('bi', ['0'], history_paths) == '/usr/bin' True - >>> kd._find_in_paths('bi', ['1'], history_paths) == '/usr/local/bin' + >>> cd._find_in_paths('bi', ['1'], history_paths) == '/usr/local/bin' True - >>> kd._find_in_paths('bi', ['2'], history_paths) == '/usr/local/bib' + >>> cd._find_in_paths('bi', ['2'], history_paths) == '/usr/local/bib' True - >>> kd._find_in_paths('tin', [], history_paths) == '/usr/tin/local' + >>> cd._find_in_paths('tin', [], history_paths) == '/usr/tin/local' True - >>> kd._find_in_paths('ti', [], history_paths) == '/usr/tin/local' + >>> cd._find_in_paths('ti', [], history_paths) == '/usr/tin/local' True - >>> not kd._find_in_paths('sin', [], history_paths) + >>> not cd._find_in_paths('sin', [], history_paths) True - >>> kd._find_in_paths('in/', ['0'], history_paths) == '/usr/tin/local' + >>> cd._find_in_paths('in/', ['0'], history_paths) == '/usr/tin/local' True - >>> kd._find_in_paths('in/v', [], history_paths) == '/usr/bin/vocal' + >>> cd._find_in_paths('in/v', [], history_paths) == '/usr/bin/vocal' True - >>> kd._find_in_paths('usr', [], history_paths) + >>> cd._find_in_paths('usr', [], history_paths) Traceback (most recent call last): ... - TryAgain: Too many possiblities + cd.TryAgain: Too many possiblities 0: /usr/bin 1: /usr/local/bin - >>> kd._find_in_paths('usr', ['3'], history_paths) == '/usr/bin/vocal' + >>> cd._find_in_paths('usr', ['3'], history_paths) == '/usr/bin/vocal' True @@ -153,10 +153,10 @@ Assuming we have remembered some paths ... (0, path_to_item, 0), ... (0, '/so/were/you', 0), ... ] - >>> new_items, changed = kd.exclude_path_from_items(history_items, path_to_item) + >>> new_items, changed = cd.exclude_path_from_items(history_items, path_to_item) >>> changed is True and len(new_items) + 1 == len(history_items) True - >>> newer_items, changed = kd.exclude_path_from_items(new_items, path_to_item) + >>> newer_items, changed = cd.exclude_path_from_items(new_items, path_to_item) >>> changed is False and len(new_items) == len(newer_items) True @@ -164,13 +164,12 @@ Assuming we have remembered some paths Python dirs ----------- -kd has a setup.py +cd has a setup.py - >>> project_dir = kd.paths.path(os.path.dirname(kd.__file__)) + >>> project_dir = cd.paths.path(os.path.dirname(cd.__file__)) >>> code_dir = project_dir / 'kd' >>> possibles = [code_dir, project_dir] - >>> kd.find_python_root_dir(possibles) == project_dir - True + >>> assert cd.find_python_root_dir(possibles) == project_dir Cleanup diff --git a/docs/summary.txt b/docs/summary.txt index e4386af..72547b4 100644 --- a/docs/summary.txt +++ b/docs/summary.txt @@ -1 +1 @@ -kd is a smarter cd command. It knows where you're going, because it knows where you've been and is written in Python for any contributions. +cd_py is a smarter cd command. It knows where you're going, because it knows where you've been. diff --git a/kd.sh b/kd.sh deleted file mode 100644 index a199cd6..0000000 --- a/kd.sh +++ /dev/null @@ -1,81 +0,0 @@ -#! /bin/cat - -[[ -n $WELCOME_BYE ]] && echo Welcome to $(basename "$BASH_SOURCE") in $(dirname $(readlink -f "$BASH_SOURCE")) on $(hostname -f) - -# This script is intended to be sourced, not run -if [[ $0 == $BASH_SOURCE ]] -then - echo "This file should be run as" - echo " source $0" - echo "and should not be run as" - echo " sh $0" -fi - -KD_PATH_ONLY=0 - -kd () { - local _kd_dir=$(dirname $(readlink -f $BASH_SOURCE)) - local _kd_script=$_kd_dir/kd.py - local _kd_result=1 - local _kd_options= - [[ $KD_PATH_ONLY == 1 ]] && _kd_options=--one - local _python=$(head -n 1 $_kd_script | cut -d' ' -f3) - if ! destination=$(PYTHONPATH=$_kd_dir $_python $_kd_script $_kd_options "$@" 2>&1) - then - echo "$destination" - elif [[ "$@" =~ -[lp] ]]; then - echo "$destination" - elif [[ $destination =~ ^[uU]sage ]]; then - PYTHONPATH=$_kd_dir $_python $_kd_script "$@" - else - local real_destination=$(PYTHONPATH=$_kd_dir $_python -c "import os; print(os.path.realpath('$destination'))") - if [[ "$destination" != "$real_destination" ]] - then - echo "cd ($destination ->) $real_destination" - destination="$real_destination" - else - [[ $1 =~ '--' ]] && shift - if [[ "$destination" != $(readlink -f "$1") && $1 != "-" ]] - then - [[ -n $KD_QUIET ]] || echo "cd $destination" - fi - fi - if [[ $KD_PATH_ONLY == 1 ]]; then - echo "$destination" - else - same_path . "$destination" || pushd "$destination" >/dev/null 2>&1 - fi - _kd_result=0 - fi - unset destination - return $_kd_result -} - -kg () { - local __doc__="Debug the kd function and script" - # set -x - kd -U "$@" - # set +x -} - -kp () { - local __doc__="Show the path that kd would go to" - KD_QUIET=1 KD_PATH_ONLY=1 kd "$@"; - local _result=$? - KD_PATH_ONLY=0 - return $_result -} - -kpp () { - if [[ -n "$1" ]]; then - kp "$@" - else - kp . - fi | grep -v -- '->' -} - -same_path () { - [[ $(readlink -f "$1") == $(readlink -f "$2") ]] -} - -[[ -n $WELCOME_BYE ]] && echo Bye from $(basename "$BASH_SOURCE") in $(dirname $(readlink -f "$BASH_SOURCE")) on $(hostname -f) diff --git a/kd.test b/kd.test deleted file mode 100644 index e13a570..0000000 --- a/kd.test +++ /dev/null @@ -1,142 +0,0 @@ -kd is a cd replacer -=================== - - >>> import kd - >>> print kd.__doc__ - kd is a better cd - ... - -More modules for testing ------------------------- - - >>> import os - >>> import sys - -Command Line ------------- - -kd is intended to be used from the command line - -On the command line we expect two arguments: - The first is a possible directory - The rest are prefixes of possible sub-directories - - >>> sys.argv = ['kd.py', 'start', 'here'] - >>> args = kd.parse_args() - >>> args.directory == 'start' and args.prefixes == ['here'] - True - -No prefixes is fine too - - >>> sys.argv = ['kd.py', 'start'] - >>> args = kd.parse_args() - >>> args.directory == 'start' and args.prefixes == [] - True - - -Finding sub-directories ------------------------ - -The parsed arguments are sent on to the kd.find_directory method - Give it a directory to start on - >>> print kd.find_directory('/usr/lib', []) - /usr/lib - -Or a file - >>> print kd.find_directory('/bin/ls', []) - /bin - -Give it the name of a (possible) sub directory - and it will combine them to one path - >>> print kd.find_directory('/usr/local', ['sbin']) - /usr/local/sbin - -The sub-dir can be a prefix - >>> print kd.find_directory('/usr/local', ['sbi']) - /usr/local/sbin - -It can handle many prefixes (albeit more slowly) - >>> print kd.find_directory('/', ['us', 'loc', 'sbi']) - /usr/local/sbin - -In case of ambiguous sub-directories a number can choose one - >>> kd.find_directory('/usr', ['li', '0']).startswith('/usr/li') - True - -Fallbacks ---------- - -If directory is not found directly then try in PATH - >>> kd.find_directory('grep', []) in { '/bin', '/usr/bin' } - True - -If that fails, then try in home directory - >>> kd.find_directory('.bashrc', []) == os.path.expanduser('~') - True - -Running from bash ------------------ - -If run from the command line the python process cannot change dir for calling process (bash) - Hence the script just prints out the result - bash can then capture that and do the actual cd - -Hence the main method will print the found directory - and then return 0 for success, 1 for fail - >>> sys.argv = ['kd', '/usr', 'local', 'lib'] - >>> kd.main() - /usr/local/lib - 0 - -Remembering history -------------------- - -kd can also remember a list of paths - - >>> kd._path_to_history().endswith('kd/history') - True - >>> class Args: - ... directory = kd.__file__ - >>> kd.add(Args()) - >>> for _rank, path, _time in kd.read_history(): - ... if path == os.path.dirname(kd.__file__): - ... print 'parent was stored' - ... break - parent was stored - >>> os.path.isfile(kd._path_to_history()) - True - - -Finding in history ------------------- - -Assuming we have remembered some paths - >>> history = [ - ... '/usr/bin', - ... '/usr/local/bin', - ... '/usr/tin/local', - ... '/usr/bin/vocal', - ... '/usr/local/bib', - ... ] - -Then we should be able to search that history for a remembered item - which should be found first is exact same as a history item - >>> expected = sought = history[2] - >>> expected == kd._find_in_paths(sought, [], history) - True - -Or even if it is just the name of one - >>> sought = os.path.basename(sought) - >>> history_paths = [kd.paths.path(_) for _ in history] - >>> expected == kd._find_in_paths(sought, [], history_paths) - True - -Or even just the stem of the name - >>> sought = sought[:3] - >>> expected == kd._find_in_paths(sought, [], history_paths) - True - - -Cleanup -======= - >>> sys.argv = [] diff --git a/readme.md b/readme.md index 625fd55..7794bdc 100644 --- a/readme.md +++ b/readme.md @@ -1,60 +1,125 @@ -kd -== - -kd is a [Python](https://github.com/jalanb/kd/blob/master/kd.py#L758)ised version of [Bash](https://github.com/jalanb/kd/blob/master/kd.sh#L12)'s cd command. - -It knows where you are going because it knows where you've been. - -The script gets a close match to a directory from command line arguments then prints that to stdout which allows a usage in at the shell like - - $ cd $(python kd.py /usr local bin) - $ cd $(python kd.py /bin/ls) - -Setup ------ - -For convenience a bash function is also provided, which can be set up like - - $ git clone https://github.com/jalanb/kd - $ source [kd/kd.sh](https://github.com/jalanb/kd/blob/master/kd.sh) - -Then one can use `kd` as a replacement for cd - - $ cd /usr/local/lib/python2.7/site-packages - $ [kd](https://github.com/jalanb/kd/blob/master/kd.py#L3) /usr lo li py si - -Use ---- +cde +=== + +`cde` is a shell function that uses `cd.py` (a more [python](https://github.com/jalanb/kd/blob/v0.5.0/cd.sh#L101)onic version of [Bash](https://github.com/jalanb/kd/blob/v0.5.0/cd.sh#L36)'s `cd` command) to find out how to get to a directory, but knows what to do once it gets there. + +`cd.py` knows where you are going because it knows where you've been, and what directory structures looked like when you were there the last time. + + +Install +======= + +This package does *not* change the `cd` command, and trys not to hurt your system in silly ways. + +We cool? + +OK, clone the repo, and source the bash functions +```shell +$ git clone https://github.com/jalanb/kd/kd.git +$ . kd/cd.sh +``` + +Add `. .../kd/cd.sh` to your `~/.bashrc`, or try the next repo. + +Usage +===== + +You just got a bash function called `cde()` which is intended as a drop-in replacement for the `cd` command. +```shell +cde -h +cd to a dir and react to it + +cde [dirname [subdirname ...]] +``` + +(Examples will work depending on system layout, please allow reasonable defaults, and no history yet) + +A dirname can abbreviate a path, e.g. +```shell +$ cd / +$ cde /u loc bi && pwd +/usr/local/bin +$ cd / +$ cd /usr/local && pwd +/usr/local +``` + +`cde` can be abbreviated to just `c`, e.g. +``` +$ c .. && pwd +/usr +``` + +And sometimes can be abbreviated away entirely, e.g. +```shell +$ c /u l b && pwd +/usr/local/bin +$ ... && pwd +/usr +``` + +args +---- + +(If `c` doesn't understand args they get passed on to `cd`, so options like `-@` might still work.) + +$ cd /usr/local/bin/.. +$ pwd +/usr/local +$ cd bin; pwd +/usr/local/bin +$ cd - +$ c b; pwd +/usr/local/bin +``` + +The first argument to `c` is a `dirname`, further arguments are `subdirnames`. This makes it easier to leave out all those annoying "/"s, e.g. +``` +$ cd / +$ c usr lo b; pwd +/usr/local/bin +``` + +Although full paths work too, e.g. +```shell +$ c /usr/local/bin; pwd +/usr/local/bin +``` +If you give `c` a path to a file, it will go to the parent directory (very handy with "/path/to/file.txt" in the clipboard) +```shell +$ c /usr/local/bin/python; pwd +/usr/local/bin +``` First argument is a directory, subsequent arguments are prefixes of sub-directories. For example: - $ [kd](https://github.com/jalanb/kd/blob/master/kd.py#L698) /usr/local bi + $ [kd](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L698) /usr/local bi is equivalent to $ cd /usr/local/bin -Or first argument is ([stem](https://github.com/jalanb/kd/blob/master/kd.py#L624) of) a [directory](https://github.com/jalanb/kd/blob/master/kd.py#L302) you have been to. For example, given that we have kd'd to it already, you can get back to /usr/local/bin (from anywhere else) by +Or first argument is ([stem](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L624) of) a [directory](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L302) you have been to. For example, given that we have kd'd to it already, you can get back to /usr/local/bin (from anywhere else) by - $ [kd](https://github.com/jalanb/kd/blob/master/kd.py#L758) b + $ [kd](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L758) b Or first argument is a file (cd'ing to a file can be very handy in conjuction with copy-and-paste of filenames), for example - $ [kd](https://github.com/jalanb/kd/blob/master/kd.py#L758) /bin/ls - + $ [kd](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L758) /bin/ls + is equivalent to - $ cd /bin + $ cd /bin -Or the first argument is a stem of a directory/file. [kd](https://github.com/jalanb/kd/blob/master/kd.py#L758).py [will add `*` on to such a stem](https://github.com/jalanb/kd/blob/master/kd.py#L108), and cd [to whatever that matches](https://github.com/jalanb/kd/blob/master/kd.sh#L30) (see below). For example, `/bin/l*` matches `/bin/ls`, which is an existing file, whose parent is `/bin`. This can be handy when tab-completion only finds part of a filename +Or the first argument is a stem of a directory/file. [kd](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L758).py [will add `*` on to such a stem](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L108), and cd [to whatever that matches](https://github.com/jalanb/kd/blob/v0.5.0/cd.sh#L30) (see below). For example, `/bin/l*` matches `/bin/ls`, which is an existing file, whose parent is `/bin`. This can be handy when tab-completion only finds part of a filename - $ [kd](https://github.com/jalanb/kd/blob/master/kd.py#L310) /bin/l + $ [kd](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L310) /bin/l -If nothing matches then it [tries directories in $PATH which have matching executables](https://github.com/jalanb/kd/blob/master/kd.py#L261). For example, this will give `/bin`: +If nothing matches then it [tries directories in $PATH which have matching executables](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L261). For example, this will give `/bin`: - $ [kd](https://github.com/jalanb/kd/blob/master/kd.py#L261) ls + $ [kd](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L261) ls -When looking for partial names kd will [look for each of these in turn](https://github.com/jalanb/kd/blob/master/kd.py#L649), stopping as soon as it gets some match +When looking for partial names kd will [look for each of these in turn](https://github.com/jalanb/kd/blob/v0.5.0/cd.py#L649), stopping as soon as it gets some match 1. directories with the same name 2. directories that start with the given part @@ -63,4 +128,4 @@ When looking for partial names kd will [look for each of these in turn](https:// 4. files with the part in their name -[![Stories in Ready](https://badge.waffle.io/jalanb/kd.png?label=ready)](http://waffle.io/jalanb/kd) +[![Stories in Ready](https://badge.waffle.io/jalanb/kd.png?label=ready)](http://waffle.io/jalanb/kd)