Faster Directory Jumps Using globstar
- posted:
2019-08-21
Jumping around directories in a Unix-like system can be achieved using the command cd. It's especially convenient when combined with shell's tab-completion feature, and shell's environment variables such as "$OLDPWD" (which refers to the previous working directory and can also be accessed using cd -) and "$HOME" (which refers to the home directory and can also be accessed using cd ~, or cd - that is with no arguments). However, it becomes inefficient when frequent navigatation is required, especially between directories that are deeply nested.
Then, it comes with some tools to address this issue by track cd history and providing an faster way to jump between frequently visited locations. One such tool is called "z", a lightweight shell script of about 200 lines. It's incredibly simple, yet fairly powerful. I have been using it for a while. It definitely streamlines my workflow. However, sometimes it doesn't work as expected - takes me to the wrong directories. For instance, executing the command z foo navigates to the highest-ranked directory matching the keyword "foo". If there are two such directories - say, fooA and fooB - and fooA ranks higher than fooB. In that case, z foo will take me to fooA. But what if I actually want to go to fooB instead? It seems theres's no way for z to do that.
Then, it turned out that I made my own wheels - a shell script called "cdf" (short for Change Directory From), which is inspired by Vim's file-searching function (check out :h file-searching in Vim). That is the pattern "**" can match directories in any depth. In shell, it has a similar function by enabling the option "globstar", and instead of directories, it can even match files. Here is the script:
shopt -s globstar cdf() { declare -a dirs declare -i pick # collect all matched directories for dir in "$@"; do if [[ -d $dir ]]; then dirs+=("$dir") fi done if (( ${#dirs[@]} == 0 )); then echo "No matched directories" elif (( ${#dirs[@]} == 1 )); then cd "${dirs[0]}" else # make a pick from the list for idx in "${!dirs[@]}"; do printf "%s.\t%s\n" "$(( $idx + 1))" "${dirs[$idx]}" done while true; do read -e -r -p "What is your pick? (1-${#dirs[@]}) " pick if (( $pick <= 0 || $pick > ${#dirs[@]} )); then echo "invalid input - please select from 1-${#dirs[@]}" else cd "${dirs[$pick - 1]}" break fi done fi }
To use the script, save it to a file, say "cdf.sh", and load the script through the command source cdf.sh, then the command cdf can be called from shell. For example:
Recursively search for a directory named "foo" from the current directory, and jump into it if a match is found:
$ cdf **/foo
Recursively search for a directory including the keyword "foo" from the current directory, and jump into it if a match is found:
$ cdf **/*foo*
Recursively search for a directory named "foo" from the directory "/usr", and jump into it if a match is found:
$ cdf /usr/**/foo
The only drawback of this script is that it can be slow when searching through directories with a large number of files. Other than that, it is straightforward to use and, most importantly, it shows precise matching directories before jumping into one.
Thanks for reading :)