Ben Lindstrom
2006-Apr-01 06:32 UTC
sftp tab completion patch (First release - NOT FOR INCLUDING YET)
This applies to the OpenBSD --current tree. Don't see why it shouldn't work under portable. Within the patch are the updates I need to make before I'll actually submit it for real, but I figured I'd make a public drop. Since I'm not on this list anymore keep me in the CC: if you want me to respond. I'll continue to work as time permits, but it would be nice if people who want this in step up and give feedback. - Ben Index: sftp.c ==================================================================RCS file: /cvs/src/usr.bin/ssh/sftp.c,v retrieving revision 1.80 diff -u -r1.80 sftp.c --- sftp.c 27 Mar 2006 23:15:46 -0000 1.80 +++ sftp.c 31 Mar 2006 23:31:13 -0000 @@ -64,6 +64,9 @@ int remote_glob(struct sftp_conn *, const char *, int, int (*)(const char *, int), glob_t *); /* proto for sftp-glob.c */ +/* sftp connection structure */ +struct sftp_conn *conn; + /* Separators for interactive commands */ #define WHITESPACE " \t\r\n" @@ -108,42 +111,49 @@ struct CMD { const char *c; const int n; + const int t; }; +/* Type of completion */ +#define NOARGS 0 +#define REMOTE 1 +#define LOCAL 2 + + static const struct CMD cmds[] = { - { "bye", I_QUIT }, - { "cd", I_CHDIR }, - { "chdir", I_CHDIR }, - { "chgrp", I_CHGRP }, - { "chmod", I_CHMOD }, - { "chown", I_CHOWN }, - { "dir", I_LS }, - { "exit", I_QUIT }, - { "get", I_GET }, - { "mget", I_GET }, - { "help", I_HELP }, - { "lcd", I_LCHDIR }, - { "lchdir", I_LCHDIR }, - { "lls", I_LLS }, - { "lmkdir", I_LMKDIR }, - { "ln", I_SYMLINK }, - { "lpwd", I_LPWD }, - { "ls", I_LS }, - { "lumask", I_LUMASK }, - { "mkdir", I_MKDIR }, - { "progress", I_PROGRESS }, - { "put", I_PUT }, - { "mput", I_PUT }, - { "pwd", I_PWD }, - { "quit", I_QUIT }, - { "rename", I_RENAME }, - { "rm", I_RM }, - { "rmdir", I_RMDIR }, - { "symlink", I_SYMLINK }, - { "version", I_VERSION }, - { "!", I_SHELL }, - { "?", I_HELP }, - { NULL, -1} + { "bye", I_QUIT, NOARGS }, + { "cd", I_CHDIR, REMOTE }, + { "chdir", I_CHDIR, REMOTE }, + { "chgrp", I_CHGRP, REMOTE }, + { "chmod", I_CHMOD, REMOTE }, + { "chown", I_CHOWN, REMOTE }, + { "dir", I_LS, REMOTE }, + { "exit", I_QUIT, NOARGS }, + { "get", I_GET, REMOTE }, + { "mget", I_GET, REMOTE }, + { "help", I_HELP, NOARGS }, + { "lcd", I_LCHDIR, LOCAL }, + { "lchdir", I_LCHDIR, LOCAL }, + { "lls", I_LLS, LOCAL }, + { "lmkdir", I_LMKDIR, LOCAL }, + { "ln", I_SYMLINK, REMOTE }, + { "lpwd", I_LPWD, LOCAL }, + { "ls", I_LS, REMOTE }, + { "lumask", I_LUMASK, NOARGS }, + { "mkdir", I_MKDIR, REMOTE }, + { "progress", I_PROGRESS, NOARGS }, + { "put", I_PUT, LOCAL }, + { "mput", I_PUT, LOCAL }, + { "pwd", I_PWD, REMOTE }, + { "quit", I_QUIT, NOARGS }, + { "rename", I_RENAME, REMOTE }, + { "rm", I_RM, REMOTE }, + { "rmdir", I_RMDIR, REMOTE }, + { "symlink", I_SYMLINK, REMOTE }, + { "version", I_VERSION, NOARGS }, + { "!", I_SHELL, NOARGS }, + { "?", I_HELP, NOARGS }, + { NULL, -1, -1} }; int interactive_loop(int fd_in, int fd_out, char *file1, char *file2); @@ -1231,13 +1241,194 @@ return ("sftp> "); } +/* + * Before Asking to be included: + * XXX Support Quoting in file completion + * XXX Fix MAXARG to be more dynamic + * XXX Better column display of multi-matches + * XXX Kill the need for MAXLIST limit [if I don't will be yelled at?] + * XXX Side by side comparision of ftp and tweak the code to better mimicing + * XXX Ask for $50 per user of this patch so I can give up my day job + * XXX Ask for Nate's (from undeadly.org fourm) head for making me "rue the day" + * XXX Wait for Theo to rip me a new one due to the code below + * XXX Sit on the patch for another year and rewrite using libunedit 2.0 + */ +char * +complete_ambiguous(char *word, char **list, size_t count) +{ + if (count > 1) { + char *match = list[0]; + size_t y, matchlen = strlen(match); + + printf("\n"); + printf("%s\t", list[0]); + for (y = 1; list[y]; y++) { + int x; + + printf("%s\t", list[y]); + for (x = 0; x < matchlen; x++) + if (match[x] != list[y][x]) + break; + + matchlen = x; + } + printf("\n"); + if (matchlen > strlen(word)) { + char *tmp = xstrdup(list[0]); + + tmp[matchlen] = NULL; + return (tmp); + } + } else + return (xstrdup(list[0])); + + return (NULL); +} + + +int +complete_cmd_parse(EditLine *el, char *cmd) +{ + int y, count = 0; + #define MAXLIST 20 + char *list[MAXLIST], *tmp; + int cmdlen = strlen(cmd); + + for (y = 0; cmds[y].c; y++) { + if (!strncasecmp(cmd, cmds[y].c, cmdlen)) { + list[count++] = xstrdup(cmds[y].c); + if (count >= MAXLIST) + fatal("Exceeded MAXLIST."); + } + list[count] = NULL; + } + + if (count > 0) { + tmp = complete_ambiguous(cmd, list, count); + for (y = 1; list[y]; y++) + xfree(list[y]); + + if (tmp != NULL && strlen(tmp) > strlen(cmd)) { + if (el_insertstr(el, tmp + strlen(cmd)) == -1) + fatal("el_insertstr failed."); + xfree(tmp); + } + } + + return (count); +} + +int +complete_is_remote(char *cmd) { + int i; + + for (i = 0; cmds[i].c; i++) { + size_t cmdlen = strlen(cmds[i].c); + + if (!strncasecmp(cmd, cmds[i].c, cmdlen)) + return cmds[i].t; + } + + return (-1); +} + +int +complete_match(EditLine *el, char *file, int remote) +{ + glob_t g; + size_t len = strlen(file) + 2; + char *tmp = xmalloc(len); + + memset(&g, 0, sizeof(g)); + snprintf(tmp, len, "%s*", file); + if (remote == LOCAL) + glob(tmp, GLOB_DOOFFS, NULL, &g); + else + remote_glob(conn, tmp, 0, NULL, &g); + + xfree(tmp); + + if (g.gl_matchc == 0) + return (0); + + tmp = complete_ambiguous(file, g.gl_pathv, g.gl_matchc); + globfree(&g); + if (tmp != NULL && strlen(tmp) > strlen(file)) { + if (el_insertstr(el, tmp + strlen(file)) == -1) + fatal("el_insertstr failed."); + xfree(tmp); + } + return (g.gl_matchc); +} + +unsigned char +complete(EditLine *el, int ch) +{ + const LineInfo *lf; + size_t len, pos; + char *line; + int argc = 0, ap_loc = 0, ap_cur = 0; + #define MAXARG 99 + char **ap, *argv[MAXARG]; + + ch = ch; /* not used */ + lf = el_line(el); + len = lf->lastchar - lf->buffer + 1; + line = (char *)malloc(len); + strlcpy(line, lf->buffer, len); + pos = len - (lf->lastchar - lf->cursor); + + /* build an array of items */ + for (ap = argv; ap < &argv[MAXARG - 1] && + (*ap = strsep(&line, " ")) != NULL; ) { + if (**ap != '\0') { + int ap_new; + + ap_new = ap_loc + strlen(*ap); + if (ap_new >= pos && ap_loc <= pos) + ap_cur = argc; + ap_loc = ap_new; + ap++; + argc++; + } + } + *ap = NULL; + + if (argc == 0) + return(CC_ERROR); + + if (pos == len) + ap_cur = argc - 1; + + /* Complete Stuff */ + if (ap_cur == 0) { /* Command Match */ + if (complete_cmd_parse(el, argv[0]) == 0) + return (CC_ERROR); + + return (CC_REDISPLAY); + } else { /* File Matching */ + int remote = complete_is_remote(argv[0]); + + if (remote != 0) { + if (complete_match(el, argv[ap_cur], remote) == 0) + return (CC_ERROR); + + return (CC_REDISPLAY); + } + + return (CC_ERROR); + } + + /* Clean up */ + xfree(line); +} + int interactive_loop(int fd_in, int fd_out, char *file1, char *file2) { char *pwd; char *dir = NULL; char cmd[2048]; - struct sftp_conn *conn; int err, interactive; EditLine *el = NULL; History *hl = NULL; @@ -1257,6 +1448,11 @@ el_set(el, EL_TERMINAL, NULL); el_set(el, EL_SIGNAL, 1); el_source(el, NULL); + + /* Tab Completion */ + el_set(el, EL_ADDFN, "ftp-complete", + "Context senstive argument completion", complete); + el_set(el, EL_BIND, "^I", "ftp-complete", NULL); } conn = do_init(fd_in, fd_out, copy_buffer_len, num_requests);