Home Blog Projects

GOTO, a cd replacement tool for Linux

written on 31/01/2015, about a 8 minute read
bash, linux, tools

Projects. Projects. Projects. After 5 semesters, cooped up in the same place, SO-VERY-FAR away from home, i finally realize what “monotonous” means. Projects are all that remain that are even remotely fun (..that is, apart from watching TV shows :D). Anyways, there’s this project i wanted to do. It’s been done by the awesome people over at YHatHQ. It’s a Fuzzy Matcher and it uses the FuzzyWuzzy Python library developed by the people at SeatGeek to handle their ticket stub duplication. Now, in the Data Science OSEMN (Obtain, Scrub, Explore, Model, iNterpret) approach, the more perfect the data is, the better the classifier accuracy. So the project seemed a good approach to remove duplicates and clean the dataset. Before moving to Python web apps i wanted to create a fuzzy finder for my terminal. Also, when you’re developing something, why not just go all-in with whatever features you can muster. The only rule: “It has to be done completely in BASH/SHELL”. No Python scripts to make certain tasks easier.

Finally i came up with goto: a ‘cd’ tool on steroids. :)

goto: A ‘cd’ tool on steroids.

goto is designed to be a complete replacement for cd, ie. the change directory tool in Linux, with extra features: a fuzzy file finder and a directory shortcut creator. Something that can make switching between different folders quite easy.

So, how does it work?

Usage:

goto -h||-help||h||help : Prints the Usage

goto <location_address> : Takes you to the <location_address>

goto s||-s <shortcut>   : Saves the Current Directory location as <shortcut>

goto l||-l              : Lists all the Bookmarks saved.

goto d||-d <shortcut>   : Deletes the <shortcut> saved.

goto p||-p <shortcut>   : Prints the GOTO directory for <shortcut>

goto f||-f||find||-find <term>     : Fuzzy-Finds all the file/folder matching REGEX in the working directory.

What do the functions do, and how are they constructed? The program divides the approach on the basis of the number of parameters passed.

Now, i should be explaining the functions that take in a single parameter, but it is necessary to explain the process i take for saving the shortcuts first, as all the other approaches are linked to that.

~$ goto s <shortcut>

The save function takes in the shortcut name and saves it in a directory at ~/.GOTO as a text file <shortcut>.skt with the current working directory inside. Example: let’s say i’m in my downloads folder, and want to save it as a shortcut d.

~$ goto ~/downloads
~$ goto s d
Shortcut Created.!

This creates a file d.skt with the location of downloads in it:

~$ cat ~/.GOTO/d.skt
/home/ankitvad/downloads

After that, we can jump to downloads with:

~$ goto d

Hence, the whole thing has a directory-file saving approach. That makes the time/space complexity both O(n). Using a .csv file that could save the shortcut with location would be a much better approach but i just couldn’t wrap my head around constantly deleting and appending values in the file. A directory approach is quite simple.

Now to the single parameter functions. Here goes:

~$ goto <location_address>

This function is two-fold. The value passed from the terminal is first checked in the shortcut directory, whether or not it exists. If it exists, we read the shortcut file and cd to the saved location:

if [ -e "$savefile$first_parameter.skt" ]; then
    x="$savefile$first_parameter.skt"
    y="$(cat $x)"
    cd $y
fi

In the previous example we created a shortcut for downloads as d. Now, when we do goto d, it first checks whether d.skt exists. If it does, we move to the value inside the file; else, we try moving directly to the value. This replicates cd’s normal features too.

if [ -e "$savefile$first_parameter.skt" ]; then
    x="$savefile$first_parameter.skt"
    y="$(cat $x)"
    cd $y
    #If not, just CD to that location.
elif [ -e "$first_parameter" ]; then
    cd "$first_parameter"
else
    echo -e "The Location/File/Folder $first_parameter does not exist."
fi

So goto ~/tmp takes us to /home/ankitvad/tmp.

The second function is goto l, which lists all the bookmarks saved till now.

if [ "$first_parameter" = "l" ] || [ "$first_parameter" = "-l" ] ; then
    if [ ! -e "$savefile" ]; then
    echo -e "You have not added any directory yet."
    mkdir $savefile
    usage
elif [ ! "$(ls $savefile)" ]; then
    echo -e "You have not added any directory yet."
    usage
else
    #Reverse the path. Delete everything after / and before .
    for file in $savefile*.skt; do
        x="$(echo "$file" | rev)"
        y=${x%%/*}
        x="$(echo -n ${y##*.} | rev)"
        echo -n "$x"
        echo " :  $(cat $file)"
        echo -e " "
        done
fi

First it checks whether the bookmark directory exists at ~/.GOTO/. If it does, a simple loop runs over all .skt files. Here is where the first complication arose. Technically,

for file in $savefile*.skt; do

chooses the file along with the whole path. Say there’s a shortcut file in ~/.GOTO called hello.skt. Then the variable file gives us /home/ankitvad/.GOTO/hello.skt, whereas we only require the filename hello. A simple regex cut could remove everything up to the 4th / and everything after the ., but i didn’t know if the 4th slash would be a good metric. What if, due to a server’s directory structure, there were more than 4 slashes? So i reversed the string, deleted everything after the first / and before the .. That made it easy to get the shortcut name.

After that, the two-parameter functions:

~$ goto d||p||find <shortcut>

which respectively delete shortcuts, print shortcut locations and fuzzy find terms. The first two are self-explanatory: just printing and deleting the text files. Now comes the fuzzy finder.

The Fuzz-Finder in Shell

The whole fuzzy finding can be broken into two parts.

regex_value="[A-Za-z0-9]*"
length=${#term}
x="$regex_value"
for (( i=0; i<$length; i++ )); do
    y="$x${term:$i:1}"
    x="$y$regex_value"
done
regex_pattern="$x"
#i just need: [A-Za-z0-9]* after each literal, or before.
ls -R $pwd | grep -i $regex_pattern

The first part is creating the regex-searchable string. Let’s say the string to search is st.cs. Technically this should return us the style.css file. For this we port it into the regex term:

[A-Za-z0-9]*s[A-Za-z0-9]*t[A-Za-z0-9]*.[A-Za-z0-9]*c[A-Za-z0-9]*s[A-Za-z0-9]*

This matches all strings that have “s, t, ., c, s” present in that order, with any characters before, between and after.

The second part is looking for files that match the pattern:

ls -R $pwd | grep -i $regex_pattern

This recursively lists all files and folders in the current working directory and feeds them to grep, which prints the names matching the regex. After the file is found, a simple find can be used to print the location. Say we are looking for “blog.html”:

~$ goto find bl.ht
blog2.html
finallyupwithablog.html
blog.html
blog.html
~$ find -name blog.html
./caffeinepost/blog.html
./ankitvad.github.io/blog/blog.html

After this, a simple goto <location_address> should suffice. And that’s it. That’s the whole program.

Installation and Stuff

The problem with running goto as a bash program is that it fails to execute the cd command. The ‘cd’ runs as a child process of the bash program execution. So, even though the code changes the directory, it dies as the goto execution finishes and reverts back to the original directory. To avoid this, we either need to create functions for the ‘cd’ command in .bashrc, or instead of running bash goto we run it as . goto, sourcing it into the parent shell. Jumping to the Makefile:

bold=`tput bold`
normal=`tput sgr0`

all:
    @echo "Run ${bold}'make install'${normal}."

install:
    bash install.sh

.PHONY: all install

Which runs install.sh:

#!/bin/bash
INSTALL=~/.local/bin
BASH_FILE=~/.bashrc
bold=`tput bold`
normal=`tput sgr0`
mkdir -p "$INSTALL"
cp goto "$INSTALL"
echo 'export PATH=$PATH:~/.local/bin/' >> "$BASH_FILE"
echo 'alias goto=". goto"' >> "$BASH_FILE"
echo -e "Added Stuff in .bashrc"
. ~/.bashrc
exit 0

This creates the install directory for goto, then writes the PATH export and the alias to .bashrc. The alias is required so that goto runs as . goto instead of bash goto.

The code along with the Makefile and installation file is on GitHub: ankitvad/goto.

To install, if you have git:

~$ git clone "https://github.com/ankitvad/goto"
~$ cd goto
~$ make install

If git is not present:

~$ wget -O goto.zip "https://github.com/ankitvad/goto/archive/master.zip"
~$ unzip goto.zip
~$ cd goto-master
~$ make install

If someone has a Mac, please do tell me how it works out?