1 /* $NetBSD: util.c,v 1.9 1997/06/10 22:00:01 lukem Exp $ */ 2 3 /* 4 * Copyright (c) 1985, 1989, 1993, 1994 5 * The Regents of the University of California. All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 1. Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 3. All advertising materials mentioning features or use of this software 16 * must display the following acknowledgement: 17 * This product includes software developed by the University of 18 * California, Berkeley and its contributors. 19 * 4. Neither the name of the University nor the names of its contributors 20 * may be used to endorse or promote products derived from this software 21 * without specific prior written permission. 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 26 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 27 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 29 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 30 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 32 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 33 * SUCH DAMAGE. 34 */ 35 36 #ifndef lint 37 static char rcsid[] = "$NetBSD: util.c,v 1.9 1997/06/10 22:00:01 lukem Exp $"; 38 #endif /* not lint */ 39 40 /* 41 * FTP User Program -- Misc support routines 42 */ 43 #include <sys/ioctl.h> 44 #include <sys/time.h> 45 #include <arpa/ftp.h> 46 47 #include <ctype.h> 48 #include <err.h> 49 #include <fcntl.h> 50 #include <glob.h> 51 #include <pwd.h> 52 #include <stdio.h> 53 #include <string.h> 54 #include <time.h> 55 #include <unistd.h> 56 57 #include "ftp_var.h" 58 #include "pathnames.h" 59 60 /* 61 * Connect to peer server and 62 * auto-login, if possible. 63 */ 64 void 65 setpeer(argc, argv) 66 int argc; 67 char *argv[]; 68 { 69 char *host; 70 short port; 71 72 if (connected) { 73 printf("Already connected to %s, use close first.\n", 74 hostname); 75 code = -1; 76 return; 77 } 78 if (argc < 2) 79 (void)another(&argc, &argv, "to"); 80 if (argc < 2 || argc > 3) { 81 printf("usage: %s host-name [port]\n", argv[0]); 82 code = -1; 83 return; 84 } 85 port = ftpport; 86 if (argc > 2) { 87 port = atoi(argv[2]); 88 if (port <= 0) { 89 printf("%s: bad port number '%s'.\n", argv[1], argv[2]); 90 printf("usage: %s host-name [port]\n", argv[0]); 91 code = -1; 92 return; 93 } 94 port = htons(port); 95 } 96 host = hookup(argv[1], port); 97 if (host) { 98 int overbose; 99 100 connected = 1; 101 /* 102 * Set up defaults for FTP. 103 */ 104 (void)strcpy(typename, "ascii"), type = TYPE_A; 105 curtype = TYPE_A; 106 (void)strcpy(formname, "non-print"), form = FORM_N; 107 (void)strcpy(modename, "stream"), mode = MODE_S; 108 (void)strcpy(structname, "file"), stru = STRU_F; 109 (void)strcpy(bytename, "8"), bytesize = 8; 110 if (autologin) 111 (void)login(argv[1], NULL, NULL); 112 113 overbose = verbose; 114 if (debug == 0) 115 verbose = -1; 116 if (command("SYST") == COMPLETE && overbose) { 117 char *cp, c; 118 c = 0; 119 cp = strchr(reply_string+4, ' '); 120 if (cp == NULL) 121 cp = strchr(reply_string+4, '\r'); 122 if (cp) { 123 if (cp[-1] == '.') 124 cp--; 125 c = *cp; 126 *cp = '\0'; 127 } 128 129 printf("Remote system type is %s.\n", reply_string + 4); 130 if (cp) 131 *cp = c; 132 } 133 if (!strncmp(reply_string, "215 UNIX Type: L8", 17)) { 134 if (proxy) 135 unix_proxy = 1; 136 else 137 unix_server = 1; 138 /* 139 * Set type to 0 (not specified by user), 140 * meaning binary by default, but don't bother 141 * telling server. We can use binary 142 * for text files unless changed by the user. 143 */ 144 type = 0; 145 (void)strcpy(typename, "binary"); 146 if (overbose) 147 printf("Using %s mode to transfer files.\n", 148 typename); 149 } else { 150 if (proxy) 151 unix_proxy = 0; 152 else 153 unix_server = 0; 154 if (overbose && 155 !strncmp(reply_string, "215 TOPS20", 10)) 156 puts( 157 "Remember to set tenex mode when transferring binary files from this machine."); 158 } 159 verbose = overbose; 160 } 161 } 162 163 164 /* 165 * login to remote host, using given username & password if supplied 166 */ 167 int 168 login(host, user, pass) 169 const char *host; 170 char *user, *pass; 171 { 172 char tmp[80]; 173 char *acct; 174 char anonpass[MAXLOGNAME + 1 + MAXHOSTNAMELEN]; /* "user@hostname" */ 175 char hostname[MAXHOSTNAMELEN]; 176 int n, aflag = 0; 177 178 acct = NULL; 179 if (user == NULL) { 180 if (ruserpass(host, &user, &pass, &acct) < 0) { 181 code = -1; 182 return (0); 183 } 184 } 185 186 /* 187 * Set up arguments for an anonymous FTP session, if necessary. 188 */ 189 if ((user == NULL || pass == NULL) && anonftp) { 190 memset(anonpass, 0, sizeof(anonpass)); 191 memset(hostname, 0, sizeof(hostname)); 192 193 /* 194 * Set up anonymous login password. 195 */ 196 user = getlogin(); 197 gethostname(hostname, MAXHOSTNAMELEN); 198 #ifndef DONT_CHEAT_ANONPASS 199 /* 200 * Every anonymous FTP server I've encountered 201 * will accept the string "username@", and will 202 * append the hostname itself. We do this by default 203 * since many servers are picky about not having 204 * a FQDN in the anonymous password. - thorpej@netbsd.org 205 */ 206 snprintf(anonpass, sizeof(anonpass) - 1, "%s@", 207 user); 208 #else 209 snprintf(anonpass, sizeof(anonpass) - 1, "%s@%s", 210 user, hp->h_name); 211 #endif 212 pass = anonpass; 213 user = "anonymous"; /* as per RFC 1635 */ 214 } 215 216 while (user == NULL) { 217 char *myname = getlogin(); 218 219 if (myname == NULL) { 220 struct passwd *pp = getpwuid(getuid()); 221 222 if (pp != NULL) 223 myname = pp->pw_name; 224 } 225 if (myname) 226 printf("Name (%s:%s): ", host, myname); 227 else 228 printf("Name (%s): ", host); 229 (void)fgets(tmp, sizeof(tmp) - 1, stdin); 230 tmp[strlen(tmp) - 1] = '\0'; 231 if (*tmp == '\0') 232 user = myname; 233 else 234 user = tmp; 235 } 236 n = command("USER %s", user); 237 if (n == CONTINUE) { 238 if (pass == NULL) 239 pass = getpass("Password:"); 240 n = command("PASS %s", pass); 241 } 242 if (n == CONTINUE) { 243 aflag++; 244 if (acct == NULL) 245 acct = getpass("Account:"); 246 n = command("ACCT %s", acct); 247 } 248 if ((n != COMPLETE) || 249 (!aflag && acct != NULL && command("ACCT %s", acct) != COMPLETE)) { 250 warnx("Login failed."); 251 return (0); 252 } 253 if (proxy) 254 return (1); 255 connected = -1; 256 for (n = 0; n < macnum; ++n) { 257 if (!strcmp("init", macros[n].mac_name)) { 258 (void)strcpy(line, "$init"); 259 makeargv(); 260 domacro(margc, margv); 261 break; 262 } 263 } 264 return (1); 265 } 266 267 /* 268 * `another' gets another argument, and stores the new argc and argv. 269 * It reverts to the top level (via main.c's intr()) on EOF/error. 270 * 271 * Returns false if no new arguments have been added. 272 */ 273 int 274 another(pargc, pargv, prompt) 275 int *pargc; 276 char ***pargv; 277 const char *prompt; 278 { 279 int len = strlen(line), ret; 280 281 if (len >= sizeof(line) - 3) { 282 puts("sorry, arguments too long."); 283 intr(); 284 } 285 printf("(%s) ", prompt); 286 line[len++] = ' '; 287 if (fgets(&line[len], sizeof(line) - len, stdin) == NULL) 288 intr(); 289 len += strlen(&line[len]); 290 if (len > 0 && line[len - 1] == '\n') 291 line[len - 1] = '\0'; 292 makeargv(); 293 ret = margc > *pargc; 294 *pargc = margc; 295 *pargv = margv; 296 return (ret); 297 } 298 299 /* 300 * glob files given in argv[] from the remote server. 301 * if errbuf isn't NULL, store error messages there instead 302 * of writing to the screen. 303 */ 304 char * 305 remglob(argv, doswitch, errbuf) 306 char *argv[]; 307 int doswitch; 308 char **errbuf; 309 { 310 char temp[MAXPATHLEN]; 311 static char buf[MAXPATHLEN]; 312 static FILE *ftemp = NULL; 313 static char **args; 314 int oldverbose, oldhash, fd; 315 char *cp, *mode; 316 317 if (!mflag) { 318 if (!doglob) 319 args = NULL; 320 else { 321 if (ftemp) { 322 (void)fclose(ftemp); 323 ftemp = NULL; 324 } 325 } 326 return (NULL); 327 } 328 if (!doglob) { 329 if (args == NULL) 330 args = argv; 331 if ((cp = *++args) == NULL) 332 args = NULL; 333 return (cp); 334 } 335 if (ftemp == NULL) { 336 (void)snprintf(temp, sizeof(temp), "%s%s", _PATH_TMP, TMPFILE); 337 if ((fd = mkstemp(temp)) < 0) { 338 warn("unable to create temporary file %s", temp); 339 return (NULL); 340 } 341 close(fd); 342 oldverbose = verbose; 343 verbose = (errbuf != NULL) ? -1 : 0; 344 oldhash = hash; 345 hash = 0; 346 if (doswitch) 347 pswitch(!proxy); 348 for (mode = "w"; *++argv != NULL; mode = "a") 349 recvrequest("NLST", temp, *argv, mode, 0); 350 if ((code / 100) != COMPLETE) { 351 if (errbuf != NULL) 352 *errbuf = reply_string; 353 } 354 if (doswitch) 355 pswitch(!proxy); 356 verbose = oldverbose; 357 hash = oldhash; 358 ftemp = fopen(temp, "r"); 359 (void)unlink(temp); 360 if (ftemp == NULL) { 361 if (errbuf == NULL) 362 puts("can't find list of remote files, oops."); 363 else 364 *errbuf = 365 "can't find list of remote files, oops."; 366 return (NULL); 367 } 368 } 369 if (fgets(buf, sizeof(buf), ftemp) == NULL) { 370 (void)fclose(ftemp); 371 ftemp = NULL; 372 return (NULL); 373 } 374 if ((cp = strchr(buf, '\n')) != NULL) 375 *cp = '\0'; 376 return (buf); 377 } 378 379 int 380 confirm(cmd, file) 381 const char *cmd, *file; 382 { 383 char line[BUFSIZ]; 384 385 if (!interactive || confirmrest) 386 return (1); 387 printf("%s %s? ", cmd, file); 388 (void)fflush(stdout); 389 if (fgets(line, sizeof(line), stdin) == NULL) 390 return (0); 391 switch (tolower(*line)) { 392 case 'n': 393 return (0); 394 case 'p': 395 interactive = 0; 396 puts("Interactive mode: off."); 397 break; 398 case 'a': 399 confirmrest = 1; 400 printf("Prompting off for duration of %s.\n", cmd); 401 break; 402 } 403 return (1); 404 } 405 406 /* 407 * Glob a local file name specification with 408 * the expectation of a single return value. 409 * Can't control multiple values being expanded 410 * from the expression, we return only the first. 411 */ 412 int 413 globulize(cpp) 414 char **cpp; 415 { 416 glob_t gl; 417 int flags; 418 419 if (!doglob) 420 return (1); 421 422 flags = GLOB_BRACE|GLOB_NOCHECK|GLOB_QUOTE|GLOB_TILDE; 423 memset(&gl, 0, sizeof(gl)); 424 if (glob(*cpp, flags, NULL, &gl) || 425 gl.gl_pathc == 0) { 426 warnx("%s: not found", *cpp); 427 globfree(&gl); 428 return (0); 429 } 430 *cpp = strdup(gl.gl_pathv[0]); /* XXX - wasted memory */ 431 globfree(&gl); 432 return (1); 433 } 434 435 /* 436 * determine size of remote file 437 */ 438 off_t 439 remotesize(file, noisy) 440 const char *file; 441 int noisy; 442 { 443 int overbose; 444 off_t size; 445 446 overbose = verbose; 447 size = -1; 448 if (debug == 0) 449 verbose = -1; 450 if (command("SIZE %s", file) == COMPLETE) 451 sscanf(reply_string, "%*s %qd", &size); 452 else if (noisy && debug == 0) 453 puts(reply_string); 454 verbose = overbose; 455 return (size); 456 } 457 458 /* 459 * determine last modification time (in GMT) of remote file 460 */ 461 time_t 462 remotemodtime(file, noisy) 463 const char *file; 464 int noisy; 465 { 466 int overbose; 467 time_t rtime; 468 469 overbose = verbose; 470 rtime = -1; 471 if (debug == 0) 472 verbose = -1; 473 if (command("MDTM %s", file) == COMPLETE) { 474 struct tm timebuf; 475 int yy, mo, day, hour, min, sec; 476 sscanf(reply_string, "%*s %04d%02d%02d%02d%02d%02d", &yy, &mo, 477 &day, &hour, &min, &sec); 478 memset(&timebuf, 0, sizeof(timebuf)); 479 timebuf.tm_sec = sec; 480 timebuf.tm_min = min; 481 timebuf.tm_hour = hour; 482 timebuf.tm_mday = day; 483 timebuf.tm_mon = mo - 1; 484 timebuf.tm_year = yy - 1900; 485 timebuf.tm_isdst = -1; 486 rtime = mktime(&timebuf); 487 if (rtime == -1 && (noisy || debug != 0)) 488 printf("Can't convert %s to a time.\n", reply_string); 489 else 490 rtime += timebuf.tm_gmtoff; /* conv. local -> GMT */ 491 } else if (noisy && debug == 0) 492 puts(reply_string); 493 verbose = overbose; 494 return (rtime); 495 } 496 497 void 498 updateprogressmeter() 499 { 500 static pid_t pgrp = -1; 501 int ctty_pgrp; 502 503 if (pgrp == -1) 504 pgrp = getpgrp(); 505 506 /* 507 * print progress bar only if we are foreground process. 508 */ 509 if (ioctl(STDOUT_FILENO, TIOCGPGRP, &ctty_pgrp) != -1 && 510 ctty_pgrp == (int)pgrp) 511 progressmeter(0); 512 } 513 514 /* 515 * Display a transfer progress bar if progress is non-zero. 516 * SIGALRM is hijacked for use by this function. 517 * - Before the transfer, set filesize to size of file (or -1 if unknown), 518 * and call with flag = -1. This starts the once per second timer, 519 * and a call to updateprogressmeter() upon SIGALRM. 520 * - During the transfer, updateprogressmeter will call progressmeter 521 * with flag = 0 522 * - After the transfer, call with flag = 1 523 */ 524 static struct timeval start; 525 526 void 527 progressmeter(flag) 528 int flag; 529 { 530 /* 531 * List of order of magnitude prefixes. 532 * The last is `P', as 2^64 = 16384 Petabytes 533 */ 534 static const char prefixes[] = " KMGTP"; 535 536 static struct timeval lastupdate; 537 static off_t lastsize; 538 struct timeval now, td, wait; 539 off_t cursize, abbrevsize; 540 double elapsed; 541 int ratio, barlength, i, remaining; 542 char buf[256]; 543 544 if (flag == -1) { 545 (void)gettimeofday(&start, (struct timezone *)0); 546 lastupdate = start; 547 lastsize = restart_point; 548 } 549 (void)gettimeofday(&now, (struct timezone *)0); 550 if (!progress || filesize <= 0) 551 return; 552 cursize = bytes + restart_point; 553 554 ratio = cursize * 100 / filesize; 555 ratio = MAX(ratio, 0); 556 ratio = MIN(ratio, 100); 557 snprintf(buf, sizeof(buf), "\r%3d%% ", ratio); 558 559 barlength = ttywidth - 30; 560 if (barlength > 0) { 561 i = barlength * ratio / 100; 562 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), 563 "|%.*s%*s|", i, 564 "*****************************************************************************" 565 "*****************************************************************************", 566 barlength - i, ""); 567 } 568 569 i = 0; 570 abbrevsize = cursize; 571 while (abbrevsize >= 100000 && i < sizeof(prefixes)) { 572 i++; 573 abbrevsize >>= 10; 574 } 575 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), 576 " %5qd %c%c ", abbrevsize, prefixes[i], 577 prefixes[i] == ' ' ? ' ' : 'B'); 578 579 timersub(&now, &lastupdate, &wait); 580 if (cursize > lastsize) { 581 lastupdate = now; 582 lastsize = cursize; 583 if (wait.tv_sec >= STALLTIME) { /* fudge out stalled time */ 584 start.tv_sec += wait.tv_sec; 585 start.tv_usec += wait.tv_usec; 586 } 587 wait.tv_sec = 0; 588 } 589 590 timersub(&now, &start, &td); 591 elapsed = td.tv_sec + (td.tv_usec / 1000000.0); 592 593 if (bytes <= 0 || elapsed <= 0.0 || cursize > filesize) { 594 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), 595 " --:-- ETA"); 596 } else if (wait.tv_sec >= STALLTIME) { 597 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), 598 " - stalled -"); 599 } else { 600 remaining = (int)((filesize - restart_point) / 601 (bytes / elapsed) - elapsed); 602 i = remaining / 3600; 603 if (i) 604 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), 605 "%2d:", i); 606 else 607 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), 608 " "); 609 i = remaining % 3600; 610 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), 611 "%02d:%02d ETA", i / 60, i % 60); 612 } 613 (void)write(STDOUT_FILENO, buf, strlen(buf)); 614 615 if (flag == -1) { 616 (void)signal(SIGALRM, updateprogressmeter); 617 alarmtimer(1); /* set alarm timer for 1 Hz */ 618 } else if (flag == 1) { 619 alarmtimer(0); 620 (void)putchar('\n'); 621 } 622 fflush(stdout); 623 } 624 625 /* 626 * Display transfer statistics. 627 * Requires start to be initialised by progressmeter(-1), 628 * direction to be defined by xfer routines, and filesize and bytes 629 * to be updated by xfer routines 630 * If siginfo is nonzero, an ETA is displayed, and the output goes to STDERR 631 * instead of STDOUT. 632 */ 633 void 634 ptransfer(siginfo) 635 int siginfo; 636 { 637 struct timeval now, td; 638 double elapsed; 639 off_t bs; 640 int meg, remaining, hh; 641 char buf[100]; 642 643 if (!verbose && !siginfo) 644 return; 645 646 (void)gettimeofday(&now, (struct timezone *)0); 647 timersub(&now, &start, &td); 648 elapsed = td.tv_sec + (td.tv_usec / 1000000.0); 649 bs = bytes / (elapsed == 0.0 ? 1 : elapsed); 650 meg = 0; 651 if (bs > (1024 * 1024)) 652 meg = 1; 653 (void)snprintf(buf, sizeof(buf), 654 "%qd byte%s %s in %.2f seconds (%.2f %sB/s)\n", 655 bytes, bytes == 1 ? "" : "s", direction, elapsed, 656 bs / (1024.0 * (meg ? 1024.0 : 1.0)), meg ? "M" : "K"); 657 if (siginfo && bytes > 0 && elapsed > 0.0 && filesize >= 0 658 && bytes + restart_point <= filesize) { 659 remaining = (int)((filesize - restart_point) / 660 (bytes / elapsed) - elapsed); 661 hh = remaining / 3600; 662 remaining %= 3600; 663 /* "buf+len(buf) -1" to overwrite \n */ 664 snprintf(buf + strlen(buf) - 1, sizeof(buf) - strlen(buf), 665 " ETA: %02d:%02d:%02d\n", hh, remaining / 60, 666 remaining % 60); 667 } 668 (void)write(siginfo ? STDERR_FILENO : STDOUT_FILENO, buf, strlen(buf)); 669 } 670 671 /* 672 * List words in stringlist, vertically arranged 673 */ 674 void 675 list_vertical(sl) 676 StringList *sl; 677 { 678 int i, j, w; 679 int columns, width, lines, items; 680 char *p; 681 682 width = items = 0; 683 684 for (i = 0 ; i < sl->sl_cur ; i++) { 685 w = strlen(sl->sl_str[i]); 686 if (w > width) 687 width = w; 688 } 689 width = (width + 8) &~ 7; 690 691 columns = ttywidth / width; 692 if (columns == 0) 693 columns = 1; 694 lines = (sl->sl_cur + columns - 1) / columns; 695 for (i = 0; i < lines; i++) { 696 for (j = 0; j < columns; j++) { 697 p = sl->sl_str[j * lines + i]; 698 if (p) 699 fputs(p, stdout); 700 if (j * lines + i + lines >= sl->sl_cur) { 701 putchar('\n'); 702 break; 703 } 704 w = strlen(p); 705 while (w < width) { 706 w = (w + 8) &~ 7; 707 (void)putchar('\t'); 708 } 709 } 710 } 711 } 712 713 /* 714 * Update the global ttywidth value, using TIOCGWINSZ. 715 */ 716 void 717 setttywidth(a) 718 int a; 719 { 720 struct winsize winsize; 721 722 if (ioctl(fileno(stdout), TIOCGWINSZ, &winsize) != -1) 723 ttywidth = winsize.ws_col; 724 else 725 ttywidth = 80; 726 } 727 728 /* 729 * Set the SIGALRM interval timer for wait seconds, 0 to disable. 730 */ 731 void 732 alarmtimer(wait) 733 int wait; 734 { 735 struct itimerval itv; 736 737 itv.it_value.tv_sec = wait; 738 itv.it_value.tv_usec = 0; 739 itv.it_interval = itv.it_value; 740 setitimer(ITIMER_REAL, &itv, NULL); 741 } 742 743 /* 744 * Setup or cleanup EditLine structures 745 */ 746 #ifndef SMALL 747 void 748 controlediting() 749 { 750 if (editing && el == NULL && hist == NULL) { 751 el = el_init(__progname, stdin, stdout); /* init editline */ 752 hist = history_init(); /* init the builtin history */ 753 history(hist, H_EVENT, 100); /* remember 100 events */ 754 el_set(el, EL_HIST, history, hist); /* use history */ 755 756 el_set(el, EL_EDITOR, "emacs"); /* default editor is emacs */ 757 el_set(el, EL_PROMPT, prompt); /* set the prompt function */ 758 759 /* add local file completion, bind to TAB */ 760 el_set(el, EL_ADDFN, "ftp-complete", 761 "Context sensitive argument completion", 762 complete); 763 el_set(el, EL_BIND, "^I", "ftp-complete", NULL); 764 765 el_source(el, NULL); /* read ~/.editrc */ 766 el_set(el, EL_SIGNAL, 1); 767 } else if (!editing) { 768 if (hist) { 769 history_end(hist); 770 hist = NULL; 771 } 772 if (el) { 773 el_end(el); 774 el = NULL; 775 } 776 } 777 } 778 #endif /* !SMALL */ 779