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