OpenSolaris_b135/lib/libtecla/common/expand.c

Compare this file to the similar file:
Show the results in this format:

/*
 * Copyright (c) 2000, 2001, 2002, 2003, 2004 by Martin C. Shepherd.
 * 
 * All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, and/or sell copies of the Software, and to permit persons
 * to whom the Software is furnished to do so, provided that the above
 * copyright notice(s) and this permission notice appear in all copies of
 * the Software and that both the above copyright notice(s) and this
 * permission notice appear in supporting documentation.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
 * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL
 * INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 * 
 * Except as contained in this notice, the name of a copyright holder
 * shall not be used in advertising or otherwise to promote the sale, use
 * or other dealings in this Software without prior written authorization
 * of the copyright holder.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * If file-system access is to be excluded, this module has no function,
 * so all of its code should be excluded.
 */
#ifndef WITHOUT_FILE_SYSTEM

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include "freelist.h"
#include "direader.h"
#include "pathutil.h"
#include "homedir.h"
#include "stringrp.h"
#include "libtecla.h"
#include "ioutil.h"
#include "expand.h"
#include "errmsg.h"

/*
 * Specify the number of elements to extend the files[] array by
 * when it proves to be too small. This also sets the initial size
 * of the array.
 */
#define MATCH_BLK_FACT 256

/*
 * A list of directory iterators is maintained using nodes of the
 * following form.
 */
typedef struct DirNode DirNode;
struct DirNode {
  DirNode *next;       /* The next directory in the list */
  DirNode *prev;       /* The node that precedes this node in the list */
  DirReader *dr;       /* The directory reader object */
};

typedef struct {
  FreeList *mem;       /* Memory for DirNode list nodes */
  DirNode *head;       /* The head of the list of used and unused cache nodes */
  DirNode *next;       /* The next unused node between head and tail */
  DirNode *tail;       /* The tail of the list of unused cache nodes */
} DirCache;

/*
 * Specify how many directory cache nodes to allocate at a time.
 */
#define DIR_CACHE_BLK 20

/*
 * Set the maximum length allowed for usernames.
 */
#define USR_LEN 100

/*
 * Set the maximum length allowed for environment variable names.
 */
#define ENV_LEN 100

/*
 * Set the default number of spaces place between columns when listing
 * a set of expansions.
 */
#define EF_COL_SEP 2

struct ExpandFile {
  ErrMsg *err;            /* The error reporting buffer */
  StringGroup *sg;        /* A list of string segments in which */
                          /*  matching filenames are stored. */
  DirCache cache;         /* The cache of directory reader objects */
  PathName *path;         /* The pathname being matched */
  HomeDir *home;          /* Home-directory lookup object */
  int files_dim;          /* The allocated dimension of result.files[] */
  char usrnam[USR_LEN+1]; /* A user name */
  char envnam[ENV_LEN+1]; /* An environment variable name */
  FileExpansion result;   /* The container used to return the results of */
                          /*  expanding a path. */
};

static int ef_record_pathname(ExpandFile *ef, const char *pathname,
			      int remove_escapes);
static char *ef_cache_pathname(ExpandFile *ef, const char *pathname,
			       int remove_escapes);
static void ef_clear_files(ExpandFile *ef);

static DirNode *ef_open_dir(ExpandFile *ef, const char *pathname);
static DirNode *ef_close_dir(ExpandFile *ef, DirNode *node);
static char *ef_expand_special(ExpandFile *ef, const char *path, int pathlen);
static int ef_match_relative_pathname(ExpandFile *ef, DirReader *dr,
				      const char *pattern, int separate);
static int ef_matches_range(int c, const char *pattern, const char **endp);
static int ef_string_matches_pattern(const char *file, const char *pattern,
				      int xplicit, const char *nextp);
static int ef_cmp_strings(const void *v1, const void *v2);

/*
 * Encapsulate the formatting information needed to layout a
 * multi-column listing of expansions.
 */
typedef struct {
  int term_width;     /* The width of the terminal (characters) */
  int column_width;   /* The number of characters within in each column. */
  int ncol;           /* The number of columns needed */
  int nline;          /* The number of lines needed */
} EfListFormat;

/*
 * Given the current terminal width, and a list of file expansions,
 * determine how to best use the terminal width to display a multi-column
 * listing of expansions.
 */
static void ef_plan_listing(FileExpansion *result, int term_width,
			    EfListFormat *fmt);

/*
 * Display a given line of a multi-column list of file-expansions.
 */
static int ef_format_line(FileExpansion *result, EfListFormat *fmt, int lnum,
			  GlWriteFn *write_fn, void *data);

/*.......................................................................
 * Create the resources needed to expand filenames.
 *
 * Output:
 *  return  ExpandFile *  The new object, or NULL on error.
 */
ExpandFile *new_ExpandFile(void)
{
  ExpandFile *ef;  /* The object to be returned */
/*
 * Allocate the container.
 */
  ef = (ExpandFile *) malloc(sizeof(ExpandFile));
  if(!ef) {
    errno = ENOMEM;
    return NULL;
  };
/*
 * Before attempting any operation that might fail, initialize the
 * container at least up to the point at which it can safely be passed
 * to del_ExpandFile().
 */
  ef->err = NULL;
  ef->sg = NULL;
  ef->cache.mem = NULL;
  ef->cache.head = NULL;
  ef->cache.next = NULL;
  ef->cache.tail = NULL;
  ef->path = NULL;
  ef->home = NULL;
  ef->result.files = NULL;
  ef->result.nfile = 0;
  ef->usrnam[0] = '\0';
  ef->envnam[0] = '\0';
/*
 * Allocate a place to record error messages.
 */
  ef->err = _new_ErrMsg();
  if(!ef->err)
    return del_ExpandFile(ef);
/*
 * Allocate a list of string segments for storing filenames.
 */
  ef->sg = _new_StringGroup(_pu_pathname_dim());
  if(!ef->sg)
    return del_ExpandFile(ef);
/*
 * Allocate a freelist for allocating directory cache nodes.
 */
  ef->cache.mem = _new_FreeList(sizeof(DirNode), DIR_CACHE_BLK);
  if(!ef->cache.mem)
    return del_ExpandFile(ef);
/*
 * Allocate a pathname buffer.
 */
  ef->path = _new_PathName();
  if(!ef->path)
    return del_ExpandFile(ef);
/*
 * Allocate an object for looking up home-directories.
 */
  ef->home = _new_HomeDir();
  if(!ef->home)
    return del_ExpandFile(ef);
/*
 * Allocate an array for files. This will be extended later if needed.
 */
  ef->files_dim = MATCH_BLK_FACT;
  ef->result.files = (char **) malloc(sizeof(ef->result.files[0]) *
				      ef->files_dim);
  if(!ef->result.files) {
    errno = ENOMEM;
    return del_ExpandFile(ef);
  };
  return ef;
}

/*.......................................................................
 * Delete a ExpandFile object.
 *
 * Input:
 *  ef     ExpandFile *  The object to be deleted.
 * Output:
 *  return ExpandFile *  The deleted object (always NULL).
 */
ExpandFile *del_ExpandFile(ExpandFile *ef)
{
  if(ef) {
    DirNode *dnode;
/*
 * Delete the string segments.
 */
    ef->sg = _del_StringGroup(ef->sg);
/*
 * Delete the cached directory readers.
 */
    for(dnode=ef->cache.head; dnode; dnode=dnode->next)
      dnode->dr = _del_DirReader(dnode->dr);
/*
 * Delete the memory from which the DirNode list was allocated, thus
 * deleting the list at the same time.
 */
    ef->cache.mem = _del_FreeList(ef->cache.mem, 1);
    ef->cache.head = ef->cache.tail = ef->cache.next = NULL;
/*
 * Delete the pathname buffer.
 */
    ef->path = _del_PathName(ef->path);
/*
 * Delete the home-directory lookup object.
 */
    ef->home = _del_HomeDir(ef->home);
/*
 * Delete the array of pointers to files.
 */
    if(ef->result.files) {
      free(ef->result.files);
      ef->result.files = NULL;
    };
/*
 * Delete the error report buffer.
 */
    ef->err = _del_ErrMsg(ef->err);
/*
 * Delete the container.
 */
    free(ef);
  };
  return NULL;
}

/*.......................................................................
 * Expand a pathname, converting ~user/ and ~/ patterns at the start
 * of the pathname to the corresponding home directories, replacing
 * $envvar with the value of the corresponding environment variable,
 * and then, if there are any wildcards, matching these against existing
 * filenames.
 *
 * If no errors occur, a container is returned containing the array of
 * files that resulted from the expansion. If there were no wildcards
 * in the input pathname, this will contain just the original pathname
 * after expansion of ~ and $ expressions. If there were any wildcards,
 * then the array will contain the files that matched them. Note that
 * if there were any wildcards but no existing files match them, this
 * is counted as an error and NULL is returned.
 *
 * The supported wildcards and their meanings are:
 *  *        -  Match any sequence of zero or more characters.
 *  ?        -  Match any single character.
 *  [chars]  -  Match any single character that appears in 'chars'.
 *              If 'chars' contains an expression of the form a-b,
 *              then any character between a and b, including a and b,
 *              matches. The '-' character looses its special meaning
 *              as a range specifier when it appears at the start
 *              of the sequence of characters.
 *  [^chars] -  The same as [chars] except that it matches any single
 *              character that doesn't appear in 'chars'.
 *
 * Wildcard expressions are applied to individual filename components.
 * They don't match across directory separators. A '.' character at
 * the beginning of a filename component must also be matched
 * explicitly by a '.' character in the input pathname, since these
 * are UNIX's hidden files.
 *
 * Input:
 *  ef         ExpandFile *  The pathname expansion resource object.
 *  path             char *  The path name to be expanded.
 *  pathlen           int    The length of the suffix of path[] that
 *                           constitutes the filename to be expanded,
 *                           or -1 to specify that the whole of the
 *                           path string should be used. Note that
 *                           regardless of the value of this argument,
 *                           path[] must contain a '\0' terminated
 *                           string, since this function checks that
 *                           pathlen isn't mistakenly too long.
 * Output:
 *  return  FileExpansion *  A pointer to a container within the given
 *                           ExpandFile object. This contains an array
 *                           of the pathnames that resulted from expanding
 *                           ~ and $ expressions and from matching any
 *                           wildcards, sorted into lexical order.
 *                           This container and its contents will be
 *                           recycled on subsequent calls, so if you need
 *                           to keep the results of two successive runs,
 *                           you will either have to allocate a private
 *                           copy of the array, or use two ExpandFile
 *                           objects.
 *
 *                           On error NULL is returned. A description
 *                           of the error can be acquired by calling the
 *                           ef_last_error() function.
 */
FileExpansion *ef_expand_file(ExpandFile *ef, const char *path, int pathlen)
{
  DirNode *dnode;       /* A directory-reader cache node */
  const char *dirname;  /* The name of the top level directory of the search */
  const char *pptr;     /* A pointer into path[] */
  int wild;             /* True if the path contains any wildcards */
/*
 * Check the arguments.
 */
  if(!ef || !path) {
    if(ef) {
      _err_record_msg(ef->err, "ef_expand_file: NULL path argument",
		      END_ERR_MSG);
    };
    errno = EINVAL;
    return NULL;
  };
/*
 * If the caller specified that the whole of path[] be matched,
 * work out the corresponding length.
 */
  if(pathlen < 0 || pathlen > strlen(path))
    pathlen = strlen(path);
/*
 * Discard previous expansion results.
 */
  ef_clear_files(ef);
/*
 * Preprocess the path, expanding ~/, ~user/ and $envvar references,
 * using ef->path as a work directory and returning a pointer to
 * a copy of the resulting pattern in the cache.
 */
  path = ef_expand_special(ef, path, pathlen);
  if(!path)
    return NULL;
/*
 * Clear the pathname buffer.
 */
  _pn_clear_path(ef->path);
/*
 * Does the pathname contain any wildcards?
 */
  for(wild=0,pptr=path; !wild && *pptr; pptr++) {
    switch(*pptr) {
    case '\\':                      /* Skip escaped characters */
      if(pptr[1])
	pptr++;
      break; 
    case '*': case '?': case '[':   /* A wildcard character? */
      wild = 1;
      break;
    };
  };
/*
 * If there are no wildcards to match, copy the current expanded
 * path into the output array, removing backslash escapes while doing so.
 */
  if(!wild) {
    if(ef_record_pathname(ef, path, 1))
      return NULL;
/*
 * Does the filename exist?
 */
    ef->result.exists = _pu_file_exists(ef->result.files[0]);
/*
 * Match wildcards against existing files.
 */
  } else {
/*
 * Only existing files that match the pattern will be returned in the
 * cache.
 */
    ef->result.exists = 1;
/*
 * Treat matching of the root-directory as a special case since it
 * isn't contained in a directory.
 */
    if(strcmp(path, FS_ROOT_DIR) == 0) {
      if(ef_record_pathname(ef, FS_ROOT_DIR, 0))
	return NULL;
    } else {
/*
 * What should the top level directory of the search be?
 */
      if(strncmp(path, FS_ROOT_DIR, FS_ROOT_DIR_LEN) == 0) {
	dirname = FS_ROOT_DIR;
	if(!_pn_append_to_path(ef->path, FS_ROOT_DIR, -1, 0)) {
	  _err_record_msg(ef->err, "Insufficient memory to record path",
			  END_ERR_MSG);
	  return NULL;
	};
	path += FS_ROOT_DIR_LEN;
      } else {
	dirname = FS_PWD;
      };
/*
 * Open the top-level directory of the search.
 */
      dnode = ef_open_dir(ef, dirname);
      if(!dnode)
	return NULL;
/*
 * Recursively match successive directory components of the path.
 */
      if(ef_match_relative_pathname(ef, dnode->dr, path, 0)) {
	dnode = ef_close_dir(ef, dnode);
	return NULL;
      };
/*
 * Cleanup.
 */
      dnode = ef_close_dir(ef, dnode);
    };
/*
 * No files matched?
 */
    if(ef->result.nfile < 1) {
      _err_record_msg(ef->err, "No files match", END_ERR_MSG);
      return NULL;
    };
/*
 * Sort the pathnames that matched.
 */
    qsort(ef->result.files, ef->result.nfile, sizeof(ef->result.files[0]),
	  ef_cmp_strings);
  };
/*
 * Return the result container.
 */
  return &ef->result;
}

/*.......................................................................
 * Attempt to recursively match the given pattern with the contents of
 * the current directory, descending sub-directories as needed.
 *
 * Input:
 *  ef      ExpandFile *  The pathname expansion resource object.
 *  dr       DirReader *  The directory reader object of the directory
 *                        to be searched.
 *  pattern const char *  The pattern to match with files in the current
 *                        directory.
 *  separate       int    When appending a filename from the specified
 *                        directory to ef->pathname, insert a directory
 *                        separator between the existing pathname and
 *                        the filename, unless separate is zero.
 * Output:
 *  return         int    0 - OK.
 *                        1 - Error.
 */
static int ef_match_relative_pathname(ExpandFile *ef, DirReader *dr,
				       const char *pattern, int separate)
{
  const char *nextp;  /* The pointer to the character that follows the part */
                      /*  of the pattern that is to be matched with files */
                      /*  in the current directory. */
  char *file;         /* The name of the file being matched */
  int pathlen;        /* The length of ef->pathname[] on entry to this */
                      /*  function */
/*
 * Record the current length of the pathname string recorded in
 * ef->pathname[].
 */
  pathlen = strlen(ef->path->name);
/*
 * Get a pointer to the character that follows the end of the part of
 * the pattern that should be matched to files within the current directory.
 * This will either point to a directory separator, or to the '\0' terminator
 * of the pattern string.
 */
  for(nextp=pattern; *nextp && strncmp(nextp, FS_DIR_SEP, FS_DIR_SEP_LEN) != 0;
      nextp++)
    ;
/*
 * Read each file from the directory, attempting to match it to the
 * current pattern.
 */
  while((file=_dr_next_file(dr)) != NULL) {
/*
 * Does the latest file match the pattern up to nextp?
 */
    if(ef_string_matches_pattern(file, pattern, file[0]=='.', nextp)) {
/*
 * Append the new directory entry to the current matching pathname.
 */
      if((separate && _pn_append_to_path(ef->path, FS_DIR_SEP, -1, 0)==NULL) ||
	 _pn_append_to_path(ef->path, file, -1, 0)==NULL) {
	_err_record_msg(ef->err, "Insufficient memory to record path",
			END_ERR_MSG);
	return 1;
      };
/*
 * If we have reached the end of the pattern, record the accumulated
 * pathname in the list of matching files.
 */
      if(*nextp == '\0') {
	if(ef_record_pathname(ef, ef->path->name, 0))
	  return 1;
/*
 * If the matching directory entry is a subdirectory, and the
 * next character of the pattern is a directory separator,
 * recursively call the current function to scan the sub-directory
 * for matches.
 */
      } else if(_pu_path_is_dir(ef->path->name) &&
		strncmp(nextp, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) {
/*
 * If the pattern finishes with the directory separator, then
 * record the pathame as matching.
 */
	if(nextp[FS_DIR_SEP_LEN] == '\0') {
	  if(ef_record_pathname(ef, ef->path->name, 0))
	    return 1;
/*
 * Match files within the directory.
 */
	} else {
	  DirNode *subdnode = ef_open_dir(ef, ef->path->name);
	  if(subdnode) {
	    if(ef_match_relative_pathname(ef, subdnode->dr,
					   nextp+FS_DIR_SEP_LEN, 1)) {
	      subdnode = ef_close_dir(ef, subdnode);
	      return 1;
	    };
	    subdnode = ef_close_dir(ef, subdnode);
	  };
	};
      };
/*
 * Remove the latest filename from the pathname string, so that
 * another matching file can be appended.
 */
      ef->path->name[pathlen] = '\0';
    };
  };
  return 0;
}

/*.......................................................................
 * Record a new matching filename.
 *
 * Input:
 *  ef        ExpandFile *  The filename-match resource object.
 *  pathname  const char *  The pathname to record.
 *  remove_escapes   int    If true, remove backslash escapes in the
 *                          recorded copy of the pathname.
 * Output:
 *  return           int     0 - OK.
 *                           1 - Error (ef->err will contain a
 *                                      description of the error).
 */
static int ef_record_pathname(ExpandFile *ef, const char *pathname,
			      int remove_escapes)
{
  char *copy;          /* The recorded copy of pathname[] */
/*
 * Attempt to make a copy of the pathname in the cache.
 */
  copy = ef_cache_pathname(ef, pathname, remove_escapes);
  if(!copy)
    return 1;
/*
 * If there isn't room to record a pointer to the recorded pathname in the
 * array of files, attempt to extend the array.
 */
  if(ef->result.nfile + 1 > ef->files_dim) {
    int files_dim = ef->files_dim + MATCH_BLK_FACT;
    char **files = (char **) realloc(ef->result.files,
				     files_dim * sizeof(files[0]));
    if(!files) {
      _err_record_msg(ef->err,
	     "Insufficient memory to record all of the matching filenames",
	     END_ERR_MSG);
      errno = ENOMEM;
      return 1;
    };
    ef->result.files = files;
    ef->files_dim = files_dim;
  };
/*
 * Record a pointer to the new match.
 */
  ef->result.files[ef->result.nfile++] = copy;
  return 0;
}

/*.......................................................................
 * Record a pathname in the cache.
 *
 * Input:
 *  ef       ExpandFile *  The filename-match resource object.
 *  pathname       char *  The pathname to record.
 *  remove_escapes  int    If true, remove backslash escapes in the
 *                         copy of the pathname.
 * Output:
 *  return         char *  The pointer to the copy of the pathname.
 *                         On error NULL is returned and a description
 *                         of the error is left in ef->err.
 */
static char *ef_cache_pathname(ExpandFile *ef, const char *pathname,
			       int remove_escapes)
{
  char *copy = _sg_store_string(ef->sg, pathname, remove_escapes);
  if(!copy)
    _err_record_msg(ef->err, "Insufficient memory to store pathname",
		    END_ERR_MSG);
  return copy;
}

/*.......................................................................
 * Clear the results of the previous expansion operation, ready for the
 * next.
 *
 * Input:
 *  ef    ExpandFile *  The pathname expansion resource object.
 */
static void ef_clear_files(ExpandFile *ef)
{
  _clr_StringGroup(ef->sg);
  _pn_clear_path(ef->path);
  ef->result.exists = 0;
  ef->result.nfile = 0;
  _err_clear_msg(ef->err);
  return;
}

/*.......................................................................
 * Get a new directory reader object from the cache.
 *
 * Input:
 *  ef        ExpandFile *  The pathname expansion resource object.
 *  pathname  const char *  The pathname of the directory.
 * Output:
 *  return       DirNode *  The cache entry of the new directory reader,
 *                          or NULL on error. On error, ef->err will
 *                          contain a description of the error.
 */
static DirNode *ef_open_dir(ExpandFile *ef, const char *pathname)
{
  char *errmsg = NULL;  /* An error message from a called function */
  DirNode *node;        /* The cache node used */
/*
 * Get the directory reader cache.
 */
  DirCache *cache = &ef->cache;
/*
 * Extend the cache if there are no free cache nodes.
 */
  if(!cache->next) {
    node = (DirNode *) _new_FreeListNode(cache->mem);
    if(!node) {
      _err_record_msg(ef->err, "Insufficient memory to open a new directory",
		      END_ERR_MSG);
      return NULL;
    };
/*
 * Initialize the cache node.
 */
    node->next = NULL;
    node->prev = NULL;
    node->dr = NULL;
/*
 * Allocate a directory reader object.
 */
    node->dr = _new_DirReader();
    if(!node->dr) {
      _err_record_msg(ef->err, "Insufficient memory to open a new directory",
		      END_ERR_MSG);
      node = (DirNode *) _del_FreeListNode(cache->mem, node);
      return NULL;
    };
/*
 * Append the node to the cache list.
 */
    node->prev = cache->tail;
    if(cache->tail)
      cache->tail->next = node;
    else
      cache->head = node;
    cache->next = cache->tail = node;
  };
/*
 * Get the first unused node, but don't remove it from the list yet.
 */
  node = cache->next;
/*
 * Attempt to open the specified directory.
 */
  if(_dr_open_dir(node->dr, pathname, &errmsg)) {
    _err_record_msg(ef->err, errmsg, END_ERR_MSG);
    return NULL;
  };
/*
 * Now that we have successfully opened the specified directory,
 * remove the cache node from the list, and relink the list around it.
 */
  cache->next = node->next;
  if(node->prev)
    node->prev->next = node->next;
  else
    cache->head = node->next;
  if(node->next)
    node->next->prev = node->prev;
  else
    cache->tail = node->prev;
  node->next = node->prev = NULL;
/*
 * Return the successfully initialized cache node to the caller.
 */
  return node;
}

/*.......................................................................
 * Return a directory reader object to the cache, after first closing
 * the directory that it was managing.
 *
 * Input:
 *  ef    ExpandFile *  The pathname expansion resource object.
 *  node     DirNode *  The cache entry of the directory reader, as returned
 *                      by ef_open_dir().
 * Output:
 *  return   DirNode *  The deleted DirNode (ie. allways NULL).
 */
static DirNode *ef_close_dir(ExpandFile *ef, DirNode *node)
{
/*
 * Get the directory reader cache.
 */
  DirCache *cache = &ef->cache;
/*
 * Close the directory.
 */
  _dr_close_dir(node->dr);
/*
 * Return the node to the tail of the cache list.
 */
  node->next = NULL;
  node->prev = cache->tail;
  if(cache->tail)
    cache->tail->next = node;
  else
    cache->head = cache->tail = node;
  if(!cache->next)
    cache->next = node;
  return NULL;
}

/*.......................................................................
 * Return non-zero if the specified file name matches a given glob
 * pattern.
 *
 * Input:
 *  file     const char *  The file-name component to be matched to the pattern.
 *  pattern  const char *  The start of the pattern to match against file[].
 *  xplicit         int    If non-zero, the first character must be matched
 *                         explicitly (ie. not with a wildcard).
 *  nextp    const char *  The pointer to the the character following the
 *                         end of the pattern in pattern[].
 * Output:
 *  return    int          0 - Doesn't match.
 *                         1 - The file-name string matches the pattern.
 */
static int ef_string_matches_pattern(const char *file, const char *pattern,
				      int xplicit, const char *nextp)
{
  const char *pptr = pattern; /* The pointer used to scan the pattern */
  const char *fptr = file;    /* The pointer used to scan the filename string */
/*
 * Match each character of the pattern in turn.
 */
  while(pptr < nextp) {
/*
 * Handle the next character of the pattern.
 */
    switch(*pptr) {
/*
 * A match zero-or-more characters wildcard operator.
 */
    case '*':
/*
 * Skip the '*' character in the pattern.
 */
      pptr++;
/*
 * If wildcards aren't allowed, the pattern doesn't match.
 */
      if(xplicit)
	return 0;
/*
 * If the pattern ends with a the '*' wildcard, then the
 * rest of the filename matches this.
 */
      if(pptr >= nextp)
	return 1;
/*
 * Using the wildcard to match successively longer sections of
 * the remaining characters of the filename, attempt to match
 * the tail of the filename against the tail of the pattern.
 */
      for( ; *fptr; fptr++) {
	if(ef_string_matches_pattern(fptr, pptr, 0, nextp))
	  return 1;
      };
      return 0; /* The pattern following the '*' didn't match */
      break;
/*
 * A match-one-character wildcard operator.
 */
    case '?':
/*
 * If there is a character to be matched, skip it and advance the
 * pattern pointer.
 */
      if(!xplicit && *fptr) {
        fptr++;
        pptr++;
/*
 * If we hit the end of the filename string, there is no character
 * matching the operator, so the string doesn't match.
 */
      } else {
        return 0;
      };
      break;
/*
 * A character range operator, with the character ranges enclosed
 * in matching square brackets.
 */
    case '[':
      if(xplicit || !ef_matches_range(*fptr++, ++pptr, &pptr))
        return 0;
      break;
/*
 * A backslash in the pattern prevents the following character as
 * being seen as a special character.
 */
    case '\\':
      pptr++;
      /* Note fallthrough to default */
/*
 * A normal character to be matched explicitly.
 */
    default:
      if(*fptr == *pptr) {
        fptr++;
        pptr++;
      } else {
        return 0;
      };
      break;
    };
/*
 * After passing the first character, turn off the explicit match
 * requirement.
 */
    xplicit = 0;
  };
/*
 * To get here the pattern must have been exhausted. If the filename
 * string matched, then the filename string must also have been
 * exhausted.
 */
  return *fptr == '\0';
}

/*.......................................................................
 * Match a character range expression terminated by an unescaped close
 * square bracket.
 *
 * Input:
 *  c               int     The character to be matched with the range
 *                          pattern.
 *  pattern  const char *   The range pattern to be matched (ie. after the
 *                          initiating '[' character).
 *  endp     const char **  On output a pointer to the character following the
 *                          range expression will be assigned to *endp.
 * Output:
 *  return          int     0 - Doesn't match.
 *                          1 - The character matched.
 */
static int ef_matches_range(int c, const char *pattern, const char **endp)
{
  const char *pptr = pattern;  /* The pointer used to scan the pattern */
  int invert = 0;              /* True to invert the sense of the match */
  int matched = 0;             /* True if the character matched the pattern */
/*
 * If the first character is a caret, the sense of the match is
 * inverted and only if the character isn't one of those in the
 * range, do we say that it matches.
 */
  if(*pptr == '^') {
    pptr++;
    invert = 1;
  };
/*
 * The hyphen is only a special character when it follows the first
 * character of the range (not including the caret).
 */
  if(*pptr == '-') {
    pptr++;
    if(c == '-') {
      *endp = pptr;
      matched = 1;
    };
/*
 * Skip other leading '-' characters since they make no sense.
 */
    while(*pptr == '-')
      pptr++;
  };
/*
 * The hyphen is only a special character when it follows the first
 * character of the range (not including the caret or a hyphen).
 */
  if(*pptr == ']') {
    pptr++;
    if(c == ']') {
      *endp = pptr;
      matched = 1;
    };
  };
/*
 * Having dealt with the characters that have special meanings at
 * the beginning of a character range expression, see if the
 * character matches any of the remaining characters of the range,
 * up until a terminating ']' character is seen.
 */
  while(!matched && *pptr && *pptr != ']') {
/*
 * Is this a range of characters signaled by the two end characters
 * separated by a hyphen?
 */
    if(*pptr == '-') {
      if(pptr[1] != ']') {
        if(c >= pptr[-1] && c <= pptr[1])
	  matched = 1;
	pptr += 2;
      };
/*
 * A normal character to be compared directly.
 */
    } else if(*pptr++ == c) {
      matched = 1;
    };
  };
/*
 * Find the terminating ']'.
 */
  while(*pptr && *pptr != ']')
    pptr++;
/*
 * Did we find a terminating ']'?
 */
  if(*pptr == ']') {
    *endp = pptr + 1;
    return matched ? !invert : invert;
  };
/*
 * If the pattern didn't end with a ']' then it doesn't match, regardless
 * of the value of the required sense of the match.
 */
  *endp = pptr;
  return 0;
}

/*.......................................................................
 * This is a qsort() comparison function used to sort strings.
 *
 * Input:
 *  v1, v2   void *  Pointers to the two strings to be compared.
 * Output:
 *  return    int    -1 -> v1 < v2.
 *                    0 -> v1 == v2
 *                    1 -> v1 > v2
 */
static int ef_cmp_strings(const void *v1, const void *v2)
{
  char * const *s1 = (char * const *) v1;
  char * const *s2 = (char * const *) v2;
  return strcmp(*s1, *s2);
}

/*.......................................................................
 * Preprocess a path, expanding ~/, ~user/ and $envvar references, using
 * ef->path as a work buffer, then copy the result into a cache entry,
 * and return a pointer to this copy.
 *
 * Input:
 *  ef    ExpandFile *  The resource object of the file matcher.
 *  pathlen      int    The length of the prefix of path[] to be expanded.
 * Output:
 *  return      char *  A pointer to a copy of the output path in the
 *                      cache. On error NULL is returned, and a description
 *                      of the error is left in ef->err.
 */
static char *ef_expand_special(ExpandFile *ef, const char *path, int pathlen)
{
  int spos;      /* The index of the start of the path segment that needs */
                 /*  to be copied from path[] to the output pathname. */
  int ppos;      /* The index of a character in path[] */
  char *pptr;    /* A pointer into the output path */
  int escaped;   /* True if the previous character was a '\' */
  int i;
/*
 * Clear the pathname buffer.
 */
  _pn_clear_path(ef->path);
/*
 * We need to perform two passes, one to expand environment variables
 * and a second to do tilde expansion. This caters for the case
 * where an initial dollar expansion yields a tilde expression.
 */
  escaped = 0;
  for(spos=ppos=0; ppos < pathlen; ppos++) {
    int c = path[ppos];
    if(escaped) {
      escaped = 0;
    } else if(c == '\\') {
      escaped = 1;
    } else if(c == '$') {
      int envlen;   /* The length of the environment variable */
      char *value;  /* The value of the environment variable */
/*
 * Record the preceding unrecorded part of the pathname.
 */
      if(spos < ppos && _pn_append_to_path(ef->path, path + spos, ppos-spos, 0)
	 == NULL) {
	_err_record_msg(ef->err, "Insufficient memory to expand path",
			END_ERR_MSG);
	return NULL;
      };
/*
 * Skip the dollar.
 */
      ppos++;
/*
 * Copy the environment variable name that follows the dollar into
 * ef->envnam[], stopping if a directory separator or end of string
 * is seen.
 */
      for(envlen=0; envlen<ENV_LEN && ppos < pathlen &&
	  strncmp(path + ppos, FS_DIR_SEP, FS_DIR_SEP_LEN); envlen++)
	ef->envnam[envlen] = path[ppos++];
/*
 * If the username overflowed the buffer, treat it as invalid (note that
 * on most unix systems only 8 characters are allowed in a username,
 * whereas our ENV_LEN is much bigger than that.
 */
      if(envlen >= ENV_LEN) {
	_err_record_msg(ef->err, "Environment variable name too long",
			END_ERR_MSG);
	return NULL;
      };
/*
 * Terminate the environment variable name.
 */
      ef->envnam[envlen] = '\0';
/*
 * Lookup the value of the environment variable.
 */
      value = getenv(ef->envnam);
      if(!value) {
	_err_record_msg(ef->err, "No expansion found for: $", ef->envnam,
			END_ERR_MSG);
	return NULL;
      };
/*
 * Copy the value of the environment variable into the output pathname.
 */
      if(_pn_append_to_path(ef->path, value, -1, 0) == NULL) {
	_err_record_msg(ef->err, "Insufficient memory to expand path",
			END_ERR_MSG);
	return NULL;
      };
/*
 * Record the start of the uncopied tail of the input pathname.
 */
      spos = ppos;
    };
  };
/*
 * Record the uncopied tail of the pathname.
 */
  if(spos < ppos && _pn_append_to_path(ef->path, path + spos, ppos-spos, 0)
     == NULL) {
    _err_record_msg(ef->err, "Insufficient memory to expand path", END_ERR_MSG);
    return NULL;
  };
/*
 * If the first character of the resulting pathname is a tilde,
 * then attempt to substitute the home directory of the specified user.
 */
  pptr = ef->path->name;
  if(*pptr == '~' && path[0] != '\\') {
    int usrlen;           /* The length of the username following the tilde */
    const char *homedir;  /* The home directory of the user */
    int homelen;          /* The length of the home directory string */
    int plen;             /* The current length of the path */
    int skip=0;           /* The number of characters to skip after the ~user */
/*
 * Get the current length of the output path.
 */
    plen = strlen(ef->path->name);
/*
 * Skip the tilde.
 */
    pptr++;
/*
 * Copy the optional username that follows the tilde into ef->usrnam[].
 */
    for(usrlen=0; usrlen<USR_LEN && *pptr &&
	strncmp(pptr, FS_DIR_SEP, FS_DIR_SEP_LEN); usrlen++)
      ef->usrnam[usrlen] = *pptr++;
/*
 * If the username overflowed the buffer, treat it as invalid (note that
 * on most unix systems only 8 characters are allowed in a username,
 * whereas our USR_LEN is much bigger than that.
 */
    if(usrlen >= USR_LEN) {
      _err_record_msg(ef->err, "Username too long", END_ERR_MSG);
      return NULL;
    };
/*
 * Terminate the username string.
 */
    ef->usrnam[usrlen] = '\0';
/*
 * Lookup the home directory of the user.
 */
    homedir = _hd_lookup_home_dir(ef->home, ef->usrnam);
    if(!homedir) {
      _err_record_msg(ef->err, _hd_last_home_dir_error(ef->home), END_ERR_MSG);
      return NULL;
    };
    homelen = strlen(homedir);
/*
 * ~user and ~ are usually followed by a directory separator to
 * separate them from the file contained in the home directory.
 * If the home directory is the root directory, then we don't want
 * to follow the home directory by a directory separator, so we must
 * erase it.
 */
    if(strcmp(homedir, FS_ROOT_DIR) == 0 &&
       strncmp(pptr, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) {
      skip = FS_DIR_SEP_LEN;
    };
/*
 * If needed, increase the size of the pathname buffer to allow it
 * to accomodate the home directory instead of the tilde expression.
 * Note that pptr may not be valid after this call.
 */
    if(_pn_resize_path(ef->path, plen - usrlen - 1 - skip + homelen)==NULL) {
      _err_record_msg(ef->err, "Insufficient memory to expand filename",
		      END_ERR_MSG);
      return NULL;
    };
/*
 * Move the part of the pathname that follows the tilde expression to
 * the end of where the home directory will need to be inserted.
 */
    memmove(ef->path->name + homelen,
	    ef->path->name + 1 + usrlen + skip, plen - usrlen - 1 - skip+1);
/*
 * Write the home directory at the beginning of the string.
 */
    for(i=0; i<homelen; i++)
      ef->path->name[i] = homedir[i];
  };
/*
 * Copy the result into the cache, and return a pointer to the copy.
 */
  return ef_cache_pathname(ef, ef->path->name, 0);
}

/*.......................................................................
 * Return a description of the last path-expansion error that occurred.
 *
 * Input:
 *  ef     ExpandFile *   The path-expansion resource object.
 * Output:
 *  return       char *   The description of the last error.
 */
const char *ef_last_error(ExpandFile *ef)
{
  return ef ? _err_get_msg(ef->err) : "NULL ExpandFile argument";
}

/*.......................................................................
 * Print out an array of matching files.
 *
 * Input:
 *  result  FileExpansion *   The container of the sorted array of
 *                            expansions.
 *  fp               FILE *   The output stream to write to.
 *  term_width        int     The width of the terminal.
 * Output:
 *  return            int     0 - OK.
 *                            1 - Error.
 */
int ef_list_expansions(FileExpansion *result, FILE *fp, int term_width)
{
  return _ef_output_expansions(result, _io_write_stdio, fp, term_width);
}

/*.......................................................................
 * Print out an array of matching files via a callback.
 *
 * Input:
 *  result  FileExpansion *  The container of the sorted array of
 *                           expansions.
 *  write_fn    GlWriteFn *  The function to call to write the
 *                           expansions or 0 to discard the output.
 *  data             void *  Anonymous data to pass to write_fn().
 *  term_width        int    The width of the terminal.
 * Output:
 *  return            int    0 - OK.
 *                           1 - Error.
 */
int _ef_output_expansions(FileExpansion *result, GlWriteFn *write_fn,
			  void *data, int term_width)
{
  EfListFormat fmt; /* List formatting information */
  int lnum;          /* The sequential number of the line to print next */
/*
 * Not enough space to list anything?
 */
  if(term_width < 1)
    return 0;
/*
 * Do we have a callback to write via, and any expansions to be listed?
 */
  if(write_fn && result && result->nfile>0) {
/*
 * Work out how to arrange the listing into fixed sized columns.
 */
    ef_plan_listing(result, term_width, &fmt);
/*
 * Print the listing to the specified stream.
 */
    for(lnum=0; lnum < fmt.nline; lnum++) {
      if(ef_format_line(result, &fmt, lnum, write_fn, data))
	return 1;
    };
  };
  return 0;
}

/*.......................................................................
 * Work out how to arrange a given array of completions into a listing
 * of one or more fixed size columns.
 *
 * Input:
 *  result   FileExpansion *   The set of completions to be listed.
 *  term_width         int     The width of the terminal. A lower limit of
 *                             zero is quietly enforced.
 * Input/Output:
 *  fmt       EfListFormat *   The formatting information will be assigned
 *                             to the members of *fmt.
 */
static void ef_plan_listing(FileExpansion *result, int term_width,
			    EfListFormat *fmt)
{
  int maxlen;    /* The length of the longest matching string */
  int i;
/*
 * Ensure that term_width >= 0.
 */
  if(term_width < 0)
    term_width = 0;
/*
 * Start by assuming the worst case, that either nothing will fit
 * on the screen, or that there are no matches to be listed.
 */
  fmt->term_width = term_width;
  fmt->column_width = 0;
  fmt->nline = fmt->ncol = 0;
/*
 * Work out the maximum length of the matching strings.
 */
  maxlen = 0;
  for(i=0; i<result->nfile; i++) {
    int len = strlen(result->files[i]);
    if(len > maxlen)
      maxlen = len;
  };
/*
 * Nothing to list?
 */
  if(maxlen == 0)
    return;
/*
 * Split the available terminal width into columns of
 * maxlen + EF_COL_SEP characters.
 */
  fmt->column_width = maxlen;
  fmt->ncol = fmt->term_width / (fmt->column_width + EF_COL_SEP);
/*
 * If the column width is greater than the terminal width, zero columns
 * will have been selected. Set a lower limit of one column. Leave it
 * up to the caller how to deal with completions who's widths exceed
 * the available terminal width.
 */
  if(fmt->ncol < 1)
    fmt->ncol = 1;
/*
 * How many lines of output will be needed?
 */
  fmt->nline = (result->nfile + fmt->ncol - 1) / fmt->ncol;
  return;
}

/*.......................................................................
 * Render one line of a multi-column listing of completions, using a
 * callback function to pass the output to an arbitrary destination.
 *
 * Input:
 *  result   FileExpansion *  The container of the sorted array of
 *                            completions.
 *  fmt       EfListFormat *  Formatting information.
 *  lnum               int    The index of the line to print, starting
 *                            from 0, and incrementing until the return
 *                            value indicates that there is nothing more
 *                            to be printed.
 *  write_fn     GlWriteFn *  The function to call to write the line, or
 *                            0 to discard the output.
 *  data              void *  Anonymous data to pass to write_fn().
 * Output:
 *  return             int    0 - Line printed ok.
 *                            1 - Nothing to print.
 */
static int ef_format_line(FileExpansion *result, EfListFormat *fmt, int lnum,
			  GlWriteFn *write_fn, void *data)
{
  int col;             /* The index of the list column being output */
/*
 * If the line index is out of bounds, there is nothing to be written.
 */
  if(lnum < 0 || lnum >= fmt->nline)
    return 1;
/*
 * If no output function has been provided, return as though the line
 * had been printed.
 */
  if(!write_fn)
    return 0;
/*
 * Print the matches in 'ncol' columns, sorted in line order within each
 * column.
 */
  for(col=0; col < fmt->ncol; col++) {
    int m = col*fmt->nline + lnum;
/*
 * Is there another match to be written? Note that in general
 * the last line of a listing will have fewer filled columns
 * than the initial lines.
 */
    if(m < result->nfile) {
      char *file = result->files[m];
/*
 * How long are the completion and type-suffix strings?
 */
      int flen = strlen(file);
/*
 * Write the completion string.
 */
      if(write_fn(data, file, flen) != flen)
	return 1;
/*
 * If another column follows the current one, pad to its start with spaces.
 */
      if(col+1 < fmt->ncol) {
/*
 * The following constant string of spaces is used to pad the output.
 */
	static const char spaces[] = "                    ";
	static const int nspace = sizeof(spaces) - 1;
/*
 * Pad to the next column, using as few sub-strings of the spaces[]
 * array as possible.
 */
	int npad = fmt->column_width + EF_COL_SEP - flen;
	while(npad>0) {
	  int n = npad > nspace ? nspace : npad;
	  if(write_fn(data, spaces + nspace - n, n) != n)
	    return 1;
	  npad -= n;
	};
      };
    };
  };
/*
 * Start a new line.
 */
  {
    char s[] = "\r\n";
    int n = strlen(s);
    if(write_fn(data, s, n) != n)
      return 1;
  };
  return 0;
}

#endif  /* ifndef WITHOUT_FILE_SYSTEM */