1/* A front-end using readline to "cook" input lines. 2 * 3 * Copyright (C) 2004, 1999 Per Bothner 4 * 5 * This front-end program is free software; you can redistribute it and/or 6 * modify it under the terms of the GNU General Public License as published 7 * by the Free Software Foundation; either version 2, or (at your option) 8 * any later version. 9 * 10 * Some code from Johnson & Troan: "Linux Application Development" 11 * (Addison-Wesley, 1998) was used directly or for inspiration. 12 * 13 * 2003-11-07 Wolfgang Taeuber <wolfgang_taeuber@agilent.com> 14 * Specify a history file and the size of the history file with command 15 * line options; use EDITOR/VISUAL to set vi/emacs preference. 16 */ 17 18/* PROBLEMS/TODO: 19 * 20 * Only tested under GNU/Linux and Mac OS 10.x; needs to be ported. 21 * 22 * Switching between line-editing-mode vs raw-char-mode depending on 23 * what tcgetattr returns is inherently not robust, plus it doesn't 24 * work when ssh/telnetting in. A better solution is possible if the 25 * tty system can send in-line escape sequences indicating the current 26 * mode, echo'd input, etc. That would also allow a user preference 27 * to set different colors for prompt, input, stdout, and stderr. 28 * 29 * When running mc -c under the Linux console, mc does not recognize 30 * mouse clicks, which mc does when not running under rlfe. 31 * 32 * Pasting selected text containing tabs is like hitting the tab character, 33 * which invokes readline completion. We don't want this. I don't know 34 * if this is fixable without integrating rlfe into a terminal emulator. 35 * 36 * Echo suppression is a kludge, but can only be avoided with better kernel 37 * support: We need a tty mode to disable "real" echoing, while still 38 * letting the inferior think its tty driver to doing echoing. 39 * Stevens's book claims SCR$ and BSD4.3+ have TIOCREMOTE. 40 * 41 * The latest readline may have some hooks we can use to avoid having 42 * to back up the prompt. (See HAVE_ALREADY_PROMPTED.) 43 * 44 * Desirable readline feature: When in cooked no-echo mode (e.g. password), 45 * echo characters are they are types with '*', but remove them when done. 46 * 47 * Asynchronous output while we're editing an input line should be 48 * inserted in the output view *before* the input line, so that the 49 * lines being edited (with the prompt) float at the end of the input. 50 * 51 * A "page mode" option to emulate more/less behavior: At each page of 52 * output, pause for a user command. This required parsing the output 53 * to keep track of line lengths. It also requires remembering the 54 * output, if we want an option to scroll back, which suggests that 55 * this should be integrated with a terminal emulator like xterm. 56 */ 57 58#include <stdio.h> 59#include <fcntl.h> 60#include <sys/types.h> 61#include <sys/socket.h> 62#include <netinet/in.h> 63#include <arpa/inet.h> 64#include <signal.h> 65#include <netdb.h> 66#include <stdlib.h> 67#include <errno.h> 68#include <grp.h> 69#include <string.h> 70#include <sys/stat.h> 71#include <unistd.h> 72#include <sys/ioctl.h> 73#include <termios.h> 74 75#include "config.h" 76#include "extern.h" 77 78#if defined (HAVE_SYS_WAIT_H) 79# include <sys/wait.h> 80#endif 81 82#ifdef READLINE_LIBRARY 83# include "readline.h" 84# include "history.h" 85#else 86# include <readline/readline.h> 87# include <readline/history.h> 88#endif 89 90#ifndef COMMAND 91#define COMMAND "/bin/bash" 92#endif 93#ifndef COMMAND_ARGS 94#define COMMAND_ARGS COMMAND 95#endif 96 97#ifndef ALT_COMMAND 98#define ALT_COMMAND "/bin/sh" 99#endif 100#ifndef ALT_COMMAND_ARGS 101#define ALT_COMMAND_ARGS ALT_COMMAND 102#endif 103 104#ifndef HAVE_MEMMOVE 105# if __GNUC__ > 1 106# define memmove(d, s, n) __builtin_memcpy(d, s, n) 107# else 108# define memmove(d, s, n) memcpy(d, s, n) 109# endif 110#else 111# define memmove(d, s, n) memcpy(d, s, n) 112#endif 113 114#define APPLICATION_NAME "rlfe" 115 116static int in_from_inferior_fd; 117static int out_to_inferior_fd; 118static void set_edit_mode (); 119static void usage_exit (); 120static char *hist_file = 0; 121static int hist_size = 0; 122 123/* Unfortunately, we cannot safely display echo from the inferior process. 124 The reason is that the echo bit in the pty is "owned" by the inferior, 125 and if we try to turn it off, we could confuse the inferior. 126 Thus, when echoing, we get echo twice: First readline echoes while 127 we're actually editing. Then we send the line to the inferior, and the 128 terminal driver send back an extra echo. 129 The work-around is to remember the input lines, and when we see that 130 line come back, we supress the output. 131 A better solution (supposedly available on SVR4) would be a smarter 132 terminal driver, with more flags ... */ 133#define ECHO_SUPPRESS_MAX 1024 134char echo_suppress_buffer[ECHO_SUPPRESS_MAX]; 135int echo_suppress_start = 0; 136int echo_suppress_limit = 0; 137 138/*#define DEBUG*/ 139 140#ifdef DEBUG 141FILE *logfile = NULL; 142#define DPRINT0(FMT) (fprintf(logfile, FMT), fflush(logfile)) 143#define DPRINT1(FMT, V1) (fprintf(logfile, FMT, V1), fflush(logfile)) 144#define DPRINT2(FMT, V1, V2) (fprintf(logfile, FMT, V1, V2), fflush(logfile)) 145#else 146#define DPRINT0(FMT) ((void) 0) /* Do nothing */ 147#define DPRINT1(FMT, V1) ((void) 0) /* Do nothing */ 148#define DPRINT2(FMT, V1, V2) ((void) 0) /* Do nothing */ 149#endif 150 151struct termios orig_term; 152 153/* Pid of child process. */ 154static pid_t child = -1; 155 156static void 157sig_child (int signo) 158{ 159 int status; 160 wait (&status); 161 if (hist_file != 0) 162 { 163 write_history (hist_file); 164 if (hist_size) 165 history_truncate_file (hist_file, hist_size); 166 } 167 DPRINT0 ("(Child process died.)\n"); 168 tcsetattr(STDIN_FILENO, TCSANOW, &orig_term); 169 exit (0); 170} 171 172volatile int propagate_sigwinch = 0; 173 174/* sigwinch_handler 175 * propagate window size changes from input file descriptor to 176 * master side of pty. 177 */ 178void sigwinch_handler(int signal) { 179 propagate_sigwinch = 1; 180} 181 182 183/* get_slave_pty() returns an integer file descriptor. 184 * If it returns < 0, an error has occurred. 185 * Otherwise, it has returned the slave file descriptor. 186 */ 187 188int get_slave_pty(char *name) { 189 struct group *gptr; 190 gid_t gid; 191 int slave = -1; 192 193 /* chown/chmod the corresponding pty, if possible. 194 * This will only work if the process has root permissions. 195 * Alternatively, write and exec a small setuid program that 196 * does just this. 197 */ 198 if ((gptr = getgrnam("tty")) != 0) { 199 gid = gptr->gr_gid; 200 } else { 201 /* if the tty group does not exist, don't change the 202 * group on the slave pty, only the owner 203 */ 204 gid = -1; 205 } 206 207 /* Note that we do not check for errors here. If this is code 208 * where these actions are critical, check for errors! 209 */ 210 chown(name, getuid(), gid); 211 /* This code only makes the slave read/writeable for the user. 212 * If this is for an interactive shell that will want to 213 * receive "write" and "wall" messages, OR S_IWGRP into the 214 * second argument below. 215 */ 216 chmod(name, S_IRUSR|S_IWUSR); 217 218 /* open the corresponding slave pty */ 219 slave = open(name, O_RDWR); 220 return (slave); 221} 222 223/* Certain special characters, such as ctrl/C, we want to pass directly 224 to the inferior, rather than letting readline handle them. */ 225 226static char special_chars[20]; 227static int special_chars_count; 228 229static void 230add_special_char(int ch) 231{ 232 if (ch != 0) 233 special_chars[special_chars_count++] = ch; 234} 235 236static int eof_char; 237 238static int 239is_special_char(int ch) 240{ 241 int i; 242#if 0 243 if (ch == eof_char && rl_point == rl_end) 244 return 1; 245#endif 246 for (i = special_chars_count; --i >= 0; ) 247 if (special_chars[i] == ch) 248 return 1; 249 return 0; 250} 251 252static char buf[1024]; 253/* buf[0 .. buf_count-1] is the what has been emitted on the current line. 254 It is used as the readline prompt. */ 255static int buf_count = 0; 256 257int do_emphasize_input = 1; 258int current_emphasize_input; 259 260char *start_input_mode = "\033[1m"; 261char *end_input_mode = "\033[0m"; 262 263int num_keys = 0; 264 265static void maybe_emphasize_input (int on) 266{ 267 if (on == current_emphasize_input 268 || (on && ! do_emphasize_input)) 269 return; 270 fprintf (rl_outstream, on ? start_input_mode : end_input_mode); 271 fflush (rl_outstream); 272 current_emphasize_input = on; 273} 274 275static void 276null_prep_terminal (int meta) 277{ 278} 279 280static void 281null_deprep_terminal () 282{ 283 maybe_emphasize_input (0); 284} 285 286static int 287pre_input_change_mode (void) 288{ 289 return 0; 290} 291 292char pending_special_char; 293 294static void 295line_handler (char *line) 296{ 297 if (line == NULL) 298 { 299 char buf[1]; 300 DPRINT0("saw eof!\n"); 301 buf[0] = '\004'; /* ctrl/d */ 302 write (out_to_inferior_fd, buf, 1); 303 } 304 else 305 { 306 static char enter[] = "\r"; 307 /* Send line to inferior: */ 308 int length = strlen (line); 309 if (length > ECHO_SUPPRESS_MAX-2) 310 { 311 echo_suppress_start = 0; 312 echo_suppress_limit = 0; 313 } 314 else 315 { 316 if (echo_suppress_limit + length > ECHO_SUPPRESS_MAX - 2) 317 { 318 if (echo_suppress_limit - echo_suppress_start + length 319 <= ECHO_SUPPRESS_MAX - 2) 320 { 321 memmove (echo_suppress_buffer, 322 echo_suppress_buffer + echo_suppress_start, 323 echo_suppress_limit - echo_suppress_start); 324 echo_suppress_limit -= echo_suppress_start; 325 echo_suppress_start = 0; 326 } 327 else 328 { 329 echo_suppress_limit = 0; 330 } 331 echo_suppress_start = 0; 332 } 333 memcpy (echo_suppress_buffer + echo_suppress_limit, 334 line, length); 335 echo_suppress_limit += length; 336 echo_suppress_buffer[echo_suppress_limit++] = '\r'; 337 echo_suppress_buffer[echo_suppress_limit++] = '\n'; 338 } 339 write (out_to_inferior_fd, line, length); 340 if (pending_special_char == 0) 341 { 342 write (out_to_inferior_fd, enter, sizeof(enter)-1); 343 if (*line) 344 add_history (line); 345 } 346 free (line); 347 } 348 rl_callback_handler_remove (); 349 buf_count = 0; 350 num_keys = 0; 351 if (pending_special_char != 0) 352 { 353 write (out_to_inferior_fd, &pending_special_char, 1); 354 pending_special_char = 0; 355 } 356} 357 358/* Value of rl_getc_function. 359 Use this because readline should read from stdin, not rl_instream, 360 points to the pty (so readline has monitor its terminal modes). */ 361 362int 363my_rl_getc (FILE *dummy) 364{ 365 int ch = rl_getc (stdin); 366 if (is_special_char (ch)) 367 { 368 pending_special_char = ch; 369 return '\r'; 370 } 371 return ch; 372} 373 374int 375main(int argc, char** argv) 376{ 377 char *path; 378 int i; 379 int master; 380 char *name; 381 int in_from_tty_fd; 382 struct sigaction act; 383 struct winsize ws; 384 struct termios t; 385 int maxfd; 386 fd_set in_set; 387 static char empty_string[1] = ""; 388 char *prompt = empty_string; 389 int ioctl_err = 0; 390 int arg_base = 1; 391 392#ifdef DEBUG 393 logfile = fopen("/tmp/rlfe.log", "w"); 394#endif 395 396 while (arg_base<argc) 397 { 398 if (argv[arg_base][0] != '-') 399 break; 400 if (arg_base+1 >= argc ) 401 usage_exit(); 402 switch(argv[arg_base][1]) 403 { 404 case 'h': 405 arg_base++; 406 hist_file = argv[arg_base]; 407 break; 408 case 's': 409 arg_base++; 410 hist_size = atoi(argv[arg_base]); 411 if (hist_size<0) 412 usage_exit(); 413 break; 414 default: 415 usage_exit(); 416 } 417 arg_base++; 418 } 419 if (hist_file) 420 read_history (hist_file); 421 422 set_edit_mode (); 423 424 rl_readline_name = APPLICATION_NAME; 425 426 if ((master = OpenPTY (&name)) < 0) 427 { 428 perror("ptypair: could not open master pty"); 429 exit(1); 430 } 431 432 DPRINT1("pty name: '%s'\n", name); 433 434 /* set up SIGWINCH handler */ 435 act.sa_handler = sigwinch_handler; 436 sigemptyset(&(act.sa_mask)); 437 act.sa_flags = 0; 438 if (sigaction(SIGWINCH, &act, NULL) < 0) 439 { 440 perror("ptypair: could not handle SIGWINCH "); 441 exit(1); 442 } 443 444 if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) 445 { 446 perror("ptypair: could not get window size"); 447 exit(1); 448 } 449 450 if ((child = fork()) < 0) 451 { 452 perror("cannot fork"); 453 exit(1); 454 } 455 456 if (child == 0) 457 { 458 int slave; /* file descriptor for slave pty */ 459 460 /* We are in the child process */ 461 close(master); 462 463#ifdef TIOCSCTTY 464 if ((slave = get_slave_pty(name)) < 0) 465 { 466 perror("ptypair: could not open slave pty"); 467 exit(1); 468 } 469#endif 470 471 /* We need to make this process a session group leader, because 472 * it is on a new PTY, and things like job control simply will 473 * not work correctly unless there is a session group leader 474 * and process group leader (which a session group leader 475 * automatically is). This also disassociates us from our old 476 * controlling tty. 477 */ 478 if (setsid() < 0) 479 { 480 perror("could not set session leader"); 481 } 482 483 /* Tie us to our new controlling tty. */ 484#ifdef TIOCSCTTY 485 if (ioctl(slave, TIOCSCTTY, NULL)) 486 { 487 perror("could not set new controlling tty"); 488 } 489#else 490 if ((slave = get_slave_pty(name)) < 0) 491 { 492 perror("ptypair: could not open slave pty"); 493 exit(1); 494 } 495#endif 496 497 /* make slave pty be standard in, out, and error */ 498 dup2(slave, STDIN_FILENO); 499 dup2(slave, STDOUT_FILENO); 500 dup2(slave, STDERR_FILENO); 501 502 /* at this point the slave pty should be standard input */ 503 if (slave > 2) 504 { 505 close(slave); 506 } 507 508 /* Try to restore window size; failure isn't critical */ 509 if (ioctl(STDOUT_FILENO, TIOCSWINSZ, &ws) < 0) 510 { 511 perror("could not restore window size"); 512 } 513 514 /* now start the shell */ 515 { 516 static char* command_args[] = { COMMAND_ARGS, NULL }; 517 static char* alt_command_args[] = { ALT_COMMAND_ARGS, NULL }; 518 if (argc <= 1) 519 { 520 execvp (COMMAND, command_args); 521 execvp (ALT_COMMAND, alt_command_args); 522 } 523 else 524 execvp (argv[arg_base], &argv[arg_base]); 525 } 526 527 /* should never be reached */ 528 exit(1); 529 } 530 531 /* parent */ 532 signal (SIGCHLD, sig_child); 533 534 /* Note that we only set termios settings for standard input; 535 * the master side of a pty is NOT a tty. 536 */ 537 tcgetattr(STDIN_FILENO, &orig_term); 538 539 t = orig_term; 540 eof_char = t.c_cc[VEOF]; 541 /* add_special_char(t.c_cc[VEOF]);*/ 542 add_special_char(t.c_cc[VINTR]); 543 add_special_char(t.c_cc[VQUIT]); 544 add_special_char(t.c_cc[VSUSP]); 545#if defined (VDISCARD) 546 add_special_char(t.c_cc[VDISCARD]); 547#endif 548 549 t.c_lflag &= ~(ICANON | ISIG | ECHO | ECHOCTL | ECHOE | \ 550 ECHOK | ECHONL 551#if defined (ECHOKE) 552 | ECHOKE 553#endif 554#if defined (ECHOPRT) 555 | ECHOPRT 556#endif 557 ); 558 t.c_iflag &= ~ICRNL; 559 t.c_iflag |= IGNBRK; 560 t.c_cc[VMIN] = 1; 561 t.c_cc[VTIME] = 0; 562 tcsetattr(STDIN_FILENO, TCSANOW, &t); 563 in_from_inferior_fd = master; 564 out_to_inferior_fd = master; 565 rl_instream = fdopen (master, "r"); 566 rl_getc_function = my_rl_getc; 567 568 rl_prep_term_function = null_prep_terminal; 569 rl_deprep_term_function = null_deprep_terminal; 570 rl_pre_input_hook = pre_input_change_mode; 571 rl_callback_handler_install (prompt, line_handler); 572 573 in_from_tty_fd = STDIN_FILENO; 574 FD_ZERO (&in_set); 575 maxfd = in_from_inferior_fd > in_from_tty_fd ? in_from_inferior_fd 576 : in_from_tty_fd; 577 for (;;) 578 { 579 int num; 580 FD_SET (in_from_inferior_fd, &in_set); 581 FD_SET (in_from_tty_fd, &in_set); 582 583 num = select(maxfd+1, &in_set, NULL, NULL, NULL); 584 585 if (propagate_sigwinch) 586 { 587 struct winsize ws; 588 if (ioctl (STDIN_FILENO, TIOCGWINSZ, &ws) >= 0) 589 { 590 ioctl (master, TIOCSWINSZ, &ws); 591 } 592 propagate_sigwinch = 0; 593 continue; 594 } 595 596 if (num <= 0) 597 { 598 perror ("select"); 599 exit (-1); 600 } 601 if (FD_ISSET (in_from_tty_fd, &in_set)) 602 { 603 extern int _rl_echoing_p; 604 struct termios term_master; 605 int do_canon = 1; 606 int do_icrnl = 1; 607 int ioctl_ret; 608 609 DPRINT1("[tty avail num_keys:%d]\n", num_keys); 610 611 /* If we can't get tty modes for the master side of the pty, we 612 can't handle non-canonical-mode programs. Always assume the 613 master is in canonical echo mode if we can't tell. */ 614 ioctl_ret = tcgetattr(master, &term_master); 615 616 if (ioctl_ret >= 0) 617 { 618 do_canon = (term_master.c_lflag & ICANON) != 0; 619 do_icrnl = (term_master.c_lflag & ICRNL) != 0; 620 _rl_echoing_p = (term_master.c_lflag & ECHO) != 0; 621 DPRINT1 ("echo,canon,crnl:%03d\n", 622 100 * _rl_echoing_p 623 + 10 * do_canon 624 + 1 * do_icrnl); 625 } 626 else 627 { 628 if (ioctl_err == 0) 629 DPRINT1("tcgetattr on master fd failed: errno = %d\n", errno); 630 ioctl_err = 1; 631 } 632 633 if (do_canon == 0 && num_keys == 0) 634 { 635 char ch[10]; 636 int count = read (STDIN_FILENO, ch, sizeof(ch)); 637 DPRINT1("[read %d chars from stdin: ", count); 638 DPRINT2(" \"%.*s\"]\n", count, ch); 639 if (do_icrnl) 640 { 641 int i = count; 642 while (--i >= 0) 643 { 644 if (ch[i] == '\r') 645 ch[i] = '\n'; 646 } 647 } 648 maybe_emphasize_input (1); 649 write (out_to_inferior_fd, ch, count); 650 } 651 else 652 { 653 if (num_keys == 0) 654 { 655 int i; 656 /* Re-install callback handler for new prompt. */ 657 if (prompt != empty_string) 658 free (prompt); 659 if (prompt == NULL) 660 { 661 DPRINT0("New empty prompt\n"); 662 prompt = empty_string; 663 } 664 else 665 { 666 if (do_emphasize_input && buf_count > 0) 667 { 668 prompt = malloc (buf_count + strlen (end_input_mode) 669 + strlen (start_input_mode) + 5); 670 sprintf (prompt, "\001%s\002%.*s\001%s\002", 671 end_input_mode, 672 buf_count, buf, 673 start_input_mode); 674 } 675 else 676 { 677 prompt = malloc (buf_count + 1); 678 memcpy (prompt, buf, buf_count); 679 prompt[buf_count] = '\0'; 680 } 681 DPRINT1("New prompt '%s'\n", prompt); 682#if 0 /* ifdef HAVE_RL_ALREADY_PROMPTED */ 683 /* Doesn't quite work when do_emphasize_input is 1. */ 684 rl_already_prompted = buf_count > 0; 685#else 686 if (buf_count > 0) 687 write (1, "\r", 1); 688#endif 689 } 690 691 rl_callback_handler_install (prompt, line_handler); 692 } 693 num_keys++; 694 maybe_emphasize_input (1); 695 rl_callback_read_char (); 696 } 697 } 698 else /* output from inferior. */ 699 { 700 int i; 701 int count; 702 int old_count; 703 if (buf_count > (sizeof(buf) >> 2)) 704 buf_count = 0; 705 count = read (in_from_inferior_fd, buf+buf_count, 706 sizeof(buf) - buf_count); 707 DPRINT2("read %d from inferior, buf_count=%d", count, buf_count); 708 DPRINT2(": \"%.*s\"", count, buf+buf_count); 709 maybe_emphasize_input (0); 710 if (count <= 0) 711 { 712 DPRINT0 ("(Connection closed by foreign host.)\n"); 713 tcsetattr(STDIN_FILENO, TCSANOW, &orig_term); 714 exit (0); 715 } 716 old_count = buf_count; 717 718 /* Look for any pending echo that we need to suppress. */ 719 while (echo_suppress_start < echo_suppress_limit 720 && count > 0 721 && buf[buf_count] == echo_suppress_buffer[echo_suppress_start]) 722 { 723 count--; 724 buf_count++; 725 echo_suppress_start++; 726 } 727 DPRINT1("suppressed %d characters of echo.\n", buf_count-old_count); 728 729 /* Write to the terminal anything that was not suppressed. */ 730 if (count > 0) 731 write (1, buf + buf_count, count); 732 733 /* Finally, look for a prompt candidate. 734 * When we get around to going input (from the keyboard), 735 * we will consider the prompt to be anything since the last 736 * line terminator. So we need to save that text in the 737 * initial part of buf. However, anything before the 738 * most recent end-of-line is not interesting. */ 739 buf_count += count; 740#if 1 741 for (i = buf_count; --i >= old_count; ) 742#else 743 for (i = buf_count - 1; i-- >= buf_count - count; ) 744#endif 745 { 746 if (buf[i] == '\n' || buf[i] == '\r') 747 { 748 i++; 749 memmove (buf, buf+i, buf_count - i); 750 buf_count -= i; 751 break; 752 } 753 } 754 DPRINT2("-> i: %d, buf_count: %d\n", i, buf_count); 755 } 756 } 757} 758 759static void set_edit_mode () 760{ 761 int vi = 0; 762 char *shellopts; 763 764 shellopts = getenv ("SHELLOPTS"); 765 while (shellopts != 0) 766 { 767 if (strncmp ("vi", shellopts, 2) == 0) 768 { 769 vi = 1; 770 break; 771 } 772 shellopts = strchr (shellopts + 1, ':'); 773 } 774 775 if (!vi) 776 { 777 if (getenv ("EDITOR") != 0) 778 vi |= strcmp (getenv ("EDITOR"), "vi") == 0; 779 } 780 781 if (vi) 782 rl_variable_bind ("editing-mode", "vi"); 783 else 784 rl_variable_bind ("editing-mode", "emacs"); 785} 786 787 788static void usage_exit () 789{ 790 fprintf (stderr, "Usage: rlfe [-h histfile] [-s size] cmd [arg1] [arg2] ...\n\n"); 791 exit (1); 792} 793