/* * Copyright (C) 1984-2002 Mark Nudelman * * You may distribute under the terms of either the GNU General Public * License or the Less License, as specified in the README file. * * For more information about less, or for information on how to * contact the author, see the README file. */ /* * Routines to mess around with filenames (and files). * Much of this is very OS dependent. */ #include "less.h" #include "lglob.h" #if MSDOS_COMPILER #include #if MSDOS_COMPILER==WIN32C && !defined(_MSC_VER) #include #endif #if MSDOS_COMPILER==DJGPPC #include #include #define _MAX_PATH PATH_MAX #endif #endif #ifdef _OSK #include #ifndef _OSK_MWC32 #include #endif #endif #if OS2 #include #endif #if HAVE_STAT #include #ifndef S_ISDIR #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) #endif #ifndef S_ISREG #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) #endif #endif extern int force_open; extern int secure; extern int use_lessopen; extern IFILE curr_ifile; extern IFILE old_ifile; #if SPACES_IN_FILENAMES extern char openquote; extern char closequote; #endif /* * Remove quotes around a filename. */ public char * shell_unquote(str) char *str; { char *name; char *p; name = p = (char *) ecalloc(strlen(str)+1, sizeof(char)); if (*str == openquote) { str++; while (*str != '\0') { if (*str == closequote) { if (str[1] != closequote) break; str++; } *p++ = *str++; } } else { char *esc = get_meta_escape(); int esclen = strlen(esc); while (*str != '\0') { if (esclen > 0 && strncmp(str, esc, esclen) == 0) str += esclen; *p++ = *str++; } } *p = '\0'; return (name); } /* * Get the shell's escape character. */ public char * get_meta_escape() { char *s; s = lgetenv("LESSMETAESCAPE"); if (s == NULL) s = DEF_METAESCAPE; return (s); } /* * Get the characters which the shell considers to be "metacharacters". */ static char * metachars() { static char *mchars = NULL; if (mchars == NULL) { mchars = lgetenv("LESSMETACHARS"); if (mchars == NULL) mchars = DEF_METACHARS; } return (mchars); } /* * Is this a shell metacharacter? */ static int metachar(c) char c; { return (strchr(metachars(), c) != NULL); } /* * Insert a backslash before each metacharacter in a string. */ public char * shell_quote(s) char *s; { char *p; char *newstr; int len; char *esc = get_meta_escape(); int esclen = strlen(esc); int use_quotes = 0; int have_quotes = 0; /* * Determine how big a string we need to allocate. */ len = 1; /* Trailing null byte */ for (p = s; *p != '\0'; p++) { len++; if (*p == openquote || *p == closequote) have_quotes = 1; if (metachar(*p)) { if (esclen == 0) { /* * We've got a metachar, but this shell * doesn't support escape chars. Use quotes. */ use_quotes = 1; } else { /* * Allow space for the escape char. */ len += esclen; } } } if (use_quotes) { if (have_quotes) /* * We can't quote a string that contains quotes. */ return (NULL); len = strlen(s) + 3; } /* * Allocate and construct the new string. */ newstr = p = (char *) ecalloc(len, sizeof(char)); if (use_quotes) { snprintf(newstr, len, "%c%s%c", openquote, s, closequote); } else { while (*s != '\0') { if (metachar(*s)) { /* * Add the escape char. */ strlcpy(p, esc, newstr + len - p); p += esclen; } *p++ = *s++; } *p = '\0'; } return (newstr); } /* * Return a pathname that points to a specified file in a specified directory. * Return NULL if the file does not exist in the directory. */ static char * dirfile(dirname, filename) char *dirname; char *filename; { char *pathname; char *qpathname; int f; size_t len; if (dirname == NULL || *dirname == '\0') return (NULL); /* * Construct the full pathname. */ len = strlen(dirname) + strlen(filename) + 2; pathname = (char *) calloc(len, sizeof(char)); if (pathname == NULL) return (NULL); snprintf(pathname, len, "%s%s%s", dirname, PATHNAME_SEP, filename); /* * Make sure the file exists. */ qpathname = shell_unquote(pathname); f = open(qpathname, OPEN_READ); if (f < 0) { free(pathname); pathname = NULL; } else { close(f); } free(qpathname); return (pathname); } /* * Return the full pathname of the given file in the "home directory". */ public char * homefile(filename) char *filename; { register char *pathname; /* * Try $HOME/filename. */ pathname = dirfile(lgetenv("HOME"), filename); if (pathname != NULL) return (pathname); #if OS2 /* * Try $INIT/filename. */ pathname = dirfile(lgetenv("INIT"), filename); if (pathname != NULL) return (pathname); #endif #if MSDOS_COMPILER || OS2 /* * Look for the file anywhere on search path. */ pathname = (char *) calloc(_MAX_PATH, sizeof(char)); #if MSDOS_COMPILER==DJGPPC { char *res = searchpath(filename); if (res == 0) *pathname = '\0'; else strlcpy(pathname, res, _MAX_PATH); } #else _searchenv(filename, "PATH", pathname); #endif if (*pathname != '\0') return (pathname); free(pathname); #endif return (NULL); } #ifdef HELPFILE /* * Find out where the help file is. */ public char * find_helpfile() { char *helpfile; if ((helpfile = getenv("LESSHELP")) != NULL && *helpfile != '\0') return (save(helpfile)); #if MSDOS_COMPILER || OS2 return (homefile(HELPFILE)); #else return (save(HELPFILE)); #endif } #endif /* * Expand a string, substituting any "%" with the current filename, * and any "#" with the previous filename. * But a string of N "%"s is just replaced with N-1 "%"s. * Likewise for a string of N "#"s. * {{ This is a lot of work just to support % and #. }} */ public char * fexpand(s) char *s; { register char *fr, *to; register int n; register char *e; IFILE ifile; #define fchar_ifile(c) \ ((c) == '%' ? curr_ifile : \ (c) == '#' ? old_ifile : NULL_IFILE) /* * Make one pass to see how big a buffer we * need to allocate for the expanded string. */ n = 0; for (fr = s; *fr != '\0'; fr++) { switch (*fr) { case '%': case '#': if (fr > s && fr[-1] == *fr) { /* * Second (or later) char in a string * of identical chars. Treat as normal. */ n++; } else if (fr[1] != *fr) { /* * Single char (not repeated). Treat specially. */ ifile = fchar_ifile(*fr); if (ifile == NULL_IFILE) n++; else n += strlen(get_filename(ifile)); } /* * Else it is the first char in a string of * identical chars. Just discard it. */ break; default: n++; break; } } e = (char *) ecalloc(n+1, sizeof(char)); /* * Now copy the string, expanding any "%" or "#". */ to = e; for (fr = s; *fr != '\0'; fr++) { switch (*fr) { case '%': case '#': if (fr > s && fr[-1] == *fr) { *to++ = *fr; } else if (fr[1] != *fr) { ifile = fchar_ifile(*fr); if (ifile == NULL_IFILE) *to++ = *fr; else { strlcpy(to, get_filename(ifile), e + n + 1 - to); to += strlen(to); } } break; default: *to++ = *fr; break; } } *to = '\0'; return (e); } #if TAB_COMPLETE_FILENAME /* * Return a blank-separated list of filenames which "complete" * the given string. */ public char * fcomplete(s) char *s; { char *fpat; char *qs; size_t len; if (secure) return (NULL); /* * Complete the filename "s" by globbing "s*". */ #if MSDOS_COMPILER && (MSDOS_COMPILER == MSOFTC || MSDOS_COMPILER == BORLANDC) /* * But in DOS, we have to glob "s*.*". * But if the final component of the filename already has * a dot in it, just do "s*". * (Thus, "FILE" is globbed as "FILE*.*", * but "FILE.A" is globbed as "FILE.A*"). */ { char *slash; for (slash = s+strlen(s)-1; slash > s; slash--) if (*slash == *PATHNAME_SEP || *slash == '/') break; len = strlen(s) + 4; fpat = (char *) ecalloc(len, sizeof(char)); if (strchr(slash, '.') == NULL) snprintf(fpat, len, "%s*.*", s); else snprintf(fpat, len, "%s*", s); } #else len = strlen(s) + 2; fpat = (char *) ecalloc(len, sizeof(char)); snprintf(fpat, len, "%s*", s); #endif qs = lglob(fpat); s = shell_unquote(qs); if (strcmp(s,fpat) == 0) { /* * The filename didn't expand. */ free(qs); qs = NULL; } free(s); free(fpat); return (qs); } #endif /* * Try to determine if a file is "binary". * This is just a guess, and we need not try too hard to make it accurate. */ public int bin_file(f) int f; { int i; int n; unsigned char data[64]; if (!seekable(f)) return (0); if (lseek(f, (off_t)0, SEEK_SET) == BAD_LSEEK) return (0); n = read(f, data, sizeof(data)); for (i = 0; i < n; i++) if (binary_char(data[i])) return (1); return (0); } /* * Try to determine the size of a file by seeking to the end. */ static POSITION seek_filesize(f) int f; { off_t spos; spos = lseek(f, (off_t)0, SEEK_END); if (spos == BAD_LSEEK) return (NULL_POSITION); return ((POSITION) spos); } /* * Read a string from a file. * Return a pointer to the string in memory. */ static char * readfd(fd) FILE *fd; { int len; int ch; char *buf; char *p; /* * Make a guess about how many chars in the string * and allocate a buffer to hold it. */ len = 100; buf = (char *) ecalloc(len, sizeof(char)); for (p = buf; ; p++) { if ((ch = getc(fd)) == '\n' || ch == EOF) break; if (p - buf >= len-1) { /* * The string is too big to fit in the buffer we have. * Allocate a new buffer, twice as big. */ len *= 2; *p = '\0'; p = (char *) ecalloc(len, sizeof(char)); strlcpy(p, buf, len); free(buf); buf = p; p = buf + strlen(buf); } *p = ch; } *p = '\0'; return (buf); } #if HAVE_POPEN FILE *popen(); /* * Execute a shell command. * Return a pointer to a pipe connected to the shell command's standard output. */ static FILE * shellcmd(cmd) char *cmd; { FILE *fd; size_t len; #if HAVE_SHELL char *shell; shell = lgetenv("SHELL"); if (shell != NULL && *shell != '\0') { char *scmd; char *esccmd; /* * Read the output of <$SHELL -c cmd>. * Escape any metacharacters in the command. */ esccmd = shell_quote(cmd); if (esccmd == NULL) { fd = popen(cmd, "r"); } else { len = strlen(shell) + strlen(esccmd) + 5; scmd = (char *) ecalloc(len, sizeof(char)); snprintf(scmd, len, "%s %s %s", shell, shell_coption(), esccmd); free(esccmd); fd = popen(scmd, "r"); free(scmd); } } else #endif { fd = popen(cmd, "r"); } /* * Redirection in `popen' might have messed with the * standard devices. Restore binary input mode. */ SET_BINARY(0); return (fd); } #endif /* HAVE_POPEN */ /* * Expand a filename, doing any system-specific metacharacter substitutions. */ public char * lglob(filename) char *filename; { char *gfilename; char *ofilename; ofilename = fexpand(filename); if (secure) return (ofilename); filename = shell_unquote(ofilename); #ifdef DECL_GLOB_LIST { /* * The globbing function returns a list of names. */ int length; char *p; char *qfilename; DECL_GLOB_LIST(list) GLOB_LIST(filename, list); if (GLOB_LIST_FAILED(list)) { free(filename); return (ofilename); } length = 1; /* Room for trailing null byte */ for (SCAN_GLOB_LIST(list, p)) { INIT_GLOB_LIST(list, p); qfilename = shell_quote(p); if (qfilename != NULL) { length += strlen(qfilename) + 1; free(qfilename); } } gfilename = (char *) ecalloc(length, sizeof(char)); for (SCAN_GLOB_LIST(list, p)) { INIT_GLOB_LIST(list, p); qfilename = shell_quote(p); if (qfilename != NULL) { snprintf(gfilename + strlen(gfilename), length - strlen(gfilename), "%s ", qfilename); free(qfilename); } } /* * Overwrite the final trailing space with a null terminator. */ gfilename[strlen(gfilename) - 1] = '\0'; GLOB_LIST_DONE(list); } #else #ifdef DECL_GLOB_NAME { /* * The globbing function returns a single name, and * is called multiple times to walk thru all names. */ register char *p; register int len; register int n; char *pathname; char *qpathname; DECL_GLOB_NAME(fnd,drive,dir,fname,ext,handle) GLOB_FIRST_NAME(filename, &fnd, handle); if (GLOB_FIRST_FAILED(handle)) { free(filename); return (ofilename); } _splitpath(filename, drive, dir, fname, ext); len = 100; gfilename = (char *) ecalloc(len, sizeof(char)); p = gfilename; do { n = strlen(drive) + strlen(dir) + strlen(fnd.GLOB_NAME) + 1; pathname = (char *) ecalloc(n, sizeof(char)); snprintf(pathname, n, "%s%s%s", drive, dir, fnd.GLOB_NAME); qpathname = shell_quote(pathname); free(pathname); if (qpathname != NULL) { n = strlen(qpathname); while (p - gfilename + n + 2 >= len) { /* * No room in current buffer. * Allocate a bigger one. */ len *= 2; *p = '\0'; p = (char *) ecalloc(len, sizeof(char)); strlcpy(p, gfilename, len); free(gfilename); gfilename = p; p = gfilename + strlen(gfilename); } strlcpy(p, qpathname, gfilename + len - p); free(qpathname); p += n; *p++ = ' '; } } while (GLOB_NEXT_NAME(handle, &fnd) == 0); /* * Overwrite the final trailing space with a null terminator. */ *--p = '\0'; GLOB_NAME_DONE(handle); } #else #if HAVE_POPEN { /* * We get the shell to glob the filename for us by passing * an "echo" command to the shell and reading its output. */ FILE *fd; char *s; char *lessecho; char *cmd; char *esc; size_t len; esc = get_meta_escape(); if (strlen(esc) == 0) esc = "-"; esc = shell_quote(esc); if (esc == NULL) { free(filename); return (ofilename); } lessecho = lgetenv("LESSECHO"); if (lessecho == NULL || *lessecho == '\0') lessecho = "lessecho"; /* * Invoke lessecho, and read its output (a globbed list of filenames). */ len = strlen(lessecho) + strlen(ofilename) + (7*strlen(metachars())) + 24; cmd = (char *) ecalloc(len, sizeof(char)); snprintf(cmd, len, "%s -p0x%x -d0x%x -e%s ", lessecho, openquote, closequote, esc); free(esc); for (s = metachars(); *s != '\0'; s++) snprintf(cmd + strlen(cmd), len - strlen(cmd), "-n0x%x ", *s); snprintf(cmd + strlen(cmd), len - strlen(cmd), "-- %s", ofilename); fd = shellcmd(cmd); free(cmd); if (fd == NULL) { /* * Cannot create the pipe. * Just return the original (fexpanded) filename. */ free(filename); return (ofilename); } gfilename = readfd(fd); pclose(fd); if (*gfilename == '\0') { free(gfilename); free(filename); return (ofilename); } } #else /* * No globbing functions at all. Just use the fexpanded filename. */ gfilename = save(filename); #endif #endif #endif free(filename); free(ofilename); return (gfilename); } /* * See if we should open a "replacement file" * instead of the file we're about to open. */ public char * open_altfile(filename, pf, pfd) char *filename; int *pf; void **pfd; { #if !HAVE_POPEN return (NULL); #else char *lessopen; char *cmd, *cp; FILE *fd; size_t i, len; int found; #if HAVE_FILENO int returnfd = 0; #endif if (!use_lessopen || secure) return (NULL); ch_ungetchar(-1); if ((lessopen = lgetenv("LESSOPEN")) == NULL) return (NULL); if (strcmp(filename, "-") == 0) return (NULL); if (*lessopen == '|') { /* * If LESSOPEN starts with a |, it indicates * a "pipe preprocessor". */ #if HAVE_FILENO lessopen++; returnfd = 1; #else error("LESSOPEN pipe is not supported", NULL_PARG); return (NULL); #endif } /* strlen(filename) is guaranteed to be > 0 */ len = strlen(lessopen) + strlen(filename); cmd = (char *) ecalloc(len, sizeof(char)); for (cp = cmd, i = 0, found = 0; i < strlen(lessopen); i++) { if (!found && lessopen[i] == '%' && lessopen[i + 1] == 's') { found = 1; strlcat(cmd, filename, len); cp += strlen(filename); i++; } else *cp++ = lessopen[i]; } fd = shellcmd(cmd); free(cmd); if (fd == NULL) { /* * Cannot create the pipe. */ return (NULL); } #if HAVE_FILENO if (returnfd) { int f; char c; /* * Read one char to see if the pipe will produce any data. * If it does, push the char back on the pipe. */ f = fileno(fd); SET_BINARY(f); if (read(f, &c, 1) != 1) { /* * Pipe is empty. This means there is no alt file. */ pclose(fd); return (NULL); } ch_ungetchar(c); *pfd = (void *) fd; *pf = f; return (save("-")); } #endif cmd = readfd(fd); pclose(fd); if (*cmd == '\0') /* * Pipe is empty. This means there is no alt file. */ return (NULL); return (cmd); #endif /* HAVE_POPEN */ } /* * Close a replacement file. */ public void close_altfile(altfilename, filename, pipefd) char *altfilename; char *filename; void *pipefd; { #if HAVE_POPEN char *lessclose; FILE *fd; char *cmd, *cp; size_t i, len; int found; if (secure) return; if (pipefd != NULL) { #if OS2 /* * The pclose function of OS/2 emx sometimes fails. * Send SIGINT to the piped process before closing it. */ kill(((FILE*)pipefd)->_pid, SIGINT); #endif pclose((FILE*) pipefd); } if ((lessclose = lgetenv("LESSCLOSE")) == NULL) return; /* strlen(filename) is guaranteed to be > 0 */ len = strlen(lessclose) + strlen(filename) + strlen(altfilename); cmd = (char *) ecalloc(len, sizeof(char)); for (cp = cmd, i = 0, found = 0; i < strlen(lessclose); i++) { if (found < 2 && lessclose[i] == '%' && lessclose[i + 1] == 's') { if (++found == 1) { strlcat(cmd, filename, len); cp += strlen(filename); } else { strlcat(cmd, altfilename, len); cp += strlen(altfilename); } i++; } else *cp++ = lessclose[i]; } fd = shellcmd(cmd); free(cmd); if (fd != NULL) pclose(fd); #endif } /* * Is the specified file a directory? */ public int is_dir(filename) char *filename; { int isdir = 0; filename = shell_unquote(filename); #if HAVE_STAT { int r; struct stat statbuf; r = stat(filename, &statbuf); isdir = (r >= 0 && S_ISDIR(statbuf.st_mode)); } #else #ifdef _OSK { register int f; f = open(filename, S_IREAD | S_IFDIR); if (f >= 0) close(f); isdir = (f >= 0); } #endif #endif free(filename); return (isdir); } /* * Returns NULL if the file can be opened and * is an ordinary file, otherwise an error message * (if it cannot be opened or is a directory, etc.) */ public char * bad_file(filename) char *filename; { register char *m = NULL; size_t len; filename = shell_unquote(filename); if (is_dir(filename)) { static char is_a_dir[] = " is a directory"; len = strlen(filename) + sizeof(is_a_dir); m = (char *) ecalloc(len, sizeof(char)); strlcpy(m, filename, len); strlcat(m, is_a_dir, len); } else { #if HAVE_STAT int r; struct stat statbuf; r = stat(filename, &statbuf); if (r < 0) { m = errno_message(filename); } else if (force_open) { m = NULL; } else if (!S_ISREG(statbuf.st_mode)) { static char not_reg[] = " is not a regular file (use -f to see it)"; len = strlen(filename) + sizeof(not_reg); m = (char *) ecalloc(len, sizeof(char)); strlcpy(m, filename, len); strlcat(m, not_reg, len); } #endif } free(filename); return (m); } /* * Return the size of a file, as cheaply as possible. * In Unix, we can stat the file. */ public POSITION filesize(f) int f; { #if HAVE_STAT struct stat statbuf; if (fstat(f, &statbuf) >= 0) return ((POSITION) statbuf.st_size); #else #ifdef _OSK long size; if ((size = (long) _gs_size(f)) >= 0) return ((POSITION) size); #endif #endif return (seek_filesize(f)); } /* * */ public char * shell_coption() { return ("-c"); }