Directory names are only locally unique. In the general case, cd
requires a full path to unambiguously specify the destination. These things are necessarily true, but for many practical everyday uses, it is also true that the destination could be disambiguated with far fewer characters, and I am nostalgic for the days of Norton CD on MS-DOS, so I finally got around to doing something about it.
Of course on a modern system with tens of thousands of directories, it can't really work by scanning the whole hard disk the way NCD did. Instead, it keeps a list of directories you recently accessed, and chooses the first matching one. So you have to specify the full path the first time:
c \MyProjects\FirstProject\Subdirectory
But after that, you can often go back there with a much shorter command like:
c su
The command I chose is c
, for maximum brevity; if you prefer to keep that reserved for something else, feel free to rename c.bat
accordingly.
The batch file c.bat
(the actual command) and the accompanying program nearest.exe
(containing most of the logic) need to be placed somewhere in your path.
The batch file is Windows-specific. Pull requests with shell scripts for other operating systems are welcome. It would also be necessary to conditionally change the location of the recently-accessed list from %USERPROFILE%\Documents\cd-nearest.csv
to something appropriate for each operating system.
ChatGPT 4o was used in writing this program. It was an interesting exercise in testing its capabilities given a simple but not entirely trivial specification. First, a Python version (I have found that to be the language ChatGPT is best at):
Q: Write a Python script for Windows, to print the nearest recent matching dir. That is:
The script keeps a list of directories the user has visited before. The list is stored in the user's Documents folder. It is called cd-nearest.csv
. It is a simple text file, one entry per line. Each entry just consists of a full pathname of a directory. If the list does not already exist, an empty one is created.
The first thing the script does is read the current list.
Then it examines the command line argument.
If the argument is an absolute pathname, then that is the directory to go to.
If it is a relative pathname, one that contains at least one \
, then go to that directory relative to the current one.
Otherwise, the argument is taken as a prefix. Go through the list of previously visited directories, and take the first one that is matched, i.e. the commandline argument is a prefix (case insensitive) of the last component of the path.
Move the chosen directory to the top of the list.
Then print that directory.
A: Here's a Python script for Windows that accomplishes the described functionality. This script reads a list of previously visited directories from a file, processes a command-line argument to determine the target directory, and updates the list accordingly.
import os
import sys
import csv
def read_directory_list(file_path):
if not os.path.exists(file_path):
with open(file_path, 'w') as f:
pass # Create an empty file if it doesn't exist
return []
with open(file_path, 'r') as f:
reader = csv.reader(f)
return [row[0] for row in reader]
def write_directory_list(file_path, dir_list):
with open(file_path, 'w', newline='') as f:
writer = csv.writer(f)
for dir in dir_list:
writer.writerow([dir])
def find_matching_directory(argument, dir_list):
for dir in dir_list:
if os.path.basename(dir).lower().startswith(argument.lower()):
return dir
return None
def main():
if len(sys.argv) != 2:
print("Usage: python script.py <directory>")
return
argument = sys.argv[1]
documents_folder = os.path.join(os.path.expanduser("~"), "Documents")
list_file = os.path.join(documents_folder, "cd-nearest.csv")
# Read the current list of directories
dir_list = read_directory_list(list_file)
if os.path.isabs(argument):
target_dir = argument
elif '\\' in argument:
target_dir = os.path.abspath(argument)
else:
target_dir = find_matching_directory(argument, dir_list)
if target_dir is None:
print(f"No matching directory found for prefix: {argument}")
return
# Move the chosen directory to the top of the list
if target_dir in dir_list:
dir_list.remove(target_dir)
dir_list.insert(0, target_dir)
# Write the updated list back to the file
write_directory_list(list_file, dir_list)
# Print the target directory
print(target_dir)
if __name__ == "__main__":
main()
- Reading the Directory List: The
read_directory_list
function reads the list of directories fromcd-nearest.csv
. If the file doesn't exist, it creates an empty one. - Writing the Directory List: The
write_directory_list
function writes the updated list of directories back tocd-nearest.csv
. - Finding a Matching Directory: The
find_matching_directory
function looks for a directory in the list whose last component matches the prefix provided in the command-line argument. - Main Function:
- Checks if the script is run with the correct number of arguments.
- Reads the current list of directories from
cd-nearest.csv
. - Determines the target directory based on whether the argument is an absolute path, a relative path, or a prefix.
- Moves the chosen directory to the top of the list.
- Writes the updated list back to the file.
- Prints the target directory.
Run the script from the command line, providing a directory argument:
python script.py <directory>
- If
<directory>
is an absolute path, it will be used directly. - If
<directory>
is a relative path, it will be resolved relative to the current directory. - If
<directory>
is a prefix, it will find the first matching directory from the list of previously visited directories and use it.
Very good! The above worked first time.
There was a perceptible delay in running the program. Entirely understandable, runtime speed is not Python's biggest selling point, but for a command I use many times a day, it would be nice to have instant response.
Q: now translate it to C++
A: Here's the equivalent C++ program that performs the same functionality:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <filesystem>
#include <algorithm>
#include <cctype>
namespace fs = std::filesystem;
std::vector<std::string> read_directory_list(const std::string& file_path) {
std::vector<std::string> dir_list;
std::ifstream infile(file_path);
if (!infile.is_open()) {
std::ofstream outfile(file_path);
return dir_list;
}
std::string line;
while (std::getline(infile, line)) {
dir_list.push_back(line);
}
infile.close();
return dir_list;
}
void write_directory_list(const std::string& file_path, const std::vector<std::string>& dir_list) {
std::ofstream outfile(file_path);
for (const auto& dir : dir_list) {
outfile << dir << "\n";
}
outfile.close();
}
std::string find_matching_directory(const std::string& argument, const std::vector<std::string>& dir_list) {
for (const auto& dir : dir_list) {
std::string dir_basename = fs::path(dir).filename().string();
std::string arg_lower = argument;
std::transform(arg_lower.begin(), arg_lower.end(), arg_lower.begin(), ::tolower);
std::string dir_basename_lower = dir_basename;
std::transform(dir_basename_lower.begin(), dir_basename_lower.end(), dir_basename_lower.begin(), ::tolower);
if (dir_basename_lower.find(arg_lower) == 0) {
return dir;
}
}
return "";
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <directory>" << std::endl;
return 1;
}
std::string argument = argv[1];
std::string documents_folder = fs::path(getenv("USERPROFILE")) / "Documents";
std::string list_file = documents_folder + "/cd-nearest.csv";
// Read the current list of directories
std::vector<std::string> dir_list = read_directory_list(list_file);
std::string target_dir;
if (fs::path(argument).is_absolute()) {
target_dir = argument;
} else if (argument.find('\\') != std::string::npos) {
target_dir = fs::absolute(argument).string();
} else {
target_dir = find_matching_directory(argument, dir_list);
if (target_dir.empty()) {
std::cerr << "No matching directory found for prefix: " << argument << std::endl;
return 1;
}
}
// Move the chosen directory to the top of the list
auto it = std::find(dir_list.begin(), dir_list.end(), target_dir);
if (it != dir_list.end()) {
dir_list.erase(it);
}
dir_list.insert(dir_list.begin(), target_dir);
// Write the updated list back to the file
write_directory_list(list_file, dir_list);
// Print the target directory
std::cout << target_dir << std::endl;
return 0;
}
- Reading the Directory List: The
read_directory_list
function reads the list of directories fromcd-nearest.csv
. If the file doesn't exist, it creates an empty one. - Writing the Directory List: The
write_directory_list
function writes the updated list of directories back tocd-nearest.csv
. - Finding a Matching Directory: The
find_matching_directory
function looks for a directory in the list whose last component matches the prefix provided in the command-line argument. - Main Function:
- Checks if the program is run with the correct number of arguments.
- Reads the current list of directories from
cd-nearest.csv
. - Determines the target directory based on whether the argument is an absolute path, a relative path, or a prefix.
- Moves the chosen directory to the top of the list.
- Writes the updated list back to the file.
- Prints the target directory.
Compile the program using a C++ compiler:
g++ -o cd-nearest cd-nearest.cpp
Run the program from the command line, providing a directory argument:
cd-nearest <directory>
- If
<directory>
is an absolute path, it will be used directly. - If
<directory>
is a relative path, it will be resolved relative to the current directory. - If
<directory>
is a prefix, it will find the first matching directory from the list of previously visited directories and use it.
This did not quite work the first time. I'll spare you the page-long compiler error messages. I did not spare ChatGPT them! It correctly noticed that the error was caused by treating std::string
and std::filesystem::path
as interchangeable, but failed to fix it. However, this was easy to fix by hand; the working version was hand-edited in only a few lines.