xref: /csrg-svn/libexec/ftpd/ftpcmd.y (revision 36304)
1 /*
2  * Copyright (c) 1985, 1988 Regents of the University of California.
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms are permitted
6  * provided that the above copyright notice and this paragraph are
7  * duplicated in all such forms and that any documentation,
8  * advertising materials, and other materials related to such
9  * distribution and use acknowledge that the software was developed
10  * by the University of California, Berkeley.  The name of the
11  * University may not be used to endorse or promote products derived
12  * from this software without specific prior written permission.
13  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
14  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
15  * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
16  *
17  *	@(#)ftpcmd.y	5.14 (Berkeley) 12/07/88
18  */
19 
20 /*
21  * Grammar for FTP commands.
22  * See RFC 765.
23  */
24 
25 %{
26 
27 #ifndef lint
28 static char sccsid[] = "@(#)ftpcmd.y	5.14 (Berkeley) 12/07/88";
29 #endif /* not lint */
30 
31 #include <sys/types.h>
32 #include <sys/socket.h>
33 
34 #include <netinet/in.h>
35 
36 #include <arpa/ftp.h>
37 
38 #include <stdio.h>
39 #include <signal.h>
40 #include <ctype.h>
41 #include <pwd.h>
42 #include <setjmp.h>
43 #include <syslog.h>
44 
45 extern	struct sockaddr_in data_dest;
46 extern	int logged_in;
47 extern	struct passwd *pw;
48 extern	int guest;
49 extern	int logging;
50 extern	int type;
51 extern	int form;
52 extern	int debug;
53 extern	int timeout;
54 extern  int pdata;
55 extern	char hostname[];
56 extern	char *globerr;
57 extern	int usedefault;
58 extern  int transflag;
59 extern  char tmpline[];
60 char	**glob();
61 
62 static	int cmd_type;
63 static	int cmd_form;
64 static	int cmd_bytesz;
65 char	cbuf[512];
66 char	*fromname;
67 
68 char	*index();
69 %}
70 
71 %token
72 	A	B	C	E	F	I
73 	L	N	P	R	S	T
74 
75 	SP	CRLF	COMMA	STRING	NUMBER
76 
77 	USER	PASS	ACCT	REIN	QUIT	PORT
78 	PASV	TYPE	STRU	MODE	RETR	STOR
79 	APPE	MLFL	MAIL	MSND	MSOM	MSAM
80 	MRSQ	MRCP	ALLO	REST	RNFR	RNTO
81 	ABOR	DELE	CWD	LIST	NLST	SITE
82 	STAT	HELP	NOOP	XMKD	XRMD	XPWD
83 	XCUP	STOU
84 
85 	LEXERR
86 
87 %start	cmd_list
88 
89 %%
90 
91 cmd_list:	/* empty */
92 	|	cmd_list cmd
93 		= {
94 			fromname = (char *) 0;
95 		}
96 	|	cmd_list rcmd
97 	;
98 
99 cmd:		USER SP username CRLF
100 		= {
101 			user((char *) $3);
102 			free((char *) $3);
103 		}
104 	|	PASS SP password CRLF
105 		= {
106 			pass((char *) $3);
107 			free((char *) $3);
108 		}
109 	|	PORT SP host_port CRLF
110 		= {
111 			usedefault = 0;
112 			if (pdata >= 0) {
113 				(void) close(pdata);
114 				pdata = -1;
115 			}
116 			reply(200, "PORT command successful.");
117 		}
118 	|	PASV CRLF
119 		= {
120 			passive();
121 		}
122 	|	TYPE SP type_code CRLF
123 		= {
124 			switch (cmd_type) {
125 
126 			case TYPE_A:
127 				if (cmd_form == FORM_N) {
128 					reply(200, "Type set to A.");
129 					type = cmd_type;
130 					form = cmd_form;
131 				} else
132 					reply(504, "Form must be N.");
133 				break;
134 
135 			case TYPE_E:
136 				reply(504, "Type E not implemented.");
137 				break;
138 
139 			case TYPE_I:
140 				reply(200, "Type set to I.");
141 				type = cmd_type;
142 				break;
143 
144 			case TYPE_L:
145 				if (cmd_bytesz == 8) {
146 					reply(200,
147 					    "Type set to L (byte size 8).");
148 					type = cmd_type;
149 				} else
150 					reply(504, "Byte size must be 8.");
151 			}
152 		}
153 	|	STRU SP struct_code CRLF
154 		= {
155 			switch ($3) {
156 
157 			case STRU_F:
158 				reply(200, "STRU F ok.");
159 				break;
160 
161 			default:
162 				reply(504, "Unimplemented STRU type.");
163 			}
164 		}
165 	|	MODE SP mode_code CRLF
166 		= {
167 			switch ($3) {
168 
169 			case MODE_S:
170 				reply(200, "MODE S ok.");
171 				break;
172 
173 			default:
174 				reply(502, "Unimplemented MODE type.");
175 			}
176 		}
177 	|	ALLO SP NUMBER CRLF
178 		= {
179 			reply(202, "ALLO command ignored.");
180 		}
181 	|	RETR check_login SP pathname CRLF
182 		= {
183 			if ($2 && $4 != NULL)
184 				retrieve((char *) 0, (char *) $4);
185 			if ($4 != NULL)
186 				free((char *) $4);
187 		}
188 	|	STOR check_login SP pathname CRLF
189 		= {
190 			if ($2 && $4 != NULL)
191 				store((char *) $4, "w", 0);
192 			if ($4 != NULL)
193 				free((char *) $4);
194 		}
195 	|	APPE check_login SP pathname CRLF
196 		= {
197 			if ($2 && $4 != NULL)
198 				store((char *) $4, "a", 0);
199 			if ($4 != NULL)
200 				free((char *) $4);
201 		}
202 	|	NLST check_login CRLF
203 		= {
204 			if ($2)
205 				retrieve("/bin/ls", "");
206 		}
207 	|	NLST check_login SP pathname CRLF
208 		= {
209 			if ($2 && $4 != NULL)
210 				retrieve("/bin/ls %s", (char *) $4);
211 			if ($4 != NULL)
212 				free((char *) $4);
213 		}
214 	|	LIST check_login CRLF
215 		= {
216 			if ($2)
217 				retrieve("/bin/ls -lg", "");
218 		}
219 	|	LIST check_login SP pathname CRLF
220 		= {
221 			if ($2 && $4 != NULL)
222 				retrieve("/bin/ls -lg %s", (char *) $4);
223 			if ($4 != NULL)
224 				free((char *) $4);
225 		}
226 	|	DELE check_login SP pathname CRLF
227 		= {
228 			if ($2 && $4 != NULL)
229 				delete((char *) $4);
230 			if ($4 != NULL)
231 				free((char *) $4);
232 		}
233 	|	RNTO SP pathname CRLF
234 		= {
235 			if (fromname) {
236 				renamecmd(fromname, (char *) $3);
237 				free(fromname);
238 				fromname = (char *) 0;
239 			} else {
240 				reply(503, "Bad sequence of commands.");
241 			}
242 			free((char *) $3);
243 		}
244 	|	ABOR CRLF
245 		= {
246 			reply(225, "ABOR command successful.");
247 		}
248 	|	CWD check_login CRLF
249 		= {
250 			if ($2)
251 				cwd(pw->pw_dir);
252 		}
253 	|	CWD check_login SP pathname CRLF
254 		= {
255 			if ($2 && $4 != NULL)
256 				cwd((char *) $4);
257 			if ($4 != NULL)
258 				free((char *) $4);
259 		}
260 	|	HELP CRLF
261 		= {
262 			help((char *) 0);
263 		}
264 	|	HELP SP STRING CRLF
265 		= {
266 			help((char *) $3);
267 		}
268 	|	NOOP CRLF
269 		= {
270 			reply(200, "NOOP command successful.");
271 		}
272 	|	XMKD check_login SP pathname CRLF
273 		= {
274 			if ($2 && $4 != NULL)
275 				makedir((char *) $4);
276 			if ($4 != NULL)
277 				free((char *) $4);
278 		}
279 	|	XRMD check_login SP pathname CRLF
280 		= {
281 			if ($2 && $4 != NULL)
282 				removedir((char *) $4);
283 			if ($4 != NULL)
284 				free((char *) $4);
285 		}
286 	|	XPWD check_login CRLF
287 		= {
288 			if ($2)
289 				pwd();
290 		}
291 	|	XCUP check_login CRLF
292 		= {
293 			if ($2)
294 				cwd("..");
295 		}
296 	|	STOU check_login SP pathname CRLF
297 		= {
298 			if ($2 && $4 != NULL)
299 				store((char *) $4, "w", 1);
300 			if ($4 != NULL)
301 				free((char *) $4);
302 		}
303 	|	QUIT CRLF
304 		= {
305 			reply(221, "Goodbye.");
306 			dologout(0);
307 		}
308 	|	error CRLF
309 		= {
310 			yyerrok;
311 		}
312 	;
313 
314 rcmd:		RNFR check_login SP pathname CRLF
315 		= {
316 			char *renamefrom();
317 
318 			if ($2 && $4) {
319 				fromname = renamefrom((char *) $4);
320 				if (fromname == (char *) 0 && $4) {
321 					free((char *) $4);
322 				}
323 			}
324 		}
325 	;
326 
327 username:	STRING
328 	;
329 
330 password:	/* empty */
331 		= {
332 			$$ = (int) "";
333 		}
334 	|	STRING
335 	;
336 
337 byte_size:	NUMBER
338 	;
339 
340 host_port:	NUMBER COMMA NUMBER COMMA NUMBER COMMA NUMBER COMMA
341 		NUMBER COMMA NUMBER
342 		= {
343 			register char *a, *p;
344 
345 			a = (char *)&data_dest.sin_addr;
346 			a[0] = $1; a[1] = $3; a[2] = $5; a[3] = $7;
347 			p = (char *)&data_dest.sin_port;
348 			p[0] = $9; p[1] = $11;
349 			data_dest.sin_family = AF_INET;
350 		}
351 	;
352 
353 form_code:	N
354 	= {
355 		$$ = FORM_N;
356 	}
357 	|	T
358 	= {
359 		$$ = FORM_T;
360 	}
361 	|	C
362 	= {
363 		$$ = FORM_C;
364 	}
365 	;
366 
367 type_code:	A
368 	= {
369 		cmd_type = TYPE_A;
370 		cmd_form = FORM_N;
371 	}
372 	|	A SP form_code
373 	= {
374 		cmd_type = TYPE_A;
375 		cmd_form = $3;
376 	}
377 	|	E
378 	= {
379 		cmd_type = TYPE_E;
380 		cmd_form = FORM_N;
381 	}
382 	|	E SP form_code
383 	= {
384 		cmd_type = TYPE_E;
385 		cmd_form = $3;
386 	}
387 	|	I
388 	= {
389 		cmd_type = TYPE_I;
390 	}
391 	|	L
392 	= {
393 		cmd_type = TYPE_L;
394 		cmd_bytesz = 8;
395 	}
396 	|	L SP byte_size
397 	= {
398 		cmd_type = TYPE_L;
399 		cmd_bytesz = $3;
400 	}
401 	/* this is for a bug in the BBN ftp */
402 	|	L byte_size
403 	= {
404 		cmd_type = TYPE_L;
405 		cmd_bytesz = $2;
406 	}
407 	;
408 
409 struct_code:	F
410 	= {
411 		$$ = STRU_F;
412 	}
413 	|	R
414 	= {
415 		$$ = STRU_R;
416 	}
417 	|	P
418 	= {
419 		$$ = STRU_P;
420 	}
421 	;
422 
423 mode_code:	S
424 	= {
425 		$$ = MODE_S;
426 	}
427 	|	B
428 	= {
429 		$$ = MODE_B;
430 	}
431 	|	C
432 	= {
433 		$$ = MODE_C;
434 	}
435 	;
436 
437 pathname:	pathstring
438 	= {
439 		/*
440 		 * Problem: this production is used for all pathname
441 		 * processing, but only gives a 550 error reply.
442 		 * This is a valid reply in some cases but not in others.
443 		 */
444 		if (logged_in && $1 && strncmp((char *) $1, "~", 1) == 0) {
445 			$$ = (int)*glob((char *) $1);
446 			if (globerr != NULL) {
447 				reply(550, globerr);
448 				$$ = NULL;
449 			}
450 			free((char *) $1);
451 		} else
452 			$$ = $1;
453 	}
454 	;
455 
456 pathstring:	STRING
457 	;
458 
459 check_login:	/* empty */
460 	= {
461 		if (logged_in)
462 			$$ = 1;
463 		else {
464 			reply(530, "Please login with USER and PASS.");
465 			$$ = 0;
466 		}
467 	}
468 	;
469 
470 %%
471 
472 extern jmp_buf errcatch;
473 
474 #define	CMD	0	/* beginning of command */
475 #define	ARGS	1	/* expect miscellaneous arguments */
476 #define	STR1	2	/* expect SP followed by STRING */
477 #define	STR2	3	/* expect STRING */
478 #define	OSTR	4	/* optional SP then STRING */
479 #define	ZSTR1	5	/* SP then optional STRING */
480 #define	ZSTR2	6	/* optional STRING after SP */
481 
482 struct tab {
483 	char	*name;
484 	short	token;
485 	short	state;
486 	short	implemented;	/* 1 if command is implemented */
487 	char	*help;
488 };
489 
490 struct tab cmdtab[] = {		/* In order defined in RFC 765 */
491 	{ "USER", USER, STR1, 1,	"<sp> username" },
492 	{ "PASS", PASS, ZSTR1, 1,	"<sp> password" },
493 	{ "ACCT", ACCT, STR1, 0,	"(specify account)" },
494 	{ "REIN", REIN, ARGS, 0,	"(reinitialize server state)" },
495 	{ "QUIT", QUIT, ARGS, 1,	"(terminate service)", },
496 	{ "PORT", PORT, ARGS, 1,	"<sp> b0, b1, b2, b3, b4" },
497 	{ "PASV", PASV, ARGS, 1,	"(set server in passive mode)" },
498 	{ "TYPE", TYPE, ARGS, 1,	"<sp> [ A | E | I | L ]" },
499 	{ "STRU", STRU, ARGS, 1,	"(specify file structure)" },
500 	{ "MODE", MODE, ARGS, 1,	"(specify transfer mode)" },
501 	{ "RETR", RETR, STR1, 1,	"<sp> file-name" },
502 	{ "STOR", STOR, STR1, 1,	"<sp> file-name" },
503 	{ "APPE", APPE, STR1, 1,	"<sp> file-name" },
504 	{ "MLFL", MLFL, OSTR, 0,	"(mail file)" },
505 	{ "MAIL", MAIL, OSTR, 0,	"(mail to user)" },
506 	{ "MSND", MSND, OSTR, 0,	"(mail send to terminal)" },
507 	{ "MSOM", MSOM, OSTR, 0,	"(mail send to terminal or mailbox)" },
508 	{ "MSAM", MSAM, OSTR, 0,	"(mail send to terminal and mailbox)" },
509 	{ "MRSQ", MRSQ, OSTR, 0,	"(mail recipient scheme question)" },
510 	{ "MRCP", MRCP, STR1, 0,	"(mail recipient)" },
511 	{ "ALLO", ALLO, ARGS, 1,	"allocate storage (vacuously)" },
512 	{ "REST", REST, STR1, 0,	"(restart command)" },
513 	{ "RNFR", RNFR, STR1, 1,	"<sp> file-name" },
514 	{ "RNTO", RNTO, STR1, 1,	"<sp> file-name" },
515 	{ "ABOR", ABOR, ARGS, 1,	"(abort operation)" },
516 	{ "DELE", DELE, STR1, 1,	"<sp> file-name" },
517 	{ "CWD",  CWD,  OSTR, 1,	"[ <sp> directory-name ]" },
518 	{ "XCWD", CWD,	OSTR, 1,	"[ <sp> directory-name ]" },
519 	{ "LIST", LIST, OSTR, 1,	"[ <sp> path-name ]" },
520 	{ "NLST", NLST, OSTR, 1,	"[ <sp> path-name ]" },
521 	{ "SITE", SITE, STR1, 0,	"(get site parameters)" },
522 	{ "STAT", STAT, OSTR, 0,	"(get server status)" },
523 	{ "HELP", HELP, OSTR, 1,	"[ <sp> <string> ]" },
524 	{ "NOOP", NOOP, ARGS, 1,	"" },
525 	{ "MKD",  XMKD, STR1, 1,	"<sp> path-name" },
526 	{ "XMKD", XMKD, STR1, 1,	"<sp> path-name" },
527 	{ "RMD",  XRMD, STR1, 1,	"<sp> path-name" },
528 	{ "XRMD", XRMD, STR1, 1,	"<sp> path-name" },
529 	{ "PWD",  XPWD, ARGS, 1,	"(return current directory)" },
530 	{ "XPWD", XPWD, ARGS, 1,	"(return current directory)" },
531 	{ "CDUP", XCUP, ARGS, 1,	"(change to parent directory)" },
532 	{ "XCUP", XCUP, ARGS, 1,	"(change to parent directory)" },
533 	{ "STOU", STOU, STR1, 1,	"<sp> file-name" },
534 	{ NULL,   0,    0,    0,	0 }
535 };
536 
537 struct tab *
538 lookup(cmd)
539 	char *cmd;
540 {
541 	register struct tab *p;
542 
543 	for (p = cmdtab; p->name != NULL; p++)
544 		if (strcmp(cmd, p->name) == 0)
545 			return (p);
546 	return (0);
547 }
548 
549 #include <arpa/telnet.h>
550 
551 /*
552  * getline - a hacked up version of fgets to ignore TELNET escape codes.
553  */
554 char *
555 getline(s, n, iop)
556 	char *s;
557 	register FILE *iop;
558 {
559 	register c;
560 	register char *cs;
561 
562 	cs = s;
563 /* tmpline may contain saved command from urgent mode interruption */
564 	for (c = 0; tmpline[c] != '\0' && --n > 0; ++c) {
565 		*cs++ = tmpline[c];
566 		if (tmpline[c] == '\n') {
567 			*cs++ = '\0';
568 			if (debug)
569 				syslog(LOG_DEBUG, "command: %s", s);
570 			tmpline[0] = '\0';
571 			return(s);
572 		}
573 		if (c == 0)
574 			tmpline[0] = '\0';
575 	}
576 	while ((c = getc(iop)) != EOF) {
577 		c &= 0377;
578 		if (c == IAC) {
579 		    if ((c = getc(iop)) != EOF) {
580 			c &= 0377;
581 			switch (c) {
582 			case WILL:
583 			case WONT:
584 				c = getc(iop);
585 				printf("%c%c%c", IAC, DONT, 0377&c);
586 				(void) fflush(stdout);
587 				continue;
588 			case DO:
589 			case DONT:
590 				c = getc(iop);
591 				printf("%c%c%c", IAC, WONT, 0377&c);
592 				(void) fflush(stdout);
593 				continue;
594 			case IAC:
595 				break;
596 			default:
597 				continue;	/* ignore command */
598 			}
599 		    }
600 		}
601 		*cs++ = c;
602 		if (--n <= 0 || c == '\n')
603 			break;
604 	}
605 	if (c == EOF && cs == s)
606 		return (NULL);
607 	*cs++ = '\0';
608 	if (debug)
609 		syslog(LOG_DEBUG, "command: %s", s);
610 	return (s);
611 }
612 
613 static int
614 toolong()
615 {
616 	time_t now;
617 	extern char *ctime();
618 	extern time_t time();
619 
620 	reply(421,
621 	  "Timeout (%d seconds): closing control connection.", timeout);
622 	(void) time(&now);
623 	if (logging) {
624 		syslog(LOG_INFO,
625 			"User %s timed out after %d seconds at %s",
626 			(pw ? pw -> pw_name : "unknown"), timeout, ctime(&now));
627 	}
628 	dologout(1);
629 }
630 
631 yylex()
632 {
633 	static int cpos, state;
634 	register char *cp;
635 	register struct tab *p;
636 	int n;
637 	char c, *strpbrk();
638 
639 	for (;;) {
640 		switch (state) {
641 
642 		case CMD:
643 			(void) signal(SIGALRM, toolong);
644 			(void) alarm((unsigned) timeout);
645 			if (getline(cbuf, sizeof(cbuf)-1, stdin) == NULL) {
646 				reply(221, "You could at least say goodbye.");
647 				dologout(0);
648 			}
649 			(void) alarm(0);
650 			if ((cp = index(cbuf, '\r')))
651 				*cp++ = '\n'; *cp = '\0';
652 			if ((cp = strpbrk(cbuf, " \n")))
653 				cpos = cp - cbuf;
654 			if (cpos == 0)
655 				cpos = 4;
656 			c = cbuf[cpos];
657 			cbuf[cpos] = '\0';
658 			upper(cbuf);
659 			p = lookup(cbuf);
660 			cbuf[cpos] = c;
661 			if (p != 0) {
662 				if (p->implemented == 0) {
663 					nack(p->name);
664 					longjmp(errcatch,0);
665 					/* NOTREACHED */
666 				}
667 				state = p->state;
668 				yylval = (int) p->name;
669 				return (p->token);
670 			}
671 			break;
672 
673 		case OSTR:
674 			if (cbuf[cpos] == '\n') {
675 				state = CMD;
676 				return (CRLF);
677 			}
678 			/* FALL THRU */
679 
680 		case STR1:
681 		case ZSTR1:
682 			if (cbuf[cpos] == ' ') {
683 				cpos++;
684 				state++;
685 				return (SP);
686 			}
687 			break;
688 
689 		case ZSTR2:
690 			if (cbuf[cpos] == '\n') {
691 				state = CMD;
692 				return (CRLF);
693 			}
694 			/* FALL THRU */
695 
696 		case STR2:
697 			cp = &cbuf[cpos];
698 			n = strlen(cp);
699 			cpos += n - 1;
700 			/*
701 			 * Make sure the string is nonempty and \n terminated.
702 			 */
703 			if (n > 1 && cbuf[cpos] == '\n') {
704 				cbuf[cpos] = '\0';
705 				yylval = copy(cp);
706 				cbuf[cpos] = '\n';
707 				state = ARGS;
708 				return (STRING);
709 			}
710 			break;
711 
712 		case ARGS:
713 			if (isdigit(cbuf[cpos])) {
714 				cp = &cbuf[cpos];
715 				while (isdigit(cbuf[++cpos]))
716 					;
717 				c = cbuf[cpos];
718 				cbuf[cpos] = '\0';
719 				yylval = atoi(cp);
720 				cbuf[cpos] = c;
721 				return (NUMBER);
722 			}
723 			switch (cbuf[cpos++]) {
724 
725 			case '\n':
726 				state = CMD;
727 				return (CRLF);
728 
729 			case ' ':
730 				return (SP);
731 
732 			case ',':
733 				return (COMMA);
734 
735 			case 'A':
736 			case 'a':
737 				return (A);
738 
739 			case 'B':
740 			case 'b':
741 				return (B);
742 
743 			case 'C':
744 			case 'c':
745 				return (C);
746 
747 			case 'E':
748 			case 'e':
749 				return (E);
750 
751 			case 'F':
752 			case 'f':
753 				return (F);
754 
755 			case 'I':
756 			case 'i':
757 				return (I);
758 
759 			case 'L':
760 			case 'l':
761 				return (L);
762 
763 			case 'N':
764 			case 'n':
765 				return (N);
766 
767 			case 'P':
768 			case 'p':
769 				return (P);
770 
771 			case 'R':
772 			case 'r':
773 				return (R);
774 
775 			case 'S':
776 			case 's':
777 				return (S);
778 
779 			case 'T':
780 			case 't':
781 				return (T);
782 
783 			}
784 			break;
785 
786 		default:
787 			fatal("Unknown state in scanner.");
788 		}
789 		yyerror((char *) 0);
790 		state = CMD;
791 		longjmp(errcatch,0);
792 	}
793 }
794 
795 upper(s)
796 	register char *s;
797 {
798 	while (*s != '\0') {
799 		if (islower(*s))
800 			*s = toupper(*s);
801 		s++;
802 	}
803 }
804 
805 copy(s)
806 	char *s;
807 {
808 	char *p;
809 	extern char *malloc(), *strcpy();
810 
811 	p = malloc((unsigned) strlen(s) + 1);
812 	if (p == NULL)
813 		fatal("Ran out of memory.");
814 	(void) strcpy(p, s);
815 	return ((int)p);
816 }
817 
818 help(s)
819 	char *s;
820 {
821 	register struct tab *c;
822 	register int width, NCMDS;
823 
824 	width = 0, NCMDS = 0;
825 	for (c = cmdtab; c->name != NULL; c++) {
826 		int len = strlen(c->name) + 1;
827 
828 		if (len > width)
829 			width = len;
830 		NCMDS++;
831 	}
832 	width = (width + 8) &~ 7;
833 	if (s == 0) {
834 		register int i, j, w;
835 		int columns, lines;
836 
837 		lreply(214,
838 	  "The following commands are recognized (* =>'s unimplemented).");
839 		columns = 76 / width;
840 		if (columns == 0)
841 			columns = 1;
842 		lines = (NCMDS + columns - 1) / columns;
843 		for (i = 0; i < lines; i++) {
844 			printf("   ");
845 			for (j = 0; j < columns; j++) {
846 				c = cmdtab + j * lines + i;
847 				printf("%s%c", c->name,
848 					c->implemented ? ' ' : '*');
849 				if (c + lines >= &cmdtab[NCMDS])
850 					break;
851 				w = strlen(c->name) + 1;
852 				while (w < width) {
853 					putchar(' ');
854 					w++;
855 				}
856 			}
857 			printf("\r\n");
858 		}
859 		(void) fflush(stdout);
860 		reply(214, "Direct comments to ftp-bugs@%s.", hostname);
861 		return;
862 	}
863 	upper(s);
864 	c = lookup(s);
865 	if (c == (struct tab *)0) {
866 		reply(502, "Unknown command %s.", s);
867 		return;
868 	}
869 	if (c->implemented)
870 		reply(214, "Syntax: %s %s", c->name, c->help);
871 	else
872 		reply(214, "%-*s\t%s; unimplemented.", width, c->name, c->help);
873 }
874