Revisiting sftp tab completion patch

Ben Lindstrom mouring at eviladmin.org
Wed Dec 12 15:22:38 EST 2007


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)


More information about the openssh-unix-dev mailing list