Commit Diff


commit - f99c17f9596718e1ec4df58727f81bec72339110
commit + d3e1ab0c5f6f6f37e4897419ae6c3591762d2f59
blob - 27a8d5b6fe8fb7ec4a46f14d23ab8b288f230dc2
blob + ee3cc466548e596fe25baf99a0aed19fec566aa7
--- kamiftp/Makefile.am
+++ kamiftp/Makefile.am
@@ -1,6 +1,7 @@
 bin_PROGRAMS =	kamiftp
 
 kamiftp_SOURCES=ftp.c				\
+		rl.c				\
 		$(top_srcdir)/lib/9pclib.c	\
 		$(top_srcdir)/lib/9pclib.h	\
 		$(top_srcdir)/lib/kami.h	\
blob - 553ad8118c5fc9b3d53743ec7f13de1be0142495
blob + 733177d3c2233b99bef23a212c437ddaab83ba02
--- kamiftp/ftp.c
+++ kamiftp/ftp.c
@@ -42,15 +42,13 @@
 #include <readline/history.h>
 #endif
 
-#ifndef nitems
-#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
-#endif
-
 #include "kami.h"
 #include "utils.h"
 #include "log.h"
 #include "9pclib.h"
 
+#include "kamiftp.h"
+
 #define TMPFSTR		"/tmp/kamiftp.XXXXXXXXXX"
 #define TMPFSTRLEN	sizeof(TMPFSTR)
 
@@ -65,6 +63,7 @@ struct evbuffer		*buf;
 struct evbuffer		*dirbuf;
 uint32_t		 msize;
 int			 bell;
+time_t			 now;
 
 volatile sig_atomic_t	 resized;
 int			 tty_p;
@@ -79,34 +78,7 @@ struct progress {
 int pwdfid;
 
 #define ASSERT_EMPTYBUF() assert(EVBUFFER_LENGTH(buf) == 0)
-
-#if !HAVE_READLINE
-char *
-readline(const char *prompt)
-{
-	char *ch, *line = NULL;
-	size_t linesize = 0;
-	ssize_t linelen;
-
-	printf("%s", prompt);
-	fflush(stdout);
-
-	linelen = getline(&line, &linesize, stdin);
-	if (linelen == -1)
-		return NULL;
 
-	if ((ch = strchr(line, '\n')) != NULL)
-		*ch = '\0';
-	return line;
-}
-
-void
-add_history(const char *line)
-{
-	return;
-}
-#endif
-
 static char *
 read_line(const char *prompt)
 {
@@ -1034,7 +1006,114 @@ prepare_wstat(struct np_stat *st)
 	st->muid = NULL;
 }
 
-static void
+static int
+print_dirent(const struct np_stat *st)
+{
+	time_t	mtime;
+	struct tm *tm;
+	const char *timfmt;
+	char fmt[FMT_SCALED_STRSIZE], tim[13];
+
+	if (fmt_scaled(st->length, fmt) == -1)
+		strlcpy(fmt, "xxx", sizeof(fmt));
+
+	mtime = st->mtime;
+
+	if (now > mtime && (now - mtime) < 365/2 * 24 * 12 * 60)
+		timfmt = "%b %e %R";
+	else
+		timfmt = "%b %e  %Y";
+
+	if ((tm = localtime(&mtime)) == NULL ||
+	    strftime(tim, sizeof(tim), timfmt, tm) == 0)
+		strlcpy(tim, "unknown", sizeof(tim));
+
+	if (st->qid.type & QTDIR)
+		printf("d");
+	else
+		printf("-");
+	printf("%s", pp_perm(st->mode >> 6));
+	printf("%s", pp_perm(st->mode >> 3));
+	printf("%s", pp_perm(st->mode));
+	printf(" %8s %12s %s%s\n", fmt, tim, st->name,
+	    st->qid.type & QTDIR ? "/" : "");
+
+	return 0;
+}
+
+int
+dir_listing(const char *path, int (*fn)(const struct np_stat *),
+    int printerr)
+{
+	struct qid	 qid;
+	struct np_stat	 st;
+	uint64_t	 off = 0;
+	uint32_t	 len;
+	int		 nfid, miss, r;
+	char		*errstr;
+
+	now = time(NULL);
+	nfid = nextfid();
+
+	errstr = walk_path(pwdfid, nfid, path, &miss, &qid);
+	if (errstr != NULL) {
+		if (printerr)
+			printf("%s: %s\n", path, errstr);
+		free(errstr);
+		return -1;
+	}
+	if (miss) {
+		if (printerr)
+			printf("%s: No such file or directory\n", path);
+		return -1;
+	}
+	if (!(qid.type & QTDIR)) {
+		if (printerr)
+			printf("%s: not a directory\n", path);
+		do_clunk(nfid);
+		return -1;
+	}
+
+	do_open(nfid, KOREAD);
+	evbuffer_drain(dirbuf, EVBUFFER_LENGTH(dirbuf));
+
+	for (;;) {
+		tread(nfid, off, msize - IOHDRSZ);
+		do_send();
+		recv_msg();
+		expect2(Rread, iota_tag);
+
+		len = np_read32(buf);
+		if (len == 0)
+			break;
+
+		evbuffer_add_buffer(dirbuf, buf);
+		off += len;
+
+		ASSERT_EMPTYBUF();
+	}
+
+	while (EVBUFFER_LENGTH(dirbuf) != 0) {
+		if (np_read_stat(dirbuf, &st) == -1)
+			errx(1, "invalid stat struct read");
+
+		r = fn(&st);
+
+		free(st.name);
+		free(st.uid);
+		free(st.gid);
+		free(st.muid);
+
+		if (r == -1)
+			break;
+	}
+
+	evbuffer_drain(dirbuf, EVBUFFER_LENGTH(dirbuf));
+	do_clunk(nfid);
+	return 0;
+}
+
+void
 cmd_bell(int argc, const char **argv)
 {
 	if (argc == 0) {
@@ -1065,7 +1144,7 @@ usage:
 	printf("bell [on | off]\n");
 }
 
-static void
+void
 cmd_bye(int argc, const char **argv)
 {
 	log_warnx("bye\n");
@@ -1073,7 +1152,7 @@ cmd_bye(int argc, const char **argv)
 	exit(0);
 }
 
-static void
+void
 cmd_cd(int argc, const char **argv)
 {
 	struct qid qid;
@@ -1104,7 +1183,7 @@ cmd_cd(int argc, const char **argv)
 	pwdfid = nfid;
 }
 
-static void
+void
 cmd_edit(int argc, const char **argv)
 {
 	struct qid qid;
@@ -1170,7 +1249,7 @@ end:
 	unlink(sfn);
 }
 
-static void
+void
 cmd_get(int argc, const char **argv)
 {
 	struct qid qid;
@@ -1216,7 +1295,7 @@ cmd_get(int argc, const char **argv)
 	close(fd);
 }
 
-static void
+void
 cmd_hexdump(int argc, const char **argv)
 {
 	if (argc == 0) {
@@ -1247,7 +1326,7 @@ usage:
 	puts("usage: hexdump [on | off]");
 }
 
-static void
+void
 cmd_lcd(int argc, const char **argv)
 {
 	const char *dir;
@@ -1269,7 +1348,7 @@ cmd_lcd(int argc, const char **argv)
 		printf("cd: %s: %s\n", dir, strerror(errno));
 }
 
-static void
+void
 cmd_lpwd(int argc, const char **argv)
 {
 	char path[PATH_MAX];
@@ -1287,110 +1366,18 @@ cmd_lpwd(int argc, const char **argv)
 	printf("%s\n", path);
 }
 
-static void
+void
 cmd_ls(int argc, const char **argv)
 {
-	struct qid qid;
-	struct np_stat st;
-	time_t now, mtime;
-	struct tm *tm;
-	uint64_t off = 0;
-	uint32_t len;
-	int nfid, miss;
-	const char *timfmt;
-	char fmt[FMT_SCALED_STRSIZE], tim[13], *errstr;
-
 	if (argc > 1) {
 		puts("usage: ls [path]");
 		return;
 	}
 
-	now = time(NULL);
-
-	nfid = nextfid();
-	if (argc == 0) {
-		if ((errstr = dup_fid(pwdfid, nfid)) != NULL) {
-			printf(".: %s\n", errstr);
-			free(errstr);
-			return;
-		}
-	} else {
-		errstr = walk_path(pwdfid, nfid, argv[0], &miss, &qid);
-		if (errstr != NULL) {
-			printf("%s: %s\n", argv[0], errstr);
-			free(errstr);
-			return;
-		}
-		if (miss) {
-			printf("%s: No such file or directory\n",
-			    argv[0]);
-			return;
-		}
-		if (!(qid.type & QTDIR)) {
-			printf("%s: not a directory\n", argv[0]);
-			do_clunk(nfid);
-			return;
-		}
-	}
-
-	do_open(nfid, KOREAD);
-
-	evbuffer_drain(dirbuf, EVBUFFER_LENGTH(dirbuf));
-
-	for (;;) {
-		tread(nfid, off, msize - IOHDRSZ);
-		do_send();
-		recv_msg();
-		expect2(Rread, iota_tag);
-
-		len = np_read32(buf);
-		if (len == 0)
-			break;
-
-		evbuffer_add_buffer(dirbuf, buf);
-		off += len;
-
-		ASSERT_EMPTYBUF();
-	}
-
-	while (EVBUFFER_LENGTH(dirbuf) != 0) {
-		if (np_read_stat(dirbuf, &st) == -1)
-			errx(1, "invalid stat struct read");
-
-		if (fmt_scaled(st.length, fmt) == -1)
-			strlcpy(fmt, "xxx", sizeof(fmt));
-
-		mtime = st.mtime;
-
-		if (now > mtime && (now - mtime) < 365/2 * 24 * 12 * 60)
-			timfmt = "%b %e %R";
-		else
-			timfmt = "%b %e  %Y";
-
-		if ((tm = localtime(&mtime)) == NULL ||
-		    strftime(tim, sizeof(tim), timfmt, tm) == 0)
-			strlcpy(tim, "unknown", sizeof(tim));
-
-		if (st.qid.type & QTDIR)
-			printf("d");
-		else
-			printf("-");
-		printf("%s", pp_perm(st.mode >> 6));
-		printf("%s", pp_perm(st.mode >> 3));
-		printf("%s", pp_perm(st.mode));
-		printf(" %8s %12s %s%s\n", fmt, tim, st.name,
-		    st.qid.type & QTDIR ? "/" : "");
-
-		free(st.name);
-		free(st.uid);
-		free(st.gid);
-		free(st.muid);
-	}
-
-	do_clunk(nfid);
+	dir_listing(argc == 0 ? "." : argv[0], print_dirent, 1);
 }
 
-static void
+void
 cmd_page(int argc, const char **argv)
 {
 	struct qid qid;
@@ -1436,7 +1423,7 @@ cmd_page(int argc, const char **argv)
 	unlink(sfn);
 }
 
-static void
+void
 cmd_pipe(int argc, const char **argv)
 {
 	struct qid qid;
@@ -1487,7 +1474,7 @@ cmd_pipe(int argc, const char **argv)
 	waitpid(pid, &status, 0);
 }
 
-static void
+void
 cmd_put(int argc, const char **argv)
 {
 	struct qid qid;
@@ -1515,7 +1502,7 @@ cmd_put(int argc, const char **argv)
 	close(fd);
 }
 
-static void
+void
 cmd_rename(int argc, const char **argv)
 {
 	struct np_stat st;
@@ -1551,7 +1538,7 @@ cmd_rename(int argc, const char **argv)
 	do_clunk(nfid);
 }
 
-static void
+void
 cmd_rm(int argc, const char **argv)
 {
 	struct qid	 qid;
@@ -1584,7 +1571,7 @@ cmd_rm(int argc, const char **argv)
 	}
 }
 
-static void
+void
 cmd_verbose(int argc, const char **argv)
 {
 	if (argc == 0) {
@@ -1618,31 +1605,11 @@ usage:
 static void
 excmd(int argc, const char **argv)
 {
-	struct cmd {
-		const char	*name;
-		void		(*fn)(int, const char **);
-	} cmds[] = {
-		{"bell",	cmd_bell},
-		{"bye",		cmd_bye},
-		{"cd",		cmd_cd},
-		{"edit",	cmd_edit},
-		{"get",		cmd_get},
-		{"hexdump",	cmd_hexdump},
-		{"lcd",		cmd_lcd},
-		{"lpwd",	cmd_lpwd},
-		{"ls",		cmd_ls},
-		{"page",	cmd_page},
-		{"pipe",	cmd_pipe},
-		{"put",		cmd_put},
-		{"quit",	cmd_bye},
-		{"rename",	cmd_rename},
-		{"rm",		cmd_rm},
-		{"verbose",	cmd_verbose},
-	};
 	size_t i;
 
 	if (argc == 0)
 		return;
+
 	for (i = 0; i < nitems(cmds); ++i) {
 		if (!strcmp(cmds[i].name, *argv)) {
 			cmds[i].fn(argc-1, argv+1);
@@ -1875,6 +1842,7 @@ main(int argc, char **argv)
 	if (path)
 		cd_or_fetch(path, outfile);
 
+	compl_setup();
 	for (;;) {
 		int argc;
 		char *line, *argv[16] = {0}, **ap;
blob - /dev/null
blob + b249f79672dbcddf9b6e871ea9a0daab7a4513b6 (mode 644)
--- /dev/null
+++ kamiftp/kamiftp.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+enum ftp_cmd {
+	CMD_UNKNOWN,
+	CMD_BELL,
+	CMD_BYE,
+	CMD_CD,
+	CMD_EDIT,
+	CMD_GET,
+	CMD_HEXDUMP,
+	CMD_LCD,
+	CMD_LPWD,
+	CMD_LS,
+	CMD_PAGE,
+	CMD_PIPE,
+	CMD_PUT,
+	CMD_RENAME,
+	CMD_RM,
+	CMD_VERBOSE,
+};
+
+void	 compl_setup(void);
+
+#if !HAVE_READLINE
+char	*readline(const char *);
+void	 add_history(const char *);
+#endif
+
+int	 dir_listing(const char *, int (*)(const struct np_stat *), int);
+
+void	 cmd_bell(int, const char **);
+void	 cmd_bye(int, const char **);
+void	 cmd_cd(int, const char **);
+void	 cmd_edit(int, const char **);
+void	 cmd_get(int, const char **);
+void	 cmd_hexdump(int, const char **);
+void	 cmd_lcd(int, const char **);
+void	 cmd_lpwd(int, const char **);
+void	 cmd_ls(int, const char **);
+void	 cmd_page(int, const char **);
+void	 cmd_pipe(int, const char **);
+void	 cmd_put(int, const char **);
+void	 cmd_rename(int, const char **);
+void	 cmd_rm(int, const char **);
+void	 cmd_verbose(int, const char **);
+
+struct cmd {
+	const char	*name;
+	int		 cmdtype;
+	void		(*fn)(int, const char **);
+};
+
+static struct cmd cmds[] = {
+	{"bell",	CMD_BELL,	cmd_bell},
+	{"bye",		CMD_BYE,	cmd_bye},
+	{"cd",		CMD_CD,		cmd_cd},
+	{"edit",	CMD_EDIT,	cmd_edit},
+	{"get",		CMD_GET,	cmd_get},
+	{"hexdump",	CMD_HEXDUMP,	cmd_hexdump},
+	{"lcd",		CMD_LCD,	cmd_lcd},
+	{"lpwd",	CMD_LPWD,	cmd_lpwd},
+	{"ls",		CMD_LS,		cmd_ls},
+	{"page",	CMD_PAGE,	cmd_page},
+	{"pipe",	CMD_PIPE,	cmd_pipe},
+	{"put",		CMD_PUT,	cmd_put},
+	{"quit",	CMD_BYE,	cmd_bye},	/* alias */
+	{"rename",	CMD_RENAME,	cmd_rename},
+	{"rm",		CMD_RM,		cmd_rm},
+	{"verbose",	CMD_VERBOSE,	cmd_verbose},
+};
blob - /dev/null
blob + bb79125082d73d0e0582a582c068c5b6fd716a7e (mode 644)
--- /dev/null
+++ kamiftp/rl.c
@@ -0,0 +1,423 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "compat.h"
+
+#if !HAVE_READLINE
+
+#include <stdio.h>
+#include <string.h>
+
+char *
+readline(const char *prompt)
+{
+	char *ch, *line = NULL;
+	size_t linesize = 0;
+	ssize_t linelen;
+
+	printf("%s", prompt);
+	fflush(stdout);
+
+	linelen = getline(&line, &linesize, stdin);
+	if (linelen == -1)
+		return NULL;
+
+	if ((ch = strchr(line, '\n')) != NULL)
+		*ch = '\0';
+	return line;
+}
+
+void
+add_history(const char *line)
+{
+	return;
+}
+
+void
+compl_setup(void)
+{
+	return;
+}
+
+#else /* HAVE_READLINE */
+
+#include <ctype.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include "kami.h"
+#include "kamiftp.h"
+
+struct compl_state {
+	size_t		  size;
+	size_t		  len;
+	char		**entries;
+};
+
+static struct compl_state compl_state;
+static char compl_prfx[PATH_MAX];
+
+static void
+compl_state_reset(void)
+{
+	size_t i;
+
+	for (i = 0; i < compl_state.len; ++i)
+		free(compl_state.entries[i]);
+	free(compl_state.entries);
+
+	compl_state.len = 0;
+	compl_state.size = 16;
+	if ((compl_state.entries = calloc(16, sizeof(char *))) == NULL)
+		compl_state.size = 0;
+}
+
+static int
+compl_add_entry(const struct np_stat *st)
+{
+	const char	*sufx = "";
+	char		*dup;
+	int		 r;
+
+	if (compl_state.len == compl_state.size) {
+		size_t newsz = compl_state.size * 1.5;
+		void *t;
+
+		t = recallocarray(compl_state.entries, compl_state.size,
+		    newsz, sizeof(char *));
+		if (t == NULL)
+			return -1;
+		compl_state.entries = t;
+		compl_state.size = newsz;
+	}
+
+	if (st->qid.type & QTDIR)
+		sufx = "/";
+
+	if (asprintf(&dup, "%s%s%s", compl_prfx, st->name, sufx) == -1)
+		return -1;
+	compl_state.entries[compl_state.len++] = dup;
+	return 0;
+}
+
+static void
+cleanword(char *buf, int brkspc)
+{
+	char	*cmd;
+	int	 escape, quote;
+
+	while (brkspc && isspace((unsigned char)*buf))
+		memmove(buf, buf + 1, strlen(buf));
+
+	escape = quote = 0;
+	for (cmd = buf; *cmd != '\0'; ++cmd) {
+		if (escape) {
+			escape = 0;
+			continue;
+		}
+		if (*cmd == '\\')
+			goto skip;
+		if (*cmd == quote) {
+			quote = 0;
+			goto skip;
+		}
+		if (*cmd == '\'' || *cmd == '"') {
+			quote = *cmd;
+			goto skip;
+		}
+		if (quote)
+			continue;
+		if (brkspc && isspace((unsigned char)*cmd))
+			break;
+		continue;
+
+	skip:
+		memmove(cmd, cmd + 1, strlen(cmd));
+		cmd--;
+	}
+	*cmd = '\0';
+}
+
+static int
+tellcmd(char *buf)
+{
+	size_t i;
+
+	cleanword(buf, 1);
+	for (i = 0; i < nitems(cmds); ++i) {
+		if (!strcmp(cmds[i].name, buf))
+			return cmds[i].cmdtype;
+	}
+
+	return CMD_UNKNOWN;
+}
+
+static int
+tell_argno(const char *cmd, int *cmdtype)
+{
+	char		 cmd0[64];	/* plenty of space */
+	const char	*start = cmd;
+	int		 escape, quote;
+	int		 argno = 0;
+
+	*cmdtype = CMD_UNKNOWN;
+
+	/* find which argument needs to be completed */
+	while (*cmd) {
+		while (isspace((unsigned char)*cmd))
+			cmd++;
+		if (*cmd == '\0')
+			break;
+
+		escape = quote = 0;
+		for (; *cmd; ++cmd) {
+			if (escape) {
+				escape = 0;
+				continue;
+			}
+			if (*cmd == '\\') {
+				escape = 1;
+				continue;
+			}
+			if (*cmd == quote) {
+				quote = 0;
+				continue;
+			}
+			if (*cmd == '\'' || *cmd == '\"') {
+				quote = *cmd;
+				continue;
+			}
+			if (quote)
+				continue;
+			if (isspace((unsigned char)*cmd))
+				break;
+		}
+		if (isspace((unsigned char)*cmd))
+			argno++;
+
+		if (argno == 1 && strlcpy(cmd0, start, sizeof(cmd0)) <
+		    sizeof(cmd0))
+			*cmdtype = tellcmd(cmd0);
+	}
+
+	return argno;
+}
+
+static char *
+ftp_cmdname_generator(const char *text, int state)
+{
+	static size_t	 i, len;
+	struct cmd	*cmd;
+
+	if (state == 0) {
+		i = 0;
+		len = strlen(text);
+	}
+
+	while (i < nitems(cmds)) {
+		cmd = &cmds[i++];
+		if (strncmp(text, cmd->name, len) == 0)
+			return strdup(cmd->name);
+	}
+
+	return NULL;
+}
+
+static char *
+ftp_bool_generator(const char *text, int state)
+{
+	static const char	*toks[] = { "on", "off" };
+	static size_t		 i, len;
+	const char		*tok;
+
+	if (state == 0) {
+		i = 0;
+		len = strlen(text);
+	}
+
+	while ((tok = toks[i++]) != NULL) {
+		if (strncmp(text, tok, len) == 0)
+			return strdup(tok);
+	}
+	return NULL;
+}
+
+static char *
+ftp_dirent_generator(const char *text, int state)
+{
+	static size_t	 i, len;
+	const char	*entry;
+
+	if (state == 0) {
+		i = 0;
+		len = strlen(text);
+	}
+
+	while (i < compl_state.len) {
+		entry = compl_state.entries[i++];
+		if (strncmp(text, entry, len) == 0)
+			return strdup(entry);
+	}
+	return NULL;
+}
+
+static char **
+ftp_remote_files(const char *text, int start, int end)
+{
+	const char	*dir;
+	char		 t[PATH_MAX];
+	char		*s, *e;
+
+	strlcpy(t, text, sizeof(t));
+	cleanword(t, 0);
+
+	if (!strcmp(t, "..")) {
+		char **cs;
+		if ((cs = calloc(2, sizeof(*cs))) == NULL)
+			return NULL;
+		cs[0] = strdup("../");
+		return cs;
+	}
+
+	s = t;
+	if (!strncmp(s, "./", 2)) {
+		s++;
+		while (*s == '/')
+			s++;
+	}
+
+	if ((e = strrchr(s, '/')) != NULL)
+		e[1] = '\0';
+	dir = t;
+
+	if (!strcmp(dir, "."))
+		strlcpy(compl_prfx, "", sizeof(compl_prfx));
+	else
+		strlcpy(compl_prfx, dir, sizeof(compl_prfx));
+
+	compl_state_reset();
+	if (dir_listing(dir, compl_add_entry, 0) == -1)
+		return NULL;
+	return rl_completion_matches(text, ftp_dirent_generator);
+}
+
+static char **
+ftp_completion(const char *text, int start, int end)
+{
+	int	 argno, cmdtype;
+	char	*line;
+
+	/* don't fall back on the default completion system by default */
+	rl_attempted_completion_over = 1;
+
+	if ((line = rl_copy_text(0, start)) == NULL)
+		return NULL;
+
+	argno = tell_argno(line, &cmdtype);
+	free(line);
+	if (argno == 0)
+		return rl_completion_matches(text, ftp_cmdname_generator);
+
+	switch (cmdtype) {
+	case CMD_BELL:
+	case CMD_HEXDUMP:
+	case CMD_VERBOSE:
+		if (argno != 1)
+			return NULL;
+		return rl_completion_matches(text, ftp_bool_generator);
+
+	case CMD_BYE:
+	case CMD_LPWD:
+		/* no args */
+		return NULL;
+
+	case CMD_CD:
+	case CMD_EDIT:
+	case CMD_LS:
+	case CMD_PAGE:
+		if (argno != 1)
+			return NULL;
+		/* fallthrough */
+	case CMD_RM:
+		return ftp_remote_files(text, start, end);
+
+	case CMD_GET:
+		if (argno > 2)
+			return NULL;
+		if (argno == 2)
+			return ftp_remote_files(text, start, end);
+		/* try local */
+		rl_attempted_completion_over = 0;
+		return NULL;
+
+	case CMD_LCD:
+		if (argno != 1)
+			return NULL;
+		/* try local  */
+		rl_attempted_completion_over = 0;
+		return NULL;
+
+	case CMD_PIPE:
+		if (argno > 2)
+			return NULL;
+		if (argno == 1)
+			return ftp_remote_files(text, start, end);
+		/* try local */
+		rl_attempted_completion_over = 0;
+		return NULL;
+
+	case CMD_PUT:
+		if (argno > 2)
+			return NULL;
+		if (argno == 1) {
+			/* try local */
+			rl_attempted_completion_over = 0;
+			return NULL;
+		}
+		return ftp_remote_files(text, start, end);
+
+	case CMD_RENAME:
+		if (argno > 2)
+			return NULL;
+		return ftp_remote_files(text, start, end);
+	}
+
+	return NULL;
+}
+
+static int
+ftp_quoted(char *line, int index)
+{
+	if (index > 0 && line[index - 1] == '\\')
+		return !ftp_quoted(line, index - 1);
+	return 0;
+}
+
+void
+compl_setup(void)
+{
+	rl_attempted_completion_function = ftp_completion;
+	rl_completer_word_break_characters = "\t ";
+	rl_completer_quote_characters = "\"'";
+	rl_char_is_quoted_p = ftp_quoted;
+}
+
+#endif