commit - f99c17f9596718e1ec4df58727f81bec72339110
commit + d3e1ab0c5f6f6f37e4897419ae6c3591762d2f59
blob - 27a8d5b6fe8fb7ec4a46f14d23ab8b288f230dc2
blob + ee3cc466548e596fe25baf99a0aed19fec566aa7
--- kamiftp/Makefile.am
+++ kamiftp/Makefile.am
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
#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)
struct evbuffer *dirbuf;
uint32_t msize;
int bell;
+time_t now;
volatile sig_atomic_t resized;
int tty_p;
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)
{
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) {
printf("bell [on | off]\n");
}
-static void
+void
cmd_bye(int argc, const char **argv)
{
log_warnx("bye\n");
exit(0);
}
-static void
+void
cmd_cd(int argc, const char **argv)
{
struct qid qid;
pwdfid = nfid;
}
-static void
+void
cmd_edit(int argc, const char **argv)
{
struct qid qid;
unlink(sfn);
}
-static void
+void
cmd_get(int argc, const char **argv)
{
struct qid qid;
close(fd);
}
-static void
+void
cmd_hexdump(int argc, const char **argv)
{
if (argc == 0) {
puts("usage: hexdump [on | off]");
}
-static void
+void
cmd_lcd(int argc, const char **argv)
{
const char *dir;
printf("cd: %s: %s\n", dir, strerror(errno));
}
-static void
+void
cmd_lpwd(int argc, const char **argv)
{
char path[PATH_MAX];
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;
unlink(sfn);
}
-static void
+void
cmd_pipe(int argc, const char **argv)
{
struct qid qid;
waitpid(pid, &status, 0);
}
-static void
+void
cmd_put(int argc, const char **argv)
{
struct qid qid;
close(fd);
}
-static void
+void
cmd_rename(int argc, const char **argv)
{
struct np_stat st;
do_clunk(nfid);
}
-static void
+void
cmd_rm(int argc, const char **argv)
{
struct qid qid;
}
}
-static void
+void
cmd_verbose(int argc, const char **argv)
{
if (argc == 0) {
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);
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
+/*
+ * 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
+/*
+ * 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