Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Symbolic Link Rotation Utility for the Support of the Rotation of the Windows® Logon/Shutdown Screen

4.64/5 (8 votes)
15 Dec 2016CPOL15 min read 13.5K   90  
Tired of the same old Windows Logon/Shutdown screen? I use a symbolic link rotation utility to cycle through a bank of backgrounds.

Background

I didn't like my PC vendor's background graphic for the logon/shutdown screen.  And having a vast arsenal of desktop background graphics available, I started investigating what it would take to cycle through my desktop backgrounds for my logon/shutdown screens.  Several good sources provided information for a one-time change (e.g. http://thedesktopteam.com/raphael/sccm-2012-cutomizing-windows-lock-screen/).  What was left was to write a utility that could be scheduled to change the logon/shutdown graphic based upon some event or schedule.  Instead of copying and renaming files, I decided to use a symbolic link, which causes the Operating System to redirect all file access on the symbolic link to execute on another file that is specified by the link (see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365680(v=vs.85).aspx for further information).

Caveats

  • Please use at your own risk.  Not that I think there are risks, but I have found some annoyances when an incompatible file is used as a logon/shutdown graphic.
    • Where Windows system directories are concerned, (e.g. %windir%\System32\oobe\Info\), you may be required to change the ownership of the directory using the advanced properties dialog as well as setting full permissions for the user and system accounts that will be used to run the LinkRotate utility.
    • Windows 7 can only handle background graphics of 256KB or smaller.  Larger files will either not load or the system will revert back to a Windows supplied default background graphic.
    • The background graphics are presented, by the logon/shutdown screen, in stretch-to-fit mode; which can ruin the aesthetics of a picture.
    • Changing the Windows Theme can undo your logon/shutdown screen registry settings and turn off the custom logon/shutdown background graphics (see https://blogs.technet.microsoft.com/win7/2011/02/16/the-value-of-oembackground-registry-key-resets-back-to-0-after-theme-gets-changed/).
  • The LinkRotation utility, presented below, is based upon the consistent output of the Windows console command for `dir /n`.
  • Yes, the LinkRotation utility might have more uses than just cycling through logon/shutdown background graphics.
  • Yes, my apologies in advance, I do have a dense coding style, ... if needed, there are code beautifiers freely available.

Why C++

There might actually be Windows APIs in C# or JAVA that allow the for the creation, introspection, and manipulation of symbolic links, but I didn't find any (please comment below, or post an article if you do know of any).  The WIN32 API had a way to create a symbolic link, but I could find no way to discover or change the target of the link.  What I did find that I could use, in C++, was _popen().  Using `dir /n` as the command parameter to _popen(),  API call, the returned stream of output from the command could be parsed to extract the value of the target of the symbolic link in the Windows file system.

The Approach

The LinkRotate Utility was designed as a console application so that it could be run, without UI from the registry or from the task scheduler.

  1. Determine the target of the current symbolic link.
  2. Sift through the directory path(s) and filter(s) provided on the command line to capture a list of files (or symbolic link files) in each.
  3. Save the first encountered file listed, to use in case no subsequent file in the search matches the target of the link to be rotated.
  4. Sift through the files in each of the specified paths looking for the file that matches the target of the current symbolic link
    1. If found,
      1. set the symbolic link to target the next file found in the search,
      2. or the saved first file, if at the very end of the search.
      3. done, ... stop searching/sifting, exit all.

Usage

lr.exe <symbolic link filename> [/p:<target path> [/f:<filename filter> [...]] [...]] [/r|/recursion] [/s|/subdirectories] [/q|/quiet] [/v|/verbose]

<symbolic link filename>
The first, unflagged, command line argument specifies the symbolic link in the file system that will be modified or rotated to the next file in the search.
/p <target path> -or- /p:<target path>
Zero or more directory paths can be specified for searching symbolic link candidates.
If unspecified, the rotational list defaults to the directory location of the symbolic link file itself.
/f <filename filter> -or- /f:<filename filter>
Zero or more wildcard filters can be specified following any path option.
If no filter is specified, all files in the directory path are considered.
/r -or- /recursion
Recurse through subdirectories in the file lists, when found. Equivalent to /s.
/s -or- /subdirectories
Include subdirectories, found in the file lists, in the search. Equivalent to /r.
/q -or- /quiet
Suppresses console output.
/v -or- /verbose
Embellishes console output.

The logon/shutdown Symbolic Link

See http://thedesktopteam.com/raphael/sccm-2012-cutomizing-windows-lock-screen/ for a full explanation of how to specify and enable a single specific logon/shutdown background.  On my own computers, I have found one of the following two files are targeted to be replaced by the desired background graphic file.

  • Either %windir%\System32\oobe\background.bmp
  • or %windir%\System32\oobe\Info\backgrounds\backgroundDefault.jpg

Instead of copying and renaming a single graphic file to one of the names above, I used the Windows mklink command to create an initial symbolic link (see http://www.computerhope.com/mklink.htm for more information on this command).  The link targes an actual graphic file that I want to use as the start of my background rotation.

Cascading Symbolic Links to Work Around Incompatible Files

It turns out on Windows 7 that some graphic files will not work for the logon/shutdown backgrounds:

  • Files larger than 256KB will not be loaded.
  • On one of my systems, files that were of higher resolution that the system's screen resolution were incompatible.
  • Files are stretch-to-fit when displayed, which might not be "incompatible", but the aesthetic might be so bad when stretched or shrunk or pixelated that it renders them unusable.
  • Even when size, dimensions, and file format should be compatible, there are some files that just don't display for the logon/shutdown screen.  I can offer no explanation for these incompatibilities.

Most of the files in my cache of desktop background graphics were not compatible for usage as a logon/shutdown background graphic, according to the measures above.  I didn't want to cull or duplicate my trove, and pleasantly discovered that symbolic links can target other symbolic links; that the OS will traverse the cascade of links to reach the final target file.  Thus I created links to the smaller set of files that I expected to be compatible, and later deleted the secondary symbolic links for the files that ended up proving incompatible.

In other words, for the logon/shutdown background, I have Windows point to a background graphic file that is itself a symbolic link.  I then use the LinkRotation utility to rotate the target of the background link file from a pool of targets that are a short-list of symbolic link files that then link to actual background graphic files.

Image 1

Administrative Privileges Required

The next stumbling block was that the Windows OS mklink command itself, and thus the LinkRotate utility, needed to run with administrative privileges.  For me, the best way to accomplish this was to set this flag in the Visual Studio configuration for the link step of the build.

Image 2

<rant> Code Project vs. GitHub </rant>

Apologies, in advance, for this small rant.  When I was first exposed to codeproject.com, I mistook some of the site's messaging.  I initially and naively started publishing on codeproject.com with the expectation that it would be a community that would help foster code and ideas.  Don't get me wrong, I absolutely value and appreciate this community and its diverse collection of ideas and knowledge.  It is a goto site for expertise.

But I find it more of a site for solitary voices and singular expertise; not a site that fosters collaboration on `code projects`.  It does have a good Open Source License (CPOL), but there is no repository support for published code projects.  If a code project is modified and republished, there is no notification to previously interested members.  (Might I recommend a codeproject.com - GitHub partnership?)

GitHub, on the other hand, does foster these activities for code projects.  Thus, though I benefit from publishing this article and source on codeproject.com, and though I greatly benefit from the contributions of others on this site, the most up-to-date source, and collaborations, for this code project will be found on GitHub. 

Code Inspection Part I - The Capturing of Directory Lists (proceed at your own interest)

Global Enumerations

The console application supports variances in verbosity and recursive subdirectory processing using global enumerations.

C++
enum verbosity { quiet = 0, normal, verbose };
enum verbosity _verbosity(verbosity::normal);

enum recursion { off = 0, on };
enum recursion _recursion(recursion::off);

The oList Template Class

This is a fusion of a standard singly-linked list class and a template class that can be applied to any node class with a recommendedly private pNext pointer.  I use it to collect and iterate through directory path specs, file filter specs, and file specs.

C++
template<typename T> class oList
{
private:
  T * pHead;
  T * pTail;

public:
  oList()
  {
    pHead = NULL;
    pTail = NULL;
  }

  ~oList()
  {
    T * n0;
    T * n = pHead;

    while (n != NULL)
    {
      n0 = n;
      n = n->pNext;
      delete n0;
    }
  }

  void Add(T * n)
  {
    if (pHead == NULL)
    {
      pHead = n;
      pTail = n;
    }

    else
    {
      pTail->pNext = n;
      pTail = n;
    }
  }

  int Count() const
  {
    int ret = 0;
    T * n = pHead;
    while (n != NULL)
    {
      n = n->pNext;
      ++ret;
    }

    return ret;
  }

  T * const operator[] (int index) const
  {
    if (index < 0) return NULL;

    T * n = pHead;
    while (n != NULL && index-- > 0) n = n->pNext;

    return n;
  }
};

The oFilterNode class

C++
class oFilterNode
{
  oFilterNode * pNext;
  char * pfilter;

public:
  oFilterNode(const char * const filter_)
  {
    pNext = NULL;
    pfilter = (filter_ != NULL) ? _strdup(filter_) : NULL;
  }

  ~oFilterNode()
  {
    if (pfilter != NULL) free(pfilter);
    pNext = NULL;
  }

  const char * const Filter() const { return pfilter; }

  friend class oList<oFilterNode>
};

*Note the friend class oList<oFilterNode>; declaration at the end of the node class.  This gives the oList template class access to the oFilterNode's pNext private member even though it is not in the node class' hierarchy.

The oPathNode class

C++
class oPathNode
{
  oPathNode * pNext;
  char *  ppath;
  oList<oFilterNode> *  pfilters;

public:

  oPathNode(const char * const path_)
  {
    pNext = NULL;
    ppath = (path_ != NULL) ? _strdup(path_) : NULL;
    pfilters = new oList<oFilterNode>();
  }

  ~oPathNode()
  {
    if (ppath != NULL) free(ppath);
    delete pfilters;
    pNext = NULL;
  }

  oList<oFilterNode> * Filters() { return pfilters; }
  const char * const Path() { return ppath; }

  friend class oList<oPathNode>;
};

*Note the friend class oList<oPathNode>; declaration at the end of the node class.  This gives the oList template class access to the oPathNode's pNext private member even though it is not in the node class' hierarchy.

The oFileNode class

First an overview, with a follow-on inspection of the workhorse method, CreateFileList().

C++
class oFileNode
{
  oFileNode *  pNext;

  char *  ppath;
  char *  pdate;
  char *  ptime;
  char *  ptype;
  char *  pname;
  char *  plink;

  char *  pfullpath;

  oFileNode(
   const char * const path_,
   const char * const date_,
   const char * const time_,
   const char * const type_,
   const char * const name_,
   const char * const link_)
  {
    ppath = (path_ != NULL) ? _strdup(path_) : NULL;
    pdate = (date_ != NULL) ? _strdup(date_) : NULL;
    ptime = (time_ != NULL) ? _strdup(time_) : NULL;
    ptype = (type_ != NULL) ? _strdup(type_) : NULL;
    pname = (name_ != NULL) ? _strdup(name_) : NULL;
    plink = (link_ != NULL) ? _strdup(link_) : NULL;

    pfullpath = NULL;
    pNext = NULL;
  }

  ~oFileNode()
  {
    if (ppath != NULL) free(ppath);
    if (pdate != NULL) free(pdate);
    if (ptime != NULL) free(ptime);
    if (ptype != NULL) free(ptype);
    if (pname != NULL) free(pname);
    if (plink != NULL) free(plink);
    if (pfullpath != NULL) free(pfullpath);
    pNext = NULL; 
  }

public:

  static oList<oFileNode> * CreateFileList(oList<oPathNode> * _paths) {...}

  const char * const FullPath() 
  {
    if (pfullpath == NULL)
    {
      size_t  len = strlen(ppath) + strlen(pname) + 3;
      pfullpath = (char *)malloc(len);
      strcpy_s(pfullpath, len, ppath);
      if (pfullpath[strlen(pfullpath) - 1] != '\\') strcat_s(pfullpath, len, "\\");
      strcat_s(pfullpath, len, pname);
    }

    return pfullpath;
  }

  const char * const Link() const { return plink; }
  const char * const Name() const { return pname; }
  const char * const Path() const { return ppath; }
  const char * const Path(const char * const path_)
  {
    if (ppath != NULL) free(ppath);
    ppath = (path_ != NULL) ? _strdup(path_) : NULL;
    return ppath;
  }

  const char * const Type() const { return ptype; }

  friend class oList<oFileNode>;
};

*Note the friend class oList<oFileNode>; declaration at the end of the node class.  This gives the oList template class access to the oFileNode's pNext private member even though it is not in the node class' hierarchy.

Also, for convenience, this class allows the Path() member to be reset and it will construct the resulting FullPath() when required.

Capturing the Contents of a Directory or Directories with the static oFileNode::CreateFileList() Method

oFileNode::CreateFileList() parses each line returned from the _popen() call and teases out the file or symbolic link details into an oList of oFileNodes.

The output of a standard graphic directory listing looks like this:

Image 3

The output of a symbolic link in the 'oobe' directory listing looks like this:

Image 4

To support the recursive descent into subdirectories, this function has to operate on a list of paths, that might have additional directory paths added to its end while it processes the enteries for another path already in the list.  This complicates the looping from a fixex list for() loop to a 'check if we are finished' while() loop.  Further, the paths to parse might have an additional list of filters to loop through as well.  The logic below unfolds these two iterators and treats them together.  It depends upon the oList method Add() to strictly append to the end of the oList and that the oList operator[] will return NULL when the index is out of bounds.

C++
static oList<oFileNode> * CreateFileList(oList<oPathNode> * _paths)
{
  oList<oFileNode> *  ret = new oList<oFileNode>();
  const char *        cmd0 = "dir /n ";
  oPathNode *         path_node = NULL;

  int  i = 0, j = 0;
  while ((path_node = (*_paths)[i]) != NULL)
  {
    const char *  path = path_node->Path();
    oList<oFilterNode> *  _filters = path_node->Filters();
    const char *  filter = ((*_filters)[j] != NULL) ? (*_filters)[j]->Filter() : NULL;

    if (filter == NULL)
    {
      ++i;
      if (j > 0)
      {
        j = 0;
        continue;
      }
    }

    else ++j;

Once a path and optionally paired file filter are identified for processing, the command line for the call to _popen() can be built.

C++
  size_t tmp_len = 0;

  if (path != NULL && (tmp_len = strlen(path)) > 0)
  {
    size_t  cmd_len = strlen(cmd0) + tmp_len + ((filter != NULL) ? strlen(filter) : 0) + 3;
    char *  cmd = new char[cmd_len];

    strcpy_s(cmd, cmd_len, cmd0);
    strcat_s(cmd, cmd_len, path);
    if (filter != NULL)
    {
      if (cmd[strlen(cmd) - 1] != '\\' && filter[0] != '\\') strcat_s(cmd, cmd_len, "\\");
      strcat_s(cmd, cmd_len, filter);
    }

    FILE * cmd_out = _popen(cmd, "rt");

_popen() returns a file stream of the output from the Windows OS command submitted (see `dir /n` output above).  The next task is to parse through the returned information to capture the file information.  This is the way I found to capture the target information of a symbolic link directory entry in the absence of an API to do the same.  To accomplish this I set up a simple, cascading state machine to guide the parsing of the output from the `dir /n <path>[<filter>]` command.

C++
    if (cmd_out != NULL)
    {
      enum parse_mode {
       startline,
       skipline,
       capturedate,
       skiptotime,
       capturetime,
       skiptotype,
       capturetype,
       capturesize,
       skiptoname,
       capturename,
       capturelink
      } cmd_parse_mode = parse_mode::startline;

      char  odate[25];
      char  otime[25];
      char  otype[25];
      char  osize[25];
      char  oname[FILENAME_MAX + 1];
      char  olink[FILENAME_MAX + 1];

      int  c = fgetc(cmd_out);
      while (feof(cmd_out) == 0)
      {

If the end of the line is unexpectedly found from the output stream, then the processing of the current line is abandoned and the processing of the next line of output is begun.  Otherwise, if the end of line is found while in a stage that expects an EOL, and execution proceeds to the else block to complete the EOL processing.

In general, each stage of the processing, to capture a field, proceeds by:

  • checking the stage's entry conditions,
  • clearing the stage's storage and initializing from the initial entry condition (e.g. the first character of the field from the output stream, that we have already read),
  • capturing the rest of the stage's field from the output stream (one character at a time),
  • then skipping over the spaces between fields until the initial conditions of the next field are met.

Here is the code that captures the date field from the `dir /n` command output:

C++
        if ((c == '\r' || c == '\n') && cmd_parse_mode != parse_mode::capturename)
          cmd_parse_mode = parse_mode::startline;

        else switch (cmd_parse_mode)
        {
        case parse_mode::startline:
          if (isdigit(c))
          {
            memset(odate, 0, sizeof(odate));
            odate[0] = (char)c;
            cmd_parse_mode = parse_mode::capturedate;
          }

          else cmd_parse_mode = parse_mode::skipline;
          break;

        case parse_mode::skipline:
          break;

        case parse_mode::capturedate:
          if (!isspace(c))
          {
            if ((tmp_len = strlen(odate)) < sizeof(odate)) odate[tmp_len] = (char)c;
          }
          else cmd_parse_mode = parse_mode::skiptotime;
          break;

After the date field, the time field is captured:

C++
        case parse_mode::skiptotime:
          if (!isspace(c))
          {
            memset(otime, 0, sizeof(otime));
            otime[0] = (char)c;
            cmd_parse_mode = parse_mode::capturetime;
          }
          break;

        case parse_mode::capturetime:
          if (!isspace(c))
          {
            if ((tmp_len = strlen(otime)) < sizeof(otime)) otime[tmp_len] = (char)c;
          }
          else cmd_parse_mode = parse_mode::skiptotype;
          break;

Then the type field, if present.  If not then the size field.  On Windows, directory entries either have a type or a size.

C++
        case parse_mode::skiptotype:
          if (c == '<')
          {
            memset(otype, 0, sizeof(otype));
            memset(osize, 0, sizeof(osize));
            cmd_parse_mode = parse_mode::capturetype;
          }

          else if (!isspace(c))
          {
            memset(otype, 0, sizeof(otype));
            memset(osize, 0,sizeof(osize));
            osize[0] = (char)c;
            cmd_parse_mode = parse_mode::capturesize;
          }
          break;

        case parse_mode::capturetype:
          if (c == '>') cmd_parse_mode = parse_mode::skiptoname;
          else
          {
            if ((tmp_len = strlen(otype)) < sizeof(otype)) otype[tmp_len] = (char)c;
          }
          break;

        case parse_mode::capturesize:
          if (!isspace(c))
          {
            if ((tmp_len = strlen(osize)) < sizeof(osize)) osize[strlen(osize)] = (char)c;
          }
          else cmd_parse_mode = parse_mode::skiptoname;
          break;

Then the name field, which terminates in an end of line character, or a square bracket character ('['), which is the initial condition for a link-target, field.

After the name and link (if applicable) fields are captured, and trimmed to remove trailing spaces, an entry is added to the accumulating oList<oFileNode> list that will eventually be returned.

If the captured entry represents a directory (other than the '.' current and '..' previous directories), and recursion is enabled, the full path of the identified directory is added to the end of the list of directories being processed along with all of the filters, if any, used by the current directory path.  This new entry, at the end of the list, is guaranteed to be processed, even though it is added late.

C++
        case parse_mode::skiptoname:
          if (!isspace(c))
          {
            memset(oname, 0, sizeof(oname));
            memset(olink, 0, sizeof(olink));
            oname[0] = (char)c;
            cmd_parse_mode = parse_mode::capturename;
          }
          break;

        case parse_mode::capturename:
          if (c == '\r' || c == '\n')
          {
            while ((tmp_len = strlen(oname) - 1) >= 0 && isspace(oname[tmp_len])) oname[tmp_len] = '\0';
            oFileNode * fn = new oFileNode(path, odate, otime, otype, oname, olink);

            if (_recursion == recursion::on
             && fn->Type() != NULL
             && strcmp(fn->Type(), "DIR") == 0
             && fn->Name() != NULL
             && fn->Name()[0] != '.')
            {
              size_t path_len_ = strlen(path) + strlen(fn->Name()) + 3;
              char * recurse_path_ = new char[path_len_];

              strcpy_s(recurse_path_, path_len_, path);
              if (path[strlen(path) - 1] != '\\') strcat_s(recurse_path_, path_len_, "\\");
              strcat_s(recurse_path_, path_len_, fn->Name());

              oPathNode * rpath_ = new oPathNode(recurse_path_);
              for (int k = 0; k < _filters->Count(); ++k)
              {
                rpath_->Filters()->Add(new oFilterNode((*_filters)[k]->Filter()));
              }
              _paths->Add(rpath_);

              delete recurse_path_;
            }

            ret->Add(fn);
            cmd_parse_mode = parse_mode::startline;
          }

          else if (c == '[')
          {
            while ((tmp_len = strlen(oname) - 1) >= 0 && isspace(oname[tmp_len])) oname[tmp_len] = '\0';
            if (otype[0] != '\0') cmd_parse_mode = parse_mode::capturelink;
            else
            {
              ret->Add(new oFileNode(path, odate, otime, otype, oname, olink));
              cmd_parse_mode = parse_mode::skipline;
            }
          }

          else if ((tmp_len = strlen(oname)) < sizeof(oname) - 1) oname[tmp_len] = (char)c;
          break;

        case parse_mode::capturelink:
          if (c == ']')
          {
            ret->Add(new oFileNode(path, odate, otime, otype, oname, olink));
            cmd_parse_mode = parse_mode::skipline;
          }

          else if ((tmp_len = strlen(olink)) < sizeof(olink) - 1) olink[tmp_len] = (char)c;
          break;
        }

At the end of the staged processing, read the next character from the output of the console command.  When there are no more characters, close the console and delete the string created to specify what OS command the console should run.

As an edge case cleanup, set the path descriptor for an unfiltered list that only returns one entry.  This commonly occurs when the path specified references an existing file instead of a directory and is used by main() to validate the symbolic link file that is having its target rotated.

Finally, return the list of results to the caller.

C++
        c = fgetc(cmd_out);

        if (_verbosity >= verbosity::verbose) _fputchar(c);
      }

      _pclose(cmd_out);
    }

    delete cmd;

    if (ret->Count() == 1 && filter == NULL)
    {
      const char * pch = strrchr(path, '\\');
      if (pch == NULL) (*ret)[0]->Path(".\\");
      else if (strcmp((pch + 1),(*ret)[0]->Name()) == 0)
      {
        char * ppath = _strdup(path);
        if (ppath != NULL)
        {
          char * ppch = strrchr(ppath, '\\');
          if (ppch != NULL)
          {
            *ppch = '\0';
            (*ret)[0]->Path(ppath);
          }

          free(ppath);
        }
      }
    }
  }

  return ret;
}

Time for a break?

Image 5

I've drug you through a lot of code; are you sure that you want to see the rest?  Or maybe it is time to take a break and imbibe in the stimulant beverage of your choice.  While you relax, check out NASA's Image of the Day Callery or NASA's Astronomy Picture of the Day; always a trove of visual discovery for earth and space science, and a good source for background graphics.  Here is a small sample while you wait:

Image 6

Code Inspection Part II - The main() of Link Rotation (proceed at your own interest)

The Processing of Command Line Arguments

The first part of the LinkRotation Utility's main() routine processes the command line arguments.  Since it is a lightweight utility, with only a handful of arguments, I take some liberties.  I key off of a single letter for a command line flag, but swallow the whole word provided (e.g. /p, -path, and /penuchle all match as a path flag).  Though not specified, I allow both space-delimited and colon-delimited (':') arguments.

Because some arguments affect the processing of others, mainly recursion, I took a two pass approach to process the precedent arguments first.

C++
int main(int argc, char ** argv)
{
  char const *        _link = NULL;
  oList<oPathNode> *  _paths = new oList<oPathNode>();

  char const usage_prompt[] = "\nUsage:\n\t%s <symbolic link filename> [/p:<target path> [/f:<filename filter> [...]] [...]] [/r|/recursion] [/s|/subdirectories] [/q|/quiet] [/v|/verbose]\n";

  int    i, j, cnt;
  char * pch;

  // Parse precedent command line arguments first.
  for (i = 1; i < argc; ++i)
  {
    switch (argv[i][0])
    {
    case '-':
    case '/':
      switch (argv[i][1])
      {
      case '?':
      case 'h': case 'H':
        printf_s(usage_prompt, argv[0]);
        return 0;

      case 'r': case 'R':
      case 's': case 'S':
        _recursion = recursion::on;
        break;

      case 'q': case 'Q':
        _verbosity = verbosity::quiet;
        break;

      case 'v': case 'V':
        _verbosity = verbosity::verbose;
        break;
      }
      break;
    }
  }

After the precedent argument have been parsed, tease out the path and filter arguments, as well as the required argument for the symbolic link file to manipulate.  Note the parser handles both space-delidelimited colon-delimited arguments.

When found, filter arguments are added to the most recently parsed path argument.  An option is appended to the filter to exclude directories from the returned values being searched.  If recursion is turned on, an additional filter is added to only return directories.  Both of these additions are required to keep the processing of files and directories separated when filters are in use.  (When filters are not in use, the extra handling is not needed.)

C++
  // Re-parse for non-precedent command line arguments.
  for (i = 1; i < argc; ++i)
  {
    const char * err_msg = "unrecognized parameter";
    switch (argv[i][0])
    {
    case '-':
    case '/':
      switch (argv[i][1])
      {
      case '?':
      case 'h': case 'H':
      case 'q': case 'Q':
      case 'r': case 'R':
      case 's': case 'S':
      case 'v': case 'V':
        //Precedent paramaters that have already been parsed.
        break;

      case 'p': case 'P':
        if ((pch = strchr(argv[i], ':')) != NULL)
        {
          _paths->Add(new oPathNode(++pch));
        }

        else if (i + 1 < argc && argv[i + 1][0] != '-' && argv[i + 1][0] != '/')
        {
          _paths->Add(new oPathNode(argv[++i]));
        }

        else
        {
          err_msg = "unparsable path parameter";
          goto cmd_parse_error;
        }
        break;

      case 'f': case 'F':
        if ((pch = strchr(argv[i], ':')) != NULL) ++pch;
        else if (i + 1 < argc && argv[i+1][0] != '-' && argv[i+1][0] != '/') pch = argv[++i];

        else
        {
          err_msg = "unparsable filter parameter";
          goto cmd_parse_error;
        }

        if ((cnt = _paths->Count()) > 0)
        {
          const char * const _dir_filter = " /A:D";
          const char * const _sans_dir_filter = " /A:-D";
          oPathNode * p = (*_paths)[cnt - 1];

          if (_recursion == recursion::on && p->Filters()->Count() == 0 )
          {
            p->Filters()->Add(new oFilterNode(_dir_filter));
          }

          size_t len_ = strlen(pch) + strlen(_sans_dir_filter) + 3;
          char * pfilter_ = new char[len_];

          strcpy_s(pfilter_, len_, pch);
          strcat_s(pfilter_, len_, _sans_dir_filter);
          p->Filters()->Add(new oFilterNode(pfilter_));

          delete pfilter_;
        }

        else
        {
          err_msg = "filter parameter found without preceeding path parameter";
          goto cmd_parse_error;
        }

        break;

      default:
cmd_parse_error:
        perror("\nbad command line parameter:\n");
        perror(err_msg);
        perror("\n");
        if (_verbosity >= verbosity::normal) printf_s(usage_prompt, argv[0]);
        return -1;
      }
      break;

    default:
      if (_link != NULL) goto cmd_parse_error;
      _link = argv[i];
      break;
    }
  }

Next, some checks are performed to setup and ensure operating prerequisites have been met.  This includes running the intended symbolic link file that will have its target rotated, through the oFileNode::CreateFileList() processor so that the target link can be extracted as well as checking for its existence as a symbolic link file.  Remember, I did not find an API that would do this for me.

When only the symbolic link file is specified, I process the link rotation in the same directory as the specified symbolic link file.

For clarity, I will begin calling the symbolic link file having its target rotated the 'PrincipalFile'.

C++
  if (_link == NULL)
  {
    perror("\nmissing symbolic link command line parameter\n");
    if (_verbosity >= verbosity::normal) printf_s(usage_prompt, argv[0]);
    return -2;
  }

  oList<oPathNode> *    pPrincipal = new oList<oPathNode>();
  pPrincipal->Add(new oPathNode(_link));
  oList<oFileNode> *    pPrincipalList = oFileNode::CreateFileList(pPrincipal);

  if (pPrincipalList->Count() > 1)
  {
    perror("\nsymbolic link cannot specify multiple files\n");
    if (_verbosity >= verbosity::normal) printf_s(usage_prompt, argv[0]);
    return (-3);
  }

  oFileNode * const    pPrincipalFile = (*pPrincipalList)[0];

  if (pPrincipalFile != NULL && pPrincipalFile->Link() == NULL)
  {
    // A link is expected to facilitate the rotation.
    // This also protects against the loss of an existing file.
    perror("\ncannot replace a real file with a symbolic link\n");
    if (_verbosity >= verbosity::normal) printf_s(usage_prompt, argv[0]);
    return (-4);
  }

  if (_paths->Count() == 0)
  {
    char * pdir = _strdup(_link);
    pch = strrchr(pdir, '\\');
    if (pch != NULL) *pch = '\0';
    else if (strcpy_s(pdir,strlen(_link),".\\") != 0)
    {
      perror("\ncould not set directory path for link candidates\n");
      if (_verbosity >= verbosity::normal) printf_s(usage_prompt, argv[0]);
      return(-5);
    }

    _paths->Add(new oPathNode(pdir));
    free(pdir);
  }

Processing the Captured Directory Entries

After the initial constraints have been verified, then the full list of files is captured to be processed.  The goal is to find the file or symbolic link file that is one past the current link targeted in the 'PrincipalFile'.  A guard must also be put in place in case the 'PrincipalFile' is itself found in the list of files being processed.

As a failsafe, pnewtarget is set to the first file that could be used as a new target of the rotation.  Once the current target of the 'PrincipalFile' is found in the list being processed, then pnewtarget is set to the next potential target.  If there is no next potential target, then the failsafe target is used.  This is how rotation works or resets even if the current target is never found, or is only found at the very last step, as the process is exiting the search loops.

C++
  if (_verbosity > verbosity::normal) printf_s("\n\n");

  char *  pnewtarget = NULL;
  bool    bprincipallinkfound = false;
  bool    bexitloops = false;

  oList<oFileNode> * pCandidateList = oFileNode::CreateFileList(_paths);
  cnt = pCandidateList->Count();
  for (j = 0; !bexitloops && j < cnt; ++j)
  {
    if (_verbosity > verbosity::normal)
    {
      printf_s("%s [%s] ?= :%s: %s [%s]\n",
          pPrincipalFile->Name(),
          pPrincipalFile->Link(),
          (*pCandidateList)[j]->Type(),
          (*pCandidateList)[j]->Name(),
          (*pCandidateList)[j]->Link());
    }

    if (((*pCandidateList)[j]->Type() == NULL || _stricmp((*pCandidateList)[j]->Type(), "DIR") !=0)
        && _stricmp(pPrincipalFile->Name(), (*pCandidateList)[j]->Name()) != 0)
    {
      if (_stricmp(pPrincipalFile->Link(), (*pCandidateList)[j]->Name()) == 0
         || _stricmp(pPrincipalFile->Link(), (*pCandidateList)[j]->FullPath()) == 0)
      {
        bprincipallinkfound = true;
      }

      else
      {
        if (bprincipallinkfound)
        {
          if (pnewtarget != NULL) free(pnewtarget);
          pnewtarget = NULL;
          bexitloops = true;
        }

        if (pnewtarget == NULL) pnewtarget = _strdup((*pCandidateList)[j]->FullPath());
      }
    }
  }
  delete pCandidateList;

Setting the Link to the Next Target in Rotation

Once the new target for the 'PrincipalFile' has been identified, then a new command line is built for the _popen() API that calls the Windows OS mklink command with the appropriate arguments.  Since symlink files cannot be edited with the mklink command, it must first be deleted (here using, with little irony, _unlink()).

Rotation, having been accomplished, clean up and exit.

C++
  if (pnewtarget != NULL) 
  {
    const char * const cmd0 = "mklink ";
    size_t cmd_len = strlen(cmd0) + strlen(pPrincipalFile->FullPath()) + strlen(pnewtarget) + 6;
    char * cmd = (char *)malloc(cmd_len);
    FILE * cmd_out = NULL;

    strcpy_s(cmd, cmd_len, cmd0);
    strcat_s(cmd, cmd_len, "\"");
    strcat_s(cmd, cmd_len, pPrincipalFile->FullPath());
    strcat_s(cmd, cmd_len, "\" \"");
    strcat_s(cmd, cmd_len, pnewtarget);
    strcat_s(cmd, cmd_len, "\"");

    if (_verbosity > verbosity::normal) printf_s("\n\ndeleting: %s\n", pPrincipalFile->FullPath());
    if (_unlink(pPrincipalFile->FullPath()) == -1)
    {
      char buf[280];
      strerror_s(buf, errno);
      perror(buf);
    }

    if (_verbosity > verbosity::normal) printf_s(command_prompt, cmd);
    if ((cmd_out = _popen(cmd,"rt")) != NULL)
    {
      int c;
      while (!feof(cmd_out))
      {
        c = fgetc(cmd_out);
        if (_verbosity > verbosity::normal) _fputchar(c);
      }
      _pclose(cmd_out);
    }
    free(cmd);
    free(pnewtarget);
  }

  else perror("\n\nLink not rotated.  No candidate files found or processed.\n\n");

  delete pPrincipalList;
  delete pPrincipal;
  delete _paths;
  return 0;
}

History

2016.12.15 - Initial Submission

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)