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
- a text file
~/.dt_table
containing tag/path entries - the actual script
~/.dt/dt_script
- the program for converting a tag into a path
~/.dt/dt_engine
- 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?
pushd
which accomplishes what this seems to be aimed at. The other related shell builtins includepopd
anddirs
. Check outman bash-builtins
and search for each of these three in turn. \$\endgroup\$