xref: /illumos-gate/usr/src/lib/libtecla/common/expand.c (revision edc8ef7d921c96b23969898aeb766cb24960bda7)
1 /*
2  * Copyright (c) 2000, 2001, 2002, 2003, 2004 by Martin C. Shepherd.
3  *
4  * All rights reserved.
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a
7  * copy of this software and associated documentation files (the
8  * "Software"), to deal in the Software without restriction, including
9  * without limitation the rights to use, copy, modify, merge, publish,
10  * distribute, and/or sell copies of the Software, and to permit persons
11  * to whom the Software is furnished to do so, provided that the above
12  * copyright notice(s) and this permission notice appear in all copies of
13  * the Software and that both the above copyright notice(s) and this
14  * permission notice appear in supporting documentation.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
19  * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
20  * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL
21  * INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING
22  * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
23  * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
24  * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
25  *
26  * Except as contained in this notice, the name of a copyright holder
27  * shall not be used in advertising or otherwise to promote the sale, use
28  * or other dealings in this Software without prior written authorization
29  * of the copyright holder.
30  */
31 
32 /*
33  * If file-system access is to be excluded, this module has no function,
34  * so all of its code should be excluded.
35  */
36 #ifndef WITHOUT_FILE_SYSTEM
37 
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <errno.h>
42 
43 #include "freelist.h"
44 #include "direader.h"
45 #include "pathutil.h"
46 #include "homedir.h"
47 #include "stringrp.h"
48 #include "libtecla.h"
49 #include "ioutil.h"
50 #include "expand.h"
51 #include "errmsg.h"
52 
53 /*
54  * Specify the number of elements to extend the files[] array by
55  * when it proves to be too small. This also sets the initial size
56  * of the array.
57  */
58 #define MATCH_BLK_FACT 256
59 
60 /*
61  * A list of directory iterators is maintained using nodes of the
62  * following form.
63  */
64 typedef struct DirNode DirNode;
65 struct DirNode {
66   DirNode *next;       /* The next directory in the list */
67   DirNode *prev;       /* The node that precedes this node in the list */
68   DirReader *dr;       /* The directory reader object */
69 };
70 
71 typedef struct {
72   FreeList *mem;       /* Memory for DirNode list nodes */
73   DirNode *head;       /* The head of the list of used and unused cache nodes */
74   DirNode *next;       /* The next unused node between head and tail */
75   DirNode *tail;       /* The tail of the list of unused cache nodes */
76 } DirCache;
77 
78 /*
79  * Specify how many directory cache nodes to allocate at a time.
80  */
81 #define DIR_CACHE_BLK 20
82 
83 /*
84  * Set the maximum length allowed for usernames.
85  */
86 #define USR_LEN 100
87 
88 /*
89  * Set the maximum length allowed for environment variable names.
90  */
91 #define ENV_LEN 100
92 
93 /*
94  * Set the default number of spaces place between columns when listing
95  * a set of expansions.
96  */
97 #define EF_COL_SEP 2
98 
99 struct ExpandFile {
100   ErrMsg *err;            /* The error reporting buffer */
101   StringGroup *sg;        /* A list of string segments in which */
102                           /*  matching filenames are stored. */
103   DirCache cache;         /* The cache of directory reader objects */
104   PathName *path;         /* The pathname being matched */
105   HomeDir *home;          /* Home-directory lookup object */
106   int files_dim;          /* The allocated dimension of result.files[] */
107   char usrnam[USR_LEN+1]; /* A user name */
108   char envnam[ENV_LEN+1]; /* An environment variable name */
109   FileExpansion result;   /* The container used to return the results of */
110                           /*  expanding a path. */
111 };
112 
113 static int ef_record_pathname(ExpandFile *ef, const char *pathname,
114 			      int remove_escapes);
115 static char *ef_cache_pathname(ExpandFile *ef, const char *pathname,
116 			       int remove_escapes);
117 static void ef_clear_files(ExpandFile *ef);
118 
119 static DirNode *ef_open_dir(ExpandFile *ef, const char *pathname);
120 static DirNode *ef_close_dir(ExpandFile *ef, DirNode *node);
121 static char *ef_expand_special(ExpandFile *ef, const char *path, int pathlen);
122 static int ef_match_relative_pathname(ExpandFile *ef, DirReader *dr,
123 				      const char *pattern, int separate);
124 static int ef_matches_range(int c, const char *pattern, const char **endp);
125 static int ef_string_matches_pattern(const char *file, const char *pattern,
126 				      int xplicit, const char *nextp);
127 static int ef_cmp_strings(const void *v1, const void *v2);
128 
129 /*
130  * Encapsulate the formatting information needed to layout a
131  * multi-column listing of expansions.
132  */
133 typedef struct {
134   int term_width;     /* The width of the terminal (characters) */
135   int column_width;   /* The number of characters within in each column. */
136   int ncol;           /* The number of columns needed */
137   int nline;          /* The number of lines needed */
138 } EfListFormat;
139 
140 /*
141  * Given the current terminal width, and a list of file expansions,
142  * determine how to best use the terminal width to display a multi-column
143  * listing of expansions.
144  */
145 static void ef_plan_listing(FileExpansion *result, int term_width,
146 			    EfListFormat *fmt);
147 
148 /*
149  * Display a given line of a multi-column list of file-expansions.
150  */
151 static int ef_format_line(FileExpansion *result, EfListFormat *fmt, int lnum,
152 			  GlWriteFn *write_fn, void *data);
153 
154 /*.......................................................................
155  * Create the resources needed to expand filenames.
156  *
157  * Output:
158  *  return  ExpandFile *  The new object, or NULL on error.
159  */
160 ExpandFile *new_ExpandFile(void)
161 {
162   ExpandFile *ef;  /* The object to be returned */
163 /*
164  * Allocate the container.
165  */
166   ef = (ExpandFile *) malloc(sizeof(ExpandFile));
167   if(!ef) {
168     errno = ENOMEM;
169     return NULL;
170   };
171 /*
172  * Before attempting any operation that might fail, initialize the
173  * container at least up to the point at which it can safely be passed
174  * to del_ExpandFile().
175  */
176   ef->err = NULL;
177   ef->sg = NULL;
178   ef->cache.mem = NULL;
179   ef->cache.head = NULL;
180   ef->cache.next = NULL;
181   ef->cache.tail = NULL;
182   ef->path = NULL;
183   ef->home = NULL;
184   ef->result.files = NULL;
185   ef->result.nfile = 0;
186   ef->usrnam[0] = '\0';
187   ef->envnam[0] = '\0';
188 /*
189  * Allocate a place to record error messages.
190  */
191   ef->err = _new_ErrMsg();
192   if(!ef->err)
193     return del_ExpandFile(ef);
194 /*
195  * Allocate a list of string segments for storing filenames.
196  */
197   ef->sg = _new_StringGroup(_pu_pathname_dim());
198   if(!ef->sg)
199     return del_ExpandFile(ef);
200 /*
201  * Allocate a freelist for allocating directory cache nodes.
202  */
203   ef->cache.mem = _new_FreeList(sizeof(DirNode), DIR_CACHE_BLK);
204   if(!ef->cache.mem)
205     return del_ExpandFile(ef);
206 /*
207  * Allocate a pathname buffer.
208  */
209   ef->path = _new_PathName();
210   if(!ef->path)
211     return del_ExpandFile(ef);
212 /*
213  * Allocate an object for looking up home-directories.
214  */
215   ef->home = _new_HomeDir();
216   if(!ef->home)
217     return del_ExpandFile(ef);
218 /*
219  * Allocate an array for files. This will be extended later if needed.
220  */
221   ef->files_dim = MATCH_BLK_FACT;
222   ef->result.files = (char **) malloc(sizeof(ef->result.files[0]) *
223 				      ef->files_dim);
224   if(!ef->result.files) {
225     errno = ENOMEM;
226     return del_ExpandFile(ef);
227   };
228   return ef;
229 }
230 
231 /*.......................................................................
232  * Delete a ExpandFile object.
233  *
234  * Input:
235  *  ef     ExpandFile *  The object to be deleted.
236  * Output:
237  *  return ExpandFile *  The deleted object (always NULL).
238  */
239 ExpandFile *del_ExpandFile(ExpandFile *ef)
240 {
241   if(ef) {
242     DirNode *dnode;
243 /*
244  * Delete the string segments.
245  */
246     ef->sg = _del_StringGroup(ef->sg);
247 /*
248  * Delete the cached directory readers.
249  */
250     for(dnode=ef->cache.head; dnode; dnode=dnode->next)
251       dnode->dr = _del_DirReader(dnode->dr);
252 /*
253  * Delete the memory from which the DirNode list was allocated, thus
254  * deleting the list at the same time.
255  */
256     ef->cache.mem = _del_FreeList(ef->cache.mem, 1);
257     ef->cache.head = ef->cache.tail = ef->cache.next = NULL;
258 /*
259  * Delete the pathname buffer.
260  */
261     ef->path = _del_PathName(ef->path);
262 /*
263  * Delete the home-directory lookup object.
264  */
265     ef->home = _del_HomeDir(ef->home);
266 /*
267  * Delete the array of pointers to files.
268  */
269     if(ef->result.files) {
270       free(ef->result.files);
271       ef->result.files = NULL;
272     };
273 /*
274  * Delete the error report buffer.
275  */
276     ef->err = _del_ErrMsg(ef->err);
277 /*
278  * Delete the container.
279  */
280     free(ef);
281   };
282   return NULL;
283 }
284 
285 /*.......................................................................
286  * Expand a pathname, converting ~user/ and ~/ patterns at the start
287  * of the pathname to the corresponding home directories, replacing
288  * $envvar with the value of the corresponding environment variable,
289  * and then, if there are any wildcards, matching these against existing
290  * filenames.
291  *
292  * If no errors occur, a container is returned containing the array of
293  * files that resulted from the expansion. If there were no wildcards
294  * in the input pathname, this will contain just the original pathname
295  * after expansion of ~ and $ expressions. If there were any wildcards,
296  * then the array will contain the files that matched them. Note that
297  * if there were any wildcards but no existing files match them, this
298  * is counted as an error and NULL is returned.
299  *
300  * The supported wildcards and their meanings are:
301  *  *        -  Match any sequence of zero or more characters.
302  *  ?        -  Match any single character.
303  *  [chars]  -  Match any single character that appears in 'chars'.
304  *              If 'chars' contains an expression of the form a-b,
305  *              then any character between a and b, including a and b,
306  *              matches. The '-' character looses its special meaning
307  *              as a range specifier when it appears at the start
308  *              of the sequence of characters.
309  *  [^chars] -  The same as [chars] except that it matches any single
310  *              character that doesn't appear in 'chars'.
311  *
312  * Wildcard expressions are applied to individual filename components.
313  * They don't match across directory separators. A '.' character at
314  * the beginning of a filename component must also be matched
315  * explicitly by a '.' character in the input pathname, since these
316  * are UNIX's hidden files.
317  *
318  * Input:
319  *  ef         ExpandFile *  The pathname expansion resource object.
320  *  path             char *  The path name to be expanded.
321  *  pathlen           int    The length of the suffix of path[] that
322  *                           constitutes the filename to be expanded,
323  *                           or -1 to specify that the whole of the
324  *                           path string should be used. Note that
325  *                           regardless of the value of this argument,
326  *                           path[] must contain a '\0' terminated
327  *                           string, since this function checks that
328  *                           pathlen isn't mistakenly too long.
329  * Output:
330  *  return  FileExpansion *  A pointer to a container within the given
331  *                           ExpandFile object. This contains an array
332  *                           of the pathnames that resulted from expanding
333  *                           ~ and $ expressions and from matching any
334  *                           wildcards, sorted into lexical order.
335  *                           This container and its contents will be
336  *                           recycled on subsequent calls, so if you need
337  *                           to keep the results of two successive runs,
338  *                           you will either have to allocate a private
339  *                           copy of the array, or use two ExpandFile
340  *                           objects.
341  *
342  *                           On error NULL is returned. A description
343  *                           of the error can be acquired by calling the
344  *                           ef_last_error() function.
345  */
346 FileExpansion *ef_expand_file(ExpandFile *ef, const char *path, int pathlen)
347 {
348   DirNode *dnode;       /* A directory-reader cache node */
349   const char *dirname;  /* The name of the top level directory of the search */
350   const char *pptr;     /* A pointer into path[] */
351   int wild;             /* True if the path contains any wildcards */
352 /*
353  * Check the arguments.
354  */
355   if(!ef || !path) {
356     if(ef) {
357       _err_record_msg(ef->err, "ef_expand_file: NULL path argument",
358 		      END_ERR_MSG);
359     };
360     errno = EINVAL;
361     return NULL;
362   };
363 /*
364  * If the caller specified that the whole of path[] be matched,
365  * work out the corresponding length.
366  */
367   if(pathlen < 0 || pathlen > strlen(path))
368     pathlen = strlen(path);
369 /*
370  * Discard previous expansion results.
371  */
372   ef_clear_files(ef);
373 /*
374  * Preprocess the path, expanding ~/, ~user/ and $envvar references,
375  * using ef->path as a work directory and returning a pointer to
376  * a copy of the resulting pattern in the cache.
377  */
378   path = ef_expand_special(ef, path, pathlen);
379   if(!path)
380     return NULL;
381 /*
382  * Clear the pathname buffer.
383  */
384   _pn_clear_path(ef->path);
385 /*
386  * Does the pathname contain any wildcards?
387  */
388   for(wild=0,pptr=path; !wild && *pptr; pptr++) {
389     switch(*pptr) {
390     case '\\':                      /* Skip escaped characters */
391       if(pptr[1])
392 	pptr++;
393       break;
394     case '*': case '?': case '[':   /* A wildcard character? */
395       wild = 1;
396       break;
397     };
398   };
399 /*
400  * If there are no wildcards to match, copy the current expanded
401  * path into the output array, removing backslash escapes while doing so.
402  */
403   if(!wild) {
404     if(ef_record_pathname(ef, path, 1))
405       return NULL;
406 /*
407  * Does the filename exist?
408  */
409     ef->result.exists = _pu_file_exists(ef->result.files[0]);
410 /*
411  * Match wildcards against existing files.
412  */
413   } else {
414 /*
415  * Only existing files that match the pattern will be returned in the
416  * cache.
417  */
418     ef->result.exists = 1;
419 /*
420  * Treat matching of the root-directory as a special case since it
421  * isn't contained in a directory.
422  */
423     if(strcmp(path, FS_ROOT_DIR) == 0) {
424       if(ef_record_pathname(ef, FS_ROOT_DIR, 0))
425 	return NULL;
426     } else {
427 /*
428  * What should the top level directory of the search be?
429  */
430       if(strncmp(path, FS_ROOT_DIR, FS_ROOT_DIR_LEN) == 0) {
431 	dirname = FS_ROOT_DIR;
432 	if(!_pn_append_to_path(ef->path, FS_ROOT_DIR, -1, 0)) {
433 	  _err_record_msg(ef->err, "Insufficient memory to record path",
434 			  END_ERR_MSG);
435 	  return NULL;
436 	};
437 	path += FS_ROOT_DIR_LEN;
438       } else {
439 	dirname = FS_PWD;
440       };
441 /*
442  * Open the top-level directory of the search.
443  */
444       dnode = ef_open_dir(ef, dirname);
445       if(!dnode)
446 	return NULL;
447 /*
448  * Recursively match successive directory components of the path.
449  */
450       if(ef_match_relative_pathname(ef, dnode->dr, path, 0)) {
451 	dnode = ef_close_dir(ef, dnode);
452 	return NULL;
453       };
454 /*
455  * Cleanup.
456  */
457       dnode = ef_close_dir(ef, dnode);
458     };
459 /*
460  * No files matched?
461  */
462     if(ef->result.nfile < 1) {
463       _err_record_msg(ef->err, "No files match", END_ERR_MSG);
464       return NULL;
465     };
466 /*
467  * Sort the pathnames that matched.
468  */
469     qsort(ef->result.files, ef->result.nfile, sizeof(ef->result.files[0]),
470 	  ef_cmp_strings);
471   };
472 /*
473  * Return the result container.
474  */
475   return &ef->result;
476 }
477 
478 /*.......................................................................
479  * Attempt to recursively match the given pattern with the contents of
480  * the current directory, descending sub-directories as needed.
481  *
482  * Input:
483  *  ef      ExpandFile *  The pathname expansion resource object.
484  *  dr       DirReader *  The directory reader object of the directory
485  *                        to be searched.
486  *  pattern const char *  The pattern to match with files in the current
487  *                        directory.
488  *  separate       int    When appending a filename from the specified
489  *                        directory to ef->pathname, insert a directory
490  *                        separator between the existing pathname and
491  *                        the filename, unless separate is zero.
492  * Output:
493  *  return         int    0 - OK.
494  *                        1 - Error.
495  */
496 static int ef_match_relative_pathname(ExpandFile *ef, DirReader *dr,
497 				       const char *pattern, int separate)
498 {
499   const char *nextp;  /* The pointer to the character that follows the part */
500                       /*  of the pattern that is to be matched with files */
501                       /*  in the current directory. */
502   char *file;         /* The name of the file being matched */
503   int pathlen;        /* The length of ef->pathname[] on entry to this */
504                       /*  function */
505 /*
506  * Record the current length of the pathname string recorded in
507  * ef->pathname[].
508  */
509   pathlen = strlen(ef->path->name);
510 /*
511  * Get a pointer to the character that follows the end of the part of
512  * the pattern that should be matched to files within the current directory.
513  * This will either point to a directory separator, or to the '\0' terminator
514  * of the pattern string.
515  */
516   for(nextp=pattern; *nextp && strncmp(nextp, FS_DIR_SEP, FS_DIR_SEP_LEN) != 0;
517       nextp++)
518     ;
519 /*
520  * Read each file from the directory, attempting to match it to the
521  * current pattern.
522  */
523   while((file=_dr_next_file(dr)) != NULL) {
524 /*
525  * Does the latest file match the pattern up to nextp?
526  */
527     if(ef_string_matches_pattern(file, pattern, file[0]=='.', nextp)) {
528 /*
529  * Append the new directory entry to the current matching pathname.
530  */
531       if((separate && _pn_append_to_path(ef->path, FS_DIR_SEP, -1, 0)==NULL) ||
532 	 _pn_append_to_path(ef->path, file, -1, 0)==NULL) {
533 	_err_record_msg(ef->err, "Insufficient memory to record path",
534 			END_ERR_MSG);
535 	return 1;
536       };
537 /*
538  * If we have reached the end of the pattern, record the accumulated
539  * pathname in the list of matching files.
540  */
541       if(*nextp == '\0') {
542 	if(ef_record_pathname(ef, ef->path->name, 0))
543 	  return 1;
544 /*
545  * If the matching directory entry is a subdirectory, and the
546  * next character of the pattern is a directory separator,
547  * recursively call the current function to scan the sub-directory
548  * for matches.
549  */
550       } else if(_pu_path_is_dir(ef->path->name) &&
551 		strncmp(nextp, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) {
552 /*
553  * If the pattern finishes with the directory separator, then
554  * record the pathame as matching.
555  */
556 	if(nextp[FS_DIR_SEP_LEN] == '\0') {
557 	  if(ef_record_pathname(ef, ef->path->name, 0))
558 	    return 1;
559 /*
560  * Match files within the directory.
561  */
562 	} else {
563 	  DirNode *subdnode = ef_open_dir(ef, ef->path->name);
564 	  if(subdnode) {
565 	    if(ef_match_relative_pathname(ef, subdnode->dr,
566 					   nextp+FS_DIR_SEP_LEN, 1)) {
567 	      subdnode = ef_close_dir(ef, subdnode);
568 	      return 1;
569 	    };
570 	    subdnode = ef_close_dir(ef, subdnode);
571 	  };
572 	};
573       };
574 /*
575  * Remove the latest filename from the pathname string, so that
576  * another matching file can be appended.
577  */
578       ef->path->name[pathlen] = '\0';
579     };
580   };
581   return 0;
582 }
583 
584 /*.......................................................................
585  * Record a new matching filename.
586  *
587  * Input:
588  *  ef        ExpandFile *  The filename-match resource object.
589  *  pathname  const char *  The pathname to record.
590  *  remove_escapes   int    If true, remove backslash escapes in the
591  *                          recorded copy of the pathname.
592  * Output:
593  *  return           int     0 - OK.
594  *                           1 - Error (ef->err will contain a
595  *                                      description of the error).
596  */
597 static int ef_record_pathname(ExpandFile *ef, const char *pathname,
598 			      int remove_escapes)
599 {
600   char *copy;          /* The recorded copy of pathname[] */
601 /*
602  * Attempt to make a copy of the pathname in the cache.
603  */
604   copy = ef_cache_pathname(ef, pathname, remove_escapes);
605   if(!copy)
606     return 1;
607 /*
608  * If there isn't room to record a pointer to the recorded pathname in the
609  * array of files, attempt to extend the array.
610  */
611   if(ef->result.nfile + 1 > ef->files_dim) {
612     int files_dim = ef->files_dim + MATCH_BLK_FACT;
613     char **files = (char **) realloc(ef->result.files,
614 				     files_dim * sizeof(files[0]));
615     if(!files) {
616       _err_record_msg(ef->err,
617 	     "Insufficient memory to record all of the matching filenames",
618 	     END_ERR_MSG);
619       errno = ENOMEM;
620       return 1;
621     };
622     ef->result.files = files;
623     ef->files_dim = files_dim;
624   };
625 /*
626  * Record a pointer to the new match.
627  */
628   ef->result.files[ef->result.nfile++] = copy;
629   return 0;
630 }
631 
632 /*.......................................................................
633  * Record a pathname in the cache.
634  *
635  * Input:
636  *  ef       ExpandFile *  The filename-match resource object.
637  *  pathname       char *  The pathname to record.
638  *  remove_escapes  int    If true, remove backslash escapes in the
639  *                         copy of the pathname.
640  * Output:
641  *  return         char *  The pointer to the copy of the pathname.
642  *                         On error NULL is returned and a description
643  *                         of the error is left in ef->err.
644  */
645 static char *ef_cache_pathname(ExpandFile *ef, const char *pathname,
646 			       int remove_escapes)
647 {
648   char *copy = _sg_store_string(ef->sg, pathname, remove_escapes);
649   if(!copy)
650     _err_record_msg(ef->err, "Insufficient memory to store pathname",
651 		    END_ERR_MSG);
652   return copy;
653 }
654 
655 /*.......................................................................
656  * Clear the results of the previous expansion operation, ready for the
657  * next.
658  *
659  * Input:
660  *  ef    ExpandFile *  The pathname expansion resource object.
661  */
662 static void ef_clear_files(ExpandFile *ef)
663 {
664   _clr_StringGroup(ef->sg);
665   _pn_clear_path(ef->path);
666   ef->result.exists = 0;
667   ef->result.nfile = 0;
668   _err_clear_msg(ef->err);
669   return;
670 }
671 
672 /*.......................................................................
673  * Get a new directory reader object from the cache.
674  *
675  * Input:
676  *  ef        ExpandFile *  The pathname expansion resource object.
677  *  pathname  const char *  The pathname of the directory.
678  * Output:
679  *  return       DirNode *  The cache entry of the new directory reader,
680  *                          or NULL on error. On error, ef->err will
681  *                          contain a description of the error.
682  */
683 static DirNode *ef_open_dir(ExpandFile *ef, const char *pathname)
684 {
685   char *errmsg = NULL;  /* An error message from a called function */
686   DirNode *node;        /* The cache node used */
687 /*
688  * Get the directory reader cache.
689  */
690   DirCache *cache = &ef->cache;
691 /*
692  * Extend the cache if there are no free cache nodes.
693  */
694   if(!cache->next) {
695     node = (DirNode *) _new_FreeListNode(cache->mem);
696     if(!node) {
697       _err_record_msg(ef->err, "Insufficient memory to open a new directory",
698 		      END_ERR_MSG);
699       return NULL;
700     };
701 /*
702  * Initialize the cache node.
703  */
704     node->next = NULL;
705     node->prev = NULL;
706     node->dr = NULL;
707 /*
708  * Allocate a directory reader object.
709  */
710     node->dr = _new_DirReader();
711     if(!node->dr) {
712       _err_record_msg(ef->err, "Insufficient memory to open a new directory",
713 		      END_ERR_MSG);
714       node = (DirNode *) _del_FreeListNode(cache->mem, node);
715       return NULL;
716     };
717 /*
718  * Append the node to the cache list.
719  */
720     node->prev = cache->tail;
721     if(cache->tail)
722       cache->tail->next = node;
723     else
724       cache->head = node;
725     cache->next = cache->tail = node;
726   };
727 /*
728  * Get the first unused node, but don't remove it from the list yet.
729  */
730   node = cache->next;
731 /*
732  * Attempt to open the specified directory.
733  */
734   if(_dr_open_dir(node->dr, pathname, &errmsg)) {
735     _err_record_msg(ef->err, errmsg, END_ERR_MSG);
736     return NULL;
737   };
738 /*
739  * Now that we have successfully opened the specified directory,
740  * remove the cache node from the list, and relink the list around it.
741  */
742   cache->next = node->next;
743   if(node->prev)
744     node->prev->next = node->next;
745   else
746     cache->head = node->next;
747   if(node->next)
748     node->next->prev = node->prev;
749   else
750     cache->tail = node->prev;
751   node->next = node->prev = NULL;
752 /*
753  * Return the successfully initialized cache node to the caller.
754  */
755   return node;
756 }
757 
758 /*.......................................................................
759  * Return a directory reader object to the cache, after first closing
760  * the directory that it was managing.
761  *
762  * Input:
763  *  ef    ExpandFile *  The pathname expansion resource object.
764  *  node     DirNode *  The cache entry of the directory reader, as returned
765  *                      by ef_open_dir().
766  * Output:
767  *  return   DirNode *  The deleted DirNode (ie. allways NULL).
768  */
769 static DirNode *ef_close_dir(ExpandFile *ef, DirNode *node)
770 {
771 /*
772  * Get the directory reader cache.
773  */
774   DirCache *cache = &ef->cache;
775 /*
776  * Close the directory.
777  */
778   _dr_close_dir(node->dr);
779 /*
780  * Return the node to the tail of the cache list.
781  */
782   node->next = NULL;
783   node->prev = cache->tail;
784   if(cache->tail)
785     cache->tail->next = node;
786   else
787     cache->head = cache->tail = node;
788   if(!cache->next)
789     cache->next = node;
790   return NULL;
791 }
792 
793 /*.......................................................................
794  * Return non-zero if the specified file name matches a given glob
795  * pattern.
796  *
797  * Input:
798  *  file     const char *  The file-name component to be matched to the pattern.
799  *  pattern  const char *  The start of the pattern to match against file[].
800  *  xplicit         int    If non-zero, the first character must be matched
801  *                         explicitly (ie. not with a wildcard).
802  *  nextp    const char *  The pointer to the the character following the
803  *                         end of the pattern in pattern[].
804  * Output:
805  *  return    int          0 - Doesn't match.
806  *                         1 - The file-name string matches the pattern.
807  */
808 static int ef_string_matches_pattern(const char *file, const char *pattern,
809 				      int xplicit, const char *nextp)
810 {
811   const char *pptr = pattern; /* The pointer used to scan the pattern */
812   const char *fptr = file;    /* The pointer used to scan the filename string */
813 /*
814  * Match each character of the pattern in turn.
815  */
816   while(pptr < nextp) {
817 /*
818  * Handle the next character of the pattern.
819  */
820     switch(*pptr) {
821 /*
822  * A match zero-or-more characters wildcard operator.
823  */
824     case '*':
825 /*
826  * Skip the '*' character in the pattern.
827  */
828       pptr++;
829 /*
830  * If wildcards aren't allowed, the pattern doesn't match.
831  */
832       if(xplicit)
833 	return 0;
834 /*
835  * If the pattern ends with a the '*' wildcard, then the
836  * rest of the filename matches this.
837  */
838       if(pptr >= nextp)
839 	return 1;
840 /*
841  * Using the wildcard to match successively longer sections of
842  * the remaining characters of the filename, attempt to match
843  * the tail of the filename against the tail of the pattern.
844  */
845       for( ; *fptr; fptr++) {
846 	if(ef_string_matches_pattern(fptr, pptr, 0, nextp))
847 	  return 1;
848       };
849       return 0; /* The pattern following the '*' didn't match */
850       break;
851 /*
852  * A match-one-character wildcard operator.
853  */
854     case '?':
855 /*
856  * If there is a character to be matched, skip it and advance the
857  * pattern pointer.
858  */
859       if(!xplicit && *fptr) {
860         fptr++;
861         pptr++;
862 /*
863  * If we hit the end of the filename string, there is no character
864  * matching the operator, so the string doesn't match.
865  */
866       } else {
867         return 0;
868       };
869       break;
870 /*
871  * A character range operator, with the character ranges enclosed
872  * in matching square brackets.
873  */
874     case '[':
875       if(xplicit || !ef_matches_range(*fptr++, ++pptr, &pptr))
876         return 0;
877       break;
878 /*
879  * A backslash in the pattern prevents the following character as
880  * being seen as a special character.
881  */
882     case '\\':
883       pptr++;
884       /* Note fallthrough to default */
885 /*
886  * A normal character to be matched explicitly.
887  */
888       /* FALLTHROUGH */
889     default:
890       if(*fptr == *pptr) {
891         fptr++;
892         pptr++;
893       } else {
894         return 0;
895       };
896       break;
897     };
898 /*
899  * After passing the first character, turn off the explicit match
900  * requirement.
901  */
902     xplicit = 0;
903   };
904 /*
905  * To get here the pattern must have been exhausted. If the filename
906  * string matched, then the filename string must also have been
907  * exhausted.
908  */
909   return *fptr == '\0';
910 }
911 
912 /*.......................................................................
913  * Match a character range expression terminated by an unescaped close
914  * square bracket.
915  *
916  * Input:
917  *  c               int     The character to be matched with the range
918  *                          pattern.
919  *  pattern  const char *   The range pattern to be matched (ie. after the
920  *                          initiating '[' character).
921  *  endp     const char **  On output a pointer to the character following the
922  *                          range expression will be assigned to *endp.
923  * Output:
924  *  return          int     0 - Doesn't match.
925  *                          1 - The character matched.
926  */
927 static int ef_matches_range(int c, const char *pattern, const char **endp)
928 {
929   const char *pptr = pattern;  /* The pointer used to scan the pattern */
930   int invert = 0;              /* True to invert the sense of the match */
931   int matched = 0;             /* True if the character matched the pattern */
932 /*
933  * If the first character is a caret, the sense of the match is
934  * inverted and only if the character isn't one of those in the
935  * range, do we say that it matches.
936  */
937   if(*pptr == '^') {
938     pptr++;
939     invert = 1;
940   };
941 /*
942  * The hyphen is only a special character when it follows the first
943  * character of the range (not including the caret).
944  */
945   if(*pptr == '-') {
946     pptr++;
947     if(c == '-') {
948       *endp = pptr;
949       matched = 1;
950     };
951 /*
952  * Skip other leading '-' characters since they make no sense.
953  */
954     while(*pptr == '-')
955       pptr++;
956   };
957 /*
958  * The hyphen is only a special character when it follows the first
959  * character of the range (not including the caret or a hyphen).
960  */
961   if(*pptr == ']') {
962     pptr++;
963     if(c == ']') {
964       *endp = pptr;
965       matched = 1;
966     };
967   };
968 /*
969  * Having dealt with the characters that have special meanings at
970  * the beginning of a character range expression, see if the
971  * character matches any of the remaining characters of the range,
972  * up until a terminating ']' character is seen.
973  */
974   while(!matched && *pptr && *pptr != ']') {
975 /*
976  * Is this a range of characters signaled by the two end characters
977  * separated by a hyphen?
978  */
979     if(*pptr == '-') {
980       if(pptr[1] != ']') {
981         if(c >= pptr[-1] && c <= pptr[1])
982 	  matched = 1;
983 	pptr += 2;
984       };
985 /*
986  * A normal character to be compared directly.
987  */
988     } else if(*pptr++ == c) {
989       matched = 1;
990     };
991   };
992 /*
993  * Find the terminating ']'.
994  */
995   while(*pptr && *pptr != ']')
996     pptr++;
997 /*
998  * Did we find a terminating ']'?
999  */
1000   if(*pptr == ']') {
1001     *endp = pptr + 1;
1002     return matched ? !invert : invert;
1003   };
1004 /*
1005  * If the pattern didn't end with a ']' then it doesn't match, regardless
1006  * of the value of the required sense of the match.
1007  */
1008   *endp = pptr;
1009   return 0;
1010 }
1011 
1012 /*.......................................................................
1013  * This is a qsort() comparison function used to sort strings.
1014  *
1015  * Input:
1016  *  v1, v2   void *  Pointers to the two strings to be compared.
1017  * Output:
1018  *  return    int    -1 -> v1 < v2.
1019  *                    0 -> v1 == v2
1020  *                    1 -> v1 > v2
1021  */
1022 static int ef_cmp_strings(const void *v1, const void *v2)
1023 {
1024   char * const *s1 = (char * const *) v1;
1025   char * const *s2 = (char * const *) v2;
1026   return strcmp(*s1, *s2);
1027 }
1028 
1029 /*.......................................................................
1030  * Preprocess a path, expanding ~/, ~user/ and $envvar references, using
1031  * ef->path as a work buffer, then copy the result into a cache entry,
1032  * and return a pointer to this copy.
1033  *
1034  * Input:
1035  *  ef    ExpandFile *  The resource object of the file matcher.
1036  *  pathlen      int    The length of the prefix of path[] to be expanded.
1037  * Output:
1038  *  return      char *  A pointer to a copy of the output path in the
1039  *                      cache. On error NULL is returned, and a description
1040  *                      of the error is left in ef->err.
1041  */
1042 static char *ef_expand_special(ExpandFile *ef, const char *path, int pathlen)
1043 {
1044   int spos;      /* The index of the start of the path segment that needs */
1045                  /*  to be copied from path[] to the output pathname. */
1046   int ppos;      /* The index of a character in path[] */
1047   char *pptr;    /* A pointer into the output path */
1048   int escaped;   /* True if the previous character was a '\' */
1049   int i;
1050 /*
1051  * Clear the pathname buffer.
1052  */
1053   _pn_clear_path(ef->path);
1054 /*
1055  * We need to perform two passes, one to expand environment variables
1056  * and a second to do tilde expansion. This caters for the case
1057  * where an initial dollar expansion yields a tilde expression.
1058  */
1059   escaped = 0;
1060   for(spos=ppos=0; ppos < pathlen; ppos++) {
1061     int c = path[ppos];
1062     if(escaped) {
1063       escaped = 0;
1064     } else if(c == '\\') {
1065       escaped = 1;
1066     } else if(c == '$') {
1067       int envlen;   /* The length of the environment variable */
1068       char *value;  /* The value of the environment variable */
1069 /*
1070  * Record the preceding unrecorded part of the pathname.
1071  */
1072       if(spos < ppos && _pn_append_to_path(ef->path, path + spos, ppos-spos, 0)
1073 	 == NULL) {
1074 	_err_record_msg(ef->err, "Insufficient memory to expand path",
1075 			END_ERR_MSG);
1076 	return NULL;
1077       };
1078 /*
1079  * Skip the dollar.
1080  */
1081       ppos++;
1082 /*
1083  * Copy the environment variable name that follows the dollar into
1084  * ef->envnam[], stopping if a directory separator or end of string
1085  * is seen.
1086  */
1087       for(envlen=0; envlen<ENV_LEN && ppos < pathlen &&
1088 	  strncmp(path + ppos, FS_DIR_SEP, FS_DIR_SEP_LEN); envlen++)
1089 	ef->envnam[envlen] = path[ppos++];
1090 /*
1091  * If the username overflowed the buffer, treat it as invalid (note that
1092  * on most unix systems only 8 characters are allowed in a username,
1093  * whereas our ENV_LEN is much bigger than that.
1094  */
1095       if(envlen >= ENV_LEN) {
1096 	_err_record_msg(ef->err, "Environment variable name too long",
1097 			END_ERR_MSG);
1098 	return NULL;
1099       };
1100 /*
1101  * Terminate the environment variable name.
1102  */
1103       ef->envnam[envlen] = '\0';
1104 /*
1105  * Lookup the value of the environment variable.
1106  */
1107       value = getenv(ef->envnam);
1108       if(!value) {
1109 	_err_record_msg(ef->err, "No expansion found for: $", ef->envnam,
1110 			END_ERR_MSG);
1111 	return NULL;
1112       };
1113 /*
1114  * Copy the value of the environment variable into the output pathname.
1115  */
1116       if(_pn_append_to_path(ef->path, value, -1, 0) == NULL) {
1117 	_err_record_msg(ef->err, "Insufficient memory to expand path",
1118 			END_ERR_MSG);
1119 	return NULL;
1120       };
1121 /*
1122  * Record the start of the uncopied tail of the input pathname.
1123  */
1124       spos = ppos;
1125     };
1126   };
1127 /*
1128  * Record the uncopied tail of the pathname.
1129  */
1130   if(spos < ppos && _pn_append_to_path(ef->path, path + spos, ppos-spos, 0)
1131      == NULL) {
1132     _err_record_msg(ef->err, "Insufficient memory to expand path", END_ERR_MSG);
1133     return NULL;
1134   };
1135 /*
1136  * If the first character of the resulting pathname is a tilde,
1137  * then attempt to substitute the home directory of the specified user.
1138  */
1139   pptr = ef->path->name;
1140   if(*pptr == '~' && path[0] != '\\') {
1141     int usrlen;           /* The length of the username following the tilde */
1142     const char *homedir;  /* The home directory of the user */
1143     int homelen;          /* The length of the home directory string */
1144     int plen;             /* The current length of the path */
1145     int skip=0;           /* The number of characters to skip after the ~user */
1146 /*
1147  * Get the current length of the output path.
1148  */
1149     plen = strlen(ef->path->name);
1150 /*
1151  * Skip the tilde.
1152  */
1153     pptr++;
1154 /*
1155  * Copy the optional username that follows the tilde into ef->usrnam[].
1156  */
1157     for(usrlen=0; usrlen<USR_LEN && *pptr &&
1158 	strncmp(pptr, FS_DIR_SEP, FS_DIR_SEP_LEN); usrlen++)
1159       ef->usrnam[usrlen] = *pptr++;
1160 /*
1161  * If the username overflowed the buffer, treat it as invalid (note that
1162  * on most unix systems only 8 characters are allowed in a username,
1163  * whereas our USR_LEN is much bigger than that.
1164  */
1165     if(usrlen >= USR_LEN) {
1166       _err_record_msg(ef->err, "Username too long", END_ERR_MSG);
1167       return NULL;
1168     };
1169 /*
1170  * Terminate the username string.
1171  */
1172     ef->usrnam[usrlen] = '\0';
1173 /*
1174  * Lookup the home directory of the user.
1175  */
1176     homedir = _hd_lookup_home_dir(ef->home, ef->usrnam);
1177     if(!homedir) {
1178       _err_record_msg(ef->err, _hd_last_home_dir_error(ef->home), END_ERR_MSG);
1179       return NULL;
1180     };
1181     homelen = strlen(homedir);
1182 /*
1183  * ~user and ~ are usually followed by a directory separator to
1184  * separate them from the file contained in the home directory.
1185  * If the home directory is the root directory, then we don't want
1186  * to follow the home directory by a directory separator, so we must
1187  * erase it.
1188  */
1189     if(strcmp(homedir, FS_ROOT_DIR) == 0 &&
1190        strncmp(pptr, FS_DIR_SEP, FS_DIR_SEP_LEN) == 0) {
1191       skip = FS_DIR_SEP_LEN;
1192     };
1193 /*
1194  * If needed, increase the size of the pathname buffer to allow it
1195  * to accomodate the home directory instead of the tilde expression.
1196  * Note that pptr may not be valid after this call.
1197  */
1198     if(_pn_resize_path(ef->path, plen - usrlen - 1 - skip + homelen)==NULL) {
1199       _err_record_msg(ef->err, "Insufficient memory to expand filename",
1200 		      END_ERR_MSG);
1201       return NULL;
1202     };
1203 /*
1204  * Move the part of the pathname that follows the tilde expression to
1205  * the end of where the home directory will need to be inserted.
1206  */
1207     memmove(ef->path->name + homelen,
1208 	    ef->path->name + 1 + usrlen + skip, plen - usrlen - 1 - skip+1);
1209 /*
1210  * Write the home directory at the beginning of the string.
1211  */
1212     for(i=0; i<homelen; i++)
1213       ef->path->name[i] = homedir[i];
1214   };
1215 /*
1216  * Copy the result into the cache, and return a pointer to the copy.
1217  */
1218   return ef_cache_pathname(ef, ef->path->name, 0);
1219 }
1220 
1221 /*.......................................................................
1222  * Return a description of the last path-expansion error that occurred.
1223  *
1224  * Input:
1225  *  ef     ExpandFile *   The path-expansion resource object.
1226  * Output:
1227  *  return       char *   The description of the last error.
1228  */
1229 const char *ef_last_error(ExpandFile *ef)
1230 {
1231   return ef ? _err_get_msg(ef->err) : "NULL ExpandFile argument";
1232 }
1233 
1234 /*.......................................................................
1235  * Print out an array of matching files.
1236  *
1237  * Input:
1238  *  result  FileExpansion *   The container of the sorted array of
1239  *                            expansions.
1240  *  fp               FILE *   The output stream to write to.
1241  *  term_width        int     The width of the terminal.
1242  * Output:
1243  *  return            int     0 - OK.
1244  *                            1 - Error.
1245  */
1246 int ef_list_expansions(FileExpansion *result, FILE *fp, int term_width)
1247 {
1248   return _ef_output_expansions(result, _io_write_stdio, fp, term_width);
1249 }
1250 
1251 /*.......................................................................
1252  * Print out an array of matching files via a callback.
1253  *
1254  * Input:
1255  *  result  FileExpansion *  The container of the sorted array of
1256  *                           expansions.
1257  *  write_fn    GlWriteFn *  The function to call to write the
1258  *                           expansions or 0 to discard the output.
1259  *  data             void *  Anonymous data to pass to write_fn().
1260  *  term_width        int    The width of the terminal.
1261  * Output:
1262  *  return            int    0 - OK.
1263  *                           1 - Error.
1264  */
1265 int _ef_output_expansions(FileExpansion *result, GlWriteFn *write_fn,
1266 			  void *data, int term_width)
1267 {
1268   EfListFormat fmt; /* List formatting information */
1269   int lnum;          /* The sequential number of the line to print next */
1270 /*
1271  * Not enough space to list anything?
1272  */
1273   if(term_width < 1)
1274     return 0;
1275 /*
1276  * Do we have a callback to write via, and any expansions to be listed?
1277  */
1278   if(write_fn && result && result->nfile>0) {
1279 /*
1280  * Work out how to arrange the listing into fixed sized columns.
1281  */
1282     ef_plan_listing(result, term_width, &fmt);
1283 /*
1284  * Print the listing to the specified stream.
1285  */
1286     for(lnum=0; lnum < fmt.nline; lnum++) {
1287       if(ef_format_line(result, &fmt, lnum, write_fn, data))
1288 	return 1;
1289     };
1290   };
1291   return 0;
1292 }
1293 
1294 /*.......................................................................
1295  * Work out how to arrange a given array of completions into a listing
1296  * of one or more fixed size columns.
1297  *
1298  * Input:
1299  *  result   FileExpansion *   The set of completions to be listed.
1300  *  term_width         int     The width of the terminal. A lower limit of
1301  *                             zero is quietly enforced.
1302  * Input/Output:
1303  *  fmt       EfListFormat *   The formatting information will be assigned
1304  *                             to the members of *fmt.
1305  */
1306 static void ef_plan_listing(FileExpansion *result, int term_width,
1307 			    EfListFormat *fmt)
1308 {
1309   int maxlen;    /* The length of the longest matching string */
1310   int i;
1311 /*
1312  * Ensure that term_width >= 0.
1313  */
1314   if(term_width < 0)
1315     term_width = 0;
1316 /*
1317  * Start by assuming the worst case, that either nothing will fit
1318  * on the screen, or that there are no matches to be listed.
1319  */
1320   fmt->term_width = term_width;
1321   fmt->column_width = 0;
1322   fmt->nline = fmt->ncol = 0;
1323 /*
1324  * Work out the maximum length of the matching strings.
1325  */
1326   maxlen = 0;
1327   for(i=0; i<result->nfile; i++) {
1328     int len = strlen(result->files[i]);
1329     if(len > maxlen)
1330       maxlen = len;
1331   };
1332 /*
1333  * Nothing to list?
1334  */
1335   if(maxlen == 0)
1336     return;
1337 /*
1338  * Split the available terminal width into columns of
1339  * maxlen + EF_COL_SEP characters.
1340  */
1341   fmt->column_width = maxlen;
1342   fmt->ncol = fmt->term_width / (fmt->column_width + EF_COL_SEP);
1343 /*
1344  * If the column width is greater than the terminal width, zero columns
1345  * will have been selected. Set a lower limit of one column. Leave it
1346  * up to the caller how to deal with completions who's widths exceed
1347  * the available terminal width.
1348  */
1349   if(fmt->ncol < 1)
1350     fmt->ncol = 1;
1351 /*
1352  * How many lines of output will be needed?
1353  */
1354   fmt->nline = (result->nfile + fmt->ncol - 1) / fmt->ncol;
1355   return;
1356 }
1357 
1358 /*.......................................................................
1359  * Render one line of a multi-column listing of completions, using a
1360  * callback function to pass the output to an arbitrary destination.
1361  *
1362  * Input:
1363  *  result   FileExpansion *  The container of the sorted array of
1364  *                            completions.
1365  *  fmt       EfListFormat *  Formatting information.
1366  *  lnum               int    The index of the line to print, starting
1367  *                            from 0, and incrementing until the return
1368  *                            value indicates that there is nothing more
1369  *                            to be printed.
1370  *  write_fn     GlWriteFn *  The function to call to write the line, or
1371  *                            0 to discard the output.
1372  *  data              void *  Anonymous data to pass to write_fn().
1373  * Output:
1374  *  return             int    0 - Line printed ok.
1375  *                            1 - Nothing to print.
1376  */
1377 static int ef_format_line(FileExpansion *result, EfListFormat *fmt, int lnum,
1378 			  GlWriteFn *write_fn, void *data)
1379 {
1380   int col;             /* The index of the list column being output */
1381 /*
1382  * If the line index is out of bounds, there is nothing to be written.
1383  */
1384   if(lnum < 0 || lnum >= fmt->nline)
1385     return 1;
1386 /*
1387  * If no output function has been provided, return as though the line
1388  * had been printed.
1389  */
1390   if(!write_fn)
1391     return 0;
1392 /*
1393  * Print the matches in 'ncol' columns, sorted in line order within each
1394  * column.
1395  */
1396   for(col=0; col < fmt->ncol; col++) {
1397     int m = col*fmt->nline + lnum;
1398 /*
1399  * Is there another match to be written? Note that in general
1400  * the last line of a listing will have fewer filled columns
1401  * than the initial lines.
1402  */
1403     if(m < result->nfile) {
1404       char *file = result->files[m];
1405 /*
1406  * How long are the completion and type-suffix strings?
1407  */
1408       int flen = strlen(file);
1409 /*
1410  * Write the completion string.
1411  */
1412       if(write_fn(data, file, flen) != flen)
1413 	return 1;
1414 /*
1415  * If another column follows the current one, pad to its start with spaces.
1416  */
1417       if(col+1 < fmt->ncol) {
1418 /*
1419  * The following constant string of spaces is used to pad the output.
1420  */
1421 	static const char spaces[] = "                    ";
1422 	static const int nspace = sizeof(spaces) - 1;
1423 /*
1424  * Pad to the next column, using as few sub-strings of the spaces[]
1425  * array as possible.
1426  */
1427 	int npad = fmt->column_width + EF_COL_SEP - flen;
1428 	while(npad>0) {
1429 	  int n = npad > nspace ? nspace : npad;
1430 	  if(write_fn(data, spaces + nspace - n, n) != n)
1431 	    return 1;
1432 	  npad -= n;
1433 	};
1434       };
1435     };
1436   };
1437 /*
1438  * Start a new line.
1439  */
1440   {
1441     char s[] = "\r\n";
1442     int n = strlen(s);
1443     if(write_fn(data, s, n) != n)
1444       return 1;
1445   };
1446   return 0;
1447 }
1448 
1449 #endif  /* ifndef WITHOUT_FILE_SYSTEM */
1450