/* * powwow -- mud client with telnet protocol * * Copyright (C) 1998,2000,2002 by Massimiliano Ghilardi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * * History: * * Initially inspired to the Tintin client by Peter Unold, * Powwow contains no Tintin code. * The original program Cancan, written by Mattias Engdegård (Yorick) * (f91-men@nada.kth.se) 1992-94, * was greatly improved upon by Vivriel, Thuzzle and Ilie and then * transformed from Cancan into Powwow by Cosmos who worked * to make it yet more powerful. * AmigaDOS porting attempt by Fror. * Many new features added by Dain. * As usual, all the developers are in debt to countless users * for suggestions and debugging. * Maintance was taken over by Steve Slaven (bpk@hoopajoo.net) in 2005 */ /* * Set this to whatever you like * * #define POWWOW_DIR "/home/gustav/powwow" */ #ifdef USE_LOCALE #define POWWOW_HACKERS "Yorick, Vivriel, Thuzzle, Ilie, Fr\363r, D\341in" #define COPYRIGHT "\251 " #else #define POWWOW_HACKERS "Yorick, Vivriel, Thuzzle, Ilie, Fror, Dain" #define COPYRIGHT "" #endif #define POWWOW_VERSION VERSION \ ", Copyright 2000-2005 by Cosmos\n" \ "Copyright 2005 by bpk - http://hoopajoo.net\n" \ "(contributions by " POWWOW_HACKERS ")\n" #define HELPNAME "powwow.help" #define COPYNAME "COPYING" #ifndef POWWOW_DIR # define POWWOW_DIR "./" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_LOCALE #include #endif /* are these really needed? */ extern int errno; extern int select(); #ifdef USE_REGEXP # include "malloc.h" # include #endif #include "defines.h" #include "main.h" #include "utils.h" #include "beam.h" #include "cmd.h" #include "cmd2.h" #include "edit.h" #include "map.h" #include "list.h" #include "tcp.h" #include "tty.h" #include "eval.h" #include "log.h" /* local function declarations */ #ifdef MOTDFILE static void printmotd __P ((void)); #endif static void mainloop __P ((void)); static void exec_delays __P ((void)); static void prompt_reset_iac __P ((void)); static void get_remote_input __P ((void)); static void get_user_input __P ((void)); static int search_action_or_prompt __P ((char *line, char clearline, char copyprompt)); #define search_action(line, clearline) search_action_or_prompt((line), (clearline), 0) #define search_prompt(line, copyprompt) search_action_or_prompt((line), 0, (copyprompt)+1) static void set_params __P ((char *line, int *match_s, int *match_e)); static void parse_commands __P ((char *command, char *arg)); static int subst_param __P ((ptr *buf, char *src)); static int jit_subst_vars __P ((ptr *buf, char *src)); /* GLOBALS */ static char *helpname = HELPNAME; static char *copyname = COPYNAME; long received = 0; /* amount of data received from remote host */ long sent = 0; /* amount of data sent to remote host */ char identified = 0; /* 1 after #identify */ VOLATILE char confirm = 0; /* 1 if just tried to quit */ int history_done = 0; /* number of recursive #history commands */ int prompt_status = 0; /* prompt status: 0 = ready -> nothing to do; * 1 if internal echo -> must redraw; * -1 if something sent to MUD -> waiting for it. */ int line_status = 0; /* input line status: 0 = ready -> nothing to do; * 1 if printed something -> must redraw. */ int limit_mem = 0; /* if !=0, max len of a string or text */ char opt_echo = 1; /* 1 if text sent to MUD must be echoed */ char opt_keyecho = 1; /* 1 if binds must be echoed */ char opt_info = 1; /* 0 if internal messages are suppressed */ char opt_exit = 0; /* 1 to autoquit when closing last conn. */ char opt_history; /* 1 if to save also history */ char opt_words = 0; /* 1 if to save also word completion list */ char opt_compact = 0; /* 1 if to clear prompt between remote messages */ char opt_debug = 0; /* 1 if to echo every line before executing it */ char opt_speedwalk = 0; /* 1 = speedwalk on */ char opt_wrap = 0; /* 1 = word wrap active */ char opt_autoprint = 0; /* 1 = automatically #print lines matched by actions */ char opt_reprint = 0; /* 1 = reprint sent commands when we get a prompt */ char opt_sendsize = 0; /* 1 = send term size upon connect */ char opt_autoclear = 1; /* 1 = clear input line before executing commands * from spawned programs. * if 0, spawned progs must #clear before printing */ char hostname[BUFSIZE]; int portnumber; static char powwow_dir[BUFSIZE]; /* default path to definition files */ char deffile[BUFSIZE]; /* name and path of definition file */ char helpfile[BUFSIZE]; /* name and path of help file */ char copyfile[BUFSIZE]; /* name and path of copyright file */ aliasnode *aliases[MAX_HASH]; /* head of alias hash list */ aliasnode *sortedaliases; /* head of (ASCII) sorted alias list */ actionnode *actions; /* head of action list */ promptnode *prompts; /* head of prompt list */ marknode *markers; /* head of mark list */ int a_nice = 0; /* default priority of new actions/marks */ keynode *keydefs; /* head of key binding list */ delaynode *delays; /* head of delayed commands list */ delaynode *dead_delays; /* head of dead-delayed commands list */ varnode *named_vars[2][MAX_HASH]; /* head of named variables hash list */ varnode *sortednamed_vars[2]; /* head of (ASCII) sorted named variables list */ int max_named_vars = 100; /* max number of named vars (grows as needed) */ int num_named_vars[2]; /* number of named variables actually used */ static param_stack paramstk; /* stack of local unnamed vars */ static unnamedvar global_var[NUMTOT]; /* global unnamed vars */ vars *var; /* vector of all vars */ ptr globptr[2]; /* global ptr buffer */ char globptrok = 1|2; /* x&i = 0 if globptr[i] is in use */ varnode *prompt; /* $prompt is always set */ ptr marked_prompt; /* $prompt with marks added */ static varnode *last_line; /* $line is always set to * the last line processed */ vtime now; /* current time */ int now_updated; /* current time is up to date */ vtime start_time; /* time of powwow timer startup */ vtime ref_time; /* time corresponding to timer == 0 */ function_any last_edit_cmd; /* GH: keep track of for repeated cmds */ clock_t start_clock, cpu_clock; char initstr[BUFSIZE]; /* initial string to send on connect */ int linemode = 0; /* line mode flags (LM_* in main.h) */ /* for line editing */ int cols=80, lines=24; /* screen size */ int cols_1=79; /* == cols if tty_wrapglitch, == cols-1 otherwise */ int olines; /* previous screen size */ int col0; /* input line offset (= printstrlen of prompt) */ int line0; /* screen line where the input line starts */ char edbuf[BUFSIZE]; /* line editing buffer */ int edlen; /* length of current input line */ int pos = 0; /* cursor position in line */ char surely_isprompt = 0; /* !=0 if last #prompt set #isprompt */ char verbatim = 0; /* 1 = don't expand aliases or process semicolons */ char prefixstr[BUFSIZE]; /* inserted in the editing buffer each time */ char inserted_next[BUFSIZE];/* inserted in buffer just once */ char flashback = 0; /* cursor is on excursion and should be put back */ int excursion; /* where the excursion is */ char edattrbeg[CAPLEN]; /* starting input line attributes */ char edattrend[CAPLEN]; /* ending input line attributes */ int edattrbg; /* input line attributes do change bg color */ /* signals handling */ VOLATILE int sig_pending, sig_winch_got, sig_chld_got; /* GH: different ID characters for different action types */ /* * Cosmos: they are hardcoded in cmd2.c, function parse_action() * so don't change them. */ char action_chars[ACTION_TYPES] = { '>', '%' }; /* GH: different delimeter modes */ char *delim_list[] = { " ;", " <>!=(),;\"'{}[]+-/*%", 0 }; int delim_len [] = { 2 , 21 , 0 }; char *delim_name[] = { "normal", "program", "custom" }; int delim_mode = DELIM_NORMAL; int main __P2 (int,argc, char **,argv) { char *p; int i; int read_file = 0; /* GH: if true, powwow was started with * a file argument, and initstr shall be ran */ #ifdef USE_LOCALE if (!setlocale(LC_ALL, "")) { fprintf(stderr, "Failed setlocale(LC_ALL, \"C\")\n"); } #endif /* initializations */ initstr[0] = 0; memzero(conn_list, sizeof(conn_list)); update_now(); ref_time = start_time = movie_last = now; start_clock = cpu_clock = clock(); #ifndef NO_RANDOM init_random((int)now.tv_sec); #endif _cmd_init(); if ((p = getenv("POWWOWDIR"))) { strcpy(powwow_dir, p); if (powwow_dir[strlen(powwow_dir) - 1] != '/') strcat(powwow_dir, "/"); } else powwow_dir[0] = '\0'; if ((p = getenv("POWWOWHELP"))) strcpy(helpfile, p); else if (powwow_dir[0]) strcpy(helpfile, powwow_dir); else strcpy(helpfile, POWWOW_DIR); if (helpfile[strlen(helpfile) - 1] != '/') strcat(helpfile, "/"); strcat(helpfile, helpname); if (access(helpfile, R_OK) == -1 && !access(helpname, R_OK)) strcpy(helpfile, helpname); if (powwow_dir[0]) strcpy(copyfile, powwow_dir); else strcpy(copyfile, POWWOW_DIR); if (copyfile[strlen(copyfile) - 1] != '/') strcat(copyfile, "/"); strcat(copyfile, copyname); if (access(copyfile, R_OK) == -1 && !access(copyname, R_OK)) strcpy(copyfile, copyname); /* initialize variables */ if ((var = (vars *)malloc(sizeof(vars)*(NUMTOT+max_named_vars)))) { for (i=0; istr = ptrnew(PARAMLEN)) && (marked_prompt = ptrnew(PARAMLEN)) && (last_line = add_varnode("last_line", 1)) && (last_line->str = ptrnew(PARAMLEN)) && (globptr[0] = ptrnew(PARAMLEN)) && (globptr[1] = ptrnew(PARAMLEN)) && !MEM_ERROR) ; else syserr("malloc"); /* ptr_bootstrap(); utils_bootstrap(); beam_bootstrap(); cmd_bootstrap(); map_bootstrap(); eval_bootstrap(); list_bootstrap(); tcp_bootstrap(); */ edit_bootstrap(); tty_bootstrap(); #ifdef MOTDFILE printmotd(); #endif printver(); if (argc == 1) { tty_printf( "\nPowwow comes with ABSOLUTELY NO WARRANTY; for details type \"#help warranty\".\n\ This is free software, and you are welcome to redistribute it\n\ under certain conditions; type \"#help copyright\" for details.\n" ); } if (argc == 1 || argc == 3) { tty_add_initial_binds(); tty_add_walk_binds(); } else if (argc == 2 || argc == 4) { /* * assuming first arg is definition file name * If three args, first is definition file name, * second and third are hostname and port number * (they overwrite the ones in definition file) */ set_deffile(argv[1]); if (access(deffile,R_OK) == -1 || access(deffile,W_OK) == -1) { char portnum[INTLEN]; tty_printf("Creating %s\nHost name :", deffile); tty_flush(); tty_gets(hostname, BUFSIZE); if (hostname[0] == '\n') hostname[0] = '\0'; else strtok(hostname, "\n"); tty_puts("Port number:"); tty_flush(); tty_gets(portnum, INTLEN); portnumber = atoi(portnum); tty_add_initial_binds(); tty_add_walk_binds(); limit_mem = 1048576; if (save_settings() < 0) exit(0); } else if (read_settings() < 0) exit(0); else read_file = 1; } if (argc == 3 || argc == 4) { /* assume last two args are hostname and port number */ my_strncpy(hostname, argv[argc - 2], BUFSIZE-1); portnumber = atoi(argv[argc - 1]); } signal_start(); tty_start(); tty_puts(tty_clreoscr); tty_putc('\n'); tty_gotoxy(col0 = 0, lines - 2); tty_puts("Type #help for help.\n"); line0 = lines - 1; FD_ZERO(&fdset); FD_SET(tty_read_fd, &fdset); if (*hostname) tcp_open("main", (*initstr ? initstr : NULL), hostname, portnumber); if (read_file && !*hostname && *initstr) { parse_instruction(initstr, 0, 0, 1); history_done = 0; } confirm = 0; mainloop(); /* NOTREACHED */ return 0; } /* * show current version */ void printver __P0 (void) { tty_printf("Powwow version %s\nOptions: %s%s\n", POWWOW_VERSION, #ifdef USE_VT100 "vt100-only," #else "termcaps," #endif #ifdef USE_SGTTY " BSD sgtty," #else " termios," #endif #ifdef USE_REGEXP " regexp," #else " no regexp," #endif #ifdef USE_LOCALE " locale," #endif #ifdef HAVE_LIBDL " modules," #endif , #if __STDC__ " compiled " __TIME__ " " __DATE__ #else " uknown compile date" #endif ); } #ifdef MOTDFILE /* * print the message of the day if present */ static void printmotd __P0 (void) { char line[BUFSIZE]; FILE *f = fopen(MOTDFILE, "r"); if (f) { while (fgets(line, BUFSIZE, f)) tty_puts(line); fclose(f); } } #endif static void redraw_everything __P0 (void) { if (prompt_status == 1 && line_status == 0) line_status = 1; if (prompt_status == 1) draw_prompt(); else if (prompt_status == -1) { promptzero(); col0 = surely_isprompt = '\0'; } if (line_status == 1) draw_input_line(); } /* how much can we sleep in select() ? */ static void compute_sleeptime __P1 (vtime **,timeout) { static vtime tbuf; int sleeptime = 0; if (delays) { update_now(); sleeptime = diff_vtime(&delays->when, &now); if (!sleeptime) sleeptime = 1; /* if sleeptime is less than 1 millisec, * set to 1 millisec */ } if (flashback && (!sleeptime || sleeptime > FLASHDELAY)) sleeptime = FLASHDELAY; if (sleeptime) { tbuf.tv_sec = sleeptime / mSEC_PER_SEC; tbuf.tv_usec = (sleeptime % mSEC_PER_SEC) * uSEC_PER_mSEC; *timeout = &tbuf; } else *timeout = (vtime *)NULL; } /* * main loop. */ static void mainloop __P0 (void) { fd_set readfds; int i, err; vtime *timeout; for (;;) { readfds = fdset; tcp_fd = tcp_main_fd; exec_delays(); do { if (sig_pending) sig_bottomhalf(); /* this might set errno... */ tcp_flush(); if (!(pos <= edlen)) { PRINTF("\n#*ARGH* assertion failed (pos <= edlen): mail bpk@hoopajoo.net\n"); pos = edlen; } redraw_everything(); tty_flush(); compute_sleeptime(&timeout); error = now_updated = 0; err = select(tcp_max_fd+1, &readfds, NULL, NULL, timeout); prompt_reset_iac(); } while (err < 0 && errno == EINTR); if (err < 0 && errno != EINTR) syserr("select"); if (flashback) putbackcursor(); /* process subsidiary and spawned connections first */ if (tcp_count > 1 || tcp_attachcount) { for (i=0; err && i= 0) prompt_status = s; if (line_status >= 0) line_status = s; } } /* * execute the delayed labels that have expired * and place them in the disabled delays list */ static void exec_delays __P0 (void) { delaynode *dying; ptr *pbuf, buf = (ptr)0; if (!delays) return; update_now(); if (cmp_vtime(&delays->when, &now) > 0) return; /* remember delayed command may modify the prompt and/or input line! */ if (prompt_status == 0) { clear_input_line(opt_compact || !opt_info); if (!opt_compact && opt_info && prompt_status == 0 && promptlen) { tty_putc('\n'); col0 = 0; status(1); } } TAKE_PTR(pbuf, buf); while (delays && cmp_vtime(&delays->when, &now) <= 0) { dying = delays; /* remove delayed command from active list */ delays = dying->next; /* and put it in the dead one */ add_node((defnode *)dying, (defnode **)&dead_delays, rev_time_sort); /* must be moved before executing delay->command * and command must be copied in a buffer * (can't you imagine why? The command may edit itself...) */ if (opt_info) tty_printf("#now [%s]\n", dying->command); if (*dying->command) { error = 0; *pbuf = ptrmcpy(*pbuf, dying->command, strlen(dying->command)); if (MEM_ERROR) errmsg("malloc (#in/#at)"); else { parse_instruction(ptrdata(*pbuf), 0, 0, 1); history_done = 0; } } } DROP_PTR(pbuf); } #define IAC_N 1024 static char *iac_v[IAC_N]; static int iac_f, iac_l; static void prompt_reset_iac __P0 (void) { iac_f = iac_l = 0; } void prompt_set_iac __P1 (char *,p) { if (iac_f == iac_l) iac_f = iac_l = 0; if (iac_l < IAC_N) iac_v[iac_l++] = p; } static char *prompt_get_iac __P0 (void) { return iac_l > iac_f ? iac_v[iac_f] : NULL; } static int grab_prompt __P3 (char *,linestart, int,len, int,islast) { char *p; int is_iac_prompt = surely_isprompt = 0; /* recognize IAC GA as end-of-prompt marker */ if ((CONN_LIST(tcp_fd).flags & IDPROMPT)) { if ((p = prompt_get_iac()) && p > linestart && p <= linestart+len) iac_f++, is_iac_prompt = len = p - linestart; else if (!islast) return 0; } /* * We may get a prompt in the middle of a bunch of lines, so * match #prompts. They usually have no #print, so we print manually * if islast is not set and a #prompt matches. */ if ((is_iac_prompt || islast || printstrlen(linestart) < cols) && ((search_prompt(linestart, 1) && surely_isprompt) || is_iac_prompt)) { char *reprint; /* * the line starts with a prompt. * #isprompt placed the actual prompt in $prompt, * we must still process the rest of the line. */ if (surely_isprompt > 0 && surely_isprompt <= len) { len = surely_isprompt; prompt_status = 1; } else if (!surely_isprompt && is_iac_prompt) { prompt->str = ptrmcpy(prompt->str, linestart, len = surely_isprompt = is_iac_prompt); if (MEM_ERROR) { promptzero(); errmsg("malloc(prompt)"); return 0; } prompt_status = 1; } /* * following data may be the reply to a previously sent command, * so we may have to reprint that command. */ if ((reprint = reprint_getline()) && *reprint) { smart_print(promptstr, 0); status(-1); tty_printf("(%s)\n", reprint); } else if (!islast) smart_print(promptstr, 1); } else if (islast) { prompt->str = ptrmcpy(prompt->str, linestart, len); if (MEM_ERROR) { promptzero(); errmsg("malloc(prompt)"); return 0; } prompt_status = 1; /* good, we got what to redraw */ } else len = 0; return len; } /* * process remote input one line at time. stop at "\n". */ static void process_singleline __P2 (char **,pbuf, int *,psize) { int size, len = 0; char *wasn = 0, *buf, *linestart = *pbuf, *lineend, *end = *pbuf + *psize; if ((lineend = memchr(linestart, '\n', *psize))) { /* ok, there is a newline */ *(wasn = lineend) = '\0'; buf = lineend + 1; /* start of next line */ } if (!lineend) /* line continues till end of buffer, no trailing \n */ buf = lineend = end; size = buf - linestart; #ifdef DEBUGCODE_2 /* debug code to see in detail what codes come from the server */ { char c; char *t; tty_putc('{'); for (t = linestart; t < lineend && (c = *t); t++) { if (c < ' ' || c > '~') tty_printf("[%d]", (int)c); else tty_putc(c); } tty_puts("}\n"); } #endif /* * Try to guess where is the prompt... really not much more than * a guess. Again, do it only on main connection: we do not want * output from other connections to mess with the prompt * of main connection :) * * Since we now have #prompt, behave more restrictively: * if no #prompts match or a #prompt matches but does not set #isprompt * (i.e. recognize it for not being a prompt), * we check for #actions on it when the \n arrives. * if a #prompt matches and sets #isprompt, then it is REALLY a prompt * so never match #actions on it. */ if (lineend == end && tcp_fd == tcp_main_fd && printstrlen(linestart) < cols) { /* * The last line in the chunk we received has no trailing \n * Assume it is a prompt. */ if (surely_isprompt && promptlen && prompt_status == 1) { draw_prompt(); tty_putc('\n'); col0 = 0; } surely_isprompt = 0; promptzero(); if (lineend > linestart && (len = grab_prompt(linestart, lineend-linestart, 1))) size = len; } else { if (tcp_fd == tcp_main_fd) { surely_isprompt = 0; promptzero(); if (linestart[0]) { /* set $last_line */ last_line->str = ptrmcpy(last_line->str, linestart, strlen(linestart)); if (MEM_ERROR) { print_error(error); return; } if (lineend > linestart && (len = grab_prompt(linestart, lineend-linestart, 0))) size = len; } } if (!len && (!*linestart || (!search_action(linestart, 0) || opt_autoprint))) { if (line0 < lines - 1) line0++; if (tcp_fd != tcp_main_fd) /* sub connection */ tty_printf("##%s> ", CONN_LIST(tcp_fd).id); smart_print(linestart, 1); } } /* * search_prompt and search_action above * might set error: clear it to avoid troubles. */ error = 0; if (wasn) *wasn = '\n'; *pbuf += size; *psize -= size; } /* * Code to merge lines from host that were splitted * into different packets: it is a horrible kludge (sigh) * and can be used only on one connection at time. * We currently do it on main connection. * * Note that this code also works for _prompts_ splitted into * different packets, as long as no #prompts execute #isprompt * on an incomplete prompt (as stated in the docs). */ static int process_first_fragment __P2 (char *,buf, int,got) { int processed = 0; /* * Don't merge if the first part of the line was intercepted * by a #prompt action which executed #isprompt * (to avoid intercepting it twice) */ if (*buf == '\n') { char deleteprompt = 0, matched = 0; if (opt_compact) { /* in compact mode, skip the first \n */ deleteprompt = 1; processed++; } /* * the prompt was actually a complete line. * no need to put it on the top of received data. * unless #isprompt was executed, demote it to a regular line, * match #actions on it, copy it in last_line. */ if (!surely_isprompt) { last_line->str = ptrcpy(last_line->str, prompt->str); if (MEM_ERROR) { print_error(error); return 0; } /* * Kludge for kludge: don't delete the old prompt immediately. * Instead, match actions on it first. * If it matches, clear the line before running the action * (done by the "1" in search_action() ) * If it doesn't match, delete it only if opt_compact != 0 */ matched = search_action(promptstr, 1); } if (!matched) clear_input_line(deleteprompt); status(-1); } else { /* * try to merge the prompt with the first line in buf * (assuming we have a line splitted into those parts) * then clear the prompt. */ char *lineend, *spinning = NULL; /* find the end of the first line. include the final newline. */ if ((lineend = strchr(buf, '\n'))) lineend++; else lineend = buf + got; if (surely_isprompt || (spinning = memchr(buf, '\b', lineend - buf))) { /* * either #isprompt _was_ executed, * or we got a MUME spinning bar. * in both cases, don't try to merge. * * print a newline (to keep the old prompt on screen) * only if !opt_compact and we didn't get a MUME spinning bar. */ clear_input_line(opt_compact); if (!spinning && !opt_compact) tty_putc('\n'), col0 = 0; promptzero(); } else { ptr *pp, p = (ptr)0; char *dummy; int dummyint; /* ok, merge this junk with the prompt */ TAKE_PTR(pp, p); *pp = ptrcpy(*pp, prompt->str); *pp = ptrmcat(*pp, buf, lineend - buf); if (MEM_ERROR) { print_error(error); return 0; } if (!*pp) return 0; dummy = ptrdata(*pp); dummyint = ptrlen(*pp); /* this also sets last_line or prompt->str : */ clear_input_line(1); process_singleline(&dummy, &dummyint); processed = lineend - buf; } } return processed; } /* * process input from remote host: * detect special sequences, trigger actions, locate prompt, * word-wrap, print to tty */ void process_remote_input __P2 (char *,buf, int,size) { if (promptlen && tcp_fd == tcp_main_fd) promptzero(); /* discard the prompt, we look for another one */ status(1); do { process_singleline(&buf, &size); } while (size > 0); } static void common_clear __P1 (int,newline) { clear_input_line(opt_compact); if (newline) { tty_putc('\n'); col0 = 0; status(1); } } /* * get data from the socket and process/display it. */ static void get_remote_input __P0 (void) { char buffer[BUFSIZE + 2]; /* allow for a terminating \0 later */ char *buf = buffer, *newline; int got, otcp_fd, i; if (CONN_LIST(tcp_fd).fragment) { if ((i = strlen(CONN_LIST(tcp_fd).fragment)) >= BUFSIZE-1) { i = 0; common_clear(promptlen && !opt_compact); tty_printf("#error: ##%s : line too long, discarded\n", CONN_LIST(tcp_fd).id); } else { buf += i; memcpy(buffer, CONN_LIST(tcp_fd).fragment, i); } free(CONN_LIST(tcp_fd).fragment); CONN_LIST(tcp_fd).fragment = 0; } else i = 0; got = tcp_read(tcp_fd, buf, BUFSIZE - i); if (!got) return; buf[got]='\0'; /* Safe, there is space. Do it now not to forget it later */ received += got; #ifdef DEBUGCODE /* debug code to see in detail what strange codes come from the server */ { char c, *t; newline = buf + got; tty_printf("%s{", edattrend); for (t = buf; t < newline; t++) { if ((c = *t) < ' ' || c > '~') tty_printf("[%d]", c); else tty_putc(c); } tty_puts("}\n"); } #endif if (!(CONN_LIST(tcp_fd).flags & ACTIVE)) return; /* process only active connections */ got += (buf - buffer); buf = buffer; if (CONN_LIST(tcp_fd).flags & SPAWN) { /* this is data from a spawned child or an attached program. * execute as if typed */ otcp_fd = tcp_fd; tcp_fd = tcp_main_fd; if ((newline = strchr(buf, '\n'))) { /* instead of newline = strtok(buf, "\n") */ *newline = '\0'; if (opt_autoclear && line_status == 0) { common_clear(!opt_compact); } do { if (opt_info) { if (line_status == 0) { common_clear(!opt_compact); } tty_printf("##%s [%s]\n", CONN_LIST(otcp_fd).id, buf); } parse_user_input(buf, 0); /* * strtok() may have been used in parse_user_input()... * cannot rely it refers on what we may have set above. * (it causes a bug in #spawned commands if they * evaluate (attr(), noattr) or they #connect ... ) * so do it manually. */ /* * buf = strtok(NULL, "\n"); */ if ((buf = newline) && (newline = strchr(++buf, '\n'))) *newline = '\0'; } while (buf && newline); } if (buf && *buf && !newline) { /* * save last fragment for later, when spawned command will * (hopefully) send the rest of the text */ CONN_LIST(otcp_fd).fragment = my_strdup(buf); if (opt_info) { if (line_status == 0) { common_clear(!opt_compact); } tty_printf("#warning: ##%s : unterminated [%s]\n", CONN_LIST(otcp_fd).id, buf); } } tcp_fd = otcp_fd; return; } if (linemode & LM_CHAR) { /* char-by-char mode: just display output, no fuss */ clear_input_line(0); tty_puts(buf); return; } /* line-at-a-time mode: process input in a number of ways */ if (tcp_fd == tcp_main_fd && promptlen) { i = process_first_fragment(buf, got); buf += i, got -= i; } else { common_clear(promptlen && !opt_compact); } if (got > 0) process_remote_input(buf, got); } #ifdef USE_REGEXP /* * GH: matches precompiled regexp, return actual params in param array * return 1 if matched, 0 if not */ static int match_regexp_action __P4 (void *,regexp, char *,line, int *,match_s, int *,match_e) { regmatch_t reg_match[NUMPARAM - 1]; if (!regexec((regex_t *)regexp, line, NUMPARAM - 1, reg_match, 0)) { int n; match_s[0] = 0; match_e[0] = strlen(line); for (n = 1; n < NUMPARAM; n++) match_s[n] = match_e[n] = 0; for (n = 0; n <= (int)((regex_t *)regexp)->re_nsub && reg_match[n].rm_so != -1 && n < NUMPARAM - 1; n++) { match_s[n+1] = reg_match[n].rm_so; match_e[n+1] = reg_match[n].rm_eo; } return 1; } return 0; } #endif /* * match action containing &1..&9 and $1..$9 and return actual params start/end * in match_s/match_e - return 1 if matched, 0 if not */ static int match_weak_action __P4 (char *,pat, char *,line, int *,match_s, int *,match_e) { char mpat[BUFSIZE], *npat=0, *npat2=0, *src=line, *nsrc=0, c; ptr *pbuf, buf = (ptr)0; char *tmp, *realpat = pat; int mbeg = 0, mword = 0, prm = -1, p; TAKE_PTR(pbuf, buf); if (jit_subst_vars(pbuf, pat)) pat = ptrdata(*pbuf); if (REAL_ERROR) { print_error(error); DROP_PTR(pbuf); return 0; } unescape(pat); for (p = 0; p < NUMPARAM; p++) match_s[p] = match_e[p] = 0; p = 0; if (*pat == '^') { pat++; mbeg = 1; /* anchor match at line start */ } if (*pat == '&' || *pat == '$') mbeg = - mbeg - 1; /* pattern starts with '&' or '$' */ while (pat && *pat) { if (((c=*pat) == '&' || c == '$')) { /* &x matches a string */ /* $x matches a single word */ tmp = pat + 1; if (isdigit(*tmp)) { p = 0; while (isdigit(*tmp) && p < NUMPARAM) { p *= 10; p += *tmp++ - '0'; } if (p <= 0 || p >= NUMPARAM) { DROP_PTR(pbuf); return 0; } prm = p; pat = tmp; if (c == '$') mword = 1; } else { PRINTF("#error: bad action pattern \"%s\"\n#missing digit after \"%s\"\n", realpat, pat); DROP_PTR(pbuf); return 0; } } npat = first_valid(pat, '&'); npat2 = first_valid(pat, '$'); if (npat2 < npat) npat = npat2; if (!*npat) npat = 0; if (npat) { my_strncpy(mpat, pat, npat-pat); /* mpat[npat - pat] = 0; */ } else strcpy(mpat, pat); if (*mpat) { nsrc = strstr(src, mpat); if (!nsrc) { DROP_PTR(pbuf); return 0; } if (mbeg > 0) { if (nsrc != src) { DROP_PTR(pbuf); return 0; } mbeg = 0; /* reset mbeg to stop further start match */ } if (prm != -1) { match_s[prm] = src - line; match_e[prm] = nsrc - line; } } else if (prm != -1) { /* end of pattern space */ match_s[prm] = src - line; match_e[prm] = strlen(line); } /* post-processing of param */ if (prm != -1 && match_e[prm] && mword) { if (mbeg == -1) { /* unanchored '$' start, take last word */ if ((tmp = memrchrs(line + match_s[prm], match_e[prm] - match_s[prm], DELIM, DELIM_LEN))) { match_s[prm] = tmp - line + 1; } } else if (!*pat) { /* '$' at end of pattern, take first word */ if ((tmp = memchrs(line + match_s[prm], match_e[prm] - match_s[prm], DELIM, DELIM_LEN))) match_e[prm] = tmp - line; } else { /* match only if param is single-worded */ if (memchrs(line + match_s[prm], match_e[prm] - match_s[prm], DELIM, DELIM_LEN)) { DROP_PTR(pbuf); return 0; } } } if (prm != -1 && match_e[prm]) mbeg = mword = 0; /* reset match flags */ src = nsrc + strlen(mpat); pat = npat; } DROP_PTR(pbuf); match_s[0] = 0; match_e[0] = strlen(line); return 1; } /* * Search for #actions or #prompts to trigger on an input line. * The line can't be trashed since we want to print it on the screen later. * Return 1 if line matched to some #action, 0 otherwise * * Optionally clear the input line before running the trigger command. */ static int search_action_or_prompt __P3 (char *,line, char,clearline, char,onprompt) { /* * we need actionnode and promptnode to be the same "triggernode" type */ triggernode *p; int ret = 0; int match_s[NUMPARAM], match_e[NUMPARAM]; for (p = onprompt ? prompts : actions; p; p = p->next) { #ifdef USE_REGEXP if (p->active && ((p->type == ACTION_WEAK && match_weak_action(p->pattern, line, match_s, match_e)) || (p->type == ACTION_REGEXP && match_regexp_action(p->regexp, line, match_s, match_e)) )) #else if (p->active && ((p->type == ACTION_WEAK && match_weak_action(p->pattern, line, match_s, match_e)) )) #endif { push_params(); if (error) return 0; ret = 1; error = 0; set_params(line, match_s, match_e); if (error) return 0; if (onprompt == 2) { prompt->str = ptrmcpy(prompt->str, line, strlen(line)); if (MEM_ERROR) { promptzero(); errmsg("malloc(prompt)"); return 0; } } if (clearline) clear_input_line(1); parse_instruction(p->command, 0, 1, 1); history_done = 0; if (error!=DYN_STACK_UND_ERROR && error!=DYN_STACK_OV_ERROR) pop_params(); break; } } if (error) return 0; return ret; } /* * read terminal input and send to parser. * decode keys that send escape sequences */ static void get_user_input __P0 (void) { int i, j, chunk = 1; static char buf[BUFSIZE+1]; /* allow for terminating \0 */ char *c = buf; static char typed[CAPLEN]; /* chars typed so far (with partial match) */ static int nchars = 0; /* number of them */ /* We have 4 possible line modes: * line mode, local echo: line editing functions in effect * line mode, no echo: sometimes used for passwords, no line editing * char mode, no echo: send a character directly, no local processing * char mode, local echo: extremely rare, do as above. */ if (!(linemode & (LM_NOECHO | LM_CHAR))) /* line mode, local echo */ chunk = BUFSIZE; while ((j = tty_read(c, chunk)) < 0 && errno == EINTR) ; if (j <= 0 || (chunk == 1 && j != chunk)) syserr("read from tty"); c[chunk] = '\0'; if (linemode & LM_CHAR) { /* char mode. chunk == 1 */ while ((i = write(tcp_fd, c, 1)) < 0 && errno == EINTR) ; if (i != 1) syserr("write to socket"); if (!(linemode & LM_NOECHO)) tty_putc(*c); last_edit_cmd = (function_any)0; } else if (linemode & LM_NOECHO) { /* sending password (line mode, no echo). chunk == 1 */ if ((*c != '\n' && *c != '\r') && edlen < BUFSIZE - 2) edbuf[edlen++] = *c; else { edbuf[edlen] = '\0'; #ifdef BUG_ANSI if (edattrbg) tty_printf("%s\n", edattrend); else #endif tty_putc('\n'); tcp_write(tcp_fd, edbuf); edlen = 0; typed[nchars = 0] = 0; } edbuf[pos = edlen] = '\0'; last_edit_cmd = (function_any)0; } else { /* normal mode (line mode, echo). chunk == BUFSIZE */ int done = 0; keynode *p; for (; j > 0; c++, j--) { /* search function key strings for match */ /* GH: support for \0 in sequence */ done = 0; typed[nchars++] = *c; while (!done) { done = 1; /* * shortcut: * an initial single ASCII char cannot match any #bind */ if (nchars == 1 && *c >= ' ' && *c <= '~') p = NULL; else { for (p = keydefs; (p && (p->seqlen < nchars || memcmp(typed, p->sequence, nchars))); p = p->next) ; } if (!p) { /* * GH: type the first character and keep processing * the rest in the input buffer */ i = 1; last_edit_cmd = (function_any)0; insert_char(typed[0]); while (i < nchars) { typed[i - 1] = typed[i]; i++; } if (--nchars) done = 0; } else if (p->seqlen == nchars) { if (flashback) putbackcursor(); p->funct(p->call_data); last_edit_cmd = (function_any)p->funct; /* GH: keep track of last command */ nchars = 0; } } } } } /* * split str into words separated by DELIM, and place in * VAR[1].str ... VAR[9].str - * the whole str is put in VAR[0].str */ static char *split_words __P1 (char *,str) { int i; char *end; ptr *prm; *VAR[0].str = ptrmcpy(*VAR[0].str, str, strlen(str)); for (i = 1; i < NUMPARAM; i++) { *VAR[i].num = 0; prm = VAR[i].str; /* skip multiple span of DELIM */ while (*str && strchr(DELIM, *str)) str++; end = str; while (*end && !strchr(DELIM, *end)) end++; *prm = ptrmcpy(*prm, str, end-str); str = end; if (MEM_ERROR) { print_error(error); return NULL; } } return str; } /* * free the whole stack and reset it to empty */ static void free_allparams __P0 (void) { int i,j; paramstk.curr = 0; /* reset stack to empty */ for (i=1; i 0) paramstk.curr--; else { print_error(error=DYN_STACK_UND_ERROR); free_allparams(); return; } for (i=0; i match_s[i]) { *VAR[i].str = ptrmcpy(*VAR[i].str, line + match_s[i], match_e[i] - match_s[i]); if (MEM_ERROR) { print_error(error); return; } } else ptrzero(*VAR[i].str); } } char *get_next_instr __P1 (char *,p) { int count, is_if; char *sep, *q; p = skipspace(p); if (!*p) return p; count = is_if = !strncmp(p, "#if", 3); do { sep = first_regular(p, CMDSEP); q = p; if (*q) do { if (*q == '#') q++; q = first_regular(q, '#'); } while (*q && strncmp(q, "#if", 3)); if (sep<=q) { if (*(p = sep)) p++; } else if (*q) p = get_next_instr(q); else { print_error(error=SYNTAX_ERROR); return NULL; } sep = skipspace(p); } while (*p && count-- && (!is_if || (!strncmp(sep, "#else", 5) && (*(p = sep + 5))))); return p; } static void send_line __P2 (char *,line, char,silent) { if (!silent && opt_echo) { PRINTF("[%s]\n", line); } tcp_write(tcp_fd, line); } /* * Parse and exec the first instruction in 'line', and return pointer to the * second instruction in 'line' (if any). */ char *parse_instruction __P4 (char *,line, char,silent, char,subs, char,jit_subs) { aliasnode *np; char *buf, *arg, *end, *ret; char last_is_sep = 0; int len, copied = 0, otcp_fd = -1; ptr p1 = (ptr)0, p2 = (ptr)0; ptr *pbuf, *pbusy, *tmp; if (error) return NULL; ret = get_next_instr(line); if (!ret || ret==line) /* error or empty instruction, bail out */ return ret; /* * remove the optional ';' after an instruction, * to have an usable string, ending with \0. * If it is escaped, DON'T remove it: it is not a separator, * and the instruction must already end with \0, or we would not be here. */ if (ret[-1] == CMDSEP) { /* instruction is not empty, ret[-1] is allowed */ if (ret > line + 1 && ret[-2] == ESC) { /* ';' is escaped */ } else { *--ret = '\0'; last_is_sep = 1; } } /* * using two buffers, p1 and p2, for four strings: * result of subs_param, result of jit_subst_vars, result of * unescape and first word of line. * * So care is required to avoid clashes. */ TAKE_PTR(pbuf, p1); TAKE_PTR(pbusy, p2); if (subs && subst_param(pbuf, line)) { line = *pbuf ? ptrdata(*pbuf) : ""; SWAP2(pbusy, pbuf, tmp); copied = 1; } if (jit_subs && jit_subst_vars(pbuf, line)) { line = *pbuf ? ptrdata(*pbuf) : ""; SWAP2(pbusy, pbuf, tmp); copied = 1; } if (!copied) { *pbuf = ptrmcpy(*pbuf, line, strlen(line)); line = *pbuf ? ptrdata(*pbuf) : ""; SWAP2(pbusy, pbuf, tmp); } if (subs || jit_subs) unescape(line); /* now line is in (pbusy) and (pbuf) is available */ /* restore integrity of original line: must still put it in history */ if (last_is_sep) *ret++ = CMDSEP; if (REAL_ERROR) { print_error(error); DROP_PTR(pbuf); DROP_PTR(pbusy); return NULL; } /* run-time debugging */ if (opt_debug) { PRINTF("#parsing: %s\n", line); } if (!*line) send_line(line, silent); else do { arg = skipspace(line); if (arg[0] == '#' && arg[1] == '#') { /* send to other connection */ *pbuf = ptrsetlen(*pbuf, len = strlen(arg)); if (REAL_ERROR) { print_error(error); break; } buf = ptrdata(*pbuf); line = split_first_word(buf, len+1, arg + 2); /* now (pbuf) is used too: first word of line */ /* line contains the rest */ otcp_fd = tcp_fd; if ((tcp_fd = tcp_find(buf))<0) { error = OUT_RANGE_ERROR; PRINTF("#no connection named \"%s\"\n", buf); break; } arg = skipspace(line); if (!*arg) { if (CONN_LIST(tcp_fd).flags & SPAWN) { error = OUT_RANGE_ERROR; PRINTF("#only MUD connections can be default ones!\n"); } else { /* set it as main connection */ tcp_set_main(tcp_fd); otcp_fd = -1; } /* stop parsing, otherwise a newline would be sent to tcp_fd */ break; } /* now we can trash (pbuf) */ } if (*arg == '{') { /* instruction contains a block */ end = first_regular(line = arg + 1, '}'); if (*end) { *end = '\0'; parse_user_input(line, silent); *end = '}'; } else print_error(error=MISSING_PAREN_ERROR); } else { int oneword; /* initial spaces are NOT skipped this time */ *pbuf = ptrsetlen(*pbuf, len = strlen(line)); if (REAL_ERROR) { print_error(error); break; } buf = ptrdata(*pbuf); arg = split_first_word(buf, len+1, line); /* buf contains the first word, arg points to arguments */ /* now pbuf is used too */ if (!*arg) oneword = 1; else oneword = 0; if ((np = *lookup_alias(buf))&&np->active) { push_params(); if (REAL_ERROR) break; split_words(arg); /* split argument into words and place them in $0 ... $9 */ parse_instruction(np->subst, 0, 1, 1); if (error!=DYN_STACK_UND_ERROR && error!=DYN_STACK_OV_ERROR) pop_params(); /* now check for internal commands */ /* placed here to allow also aliases starting with "#" */ } else if (*(end = skipspace(line)) == '#') { if (*(end = skipspace(end + 1)) == '(') { /* execute #() */ end++; (void)evaln(&end); if (REAL_ERROR) print_error(error); } else parse_commands(buf + 1, arg); /* ok, buf contains skipspace(first word) */ } else if (!oneword || !map_walk(buf, silent, 0)) { /* it is ok, map_walk accepts only one word */ if (!subs && !jit_subs) unescape(line); send_line(line, silent); } } } while (0); if (otcp_fd != -1) tcp_fd = otcp_fd; DROP_PTR(pbuf); DROP_PTR(pbusy); return !REAL_ERROR ? ret : NULL; } /* * parse input from user: calls parse_instruction for each instruction * in cmd_line. * silent = 1 if the line should not be echoed, 0 otherwise. */ void parse_user_input __P2 (char *,line, char,silent) { do { line = parse_instruction(line, silent, 0, 0); } while (!error && line && *line); } /* * parse powwow's own commands */ static void parse_commands __P2 (char *,command, char *,arg) { int i, j; cmdstruct *c; /* We ALLOW special commands also on subsidiary connections ! */ /* assume output will be enough to make input line = last screen line */ /* line0 = lines - 1; */ if (isdigit(*command) && (i = atoi(command))) { if (i >= 0) { while (i--) (void)parse_instruction(arg, 1, 0, 1); } else { PRINTF("#bogus repeat count\n"); } } else { j = strlen(command); if( j == 0 ) { /* comment */ return; } for( c = commands; c != NULL; c = c -> next ) if (!strncmp(command, c -> name, j)) { if (c -> funct) { (*(c -> funct))(arg); return; } } PRINTF("#unknown powwow command \"%s\"\n", command); } } /* * substitute $0..$9 and @0..@9 in a string * (unless $ or @ is escaped with backslash) * * return 0 if dst not filled. if returned 0 and not error, * there was nothing to substitute. */ static int subst_param __P2 (ptr *,buf, char *,src) { int done, i; char *dst, *tmp, kind; if (!strchr(src, '$') && !strchr(src, '@')) return 0; i = strlen(src); if (!*buf || ptrlen(*buf) < i) { *buf = ptrsetlen(*buf, i); if (REAL_ERROR) return 0; } dst = ptrdata(*buf); while (*src) { while (*src && *src != '$' && *src != '@' && *src != ESC) *dst++ = *src++; if (*src == ESC) { while (*src == ESC) *dst++ = *src++; if (*src) *dst++ = *src++; } done = 0; if (*src == '$' || *src == '@') { kind = *src == '$' ? 1 : 0; tmp = src + 1; if (isdigit(*tmp)) { i = atoi(tmp); while (isdigit(*tmp)) tmp++; if (i < NUMPARAM) { int max = 0, n; char *data = NULL, buf2[LONGLEN]; done = 1; src = tmp; /* now the actual substitution */ if (kind) { if (*VAR[i].str && (data = ptrdata(*VAR[i].str))) max = ptrlen(*VAR[i].str); } else { sprintf(data = buf2, "%ld", *VAR[i].num); max = strlen(buf2); } if (data && max) { n = dst - ptrdata(*buf); *buf = ptrpad(*buf, max); if (REAL_ERROR) return 0; dst = ptrdata(*buf) + n; memcpy(dst, data, max); dst += max; } } } } if (!done && (*src == '$' || *src == '@')) *dst++ = *src++; } *dst = '\0'; return 1; } /* * just-in-time substitution: * substitute ${name}, @{name} and #{expression} in a string * (unless "${", "@{" or "#{" are escaped with backslash) * * return 0 if dst not filled. if returned 0 and not error, * there was nothing to substitute. */ static int jit_subst_vars __P2 (ptr *,buf, char *,src) { int i, done, kind; char *tmp, *name, *dst, c; varnode *named_var; if (!strstr(src, "${") && !strstr(src, "@{") && !strstr(src, "#{")) return 0; i = strlen(src); if (!*buf || ptrlen(*buf) < i) { *buf = ptrsetlen(*buf, i); if (REAL_ERROR) return 0; } dst = ptrdata(*buf); while (*src) { while (*src && *src != '$' && *src != '@' && *src != '#' && *src != ESC) *dst++ = *src++; if (*src == ESC) { while (*src == ESC) *dst++ = *src++; if (*src) *dst++ = *src++; } done = 0; if (*src == '$' || *src == '@') { i = 0; kind = *src == '$' ? 1 : 0; tmp = src + 1; if (*tmp == '{') { tmp = skipspace(tmp+1); if (isdigit(*tmp) || *tmp == '-') { /* numbered variable */ i = atoi(tmp); if (i >= -NUMVAR && i < NUMPARAM) { if (*tmp == '-') tmp++; while (isdigit(*tmp)) tmp++; done = 1; } } else if (isalpha(*tmp) || *tmp == '_') { /* named variable */ name = tmp++; while (isalnum(*tmp) || *tmp == '_') tmp++; c = *tmp; *tmp = '\0'; named_var = *lookup_varnode(name, kind); *tmp = c; if (named_var) { i = named_var->index; done = 1; } } tmp = skipspace(tmp); if (done) { int max = 0, n; char *data = NULL, buf2[LONGLEN]; src = tmp + 1; /* skip the '}' */ /* now the actual substitution */ if (kind == 1) { if (*VAR[i].str && (data = ptrdata(*VAR[i].str))) max = ptrlen(*VAR[i].str); } else { sprintf(data = buf2, "%ld", *VAR[i].num); max = strlen(buf2); } if (data && max) { n = dst - ptrdata(*buf); *buf = ptrpad(*buf, max); if (REAL_ERROR) return 0; dst = ptrdata(*buf) + n; memcpy(dst, data, max); dst += max; } } else if (*tmp == '}') /* met an undefined variable, consider empty */ src = tmp + 1; /* else syntax error, do nothing */ } } else if (src[0] == '#' && src[1] == '{') { int max, n; ptr pbuf = (ptr)0; src += 2; (void)evalp(&pbuf, &src); if (REAL_ERROR) { ptrdel(pbuf); return 0; } if (pbuf) { max = ptrlen(pbuf); n = dst - ptrdata(*buf); *buf = ptrpad(*buf, max); if (REAL_ERROR) { ptrdel(pbuf); return 0; } dst = ptrdata(*buf) + n; memcpy(dst, ptrdata(pbuf), max); dst += max; } ptrdel(pbuf); if (*src) src = skipspace(src); if (*src != '}') { PRINTF("#{}: "); print_error(error=MISSING_PAREN_ERROR); return 0; } done = 1; if (*src) src++; } if (!done && (*src == '$' || *src == '@' || *src == '#')) /* not matched, just copy */ *dst++ = *src++; } *dst = '\0'; ptrtrunc(*buf, dst - ptrdata(*buf)); return 1; } /* * set definition file: * rules: if powwow_dir is set it is searched first. * if file doesn't exist, it is created there. * If a slash appears in the name, the powwow_dir isn't used. */ void set_deffile __P1 (char *,arg) { if (!strchr(arg, '/') && *powwow_dir) { strcpy(deffile, powwow_dir); strcat(deffile, arg); if ((access(deffile, R_OK) == -1 || access(deffile, W_OK) == -1) && !access(arg,R_OK) && !access(arg, W_OK)) strcpy(deffile, arg); } else strcpy(deffile, arg); } /* * GH: return true if var is one of the permanent variables */ int is_permanent_variable __P1 (varnode *,v) { return (v == prompt || v == last_line); }