I've finally took the time to figure the last few bugs (that I know of). This patch will be submit to be included in a few weeks. This patch should be generic enough for portable without too much hassle. This patch mimics OpenBSD's ftp behavior. I'm not sure like that (e.g. it doesn't put / at the end of directories by default), but that is more a question for the community at large. Yes I'm back on the list. =) - Ben Index: sftp.c ==================================================================RCS file: /cvs/src/usr.bin/ssh/sftp.c,v retrieving revision 1.97 diff -u -r1.97 sftp.c --- sftp.c 24 Oct 2007 03:30:02 -0000 1.97 +++ sftp.c 12 Dec 2007 03:19:32 -0000 @@ -71,6 +71,12 @@ 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; + +/* sftp remote path */ +char *remote_path; + /* Separators for interactive commands */ #define WHITESPACE " \t\r\n" @@ -115,42 +121,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); @@ -1344,13 +1357,237 @@ return ("sftp> "); } +void +complete_display(char **list, u_int len) +{ + u_int y, m = 0, width = 80, columns = 1, colspace = 0; + struct winsize ws; + + /* Count entries for sort and find longest filename */ + for (y = 0; list[y]; y++) + m = MAX(m, strlen(list[y])); + + if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) != -1) + width = ws.ws_col; + + m -= len; + columns = width / (m + 2); + columns = MAX(columns, 1); + colspace = width / columns; + colspace = MIN(colspace, width); + + printf("\n"); + m = 1; + for (y = 0; list[y]; y++) { + char *tmp = list[y]; + + tmp += len; + printf("%-*s", colspace, tmp); + if (m >= columns) { + printf("\n"); + m = 1; + } else + m++; + } + printf("\n"); +} + +char * +complete_ambiguous(const char *word, char **list, size_t count) +{ + if (word == NULL) + return (NULL); + + if (count > 0) { + u_int y, matchlen = strlen(list[0]); + + for (y = 1; list[y]; y++) { + int x; + + for (x = 0; x < matchlen; x++) + if (list[0][x] != list[y][x]) + break; + + matchlen = x; + } + + if (matchlen > strlen(word)) { + char *tmp = xstrdup(list[0]); + + tmp[matchlen] = NULL; + return (tmp); + } + } + + return (xstrdup(word)); +} + + +int +complete_cmd_parse(EditLine *el, char *cmd) +{ + u_int y, count = 0, cmdlen; + char **list; + + if (cmd == NULL) + return (0); + + list = xcalloc((sizeof(cmds) / sizeof(*cmds)), sizeof(char *)); + cmdlen = strlen(cmd); + for (y = 0; cmds[y].c; y++) { + if (!strncasecmp(cmd, cmds[y].c, cmdlen)) + list[count++] = xstrdup(cmds[y].c); + + list[count] = NULL; + } + + if (count > 0) { + char *tmp = complete_ambiguous(cmd, list, count); + + if (count > 1) + complete_display(list, 0); + + for (y = 1; list[y]; y++) + xfree(list[y]); + xfree(list); + + if (tmp != NULL) { + if (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; + + if (cmd == NULL) + return (-1); + + 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; + char *tmp, *tmp2, *pwd; + u_int len; + + if (file == NULL) + return (0); + + len = strlen(file) + 2; /* NULL + Wildcard */ + tmp = xmalloc(len); + snprintf(tmp, len, "%s*", file); + + memset(&g, 0, sizeof(g)); + if (remote != LOCAL) { + tmp = make_absolute(tmp, remote_path); + remote_glob(conn, tmp, 0, NULL, &g); + } else + glob(tmp, GLOB_DOOFFS, NULL, &g); + + xfree(tmp); + + if (g.gl_matchc == 0) + return (0); + + tmp2 = complete_ambiguous(file, g.gl_pathv, g.gl_matchc); + tmp = path_strip(tmp2, remote_path); + xfree(tmp2); + + if (g.gl_matchc > 1) { + char *pwd = strrchr(g.gl_pathv[0], '/'); + u_int len = 0; + + if (pwd != NULL) + len = strlen(g.gl_pathv[0]) - strlen(pwd) + 1; + + complete_display(g.gl_pathv, len); + } + + globfree(&g); + if (tmp != NULL) { + if (strlen(tmp) > strlen(file)) { + char *ap, *tmp2 = tmp + strlen(file); + u_int len = strlen(tmp2); + + while ((ap = strsep(&tmp2, " ")) != NULL) { + if (strlen(ap) > 0) { + if (el_insertstr(el, ap) == -1) + fatal("el_insertstr failed."); + len -= strlen(ap); + } + if (len > 0) { + len--; + if (el_insertstr(el, "\\ ") == -1) + fatal("el_insertstr failed."); + } + } + } + + xfree(tmp); + } + + return (g.gl_matchc); +} + +unsigned char +complete(EditLine *el, int ch) +{ + char **argv, *line; + u_int x, argc, carg, len, ret = CC_ERROR; + const LineInfo *lf; + + lf = el_line(el); + + /* Figure out which argument we are on */ + len = lf->cursor - lf->buffer + 1; + line = (char *)xmalloc(len); + strlcpy(line, lf->buffer, len); + argv = makeargv(line, &carg); + xfree(line); + + /* now get the real argument */ + len = lf->lastchar - lf->buffer + 1; + line = (char *)xmalloc(len); + strlcpy(line, lf->buffer, len); + argv = makeargv(line, &argc); + xfree(line); + + if (carg == 1) { /* Handle the command parsing */ + if (complete_cmd_parse(el, argv[0]) != 0) + ret = CC_REDISPLAY; + } else if (carg > 1) { /* Handle file parsing */ + int remote = complete_is_remote(argv[0]); + + if (remote != 0 && complete_match(el, argv[carg - 1], + remote) != 0) + ret = CC_REDISPLAY; + } + + return (ret); +} + 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; @@ -1370,26 +1607,31 @@ 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); if (conn == NULL) fatal("Couldn't initialise connection to server"); - pwd = do_realpath(conn, "."); - if (pwd == NULL) + remote_path = do_realpath(conn, "."); + if (remote_path == NULL) fatal("Need cwd"); if (file1 != NULL) { dir = xstrdup(file1); - dir = make_absolute(dir, pwd); + dir = make_absolute(dir, remote_path); if (remote_is_dir(conn, dir) && file2 == NULL) { printf("Changing to: %s\n", dir); snprintf(cmd, sizeof cmd, "cd \"%s\"", dir); - if (parse_dispatch_command(conn, cmd, &pwd, 1) != 0) { + if (parse_dispatch_command(conn, cmd, &remote_path, 1) != 0) { xfree(dir); - xfree(pwd); + xfree(remote_path); xfree(conn); return (-1); } @@ -1400,9 +1642,9 @@ snprintf(cmd, sizeof cmd, "get %s %s", dir, file2); - err = parse_dispatch_command(conn, cmd, &pwd, 1); + err = parse_dispatch_command(conn, cmd, &remote_path, 1); xfree(dir); - xfree(pwd); + xfree(remote_path); xfree(conn); return (err); } @@ -1455,11 +1697,11 @@ interrupted = 0; signal(SIGINT, cmd_interrupt); - err = parse_dispatch_command(conn, cmd, &pwd, batchmode); + err = parse_dispatch_command(conn, cmd, &remote_path, batchmode); if (err != 0) break; } - xfree(pwd); + xfree(remote_path); xfree(conn); if (el != NULL)