[BACK]Return to main.c CVS log [TXT][DIR] Up to [local] / src / usr.bin / ftp

File: [local] / src / usr.bin / ftp / main.c (download)

Revision 1.139, Thu Nov 9 18:18:59 2023 UTC (7 months ago) by kn
Branch: MAIN
Changes since 1.138: +7 -5 lines

-C/resume without "proc exec"

ftp(1) has "proc exec" to run sh(1) on interactive ! commands and filenames
starting with "|";  this is orthogonal to continuing transfers using the
existing file size as offsets.

There seems to be no case where a) the argument is an URL, i.e. we pledge,
and b) a shell is spawned somehow, so avoid these promises when resuming.

bsd.port.mk(5) FETCH_CMD uses -C by default.

OK millert

/*	$OpenBSD: main.c,v 1.139 2023/11/09 18:18:59 kn Exp $	*/
/*	$NetBSD: main.c,v 1.24 1997/08/18 10:20:26 lukem Exp $	*/

/*
 * Copyright (C) 1997 and 1998 WIDE Project.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * Copyright (c) 1985, 1989, 1993, 1994
 *	The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * FTP User Program -- Command Interface.
 */
#include <sys/types.h>
#include <sys/socket.h>

#include <ctype.h>
#include <err.h>
#include <fcntl.h>
#include <netdb.h>
#include <pwd.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <tls.h>

#include "cmds.h"
#include "ftp_var.h"

int	trace;
int	hash;
int	mark;
int	sendport;
int	verbose;
int	connected;
int	fromatty;
int	interactive;
#ifndef SMALL
int	confirmrest;
int	debug;
int	bell;
char   *altarg;
#endif /* !SMALL */
int	doglob;
int	autologin;
int	proxy;
int	proxflag;
int	gatemode;
char   *gateserver;
int	sunique;
int	runique;
int	mcase;
int	ntflag;
int	mapflag;
int	preserve;
int	progress;
int	code;
int	crflag;
char	pasv[BUFSIZ];
int	passivemode;
int	activefallback;
char	ntin[17];
char	ntout[17];
char	mapin[PATH_MAX];
char	mapout[PATH_MAX];
char	typename[32];
int	type;
int	curtype;
char	structname[32];
int	stru;
char	formname[32];
int	form;
char	modename[32];
int	mode;
char	bytename[32];
int	bytesize;
int	anonftp;
int	dirchange;
unsigned int retry_connect;
int	ttywidth;
int	epsv4;
int	epsv4bad;

#ifndef SMALL
int	  editing;
EditLine *el;
History  *hist;
char	 *cursor_pos;
size_t	  cursor_argc;
size_t	  cursor_argo;
int	  resume;
char	 *srcaddr;
int  	  timestamp;
#endif /* !SMALL */

char	 *cookiefile;

off_t	bytes;
off_t	filesize;
char   *direction;

char   *hostname;
int	unix_server;
int	unix_proxy;

char *ftpport;
char *httpport;
#ifndef NOSSL
char *httpsport;
#endif /* !SMALL */
char *httpuseragent;
char *gateport;

jmp_buf	toplevel;

#ifndef SMALL
char	line[FTPBUFLEN];
char	*argbase;
char	*stringbase;
char	argbuf[FTPBUFLEN];
StringList *marg_sl;
int	margc;
int	options;
#endif /* !SMALL */

int	cpend;
int	mflag;

#ifndef SMALL
int macnum;
struct macel macros[16];
char macbuf[4096];
#endif /* !SMALL */

FILE	*ttyout;

int connect_timeout;

#ifndef SMALL
/* enable using server timestamps by default */
int server_timestamps = 1;
#endif

#ifndef NOSSL
char * const ssl_verify_opts[] = {
#define SSL_CAFILE		0
	"cafile",
#define SSL_CAPATH		1
	"capath",
#define SSL_CIPHERS		2
	"ciphers",
#define SSL_DONTVERIFY		3
	"dont",
#define SSL_DOVERIFY		4
	"do",
#define SSL_VERIFYDEPTH		5
	"depth",
#define SSL_MUSTSTAPLE		6
	"muststaple",
#define SSL_NOVERIFYTIME	7
	"noverifytime",
#define SSL_SESSION		8
	"session",
#define SSL_PROTOCOLS		9
	"protocols",
	NULL
};

struct tls_config *tls_config;
int tls_session_fd = -1;

static void
process_ssl_options(char *cp)
{
	const char *errstr;
	char *str;
	int depth;
	uint32_t protocols;

	while (*cp) {
		switch (getsubopt(&cp, ssl_verify_opts, &str)) {
		case SSL_CAFILE:
			if (str == NULL)
				errx(1, "missing CA file");
			if (tls_config_set_ca_file(tls_config, str) != 0)
				errx(1, "tls ca file failed: %s",
				    tls_config_error(tls_config));
			break;
		case SSL_CAPATH:
			if (str == NULL)
				errx(1, "missing CA directory path");
			if (tls_config_set_ca_path(tls_config, str) != 0)
				errx(1, "tls ca path failed: %s",
				    tls_config_error(tls_config));
			break;
		case SSL_CIPHERS:
			if (str == NULL)
				errx(1, "missing cipher list");
			if (tls_config_set_ciphers(tls_config, str) != 0)
				errx(1, "tls ciphers failed: %s",
				    tls_config_error(tls_config));
			break;
		case SSL_DONTVERIFY:
			tls_config_insecure_noverifycert(tls_config);
			tls_config_insecure_noverifyname(tls_config);
			break;
		case SSL_DOVERIFY:
			tls_config_verify(tls_config);
			break;
		case SSL_VERIFYDEPTH:
			if (str == NULL)
				errx(1, "missing depth");
			depth = strtonum(str, 0, INT_MAX, &errstr);
			if (errstr)
				errx(1, "certificate validation depth is %s",
				    errstr);
			tls_config_set_verify_depth(tls_config, depth);
			break;
		case SSL_MUSTSTAPLE:
			tls_config_ocsp_require_stapling(tls_config);
			break;
		case SSL_NOVERIFYTIME:
			tls_config_insecure_noverifytime(tls_config);
			break;
		case SSL_SESSION:
			if (str == NULL)
				errx(1, "missing session file");
			if ((tls_session_fd = open(str, O_RDWR|O_CREAT,
			    0600)) == -1)
				err(1, "failed to open or create session file "
				    "'%s'", str);
			if (tls_config_set_session_fd(tls_config,
			    tls_session_fd) == -1)
				errx(1, "failed to set session: %s",
				    tls_config_error(tls_config));
			break;
		case SSL_PROTOCOLS:
			if (str == NULL)
				errx(1, "missing protocol name");
			if (tls_config_parse_protocols(&protocols, str) != 0)
				errx(1, "failed to parse TLS protocols");
			if (tls_config_set_protocols(tls_config, protocols) != 0)
				errx(1, "failed to set TLS protocols: %s",
				    tls_config_error(tls_config));
			break;
		default:
			errx(1, "unknown -S suboption `%s'",
			    suboptarg ? suboptarg : "");
			/* NOTREACHED */
		}
	}
}
#endif /* !NOSSL */

int family = PF_UNSPEC;
int pipeout;

int
main(volatile int argc, char *argv[])
{
	int ch, rval;
#ifndef SMALL
	int top;
#endif
	struct passwd *pw = NULL;
	char *cp, homedir[PATH_MAX];
	char *outfile = NULL;
	const char *errstr;
	int dumb_terminal = 0;

	ftpport = "ftp";
	httpport = "http";
#ifndef NOSSL
	httpsport = "https";
#endif /* !NOSSL */
	gateport = getenv("FTPSERVERPORT");
	if (gateport == NULL || *gateport == '\0')
		gateport = "ftpgate";
	doglob = 1;
	interactive = 1;
	autologin = 1;
	passivemode = 1;
	activefallback = 1;
	preserve = 1;
	verbose = 0;
	progress = 0;
	gatemode = 0;
#ifndef NOSSL
	cookiefile = NULL;
#endif /* NOSSL */
#ifndef SMALL
	editing = 0;
	el = NULL;
	hist = NULL;
	resume = 0;
	timestamp = 0;
	srcaddr = NULL;
	marg_sl = sl_init();
#endif /* !SMALL */
	mark = HASHBYTES;
	epsv4 = 1;
	epsv4bad = 0;

	/* Set default operation mode based on FTPMODE environment variable */
	if ((cp = getenv("FTPMODE")) != NULL && *cp != '\0') {
		if (strcmp(cp, "passive") == 0) {
			passivemode = 1;
			activefallback = 0;
		} else if (strcmp(cp, "active") == 0) {
			passivemode = 0;
			activefallback = 0;
		} else if (strcmp(cp, "gate") == 0) {
			gatemode = 1;
		} else if (strcmp(cp, "auto") == 0) {
			passivemode = 1;
			activefallback = 1;
		} else
			warnx("unknown FTPMODE: %s.  Using defaults", cp);
	}

	if (strcmp(__progname, "gate-ftp") == 0)
		gatemode = 1;
	gateserver = getenv("FTPSERVER");
	if (gateserver == NULL)
		gateserver = "";
	if (gatemode) {
		if (*gateserver == '\0') {
			warnx(
"Neither $FTPSERVER nor $GATE_SERVER is defined; disabling gate-ftp");
			gatemode = 0;
		}
	}

	cp = getenv("TERM");
	dumb_terminal = (cp == NULL || *cp == '\0' || !strcmp(cp, "dumb") ||
	    !strcmp(cp, "emacs") || !strcmp(cp, "su"));
	fromatty = isatty(fileno(stdin));
	if (fromatty) {
		verbose = 1;		/* verbose if from a tty */
#ifndef SMALL
		if (!dumb_terminal)
			editing = 1;	/* editing mode on if tty is usable */
#endif /* !SMALL */
	}

	ttyout = stdout;
	if (isatty(fileno(ttyout)) && !dumb_terminal && foregroundproc())
		progress = 1;		/* progress bar on if tty is usable */

#ifndef NOSSL
	cookiefile = getenv("http_cookies");
	if (tls_config == NULL) {
		tls_config = tls_config_new();
		if (tls_config == NULL)
			errx(1, "tls config failed");
		if (tls_config_set_protocols(tls_config,
		    TLS_PROTOCOLS_ALL) != 0)
			errx(1, "tls set protocols failed: %s",
			    tls_config_error(tls_config));
		if (tls_config_set_ciphers(tls_config, "legacy") != 0)
			errx(1, "tls set ciphers failed: %s",
			    tls_config_error(tls_config));
	}
#endif /* !NOSSL */

	httpuseragent = NULL;

	while ((ch = getopt(argc, argv,
		    "46AaCc:dD:EeN:gik:Mmno:pP:r:S:s:TtU:uvVw:")) != -1) {
		switch (ch) {
		case '4':
			family = PF_INET;
			break;
		case '6':
			family = PF_INET6;
			break;
		case 'A':
			activefallback = 0;
			passivemode = 0;
			break;

		case 'N':
			setprogname(optarg);
			break;
		case 'a':
			anonftp = 1;
			break;

		case 'C':
#ifndef SMALL
			resume = 1;
#endif /* !SMALL */
			break;

		case 'c':
#ifndef SMALL
			cookiefile = optarg;
#endif /* !SMALL */
			break;

		case 'D':
			action = optarg;
			break;
		case 'd':
#ifndef SMALL
			options |= SO_DEBUG;
			debug++;
#endif /* !SMALL */
			break;

		case 'E':
			epsv4 = 0;
			break;

		case 'e':
#ifndef SMALL
			editing = 0;
#endif /* !SMALL */
			break;

		case 'g':
			doglob = 0;
			break;

		case 'i':
			interactive = 0;
			break;

		case 'k':
			keep_alive_timeout = strtonum(optarg, 0, INT_MAX,
			    &errstr);
			if (errstr != NULL) {
				warnx("keep alive amount is %s: %s", errstr,
					optarg);
				usage();
			}
			break;
		case 'M':
			progress = 0;
			break;
		case 'm':
			progress = -1;
			break;

		case 'n':
			autologin = 0;
			break;

		case 'o':
			outfile = optarg;
			if (*outfile == '\0') {
				pipeout = 0;
				outfile = NULL;
				ttyout = stdout;
			} else {
				pipeout = strcmp(outfile, "-") == 0;
				ttyout = pipeout ? stderr : stdout;
			}
			break;

		case 'p':
			passivemode = 1;
			activefallback = 0;
			break;

		case 'P':
			ftpport = optarg;
			break;

		case 'r':
			retry_connect = strtonum(optarg, 0, INT_MAX, &errstr);
			if (errstr != NULL) {
				warnx("retry amount is %s: %s", errstr,
					optarg);
				usage();
			}
			break;

		case 'S':
#ifndef NOSSL
			process_ssl_options(optarg);
#endif /* !NOSSL */
			break;

		case 's':
#ifndef SMALL
			srcaddr = optarg;
#endif /* !SMALL */
			break;

#ifndef SMALL
		case 'T':
			timestamp = 1;
			break;
#endif /* !SMALL */
		case 't':
			trace = 1;
			break;

#ifndef SMALL
		case 'U':
			free (httpuseragent);
			if (strcspn(optarg, "\r\n") != strlen(optarg))
				errx(1, "Invalid User-Agent: %s.", optarg);
			if (asprintf(&httpuseragent, "User-Agent: %s",
			    optarg) == -1)
				errx(1, "Can't allocate memory for HTTP(S) "
				    "User-Agent");
			break;
		case 'u':
			server_timestamps = 0;
			break;
#endif /* !SMALL */

		case 'v':
			verbose = 1;
			break;

		case 'V':
			verbose = 0;
			break;

		case 'w':
			connect_timeout = strtonum(optarg, 0, 200, &errstr);
			if (errstr)
				errx(1, "-w: %s", errstr);
			break;
		default:
			usage();
		}
	}
	argc -= optind;
	argv += optind;

#ifndef NOSSL
	cookie_load();
#endif /* !NOSSL */
	if (httpuseragent == NULL)
		httpuseragent = HTTP_USER_AGENT;

	cpend = 0;	/* no pending replies */
	proxy = 0;	/* proxy not active */
	crflag = 1;	/* strip c.r. on ascii gets */
	sendport = -1;	/* not using ports */
	/*
	 * Set up the home directory in case we're globbing.
	 */
	cp = getlogin();
	if (cp != NULL) {
		pw = getpwnam(cp);
	}
	if (pw == NULL)
		pw = getpwuid(getuid());
	if (pw != NULL) {
		(void)strlcpy(homedir, pw->pw_dir, sizeof homedir);
		home = homedir;
	}

	setttywidth(0);
	(void)signal(SIGWINCH, setttywidth);

	if (argc > 0) {
		if (isurl(argv[0])) {
			if (pipeout) {
#ifndef SMALL
			    if (!resume) {
				if (pledge("stdio rpath dns tty inet proc exec fattr",
				    NULL) == -1)
					err(1, "pledge");
			    } else
#endif /* !SMALL */
				if (pledge("stdio rpath dns tty inet fattr",
				    NULL) == -1)
					err(1, "pledge");
			} else {
#ifndef SMALL
			    if (!resume) {
				if (pledge("stdio rpath wpath cpath dns tty inet proc exec fattr",
				    NULL) == -1)
					err(1, "pledge");
			    } else
#endif /* !SMALL */
				if (pledge("stdio rpath wpath cpath dns tty inet fattr",
				    NULL) == -1)
					err(1, "pledge");
			}

			rval = auto_fetch(argc, argv, outfile);
			if (rval >= 0)		/* -1 == connected and cd-ed */
				exit(rval);
		} else {
#ifndef SMALL
			char *xargv[5];

			if (setjmp(toplevel))
				exit(0);
			(void)signal(SIGINT, (sig_t)intr);
			(void)signal(SIGPIPE, (sig_t)lostpeer);
			xargv[0] = __progname;
			xargv[1] = argv[0];
			xargv[2] = argv[1];
			xargv[3] = argv[2];
			xargv[4] = NULL;
			do {
				setpeer(argc+1, xargv);
				if (!retry_connect)
					break;
				if (!connected) {
					macnum = 0;
					fputs("Retrying...\n", ttyout);
					sleep(retry_connect);
				}
			} while (!connected);
			retry_connect = 0; /* connected, stop hiding msgs */
#endif /* !SMALL */
		}
	}
#ifndef SMALL
	controlediting();
	top = setjmp(toplevel) == 0;
	if (top) {
		(void)signal(SIGINT, (sig_t)intr);
		(void)signal(SIGPIPE, (sig_t)lostpeer);
	}
	for (;;) {
		cmdscanner(top);
		top = 1;
	}
#else /* !SMALL */
	usage();
#endif /* !SMALL */
}

void
intr(void)
{
	int save_errno = errno;

	write(fileno(ttyout), "\n\r", 2);
	alarmtimer(0);

	errno = save_errno;
	longjmp(toplevel, 1);
}

void
lostpeer(void)
{
	int save_errno = errno;

	alarmtimer(0);
	if (connected) {
		if (cout != NULL) {
			(void)shutdown(fileno(cout), SHUT_RDWR);
			(void)fclose(cout);
			cout = NULL;
		}
		if (data >= 0) {
			(void)shutdown(data, SHUT_RDWR);
			(void)close(data);
			data = -1;
		}
		connected = 0;
	}
	pswitch(1);
	if (connected) {
		if (cout != NULL) {
			(void)shutdown(fileno(cout), SHUT_RDWR);
			(void)fclose(cout);
			cout = NULL;
		}
		connected = 0;
	}
	proxflag = 0;
	pswitch(0);
	errno = save_errno;
}

#ifndef SMALL
/*
 * Generate a prompt
 */
char *
prompt(void)
{
	return ("ftp> ");
}

/*
 * Command parser.
 */
void
cmdscanner(int top)
{
	struct cmd *c;
	int num;
	HistEvent hev;

	if (!top && !editing)
		(void)putc('\n', ttyout);
	for (;;) {
		if (!editing) {
			if (fromatty) {
				fputs(prompt(), ttyout);
				(void)fflush(ttyout);
			}
			if (fgets(line, sizeof(line), stdin) == NULL)
				quit(0, 0);
			num = strlen(line);
			if (num == 0)
				break;
			if (line[--num] == '\n') {
				if (num == 0)
					break;
				line[num] = '\0';
			} else if (num == sizeof(line) - 2) {
				fputs("sorry, input line too long.\n", ttyout);
				while ((num = getchar()) != '\n' && num != EOF)
					/* void */;
				break;
			} /* else it was a line without a newline */
		} else {
			const char *buf;
			cursor_pos = NULL;

			if ((buf = el_gets(el, &num)) == NULL || num == 0) {
				putc('\n', ttyout);
				fflush(ttyout);
				quit(0, 0);
			}
			if (buf[--num] == '\n') {
				if (num == 0)
					break;
			}
			if (num >= sizeof(line)) {
				fputs("sorry, input line too long.\n", ttyout);
				break;
			}
			memcpy(line, buf, (size_t)num);
			line[num] = '\0';
			history(hist, &hev, H_ENTER, buf);
		}

		makeargv();
		if (margc == 0)
			continue;
		c = getcmd(margv[0]);
		if (c == (struct cmd *)-1) {
			fputs("?Ambiguous command.\n", ttyout);
			continue;
		}
		if (c == 0) {
			/*
			 * Give editline(3) a shot at unknown commands.
			 * XXX - bogus commands with a colon in
			 *       them will not elicit an error.
			 */
			if (editing &&
			    el_parse(el, margc, (const char **)margv) != 0)
				fputs("?Invalid command.\n", ttyout);
			continue;
		}
		if (c->c_conn && !connected) {
			fputs("Not connected.\n", ttyout);
			continue;
		}
		confirmrest = 0;
		(*c->c_handler)(margc, margv);
		if (bell && c->c_bell)
			(void)putc('\007', ttyout);
		if (c->c_handler != help)
			break;
	}
	(void)signal(SIGINT, (sig_t)intr);
	(void)signal(SIGPIPE, (sig_t)lostpeer);
}

struct cmd *
getcmd(const char *name)
{
	const char *p, *q;
	struct cmd *c, *found;
	int nmatches, longest;

	if (name == NULL)
		return (0);

	longest = 0;
	nmatches = 0;
	found = 0;
	for (c = cmdtab; (p = c->c_name) != NULL; c++) {
		for (q = name; *q == *p++; q++)
			if (*q == 0)		/* exact match? */
				return (c);
		if (!*q) {			/* the name was a prefix */
			if (q - name > longest) {
				longest = q - name;
				nmatches = 1;
				found = c;
			} else if (q - name == longest)
				nmatches++;
		}
	}
	if (nmatches > 1)
		return ((struct cmd *)-1);
	return (found);
}

/*
 * Slice a string up into argc/argv.
 */

int slrflag;

void
makeargv(void)
{
	char *argp;

	stringbase = line;		/* scan from first of buffer */
	argbase = argbuf;		/* store from first of buffer */
	slrflag = 0;
	marg_sl->sl_cur = 0;		/* reset to start of marg_sl */
	for (margc = 0; ; margc++) {
		argp = slurpstring();
		sl_add(marg_sl, argp);
		if (argp == NULL)
			break;
	}
	if (cursor_pos == line) {
		cursor_argc = 0;
		cursor_argo = 0;
	} else if (cursor_pos != NULL) {
		cursor_argc = margc;
		cursor_argo = strlen(margv[margc-1]);
	}
}

#define INC_CHKCURSOR(x)	{ (x)++ ; \
				if (x == cursor_pos) { \
					cursor_argc = margc; \
					cursor_argo = ap-argbase; \
					cursor_pos = NULL; \
				} }

/*
 * Parse string into argbuf;
 * implemented with FSM to
 * handle quoting and strings
 */
char *
slurpstring(void)
{
	int got_one = 0;
	char *sb = stringbase;
	char *ap = argbase;
	char *tmp = argbase;		/* will return this if token found */

	if (*sb == '!' || *sb == '$') {	/* recognize ! as a token for shell */
		switch (slrflag) {	/* and $ as token for macro invoke */
			case 0:
				slrflag++;
				INC_CHKCURSOR(stringbase);
				return ((*sb == '!') ? "!" : "$");
				/* NOTREACHED */
			case 1:
				slrflag++;
				altarg = stringbase;
				break;
			default:
				break;
		}
	}

S0:
	switch (*sb) {

	case '\0':
		goto OUT;

	case ' ':
	case '\t':
		INC_CHKCURSOR(sb);
		goto S0;

	default:
		switch (slrflag) {
			case 0:
				slrflag++;
				break;
			case 1:
				slrflag++;
				altarg = sb;
				break;
			default:
				break;
		}
		goto S1;
	}

S1:
	switch (*sb) {

	case ' ':
	case '\t':
	case '\0':
		goto OUT;	/* end of token */

	case '\\':
		INC_CHKCURSOR(sb);
		goto S2;	/* slurp next character */

	case '"':
		INC_CHKCURSOR(sb);
		goto S3;	/* slurp quoted string */

	default:
		*ap = *sb;	/* add character to token */
		ap++;
		INC_CHKCURSOR(sb);
		got_one = 1;
		goto S1;
	}

S2:
	switch (*sb) {

	case '\0':
		goto OUT;

	default:
		*ap = *sb;
		ap++;
		INC_CHKCURSOR(sb);
		got_one = 1;
		goto S1;
	}

S3:
	switch (*sb) {

	case '\0':
		goto OUT;

	case '"':
		INC_CHKCURSOR(sb);
		goto S1;

	default:
		*ap = *sb;
		ap++;
		INC_CHKCURSOR(sb);
		got_one = 1;
		goto S3;
	}

OUT:
	if (got_one)
		*ap++ = '\0';
	argbase = ap;			/* update storage pointer */
	stringbase = sb;		/* update scan pointer */
	if (got_one) {
		return (tmp);
	}
	switch (slrflag) {
		case 0:
			slrflag++;
			break;
		case 1:
			slrflag++;
			altarg = (char *) 0;
			break;
		default:
			break;
	}
	return (NULL);
}

/*
 * Help command.
 * Call each command handler with argc == 0 and argv[0] == name.
 */
void
help(int argc, char *argv[])
{
	struct cmd *c;

	if (argc == 1) {
		StringList *buf;

		buf = sl_init();
		fprintf(ttyout, "%sommands may be abbreviated.  Commands are:\n\n",
		    proxy ? "Proxy c" : "C");
		for (c = cmdtab; c < &cmdtab[NCMDS]; c++)
			if (c->c_name && (!proxy || c->c_proxy))
				sl_add(buf, c->c_name);
		list_vertical(buf);
		sl_free(buf, 0);
		return;
	}

#define HELPINDENT ((int) sizeof("disconnect"))

	while (--argc > 0) {
		char *arg;

		arg = *++argv;
		c = getcmd(arg);
		if (c == (struct cmd *)-1)
			fprintf(ttyout, "?Ambiguous help command %s\n", arg);
		else if (c == NULL)
			fprintf(ttyout, "?Invalid help command %s\n", arg);
		else
			fprintf(ttyout, "%-*s\t%s\n", HELPINDENT,
				c->c_name, c->c_help);
	}
}
#endif /* !SMALL */

__dead void
usage(void)
{
	fprintf(stderr, "usage: "
#ifndef SMALL
	    "ftp [-46AadEegiMmnptVv] [-D title] [-k seconds] [-P port] "
	    "[-r seconds]\n"
	    "           [-s sourceaddr] [host [port]]\n"
	    "       ftp [-C] [-N name] [-o output] [-s sourceaddr]\n"
	    "           ftp://[user:password@]host[:port]/file[/] ...\n"
	    "       ftp [-CTu] [-c cookie] [-N name] [-o output] [-S ssl_options] "
	    "[-s sourceaddr]\n"
	    "           [-U useragent] [-w seconds] "
	    "http[s]://[user:password@]host[:port]/file ...\n"
	    "       ftp [-C] [-N name] [-o output] [-s sourceaddr] file:file ...\n"
	    "       ftp [-C] [-N name] [-o output] [-s sourceaddr] host:/file[/] ...\n"
#else /* !SMALL */
	    "ftp [-N name] [-o output] "
	    "ftp://[user:password@]host[:port]/file[/] ...\n"
#ifndef NOSSL
	    "       ftp [-N name] [-o output] [-S ssl_options] [-w seconds] "
	    "http[s]://[user:password@]host[:port]/file ...\n"
#else
	    "       ftp [-N name] [-o output] [-w seconds] http://host[:port]/file ...\n"
#endif /* NOSSL */
	    "       ftp [-N name] [-o output] file:file ...\n"
	    "       ftp [-N name] [-o output] host:/file[/] ...\n"
#endif /* !SMALL */
	    );
	exit(1);
}