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