+
+/* Display entries in 'list' after skipping the first 'len' chars */
+static void
+complete_display(char **list, u_int len)
+{
+ u_int y, m = 0, width = 80, columns = 1, colspace = 0, llen;
+ struct winsize ws;
+ char *tmp;
+
+ /* Count entries for sort and find longest */
+ for (y = 0; list[y]; y++)
+ m = MAX(m, strlen(list[y]));
+
+ if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) != -1)
+ width = ws.ws_col;
+
+ m = m > len ? m - len : 0;
+ 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++) {
+ llen = strlen(list[y]);
+ tmp = llen > len ? list[y] + len : "";
+ printf("%-*s", colspace, tmp);
+ if (m >= columns) {
+ printf("\n");
+ m = 1;
+ } else
+ m++;
+ }
+ printf("\n");
+}
+
+/*
+ * Given a "list" of words that begin with a common prefix of "word",
+ * attempt to find an autocompletion to extends "word" by the next
+ * characters common to all entries in "list".
+ */
+static 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]);
+
+ /* Find length of common stem */
+ for (y = 1; list[y]; y++) {
+ u_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] = '\0';
+ return tmp;
+ }
+ }
+
+ return xstrdup(word);
+}
+
+/* Autocomplete a sftp command */
+static int
+complete_cmd_parse(EditLine *el, char *cmd, int lastarg, char quote,
+ int terminated)
+{
+ u_int y, count = 0, cmdlen, tmplen;
+ char *tmp, **list, argterm[3];
+ const LineInfo *lf;
+
+ list = xcalloc((sizeof(cmds) / sizeof(*cmds)) + 1, sizeof(char *));
+
+ /* No command specified: display all available commands */
+ if (cmd == NULL) {
+ for (y = 0; cmds[y].c; y++)
+ list[count++] = xstrdup(cmds[y].c);
+
+ list[count] = NULL;
+ complete_display(list, 0);
+
+ for (y = 0; list[y] != NULL; y++)
+ xfree(list[y]);
+ xfree(list);
+ return count;
+ }
+
+ /* Prepare subset of commands that start with "cmd" */
+ 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)
+ return 0;
+
+ /* Complete ambigious command */
+ tmp = complete_ambiguous(cmd, list, count);
+ if (count > 1)
+ complete_display(list, 0);
+
+ for (y = 0; list[y]; y++)
+ xfree(list[y]);
+ xfree(list);
+
+ if (tmp != NULL) {
+ tmplen = strlen(tmp);
+ cmdlen = strlen(cmd);
+ /* If cmd may be extended then do so */
+ if (tmplen > cmdlen)
+ if (el_insertstr(el, tmp + cmdlen) == -1)
+ fatal("el_insertstr failed.");
+ lf = el_line(el);
+ /* Terminate argument cleanly */
+ if (count == 1) {
+ y = 0;
+ if (!terminated)
+ argterm[y++] = quote;
+ if (lastarg || *(lf->cursor) != ' ')
+ argterm[y++] = ' ';
+ argterm[y] = '\0';
+ if (y > 0 && el_insertstr(el, argterm) == -1)
+ fatal("el_insertstr failed.");
+ }
+ xfree(tmp);
+ }
+
+ return count;
+}
+
+/*
+ * Determine whether a particular sftp command's arguments (if any)
+ * represent local or remote files.
+ */
+static int
+complete_is_remote(char *cmd) {
+ int i;
+
+ if (cmd == NULL)
+ return -1;
+
+ for (i = 0; cmds[i].c; i++) {
+ if (!strncasecmp(cmd, cmds[i].c, strlen(cmds[i].c)))
+ return cmds[i].t;
+ }
+
+ return -1;
+}
+
+/* Autocomplete a filename "file" */
+static int
+complete_match(EditLine *el, struct sftp_conn *conn, char *remote_path,
+ char *file, int remote, int lastarg, char quote, int terminated)
+{
+ glob_t g;
+ char *tmp, *tmp2, ins[3];
+ u_int i, hadglob, pwdlen, len, tmplen, filelen;
+ const LineInfo *lf;
+
+ /* Glob from "file" location */
+ if (file == NULL)
+ tmp = xstrdup("*");
+ else
+ xasprintf(&tmp, "%s*", file);
+
+ memset(&g, 0, sizeof(g));
+ if (remote != LOCAL) {
+ tmp = make_absolute(tmp, remote_path);
+ remote_glob(conn, tmp, GLOB_DOOFFS|GLOB_MARK, NULL, &g);
+ } else
+ glob(tmp, GLOB_DOOFFS|GLOB_MARK, NULL, &g);
+
+ /* Determine length of pwd so we can trim completion display */
+ for (hadglob = tmplen = pwdlen = 0; tmp[tmplen] != 0; tmplen++) {
+ /* Terminate counting on first unescaped glob metacharacter */
+ if (tmp[tmplen] == '*' || tmp[tmplen] == '?') {
+ if (tmp[tmplen] != '*' || tmp[tmplen + 1] != '\0')
+ hadglob = 1;
+ break;
+ }
+ if (tmp[tmplen] == '\\' && tmp[tmplen + 1] != '\0')
+ tmplen++;
+ if (tmp[tmplen] == '/')
+ pwdlen = tmplen + 1; /* track last seen '/' */
+ }
+ xfree(tmp);
+
+ if (g.gl_matchc == 0)
+ goto out;
+
+ if (g.gl_matchc > 1)
+ complete_display(g.gl_pathv, pwdlen);
+
+ tmp = NULL;
+ /* Don't try to extend globs */
+ if (file == NULL || hadglob)
+ goto out;
+
+ tmp2 = complete_ambiguous(file, g.gl_pathv, g.gl_matchc);
+ tmp = path_strip(tmp2, remote_path);
+ xfree(tmp2);
+
+ if (tmp == NULL)
+ goto out;
+
+ tmplen = strlen(tmp);
+ filelen = strlen(file);
+
+ if (tmplen > filelen) {
+ tmp2 = tmp + filelen;
+ len = strlen(tmp2);
+ /* quote argument on way out */
+ for (i = 0; i < len; i++) {
+ ins[0] = '\\';
+ ins[1] = tmp2[i];
+ ins[2] = '\0';
+ switch (tmp2[i]) {
+ case '\'':
+ case '"':
+ case '\\':
+ case '\t':
+ case ' ':
+ if (quote == '\0' || tmp2[i] == quote) {
+ if (el_insertstr(el, ins) == -1)
+ fatal("el_insertstr "
+ "failed.");
+ break;
+ }
+ /* FALLTHROUGH */
+ default:
+ if (el_insertstr(el, ins + 1) == -1)
+ fatal("el_insertstr failed.");
+ break;
+ }
+ }
+ }
+
+ lf = el_line(el);
+ if (g.gl_matchc == 1) {
+ i = 0;
+ if (!terminated)
+ ins[i++] = quote;
+ if (*(lf->cursor - 1) != '/' &&
+ (lastarg || *(lf->cursor) != ' '))
+ ins[i++] = ' ';
+ ins[i] = '\0';
+ if (i > 0 && el_insertstr(el, ins) == -1)
+ fatal("el_insertstr failed.");
+ }
+ xfree(tmp);
+
+ out:
+ globfree(&g);
+ return g.gl_matchc;
+}
+
+/* tab-completion hook function, called via libedit */
+static unsigned char
+complete(EditLine *el, int ch)
+{
+ char **argv, *line, quote;
+ u_int argc, carg, cursor, len, terminated, ret = CC_ERROR;
+ const LineInfo *lf;
+ struct complete_ctx *complete_ctx;
+
+ lf = el_line(el);
+ if (el_get(el, EL_CLIENTDATA, (void**)&complete_ctx) != 0)
+ fatal("%s: el_get failed", __func__);
+
+ /* Figure out which argument the cursor points to */
+ cursor = lf->cursor - lf->buffer;
+ line = (char *)xmalloc(cursor + 1);
+ memcpy(line, lf->buffer, cursor);
+ line[cursor] = '\0';
+ argv = makeargv(line, &carg, 1, "e, &terminated);
+ xfree(line);
+
+ /* Get all the arguments on the line */
+ len = lf->lastchar - lf->buffer;
+ line = (char *)xmalloc(len + 1);
+ memcpy(line, lf->buffer, len);
+ line[len] = '\0';
+ argv = makeargv(line, &argc, 1, NULL, NULL);
+
+ /* Ensure cursor is at EOL or a argument boundary */
+ if (line[cursor] != ' ' && line[cursor] != '\0' &&
+ line[cursor] != '\n') {
+ xfree(line);
+ return ret;
+ }
+
+ if (carg == 0) {
+ /* Show all available commands */
+ complete_cmd_parse(el, NULL, argc == carg, '\0', 1);
+ ret = CC_REDISPLAY;
+ } else if (carg == 1 && cursor > 0 && line[cursor - 1] != ' ') {
+ /* Handle the command parsing */
+ if (complete_cmd_parse(el, argv[0], argc == carg,
+ quote, terminated) != 0)
+ ret = CC_REDISPLAY;
+ } else if (carg >= 1) {
+ /* Handle file parsing */
+ int remote = complete_is_remote(argv[0]);
+ char *filematch = NULL;
+
+ if (carg > 1 && line[cursor-1] != ' ')
+ filematch = argv[carg - 1];
+
+ if (remote != 0 &&
+ complete_match(el, complete_ctx->conn,
+ *complete_ctx->remote_pathp, filematch,
+ remote, carg == argc, quote, terminated) != 0)
+ ret = CC_REDISPLAY;
+ }
+
+ xfree(line);
+ return ret;
+}
+#endif /* USE_LIBEDIT */