loc - my own simple .NET version of the unix 'which' facility

As is often the case, I wrote a small program to do something useful for myself. I have a (Win32) copy of the unix “which” utility, but I wanted something that would simultaneously search my path, include, and lib environment variables. What I came up with was loc, and it is a pretty simple, but pretty cool, example of what you can do with the .NET runtime.

I'm including both the old and new syntax versions of loc, as locOS.cpp and locNS.cpp. (Take a guess which is old syntax and which is new.) Really, the conversion from old to new in this case was really painless. I just had to fix the array syntax, and use handles instead of gc pointers. Enough small talk, let's get on with the code! As is my preference, I'll use the new syntax to describe the code. (Scroll to the very bottom of this post to get the code.)


using namespace System;

using namespace System::IO;
using namespace stdcli::language;

Some fairly straightforward using namespace declarations. I need System for general things (like String), System::IO for some of the file IO stuff I'm going to use, and stdcli::language for the CLI array.

#define STRARR(x) (((System::String^)x)->ToCharArray())

Here's an interesting macro. As is the case with most macros, it isn't necessary, its just a time-saving measure. Here's the rub: some of the .NET runtime functions (like String::Split) take, as a parameter, a managed array of System::Chars (wchar_ts). Sometimes, you want to pass a string literal to these functions, and we don't provide a standard conversion from a string literal to a managed array of System::Chars. However, we do provide a conversion to a String^, and the System::String object has a member function, String::ToCharArray, that will give me that elusive managed array. So, I create an unnamed temporary String object out of x (which is my string literal) and call ToCharArray. Though I only happen to use it once in this program, I could use it a whole lot more. (And I have, in other code.)

int envSearch(String^ file, String^ env);

int main(int argc, char**argv){

if(argc!=2){

Console::WriteLine("usage: loc <file>");

return 1;
}

A couple of things here. First, a pretty straightforward global function prototype. Second, my main function, with the standard argument list. Then, a simple check to make sure that the user specified one (and only one) file name. Remember, the name of the program always counts as a command-line argument - that's why I check for two arguments. I print the usage for my program, and return 1 if the user didn't specify a file name.

String ^f = argv[1];

int found=0;

//we want to search a number of ENVs - path, include, lib

found+=envSearch(f, "path");

found+=envSearch(f, "include");

found+=envSearch(f, "lib");

if(found) return 0;

else return 1;
}

Here's the rest of my main function. I copy the filename the user gave me into a System::String, and create an int, found, that is going to remember if I found the file I'm looking for across several calls of envSearch. Everything else is pretty straightforward. Let's look at the real guts of the program, contained in the envSearch function.

int envSearch(String^ file, String^ env){

int found=0;

array<String^>^ dirs = (Environment::GetEnvironmentVariable(env))->Split(STRARR(";"));

for(int i=0; i<dirs->Length; i++){

String^ tmpFn = String::Concat(dirs[i], "\\", file);

if (File::Exists(tmpFn)){

Console::WriteLine("{0} ({1})", tmpFn, env);

found++;

}

}

return found;

}

Looks like founds are running cheap today. This one has the same role, just remembering if I've actually found the file I'm looking for or not, so I can give the user a meaningful return code from my program.

I use a static method of the .NET runtime, Environment::GetEnvironmentVariable to, you guessed it, get the contents of an environment variable. Unfortunately, it comes across as a single System::String, semi-colon delimited, and I'd prefer a CLI array of Strings. So, I use the String::Split function (and my handy STRARR macro) to split that string, on-the-fly, into an array of Strings.

Then, I loop through that array. I create a temporary filename by concatenating the directory from the environment variable with a backslash (escaped with a second one), and the name of the file I'm looking for. Then, I use the .NET method File::Exists to see if the file I want exists at the location I created. If it does, I output it, along with the environment variable the path was contained in (in parentheses), and increment found. That's it!

Getting the files! Of course, what we're all interested in, is where to get the files. You can get them here. There are two files in this zip:

  • locOS.cpp - the loc program source, in Managed Extensions (Everett) syntax.
  • locNS.cpp - the loc program source, in Whidbey C++ syntax.