/* * Copyright (c) 2000, 2001, 2002, 2003, 2004, 2012 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. */ /* * 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 #include #include #include #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; envlenenvnam[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; usrlenusrnam[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; ipath->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; infile; 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 */