2
\$\begingroup\$

MOTIVATION

I have this small software package that allow users to maintain a simple text file that looks like

tag1 path1
tag2 path2
.
.
.

For example, it may look like

home ~
root /
funk ~/funky/funk

Now, suppose the user is in the root directory /. When the user types dt funk, his current working directory changes to ~/funky/funk. As an additional feature, the user may type in simply dt and his current directory will be changed to /. Typing dt again, will change the current directory back to funk. This way, it is convenient to work between two distinct directories.

Also, what comes to tag matching, my implementation allows approximate matching using Levenshtein distances in case a user makes a typo.

OTHER SOFTWARE

There is already similar programs that are more feature reach than this one, one of them is bashmarks. There should be at least one similar program as well; I don't recall how it is called.

CONSTRAINTS

As a child process cannot change its parent's current working directory, there is no way you can achieve directory switching behaviour from a compiled program; you need to work with scripting facilities of bash as well.

DESIGN

Basically, you have

  1. a text file ~/.dt_table containing tag/path entries
  2. the actual script ~/.dt/dt_script
  3. the program for converting a tag into a path ~/.dt/dt_engine
  4. an alias alias dt="source ~/.dt/dt_script"

What comes to the alias, regardless where you put it (.bashrc or .bash_aliases), don't forget to rerun that very file so that bash knows what to do when it encounters a command dt.

CODE

main.cpp:

#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <limits>
#include <string>
#include <vector>
#include <unistd.h>

using std::cin;
using std::cout;
using std::endl;
using std::ifstream;
using std::ofstream;
using std::min;
using std::numeric_limits;
using std::string;
using std::vector;

static const char* TABLE_FILE_NAME = ".dt_table";
static const char* PREVIOUS_TAG_FILE_NAME = ".dt_prev";

/*******************************************************************************
* Implements the algorithm for computing the Levenshtein distance between two  *
* input strings.                                                               *
*******************************************************************************/ 
static size_t get_edit_distance(string s1, string s2, int i, int j) 
{
    if (i == 0) 
    {
        return j;
    }
    else if (j == 0)
    {
        return i;
    }

    return min(min(get_edit_distance(s1, s2, i - 1, j) + 1,
                   get_edit_distance(s1, s2, i, j - 1) + 1),
               get_edit_distance(s1, s2, i - 1, j - 1) +
               (s1[i - 1] != s2[j - 1] ? 1 : 0));
}


/*******************************************************************************
* Computes the Levenshtein distance between two input strings.                 *
*******************************************************************************/ 
static size_t get_edit_distance(string s1, string s2) 
{
    return get_edit_distance(s1, s2, s1.length(), s2.length());
}

/*******************************************************************************
* Returns the name of the current working directory.                           * 
*******************************************************************************/ 
static string get_current_directory() 
{
    return string(getcwd(NULL, 0));
}

/*******************************************************************************
* Returns the name of the user's home directory.                               * 
*******************************************************************************/ 
static string get_home_directory_name() 
{
    return string(getenv("HOME"));
}

/*******************************************************************************
* Writes to the file called 'prev_file_name' the name of the current directory *
* denoted by 'current_directory'.                                              *   
*******************************************************************************/ 
static void save_current_directory(string current_directory, 
                                   string prev_file_name) 
{
    ofstream file_out;
    file_out.open(prev_file_name, ofstream::out | ofstream::trunc);
    file_out << current_directory;
}

/*******************************************************************************
* Processes the input tag by looking in the table file for tag/path pairs. If  *
* the table is empty, the directory is not changed. If the table file is not   *
* empty, this function tries to find the tag whose Levenshtein distance is no  *
* more than 2, and if found, changes the current working directory to the path *
* of the closest tag.                                                          *        
*******************************************************************************/ 
static void process_tag(string input_tag) 
{
    string home_directory_name = get_home_directory_name();
    string table_file_name = home_directory_name + "/" + TABLE_FILE_NAME;

    ifstream f(table_file_name);

    string tag;
    string directory;

    size_t best_edit_distance_so_far = numeric_limits<size_t>::max();
    string best_directory;

    while (f >> tag >> directory) 
    {
        size_t edit_distance = get_edit_distance(tag, input_tag);

        if (best_edit_distance_so_far > edit_distance)
        {
            best_edit_distance_so_far = edit_distance;
            best_directory = directory;

            if (edit_distance == 0)
            {
                break;
            }
        }
    }

    if (best_edit_distance_so_far > 2) 
    { 
        // Do not change the directory.
        string current_directory_name = get_current_directory();

        if (current_directory_name == "/") 
        {
            // Handle this special case so that the path is printed as 
            // "/" and not "//".
            cout << "/" << endl;
        } 
        else {
            cout << get_current_directory() << "/." << endl; 
        }

        // Just return so that the previous tag is not updated.
        return;
    }
    else 
    {
        // Yes, we have (at least approximate) match. Change the directory.   
        if (best_directory[0] == '~') 
        {
            // Once here, the first character is '~' (tilde); substitute it 
            // with the actual home directory name.
            cout << home_directory_name 
                 << best_directory.substr(1, best_directory.length() -  1);
        } 
        else {
            cout << best_directory << endl;
        }
    }

    // Update the file holding the previous directory.
    string prev_file_name = get_home_directory_name() 
                            + "/" + PREVIOUS_TAG_FILE_NAME;

    save_current_directory(get_current_directory(), prev_file_name);
}

/*******************************************************************************
* Changes the current working directory to the previous one.                   *
*******************************************************************************/  
static void move_to_previous_directory()
{
    string home_directory_name = get_home_directory_name();
    string prev_file_name = home_directory_name + "/" + PREVIOUS_TAG_FILE_NAME;

    ifstream file_in(prev_file_name);
    string previous_directory;
    string current_directory = get_current_directory();
    file_in >> previous_directory;

    // Move to the previous directory.
    cout << previous_directory << endl;

    // Write current working directory as a previous one.
    save_current_directory(current_directory, prev_file_name);
}

int main(int argc, char** argv) {
    if (argc >= 2) 
    {
        string tag = argv[1];
        process_tag(tag);
    }
    else 
    {
        move_to_previous_directory();
    }

    return 0;
}

Compiling main.cpp:

g++ main.cpp -o dt_engine

dt_script:

if [ "$#" -ge "1" ]; then
    cd $($HOME/.dt/dt_engine $1)
else
    cd $($HOME/.dt/dt_engine)
fi

DEMO

Now, your session may look like:

[~] dt rot
[/] dt
[~] dt
[/] dt forgt
[~/forget] dt
[/] dt
[~/forget] cd uni/
[~/forget/uni] dt
[/] dt
[~/forget/uni] dt home
[~] dt
[~/forget/uni] 

So what do you think?

\$\endgroup\$
4
  • \$\begingroup\$ You should be aware that there is shell builtin called pushd which accomplishes what this seems to be aimed at. The other related shell builtins include popd and dirs. Check out man bash-builtins and search for each of these three in turn. \$\endgroup\$
    – Wildcard
    Oct 21, 2015 at 10:24
  • \$\begingroup\$ @Wildcard That's probably why OP attached reinventing-the-wheel. \$\endgroup\$
    – Mast
    Oct 21, 2015 at 12:27
  • \$\begingroup\$ @Mast Yes, and he made a mention of other software packages that do this. But there is a difference between a software package and a shell builtin. Either way, I thought I should mention what wheel, specifically, is being reinvented. \$\endgroup\$
    – Wildcard
    Oct 21, 2015 at 12:31
  • \$\begingroup\$ @Wildcard Fair enough. OP may want to use that information to clear-up the post. Better post = better answers. \$\endgroup\$
    – Mast
    Oct 21, 2015 at 12:32

1 Answer 1

1
\$\begingroup\$

Previous path

Currently you store the previous path in a file. This could be problematic if you have two shells open, because they will each overwrite the same file. I would recommend storing the previous path in an environment variable so that each shell can have its own previous path.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.