/*1* tclGlob.c --2*3* This file provides procedures and commands for file name4* manipulation, such as tilde expansion and globbing.5*6* Copyright (c) 1990-1994 The Regents of the University of California.7* Copyright (c) 1994 Sun Microsystems, Inc.8*9* See the file "license.terms" for information on usage and redistribution10* of this file, and for a DISCLAIMER OF ALL WARRANTIES.11*/1213#ifndef lint14static char sccsid[] = "@(#) tclGlob.c 1.42 95/06/08 10:56:13";15#endif /* not lint */1617#include "tclInt.h"18#include "tclPort.h"1920/*21* Declarations for procedures local to this file:22*/2324static int DoGlob _ANSI_ARGS_((Tcl_Interp *interp, char *dir,25char *rem));2627/*28*----------------------------------------------------------------------29*30* DoGlob --31*32* This recursive procedure forms the heart of the globbing33* code. It performs a depth-first traversal of the tree34* given by the path name to be globbed.35*36* Results:37* The return value is a standard Tcl result indicating whether38* an error occurred in globbing. After a normal return the39* result in interp will be set to hold all of the file names40* given by the dir and rem arguments. After an error the41* result in interp will hold an error message.42*43* Side effects:44* None.45*46*----------------------------------------------------------------------47*/4849static int50DoGlob(interp, dir, rem)51Tcl_Interp *interp; /* Interpreter to use for error52* reporting (e.g. unmatched brace). */53char *dir; /* Name of a directory at which to54* start glob expansion. This name55* is fixed: it doesn't contain any56* globbing chars. */57char *rem; /* Path to glob-expand. */58{59/*60* When this procedure is entered, the name to be globbed may61* already have been partly expanded by ancestor invocations of62* DoGlob. The part that's already been expanded is in "dir"63* (this may initially be empty), and the part still to expand64* is in "rem". This procedure expands "rem" one level, making65* recursive calls to itself if there's still more stuff left66* in the remainder.67*/6869Tcl_DString newName; /* Holds new name consisting of70* dir plus the first part of rem. */71register char *p;72register char c;73char *openBrace, *closeBrace, *name, *dirName;74int gotSpecial, baseLength;75int result = TCL_OK;76struct stat statBuf;7778/*79* Make sure that the directory part of the name really is a80* directory. If the directory name is "", use the name "."81* instead, because some UNIX systems don't treat "" like "."82* automatically. Keep the "" for use in generating file names,83* otherwise "glob foo.c" would return "./foo.c".84*/8586if (*dir == '\0') {87dirName = ".";88} else {89dirName = dir;90}91if ((stat(dirName, &statBuf) != 0) || !S_ISDIR(statBuf.st_mode)) {92return TCL_OK;93}94Tcl_DStringInit(&newName);9596/*97* First, find the end of the next element in rem, checking98* along the way for special globbing characters.99*/100101gotSpecial = 0;102openBrace = closeBrace = NULL;103for (p = rem; ; p++) {104c = *p;105if ((c == '\0') || ((openBrace == NULL) && (c == '/'))) {106break;107}108if ((c == '{') && (openBrace == NULL)) {109openBrace = p;110}111if ((c == '}') && (openBrace != NULL) && (closeBrace == NULL)) {112closeBrace = p;113}114if ((c == '*') || (c == '[') || (c == '\\') || (c == '?')) {115gotSpecial = 1;116}117}118119/*120* If there is an open brace in the argument, then make a recursive121* call for each element between the braces. In this case, the122* recursive call to DoGlob uses the same "dir" that we got.123* If there are several brace-pairs in a single name, we just handle124* one here, and the others will be handled in recursive calls.125*/126127if (openBrace != NULL) {128char *element;129130if (closeBrace == NULL) {131Tcl_ResetResult(interp);132interp->result = "unmatched open-brace in file name";133result = TCL_ERROR;134goto done;135}136Tcl_DStringAppend(&newName, rem, openBrace-rem);137baseLength = newName.length;138for (p = openBrace; *p != '}'; ) {139element = p+1;140for (p = element; ((*p != '}') && (*p != ',')); p++) {141/* Empty loop body. */142}143Tcl_DStringAppend(&newName, element, p-element);144Tcl_DStringAppend(&newName, closeBrace+1, -1);145result = DoGlob(interp, dir, newName.string);146if (result != TCL_OK) {147goto done;148}149newName.length = baseLength;150}151goto done;152}153154/*155* Start building up the next-level name with dir plus a slash if156* needed to separate it from the next file name.157*/158159Tcl_DStringAppend(&newName, dir, -1);160if ((dir[0] != 0) && (newName.string[newName.length-1] != '/')) {161Tcl_DStringAppend(&newName, "/", 1);162}163baseLength = newName.length;164165/*166* If there were any pattern-matching characters, then scan through167* the directory to find all the matching names.168*/169170if (gotSpecial) {171DIR *d;172struct dirent *entryPtr;173char savedChar;174175d = opendir(dirName);176if (d == NULL) {177Tcl_ResetResult(interp);178Tcl_AppendResult(interp, "couldn't read directory \"",179dirName, "\": ", Tcl_PosixError(interp), (char *) NULL);180result = TCL_ERROR;181goto done;182}183184/*185* Temporarily store a null into rem so that the pattern string186* is now null-terminated.187*/188189savedChar = *p;190*p = 0;191192while (1) {193entryPtr = readdir(d);194if (entryPtr == NULL) {195break;196}197198/*199* Don't match names starting with "." unless the "." is200* present in the pattern.201*/202203if ((*entryPtr->d_name == '.') && (*rem != '.')) {204continue;205}206if (Tcl_StringMatch(entryPtr->d_name, rem)) {207newName.length = baseLength;208Tcl_DStringAppend(&newName, entryPtr->d_name, -1);209if (savedChar == 0) {210Tcl_AppendElement(interp, newName.string);211} else {212result = DoGlob(interp, newName.string, p+1);213if (result != TCL_OK) {214break;215}216}217}218}219closedir(d);220*p = savedChar;221goto done;222}223224/*225* The current element is a simple one with no fancy features. Add226* it to the new name. If there are more elements still to come,227* then recurse to process them.228*/229230Tcl_DStringAppend(&newName, rem, p-rem);231if (*p != 0) {232result = DoGlob(interp, newName.string, p+1);233goto done;234}235236/*237* There are no more elements in the pattern. Check to be sure the238* file actually exists, then add its name to the list being formed239* in interp-result.240*/241242name = newName.string;243if (*name == 0) {244name = ".";245}246if (access(name, F_OK) != 0) {247goto done;248}249Tcl_AppendElement(interp, name);250251done:252Tcl_DStringFree(&newName);253return result;254}255256/*257*----------------------------------------------------------------------258*259* Tcl_TildeSubst --260*261* Given a name starting with a tilde, produce a name where262* the tilde and following characters have been replaced by263* the home directory location for the named user.264*265* Results:266* The result is a pointer to a static string containing267* the new name. If there was an error in processing the268* tilde, then an error message is left in interp->result269* and the return value is NULL. The result may be stored270* in bufferPtr; the caller must call Tcl_DStringFree(bufferPtr)271* to free the name.272*273* Side effects:274* Information may be left in bufferPtr.275*276*----------------------------------------------------------------------277*/278279char *280Tcl_TildeSubst(interp, name, bufferPtr)281Tcl_Interp *interp; /* Interpreter in which to store error282* message (if necessary). */283char *name; /* File name, which may begin with "~/"284* (to indicate current user's home directory)285* or "~<user>/" (to indicate any user's286* home directory). */287Tcl_DString *bufferPtr; /* May be used to hold result. Must not hold288* anything at the time of the call, and need289* not even be initialized. */290{291char *dir;292register char *p;293294Tcl_DStringInit(bufferPtr);295if (name[0] != '~') {296return name;297}298299if ((name[1] == '/') || (name[1] == '\0')) {300dir = getenv("HOME");301if (dir == NULL) {302Tcl_ResetResult(interp);303Tcl_AppendResult(interp, "couldn't find HOME environment ",304"variable to expand \"", name, "\"", (char *) NULL);305return NULL;306}307Tcl_DStringAppend(bufferPtr, dir, -1);308Tcl_DStringAppend(bufferPtr, name+1, -1);309} else {310struct passwd *pwPtr;311312for (p = &name[1]; (*p != 0) && (*p != '/'); p++) {313/* Null body; just find end of name. */314}315Tcl_DStringAppend(bufferPtr, name+1, p - (name+1));316pwPtr = getpwnam(bufferPtr->string);317if (pwPtr == NULL) {318endpwent();319Tcl_ResetResult(interp);320Tcl_AppendResult(interp, "user \"", bufferPtr->string,321"\" doesn't exist", (char *) NULL);322Tcl_DStringFree(bufferPtr);323return NULL;324}325Tcl_DStringFree(bufferPtr);326Tcl_DStringAppend(bufferPtr, pwPtr->pw_dir, -1);327Tcl_DStringAppend(bufferPtr, p, -1);328endpwent();329}330return bufferPtr->string;331}332333/*334*----------------------------------------------------------------------335*336* Tcl_GlobCmd --337*338* This procedure is invoked to process the "glob" Tcl command.339* See the user documentation for details on what it does.340*341* Results:342* A standard Tcl result.343*344* Side effects:345* See the user documentation.346*347*----------------------------------------------------------------------348*/349350/* ARGSUSED */351int352Tcl_GlobCmd(dummy, interp, argc, argv)353ClientData dummy; /* Not used. */354Tcl_Interp *interp; /* Current interpreter. */355int argc; /* Number of arguments. */356char **argv; /* Argument strings. */357{358int i, result, noComplain, firstArg;359360if (argc < 2) {361notEnoughArgs:362Tcl_AppendResult(interp, "wrong # args: should be \"", argv[0],363" ?switches? name ?name ...?\"", (char *) NULL);364return TCL_ERROR;365}366noComplain = 0;367for (firstArg = 1; (firstArg < argc) && (argv[firstArg][0] == '-');368firstArg++) {369if (strcmp(argv[firstArg], "-nocomplain") == 0) {370noComplain = 1;371} else if (strcmp(argv[firstArg], "--") == 0) {372firstArg++;373break;374} else {375Tcl_AppendResult(interp, "bad switch \"", argv[firstArg],376"\": must be -nocomplain or --", (char *) NULL);377return TCL_ERROR;378}379}380if (firstArg >= argc) {381goto notEnoughArgs;382}383384for (i = firstArg; i < argc; i++) {385char *thisName;386Tcl_DString buffer;387388thisName = Tcl_TildeSubst(interp, argv[i], &buffer);389if (thisName == NULL) {390if (noComplain) {391Tcl_ResetResult(interp);392continue;393} else {394return TCL_ERROR;395}396}397if (*thisName == '/') {398if (thisName[1] == '/') {399/*400* This is a special hack for systems like those from Apollo401* where there is a super-root at "//": need to treat the402* double-slash as a single name.403*/404result = DoGlob(interp, "//", thisName+2);405} else {406result = DoGlob(interp, "/", thisName+1);407}408} else {409result = DoGlob(interp, "", thisName);410}411Tcl_DStringFree(&buffer);412if (result != TCL_OK) {413return result;414}415}416if ((*interp->result == 0) && !noComplain) {417char *sep = "";418419Tcl_AppendResult(interp, "no files matched glob pattern",420(argc == 2) ? " \"" : "s \"", (char *) NULL);421for (i = firstArg; i < argc; i++) {422Tcl_AppendResult(interp, sep, argv[i], (char *) NULL);423sep = " ";424}425Tcl_AppendResult(interp, "\"", (char *) NULL);426return TCL_ERROR;427}428return TCL_OK;429}430431432