/* Complete.c */ /* Tim MacKenzie, t.mackenzie@trl.oz.au, April '95 */ #include "Sys.h" #include "LineList.h" #include "Cmdline.h" #include "Complete.h" #include "Prefs.h" #include "Bookmark.h" #include "Util.h" #include "List.h" #include #include extern int gNumCommands; /* Cmdlist.c */ extern Command gCommands[]; /* Cmdlist.c */ extern PrefOpt gPrefOpts[]; /*Prefs.c */ extern int gNumEditablePrefOpts; /*Prefs.c */ extern BookmarkPtr gFirstRsi; /* Bookmark.c */ extern longstring gRemoteCWD; /* Cmds.c */ extern Bookmark gRmtInfo; /* Bookmark.c */ extern int gScreenWidth; /* Win.c */ /* We can't use a linked list because we need random access for the * output routine... */ typedef struct { char **names; int count; int alloc; } FileList; struct _DirCache { struct _DirCache *next; FileList files; char *name; int flags; /* Combination of flags below */ }; static DirCache *cacheHead; static DirCache *currentCache; static void ForgetCurrent(void); /* Upon receipt of a signal we have to abort the completion */ static jmp_buf gCompleteJmp; #define LS_F 1 /* ls -F */ #define LS_L 2 /* ls -l */ #define LS_DIR 4 /* Have been supplied a directory to list */ #define LS_R 8 /* ls -R */ typedef char * (*CompleteFunc)(char *, int); #ifdef HAVE_LIBREADLINE #include #ifdef HAVE_FILENAME_COMPLETION_FUNCTION /* This should have been in your readline.h already, but some older * versions of the library are still lurking out there. */ extern char *filename_completion_function (); /* Yes, not a prototype... */ #endif #endif static char * Strmcpy(char *dst, char *src, int howMany, size_t dstSize) { size_t max; /* If you want to limit Strncpy to x characters, you * must pass x + 1 because Strncpy subtracts one for the * nul terminator. */ max = howMany + 1; if (max > dstSize) max = dstSize; return (Strncpy(dst, src, max)); } /* Strmcpy */ static char * StrnDup(char *s, int len) { char *res; res = (char *) malloc(len+1); strncpy(res,s,len); res[len] = 0; return res; } static char * ConvertDir(char *dir) { int baselen; string res; int i,j; baselen = (int) strlen(gRemoteCWD); while (dir && *dir) { if (*dir == '/') { /* It's an absolute pathname */ baselen = 0; break; } if (*dir == '.' && dir[1] == '.') { /* ../ remove bit from base */ dir += 2; while (*dir && *dir == '/') dir++; while (baselen > 0 && gRemoteCWD[baselen-1] != '/') baselen--; baselen--; /* Move back past the '/' */ if (baselen < 0) baselen = 0; continue; } if (*dir == '.' && (dir[1] == '/' || dir[1] == 0)) { /* . or ./ just remove it from dirname */ dir += 1; while (*dir && *dir == '/') dir++; continue; } break; } if (!dir) dir = ""; Strmcpy(res, gRemoteCWD, baselen, sizeof(res)); STRNCAT(res, "/"); STRNCAT(res, dir); STRNCAT(res, "/"); /* Remove //'s in the name */ for (i=j=1;res[i];i++) { if (res[i] == '/' && res[j-1] == '/') continue; res[j++] = res[i]; } res[j] = 0; return StrDup(res); } static DirCache * FindDirCache(char *dir) { DirCache *l; dir = ConvertDir(dir); for (l = cacheHead;l;l=l->next) if (!strcmp(dir, l->name)) break; free(dir); return l; } static void SigComplete(/* int sigNumUNUSED */ void) { alarm(0); longjmp(gCompleteJmp,1); } /* Get the filenames from the current directory */ static DirCache * GetCompleteFiles(char *dir) { LineList fileList; LinePtr f; DirCache *volatile l; volatile Sig_t si, sp; l = FindDirCache(dir); if (l) return l; InitLineList(&fileList); si = SIGNAL(SIGINT, SigComplete); sp = SIGNAL(SIGPIPE, SigComplete); SetBar(NULL, "GETTING COMPLETIONS", NULL, -1, 1); if (setjmp(gCompleteJmp)) { /* Complete was interrupted */ ForgetCurrent(); } else { GetFileList(&fileList, dir); /* GetFileList returns a list of filenames with a preceding character * denoting type */ l = CompleteStart(dir); /* GetFileList gets types, so might as well set -F */ CompleteSetFlags("-F"); for (f = fileList.first; f != NULL; f = f->next) { char *tmp = (char *)malloc(strlen(f->line)+1); strcpy(tmp,f->line+1); if (f->line[0] == 'd') strcat(tmp,"/"); CompleteParse(tmp); free(tmp); } } SetBar(NULL, NULL, NULL, -1, 1); /* Reset bar */ CompleteFinish(); DisposeLineListContents(&fileList); SIGNAL(SIGINT, si); SIGNAL(SIGPIPE, sp); return l; } static void InitFileList(FileList *f) { f->names = 0; f->count = 0; f->alloc = 0; } static void EmptyFileList(FileList *f) { int i; if (!f->names) return; for (i=0;icount;i++) free(f->names[i]); free(f->names); InitFileList(f); } /* Add a filename to the list */ static void FileListAdd(FileList *f, char *s) { if (f->alloc <= f->count+1) { f->alloc += 10; if (f->names) f->names = (char **) realloc(f->names, f->alloc * sizeof(char*)); else f->names = (char **) malloc(f->alloc * sizeof(char*)); } f->names[f->count++] = s; f->names[f->count] = 0; } static int CompareStrings(char **a, char **b) { #ifdef __FreeBSD__ return strcoll(*a, *b); #else return strcmp(*a, *b); #endif } static void CompleteMatches(FileList *l, char *word, CompleteFunc f) { char *s; InitFileList(l); for (s = (*f)(word, 0); s ; s = (*f)(word, 1)) FileListAdd(l,s); if (l->count > 1) QSORT(l->names, l->count, sizeof (char*), CompareStrings); return; } void ClearDirCache(void) { DirCache *next, *curr; for (curr = cacheHead ; curr ; curr = next) { next = curr->next; EmptyFileList(&curr->files); if (curr->name) free(curr->name); free(curr); } cacheHead = 0; currentCache = 0; } /* Start a completion cycle - the initializes some memory to put the * next listing into. */ DirCache * CompleteStart(char *dir) { /* The flag determines whether this is a clear due to a cd or other * cache invalidating command (flag=0) or due to the start of an ls * command (flag=1) */ DirCache *res; if (currentCache) { Error(kDontPerror, "Starting new completion without finishing last one\n"); CompleteFinish(); } dir = ConvertDir(dir); res = (DirCache *)malloc(sizeof *res); res->name = dir; res->flags = 0; res->next = 0; InitFileList(&res->files); currentCache = res; return res; } static int BetterCache(DirCache *a, DirCache *b) { /* Returns true if a is better than b */ /* The only way b is better is if it has directory information and * a doesn't */ if (b->flags & (LS_L|LS_F) && !(a->flags & (LS_L|LS_F))) return 0; return 1; } void CompleteFinish(void) { /* Finish off the current completion, adding it to the cache if it's * better than the old one */ DirCache *c; if (!currentCache) return; c = FindDirCache(currentCache->name); if (!c) { currentCache->next = cacheHead; cacheHead = currentCache; } else { if (BetterCache(currentCache, c)) { /* Replace old with new - we just nuke the old and copy the new * over it */ currentCache->next = c->next; EmptyFileList(&c->files); free(c->name); *c = *currentCache; } else { /* Discard new */ EmptyFileList(¤tCache->files); free(currentCache->name); } /* We free the memory allocated to the new since we copied over the * old or just left the old */ free(currentCache); } currentCache = 0; } /* Generate dir/file names for the readline completion generator */ static char * CompleteDirFileGenerator(char *text,int state, int dir) { static int len,ind; static DirCache *c; static char *find; static string base; string res; char *cmd; char *s; if (!state) { ind = 0; s = strrchr(text,'/'); if (s) { Strmcpy(base, text, s - text + 1, sizeof(base)); find = s+1; c = GetCompleteFiles(base); } else { c = GetCompleteFiles("."); find = text; base[0] = '\0'; } if (!c) { return 0; } len = strlen(find); } while (ind < c->files.count) { cmd = c->files.names[ind++]; if (!strncmp(cmd,find,len)) { if (dir && (c->flags & (LS_L|LS_F)) && !(cmd[strlen(cmd)-1] == '/' || cmd[strlen(cmd)-1] == '@')) continue; STRNCPY(res, base); STRNCAT(res, cmd); return (StrDup(res)); } } return 0; } /* Generate file names for the readline completion generator */ static char * CompleteFileGenerator(char *text,int state) { return CompleteDirFileGenerator(text,state,0); } /* Generate directories for the readline completion generator */ static char * CompleteDirGenerator(char *text,int state) { return CompleteDirFileGenerator(text,state,1); } /* Generate commands for the readline completion routines */ static char * CompleteCommandGenerator(char *text,int state) { static int len,ind; char *cmd; if (!state) { len = strlen(text); ind = 0; } while (ind < gNumCommands) { cmd = gCommands[ind].name; ind ++; if (!strncmp(cmd,text,len)) return StrDup(cmd); } return 0; } /* Generate options for the readline completion routines */ static char * CompleteOptionGenerator(char *text,int state) { static int len,ind; char *cmd; if (!state) { len = strlen(text); ind = 0; } while (ind < gNumEditablePrefOpts) { cmd = gPrefOpts[ind].name; ind ++; if (!strncmp(cmd,text,len)) return StrDup(cmd); } return 0; } /* Generate options for the readline completion routines */ static char * CompleteHostGenerator(char *text,int state) { static int len; static BookmarkPtr curr; char *cmd; if (!state) { len = strlen(text); curr = gFirstRsi; } while (curr) { cmd = curr->bookmarkName; curr = curr->next; if (!strncmp(cmd,text,len)) return StrDup(cmd); } return 0; } static char * CompleteNoneGenerator(void) { return 0; } static CompleteFunc FindCompleteFunc(char *line, int start) { int len= 0; string cmd; Command *c; if (start == 0) return CompleteCommandGenerator; while (line[len] && isspace(line[len])) len++; while (line[len] && !isspace(line[len])) len++; Strmcpy(cmd, line, len, sizeof(cmd)); c = GetCommand(cmd, 0); if (!c) return CompleteFileGenerator; switch (c->complete) { case kCompleteDir: if (gRmtInfo.isUnix) return CompleteDirGenerator; /* Fall through */ case kCompleteFile: return CompleteFileGenerator; case kCompleteCmd: return CompleteCommandGenerator; case kCompleteOption: return CompleteOptionGenerator; case kCompleteHost: return CompleteHostGenerator; case kCompleteLocal: #ifdef HAVE_FILENAME_COMPLETION_FUNCTION return filename_completion_function; #endif default: return (CompleteFunc) CompleteNoneGenerator; } } static void ForgetCurrent(void) { DirCache *c; if (!currentCache) return; c = cacheHead; if (c != currentCache) return; /* Something really weird is happening */ EmptyFileList(&c->files); cacheHead = c->next; free(c); currentCache = 0; } /* Look through ls flags to determine if this is ls -l/ls -F, etc. */ void CompleteSetFlags(char *s) { if (!currentCache) return; if (*s == '-') { if (strchr(s,'l')) currentCache->flags |= LS_L; if (strchr(s,'F')) currentCache->flags |= LS_F; if (strchr(s,'R')) currentCache->flags |= LS_R; } else if (*s) { if (strchr(s,'*')) ForgetCurrent(); if (strchr(s,'[')) ForgetCurrent(); if (currentCache->flags & LS_DIR) ForgetCurrent(); if (currentCache) { free(currentCache->name); currentCache->name = ConvertDir(s); currentCache->flags |= LS_DIR; } } } /* Parse the output of ls for filenames */ void CompleteParse(char *s) { char *t; int len=0; string ss; string tmp; if (!*s) return; if (!currentCache) return; if (currentCache->flags & LS_R) { while (s[len] && s[len] != ':') len++; if (s[len] == ':') { int tmp_type = currentCache->flags; /* Save the type which is clobbered by CompleteStart */ Strmcpy(ss, s, len, sizeof(ss)); CompleteFinish(); CompleteStart(ss); currentCache->flags = tmp_type; return; } } if (currentCache->flags & LS_L) { /* Only use the last word on the line */ /* If it's a "total " line, ignore it. */ if (!strncmp(s,"total ",6)) return; t = s+strlen(s) - 1; while (!isspace(*t) && t>s) t--; if (isspace(*t)) t++; if (!(currentCache->flags & LS_F) && s[0] == 'd') { /* We have a dir with no -F flag - * get type from first character on line */ STRNCPY(tmp, t); STRNCAT(tmp, "/"); t = tmp; } else if (s[0] == 'l') { /* It's a soft link - get the name of the link, not where it goes */ char *end = t-1; /* Line should be lrwxrwxrwx .... file -> dest */ while (!(*end == '-' && end[1] == '>') && end > s) end--; /* Sometimes the "-> dest" is missing */ if (end > s) { /* end points to the space before "->" */ end --; t = end - 1; /* Find the start of the word */ while (!isspace(*t) && t>s) t--; if (isspace(*t)) t++; Strmcpy(tmp, t, end-t+1, sizeof(tmp)); strcpy(tmp + (end-t), "@"); t = tmp; } } } else { /* Use every word on the line */ t = s; } while ((*t != '\0') && (isspace(*t))) t++; while (*t != '\0') { len = 0; while (t[len] && !isspace(t[len])) len++; /* Ignore last char if we gave -F to ls and it is one of @=* */ /* Note: We don't remove the '/' off directories */ if ((currentCache->flags & LS_F) && strchr("=*",t[len-1])) FileListAdd(¤tCache->files,StrnDup(t,len-1)); else FileListAdd(¤tCache->files,StrnDup(t,len)); t+=len; while ((*t != '\0') && (isspace(*t))) t++; } } /* How much of these 2 strings match? */ static int MatchingLen(char *a, char *b) { int i; for (i=0;a[i] && b[i];i++) if (a[i] != b[i]) break; return i; } /* Get the completion characters for the word in line that ends at * position off */ char * CompleteGet(char *line, int off) { int i; int wstart; int matchlen; int cplen; int alen; char *cp; string res; CompleteFunc f; string match; FileList files; if (!line) return 0; /* Find the start of the word */ for (wstart = off - 1; wstart >= 0 ; wstart --) { if (strchr(" \t\n",line[wstart])) break; } wstart++; Strmcpy(match, line+wstart, off-wstart, sizeof(match)); f = FindCompleteFunc(line,wstart); CompleteMatches(&files, match, f); /* No matching files - give up */ if (!files.count) return 0; /* If there was only one match, we complete the word and add a space * as well (but only if it doesn't end in '/' */ if (files.count == 1) { matchlen = (int) strlen(files.names[0]); cp = files.names[0]+off-wstart; cplen = matchlen - (off-wstart) + 1; Strmcpy(res, cp, cplen, sizeof(res)); alen = matchlen - 1; if (files.names[0][alen] == '@') { res[strlen(res)-1] = 0; } else if (files.names[0][alen] != '/') { STRNCAT(res, " "); } EmptyFileList(&files); cp = StrDup(res); return (cp); } /* Otherwise, find the longest common prefix of all words that match */ matchlen = strlen(files.names[0]); for (i=1;i off-wstart) { cp = StrnDup(files.names[0]+off-wstart, matchlen - (off-wstart)); } else { cp = 0; } EmptyFileList(&files); return (cp); } /* Find the start of the last pathname component */ static char * FindStart(char *s) { char *tmp; for (tmp = s;*tmp;tmp++) if (*tmp == '/' && tmp[1]) s = tmp+1; return s; } /* Print out what options we have for completing this word */ void CompleteOptions(char *line, int off) { int wstart; int maxlen,len,i,j; CompleteFunc f; string match; FileList files; int lines, columns; if (!line) return; /* Find start of word */ for (wstart = off - 1; wstart >= 0 ; wstart --) { if (strchr(" \t\n",line[wstart])) break; } wstart++; Strmcpy(match, line+wstart, off-wstart, sizeof(match)); f = FindCompleteFunc(line,wstart); CompleteMatches(&files, match, f); if (!files.count) return; /* Find the maximum length filename that matches (for nice outputting) */ maxlen = strlen(FindStart(files.names[0])); for (i=1;imaxlen) maxlen = len; } /* Calculate how many lines to display on: we want to display like ls * does: 1 3 5 * 2 4 6 * Use (gScreenWidth+1)/(maxlen+2) since we never print the last pair * of spaces on a line. */ columns = (gScreenWidth+1) / (maxlen+2); if (columns < 1) columns = 1; lines = (files.count + columns -1) / columns; /* A blank line so we can see where things start */ PrintF("\n"); for (i = 0; i= files.count) continue; start = FindStart(files.names[off2]); PrintF("%-*.*s",maxlen,maxlen,start); if (j < columns-1) PrintF(" "); } PrintF("\n"); } /* Ensure that we can see the things that have just arrived */ UpdateScreen(1); EmptyFileList(&files); } #ifndef HAVE_LIBREADLINE void InitReadline(void) { } #else /* Completion function for readline */ static char ** ncftp_completion(char *text, int start, int end) { CompleteFunc f; if (end < start) return NULL; f = FindCompleteFunc(text, start); return completion_matches(text,f); } void InitReadline(void) { rl_readline_name = "ncftp"; rl_attempted_completion_function = ncftp_completion; } #endif