A brief tour of the console alias functions

Date:February 1, 2016 / year-entry #23
Tags:code
Orig Link:https://blogs.msdn.microsoft.com/oldnewthing/20160201-00/?p=92952
Comments:    10
Summary:Kicking the tires.

Today's Little Program exercises the console alias functions. These functions let you define console aliases which are active when a target program reads a line of text from the console. The alias is recognized when it is entered at the start of a line. (Therefore, a way to defeat an alias is to put a space in front of it.) More details about console aliases can be found in the documentation for DOSKEY.

The program we'll write has five commands:

add program.exe alias "value"

This defines a console alias for the specified program.

delete program.exe alias

This deletes a console alias definition.

show program.exe alias

This shows the current definition of an alias.

showall program.exe

This shows all aliases defined for the specified program.

showexes

This shows all programs that have aliases defined.

Let's dive in.

#define UNICODE
#define _UNICODE
#include <windows.h>
#include <iostream>
#include <vector>

void do_add(int argc, wchar_t **argv);
void do_delete(int argc, wchar_t **argv);
void do_show(int argc, wchar_t **argv);
void do_showall(int argc, wchar_t **argv);
void do_showexes(int argc, wchar_t **argv);

int __cdecl wmain(int argc, wchar_t **argv)
{
  auto command = argv[1];

  if (wcscmp(command, L"add") == 0) {
    do_add(argc, argv);
  } else if (wcscmp(command, L"delete") == 0) {
    do_delete(argc, argv);
  } else if (wcscmp(command, L"show") == 0) {
    do_show(argc, argv);
  } else if (wcscmp(command, L"showall") == 0) {
    do_showall(argc, argv);
  } else if (wcscmp(command, L"showexes") == 0) {
    do_showexes(argc, argv);
  }
  return 0;
}

The main program looks at the first command line argument and dispatches the rest of the work to the appropriate handler function. Now let's look at each of the handlers. Remember, Little Programs do little to no error checking.

void do_add(int argc, wchar_t **argv)
{
  auto program = argv[2];
  auto alias = argv[3];
  auto value = argv[4];
  if (AddConsoleAlias(alias, value, program)) {
    std::wcout << alias << L"=" << value << std::endl;
  } else {
    std::wcout << L"Failed to add alias" << std::endl;
  }
}

To add an alias, we call Add­Console­Alias with the alias, the value, and the program it should be applied to. An example alias might be

scratch add cmd.exe proj
  "cd /D \"%USERPROFILE%\Documents\Visual Studio 2015\Projects\$*\""

(All one line; split into two for expository purposes.)

This lets you type proj to go to your Visual Studio projects directory, and proj scratch to go to the scratch project. Note that we had to quote the value twice, once to get it past the scratch program's command line parser, and a second time to get it past cmd.exe's command line parser.

Next is deletion: To delete an alias, you set it to a null pointer.

void do_delete(int argc, wchar_t **argv)
{
  auto program = argv[2];
  auto alias = argv[3];
  if (AddConsoleAlias(alias, nullptr, program)) {
    std::wcout << alias << L" deleted" << std::endl;
  } else {
    std::wcout << L"Failed to delete alias" << std::endl;
  }
}

Continuing our example, if you get bored of the proj alias, you can delete it by saying scratch delete cmd.exe proj.

The next command is for showing the value of an alias,

void do_show(int argc, wchar_t **argv)
{
  auto program = argv[2];
  auto alias = argv[3];
  wchar_t value[8192];
  if (GetConsoleAlias(alias, value, sizeof(value), program)) {
    std::wcout << alias << L"=" << value << std::endl;
  } else {
    std::wcout << L"Cannot show (maybe it isn't defined)" << std::endl;
  }
}

There is no way to query the length of an alias's value, but since the maximum command line length supported by cmd.exe is 8192, a buffer size of 8192 is a safe bet for now. (This is a Little Program and doesn't need to worry itself with pesky things like forward compatibility.)

The last two commands are for showing all the aliases defined for a specific program, and for showing the programs that have aliases defined. The two functions are very similar, so we present them together. First, a simple version that is subtly defective:

// code in italics is wrong
void do_showall(int argc, wchar_t **argv)
{
  auto program = argv[2];
  auto bytes = GetConsoleAliasesLength(program);
  std::vector<wchar_t> buffer(
    (bytes + sizeof(wchar_t) + 1) / sizeof(wchar_t));
  if (GetConsoleAliases(buffer.data(), bytes, program)) {
    for (auto current = buffer.data();
         current < buffer.data() + buffer.size();
         current += wcslen(current) + 1) {
      std::wcout << current << std::endl;
    }
  }
}

void do_showexes(int argc, wchar_t **argv)
{
  auto bytes = GetConsoleAliasExesLength();
  std::vector<wchar_t> buffer(
    (bytes + sizeof(wchar_t) + 1) / sizeof(wchar_t));
  if (GetConsoleAliasExes(buffer.data(), bytes)) {
    for (auto current = buffer.data();
         current < buffer.data() + buffer.size();
         current += wcslen(current) + 1) {
      std::wcout << current << std::endl;
    }
  }
}

One annoyance here is that the Get­Console­Aliases­Length function returns a byte count rather than a TCHAR count, so we have to do conversion between bytes and TCHARs. In case we get an odd number back (which shouldn't ever happen, but better safe than sorry), we round up to get the number of wchar_ts.

The next annoyance is that the Get­Console­Aliases function returns a series of null-terminated strings, but the last string is not double-null-terminated (or more accurately, terminated with an empty string). This means that you don't know when you're finished! After you process one string, the next byte could be the start of the next string, or it could just be uninitialized garbage. If another thread deletes an alias between the calls to Get­Console­Aliases­Length and Get­Console­Aliases, then we pass a too-large buffer to Get­Console­Aliases, and the unused bytes contain uninitialized garbage, and we have no way to know when the valid data ends and the uninitialized garbage begins.

Not knowing when you have reached the end of the valid data is a really bad situation for a program to be in.

We can work around this problem by zeroing out the memory before we call Get­Console­Aliases; that way, if the buffer we pass turns out to be too large (because another thread deleted an alias in the meantime), the extra zeros we wrote created a double-null-terminated string buffer.

On the other hand, if that didn't happen, then we want to stop when we reached the end of the buffer.

The final problem is that another thread could add an alias in between our calls to Get­Console­Aliases­Length and Get­Console­Aliases, and the call to Get­Console­Aliases will fail because the buffer is too small. In that case, we want to loop back and try again with a bigger buffer.

All of the preceding issues with Get­Console­Aliases also apply to Get­Console­Alias­Exes.

Here's the resulting code that tries to solve all of the problems:

template<typename GetLengthBytes, typename GetContents>
void PrintAliasValue(
  const GetLengthBytes& getLengthBytes,
  const GetContents& getContents)
{
  std::vector<wchar_t> buffer;
  do {
    auto bytes = getLengthBytes();
    auto length = (bytes + sizeof(wchar_t) - 1) / sizeof(wchar_t);
    buffer.resize(length);
    ZeroMemory(buffer.data(), bytes);
    SetLastError(ERROR_SUCCESS);
    if (getContents(buffer.data(), bytes)) {
      for (auto current = buffer.data();
           current < buffer.data() + buffer.size() && *current;
           current += wcslen(current) + 1) {
        std::wcout << current << std::endl;
      }
    }
  } while (GetLastError() == ERROR_MORE_DATA);
}

void do_showall(int argc, wchar_t **argv)
{
  auto program = argv[2];
  PrintAliasValue(
    [program]() { return GetConsoleAliasesLength(program); },
    [program](LPTSTR buffer, DWORD length) {
        return GetConsoleAliases(buffer, length, program); });
}

void do_showexes(int argc, wchar_t **argv)
{
  PrintAliasValue(
    []() { return GetConsoleAliasExesLength(); },
    [](LPTSTR buffer, DWORD length) {
       return GetConsoleAliasExes(buffer, length); });
}

The underlying algorithm is the same: Get the byte length, allocate a vector of characters, zero-initialize the data in the vector so that we can detect that a short buffer was returned, then ask for the data. If it succeeds, then read out the data, but stop when we hit one of our preallocated zeroes, or when we reach the end of the buffer, whichever comes first. If it fails because the buffer is too small, then loop back and try again.

So there you have it. A quick tour of the console alias functions. Now you can write your own DOSKEY replacement.


Comments (10)
  1. skSdnW says:

    I have used doskey in the console autorun value for 15+ years and the one annoying thing about it is that doskey crashes every now and then when started from that key. Maybe this series(?) is the kick in the butt I need to fix this myself once and for all.

    Is there any hope of getting the history of some of the other console stuff like full graphics support, custom menus and the other undocumented things that go back to the early days of NT?

  2. Mike says:

    Didn’t Microsoft beat its chest claiming API’s are no longer UCS-2 but UTF-16? (we all know that’s impossible, especially if you think of NTFS and $UpCase, but let’s play along). So potentially, you’d need 4, 6 or 8 times the amount of memory, why returning # of bytes is definitely the most sane (the opposite, returning # of glyphs, would be insane).

    I think I prefer byte count over some randomized glyph-count (especially when it’s based on a lie).

    1. Most Win32 functions return the number of TCHARs; i.e., the number of code units. There may be some special-purpose functions that return the number of code points (which I think is what you mean by glyphs), but they are the exception rather than the rule.

    2. I don’t see that $UpCase is a problem per se, assuming that it translates the surrogates to themselves. That means anything outside the BMP plane is necessarily case-sensitive, but does that matter?

  3. Tim! says:

    What makes this method better than adding a directory to the PATH that contains useful .bat files?

    1. This can be used to create aliases for any app that reads command lines (such as ntsd and cdb or my fake debugger app).

  4. exchange development blog team says:

    >More details about console aliases can be found in the documentation for DOSKEY.

    Uhh, not they aren’t. Only one occurrence of “console” on that page and it doesn’t mention aliases.

    1. Hey look, that page says “For more information on the special codes that can be used in Doskey macro definitions, see the command-line help for Doskey.exe or Doskey on TechNet.”

  5. DOS JOCKEY says:

    Can you believe that the DOSKEY instructions include this?

    To create a macro that performs a quick and unconditional format of a disk, type:
    doskey qf=format $1 /q /u

    To format a disk in drive A quickly and unconditionally, type:
    qf a:

Comments are closed.


*DISCLAIMER: I DO NOT OWN THIS CONTENT. If you are the owner and would like it removed, please contact me. The content herein is an archived reproduction of entries from Raymond Chen's "Old New Thing" Blog (most recent link is here). It may have slight formatting modifications for consistency and to improve readability.

WHY DID I DUPLICATE THIS CONTENT HERE? Let me first say this site has never had anything to sell and has never shown ads of any kind. I have nothing monetarily to gain by duplicating content here. Because I had made my own local copy of this content throughout the years, for ease of using tools like grep, I decided to put it online after I discovered some of the original content previously and publicly available, had disappeared approximately early to mid 2019. At the same time, I present the content in an easily accessible theme-agnostic way.

The information provided by Raymond's blog is, for all practical purposes, more authoritative on Windows Development than Microsoft's own MSDN documentation and should be considered supplemental reading to that documentation. The wealth of missing details provided by this blog that Microsoft could not or did not document about Windows over the years is vital enough, many would agree an online "backup" of these details is a necessary endeavor. Specifics include:

<-- Back to Old New Thing Archive Index