xref: /openbsd/bin/ed/main.c (revision db3296cf)
1 /*	$OpenBSD: main.c,v 1.28 2003/06/11 23:42:12 deraadt Exp $	*/
2 /*	$NetBSD: main.c,v 1.3 1995/03/21 09:04:44 cgd Exp $	*/
3 
4 /* main.c: This file contains the main control and user-interface routines
5    for the ed line editor. */
6 /*-
7  * Copyright (c) 1993 Andrew Moore, Talke Studio.
8  * All rights reserved.
9  *
10  * Redistribution and use in source and binary forms, with or without
11  * modification, are permitted provided that the following conditions
12  * are met:
13  * 1. Redistributions of source code must retain the above copyright
14  *    notice, this list of conditions and the following disclaimer.
15  * 2. Redistributions in binary form must reproduce the above copyright
16  *    notice, this list of conditions and the following disclaimer in the
17  *    documentation and/or other materials provided with the distribution.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29  * SUCH DAMAGE.
30  */
31 
32 #ifndef lint
33 char *copyright =
34 "@(#) Copyright (c) 1993 Andrew Moore, Talke Studio. \n\
35  All rights reserved.\n";
36 #endif /* not lint */
37 
38 #ifndef lint
39 #if 0
40 static char *rcsid = "@(#)main.c,v 1.1 1994/02/01 00:34:42 alm Exp";
41 #else
42 static char rcsid[] = "$OpenBSD: main.c,v 1.28 2003/06/11 23:42:12 deraadt Exp $";
43 #endif
44 #endif /* not lint */
45 
46 /*
47  * CREDITS
48  *
49  *	This program is based on the editor algorithm described in
50  *	Brian W. Kernighan and P. J. Plauger's book "Software Tools
51  *	in Pascal," Addison-Wesley, 1981.
52  *
53  *	The buffering algorithm is attributed to Rodney Ruddock of
54  *	the University of Guelph, Guelph, Ontario.
55  *
56  *	The cbc.c encryption code is adapted from
57  *	the bdes program by Matt Bishop of Dartmouth College,
58  *	Hanover, NH.
59  *
60  */
61 
62 #include <sys/ioctl.h>
63 #include <sys/stat.h>
64 #include <sys/wait.h>
65 #include <ctype.h>
66 #include <setjmp.h>
67 #include <unistd.h>
68 #include <pwd.h>
69 
70 #include "ed.h"
71 
72 
73 #ifdef _POSIX_SOURCE
74 sigjmp_buf env;
75 #else
76 jmp_buf env;
77 #endif
78 
79 /* static buffers */
80 char stdinbuf[1];		/* stdin buffer */
81 char *shcmd;			/* shell command buffer */
82 int shcmdsz;			/* shell command buffer size */
83 int shcmdi;			/* shell command buffer index */
84 char *ibuf;			/* ed command-line buffer */
85 int ibufsz;			/* ed command-line buffer size */
86 char *ibufp;			/* pointer to ed command-line buffer */
87 
88 /* global flags */
89 int des = 0;			/* if set, use crypt(3) for i/o */
90 int garrulous = 0;		/* if set, print all error messages */
91 int isbinary;			/* if set, buffer contains ASCII NULs */
92 int isglobal;			/* if set, doing a global command */
93 int modified;			/* if set, buffer modified since last write */
94 int mutex = 0;			/* if set, signals set "sigflags" */
95 int red = 0;			/* if set, restrict shell/directory access */
96 int scripted = 0;		/* if set, suppress diagnostics */
97 int sigflags = 0;		/* if set, signals received while mutex set */
98 int sigactive = 0;		/* if set, signal handlers are enabled */
99 int interactive = 0;		/* if set, we are in interactive mode */
100 
101 char old_filename[MAXPATHLEN] = "";	/* default filename */
102 int current_addr;		/* current address in editor buffer */
103 int addr_last;			/* last address in editor buffer */
104 int lineno;			/* script line number */
105 char *prompt;			/* command-line prompt */
106 char *dps = "*";		/* default command-line prompt */
107 
108 char *usage = "usage: %s [-] [-sx] [-p string] [name]\n";
109 
110 char *home;		/* home directory */
111 
112 void
113 seterrmsg(char *s)
114 {
115 	strlcpy(errmsg, s, sizeof(errmsg));
116 }
117 
118 /* ed: line editor */
119 int
120 main(volatile int argc, char ** volatile argv)
121 {
122 	int c, n;
123 	int status = 0;
124 
125 	home = getenv("HOME");
126 
127 	red = (n = strlen(argv[0])) > 2 && argv[0][n - 3] == 'r';
128 top:
129 	while ((c = getopt(argc, argv, "p:sx")) != -1)
130 		switch (c) {
131 		case 'p':				/* set prompt */
132 			prompt = optarg;
133 			break;
134 		case 's':				/* run script */
135 			scripted = 1;
136 			break;
137 		case 'x':				/* use crypt */
138 #ifdef DES
139 			des = get_keyword();
140 #else
141 			fprintf(stderr, "crypt unavailable\n?\n");
142 #endif
143 			break;
144 
145 		default:
146 			fprintf(stderr, usage, argv[0]);
147 			exit(1);
148 		}
149 	argv += optind;
150 	argc -= optind;
151 	if (argc && **argv == '-') {
152 		scripted = 1;
153 		if (argc > 1) {
154 			optind = 1;
155 			goto top;
156 		}
157 		argv++;
158 		argc--;
159 	}
160 
161 	if (!(interactive = isatty(0))) {
162 		struct stat sb;
163 
164 		/* assert: pipes show up as fifo's when fstat'd */
165 		if (fstat(0, &sb) || !S_ISFIFO(sb.st_mode)) {
166 			if (lseek(0, 0, SEEK_CUR)) {
167 				interactive = 1;
168 				setlinebuf(stdout);
169 			}
170 		}
171 	}
172 
173 	/* assert: reliable signals! */
174 #ifdef SIGWINCH
175 	handle_winch(SIGWINCH);
176 	if (isatty(0))
177 		signal(SIGWINCH, handle_winch);
178 #endif
179 	signal(SIGHUP, signal_hup);
180 	signal(SIGQUIT, SIG_IGN);
181 	signal(SIGINT, signal_int);
182 #ifdef _POSIX_SOURCE
183 	if (status = sigsetjmp(env, 1))
184 #else
185 	if ((status = setjmp(env)) != 0)
186 #endif
187 	{
188 		fputs("\n?\n", stderr);
189 		seterrmsg("interrupt");
190 	} else {
191 		init_buffers();
192 		sigactive = 1;			/* enable signal handlers */
193 		if (argc && **argv && is_legal_filename(*argv)) {
194 			if (read_file(*argv, 0) < 0 && !interactive)
195 				quit(2);
196 			else if (**argv != '!')
197 				strlcpy(old_filename, *argv,
198 				    sizeof old_filename);
199 		} else if (argc) {
200 			fputs("?\n", stderr);
201 			if (**argv == '\0')
202 				seterrmsg("invalid filename");
203 			if (!interactive)
204 				quit(2);
205 		}
206 	}
207 	for (;;) {
208 		if (status < 0 && garrulous)
209 			fprintf(stderr, "%s\n", errmsg);
210 		if (prompt) {
211 			fputs(prompt, stdout);
212 			fflush(stdout);
213 		}
214 		if ((n = get_tty_line()) < 0) {
215 			status = ERR;
216 			continue;
217 		} else if (n == 0) {
218 			if (modified && !scripted) {
219 				fputs("?\n", stderr);
220 				seterrmsg("warning: file modified");
221 				if (!interactive) {
222 					fprintf(stderr, garrulous ?
223 					    "script, line %d: %s\n" :
224 					    "", lineno, errmsg);
225 					quit(2);
226 				}
227 				clearerr(stdin);
228 				modified = 0;
229 				status = EMOD;
230 				continue;
231 			} else
232 				quit(0);
233 		} else if (ibuf[n - 1] != '\n') {
234 			/* discard line */
235 			seterrmsg("unexpected end-of-file");
236 			clearerr(stdin);
237 			status = ERR;
238 			continue;
239 		}
240 		isglobal = 0;
241 		if ((status = extract_addr_range()) >= 0 &&
242 		    (status = exec_command()) >= 0)
243 			if (!status || (status &&
244 			    (status = display_lines(current_addr, current_addr,
245 				status)) >= 0))
246 				continue;
247 		switch (status) {
248 		case EOF:
249 			quit(0);
250 		case EMOD:
251 			modified = 0;
252 			fputs("?\n", stderr);		/* give warning */
253 			seterrmsg("warning: file modified");
254 			if (!interactive) {
255 				fprintf(stderr, garrulous ?
256 				    "script, line %d: %s\n" :
257 				    "", lineno, errmsg);
258 				quit(2);
259 			}
260 			break;
261 		case FATAL:
262 			if (!interactive)
263 				fprintf(stderr, garrulous ?
264 				    "script, line %d: %s\n" : "",
265 				    lineno, errmsg);
266 			else
267 				fprintf(stderr, garrulous ? "%s\n" : "",
268 				    errmsg);
269 			quit(3);
270 		default:
271 			fputs("?\n", stderr);
272 			if (!interactive) {
273 				fprintf(stderr, garrulous ?
274 				    "script, line %d: %s\n" : "",
275 				    lineno, errmsg);
276 				quit(2);
277 			}
278 			break;
279 		}
280 	}
281 	/*NOTREACHED*/
282 }
283 
284 int first_addr, second_addr, addr_cnt;
285 
286 /* extract_addr_range: get line addresses from the command buffer until an
287    illegal address is seen; return status */
288 int
289 extract_addr_range(void)
290 {
291 	int addr;
292 
293 	addr_cnt = 0;
294 	first_addr = second_addr = current_addr;
295 	while ((addr = next_addr()) >= 0) {
296 		addr_cnt++;
297 		first_addr = second_addr;
298 		second_addr = addr;
299 		if (*ibufp != ',' && *ibufp != ';')
300 			break;
301 		else if (*ibufp++ == ';')
302 			current_addr = addr;
303 	}
304 	if ((addr_cnt = min(addr_cnt, 2)) == 1 || second_addr != addr)
305 		first_addr = second_addr;
306 	return (addr == ERR) ? ERR : 0;
307 }
308 
309 
310 #define	SKIP_BLANKS() \
311 	do { \
312 		while (isspace(*ibufp) && *ibufp != '\n') \
313 			ibufp++; \
314 	} while (0)
315 
316 #define MUST_BE_FIRST() \
317 	do { \
318 		if (!first) { \
319 			seterrmsg("invalid address"); \
320 			return ERR; \
321 		} \
322 	} while (0)
323 
324 
325 /*  next_addr: return the next line address in the command buffer */
326 int
327 next_addr(void)
328 {
329 	char *hd;
330 	int addr = current_addr;
331 	int n;
332 	int first = 1;
333 	int c;
334 
335 	SKIP_BLANKS();
336 	for (hd = ibufp;; first = 0)
337 		switch ((c = *ibufp)) {
338 		case '+':
339 		case '\t':
340 		case ' ':
341 		case '-':
342 		case '^':
343 			ibufp++;
344 			SKIP_BLANKS();
345 			if (isdigit(*ibufp)) {
346 				STRTOI(n, ibufp);
347 				addr += (c == '-' || c == '^') ? -n : n;
348 			} else if (!isspace(c))
349 				addr += (c == '-' || c == '^') ? -1 : 1;
350 			break;
351 		case '0': case '1': case '2':
352 		case '3': case '4': case '5':
353 		case '6': case '7': case '8': case '9':
354 			MUST_BE_FIRST();
355 			STRTOI(addr, ibufp);
356 			break;
357 		case '.':
358 		case '$':
359 			MUST_BE_FIRST();
360 			ibufp++;
361 			addr = (c == '.') ? current_addr : addr_last;
362 			break;
363 		case '/':
364 		case '?':
365 			MUST_BE_FIRST();
366 			if ((addr = get_matching_node_addr(
367 			    get_compiled_pattern(), c == '/')) < 0)
368 				return ERR;
369 			else if (c == *ibufp)
370 				ibufp++;
371 			break;
372 		case '\'':
373 			MUST_BE_FIRST();
374 			ibufp++;
375 			if ((addr = get_marked_node_addr(*ibufp++)) < 0)
376 				return ERR;
377 			break;
378 		case '%':
379 		case ',':
380 		case ';':
381 			if (first) {
382 				ibufp++;
383 				addr_cnt++;
384 				second_addr = (c == ';') ? current_addr : 1;
385 				addr = addr_last;
386 				break;
387 			}
388 			/* FALL THROUGH */
389 		default:
390 			if (ibufp == hd)
391 				return EOF;
392 			else if (addr < 0 || addr_last < addr) {
393 				seterrmsg("invalid address");
394 				return ERR;
395 			} else
396 				return addr;
397 		}
398 	/* NOTREACHED */
399 }
400 
401 
402 #ifdef BACKWARDS
403 /* GET_THIRD_ADDR: get a legal address from the command buffer */
404 #define GET_THIRD_ADDR(addr) \
405 	do { \
406 		int ol1, ol2; \
407 		\
408 		ol1 = first_addr; \
409 		ol2 = second_addr; \
410 		if (extract_addr_range() < 0) \
411 			return ERR; \
412 		else if (addr_cnt == 0) { \
413 			seterrmsg("destination expected"); \
414 			return ERR; \
415 		} else if (second_addr < 0 || addr_last < second_addr) { \
416 			seterrmsg("invalid address"); \
417 			return ERR; \
418 		} \
419 		addr = second_addr; \
420 		first_addr = ol1; \
421 		second_addr = ol2; \
422 	} while (0)
423 
424 #else	/* BACKWARDS */
425 /* GET_THIRD_ADDR: get a legal address from the command buffer */
426 #define GET_THIRD_ADDR(addr) \
427 	do { \
428 		int ol1, ol2; \
429 		\
430 		ol1 = first_addr; \
431 		ol2 = second_addr; \
432 		if (extract_addr_range() < 0) \
433 			return ERR; \
434 		if (second_addr < 0 || addr_last < second_addr) { \
435 			seterrmsg("invalid address"); \
436 			return ERR; \
437 		} \
438 		addr = second_addr; \
439 		first_addr = ol1; \
440 		second_addr = ol2; \
441 	} while (0)
442 #endif
443 
444 
445 /* GET_COMMAND_SUFFIX: verify the command suffix in the command buffer */
446 #define GET_COMMAND_SUFFIX() \
447 	do { \
448 		int done = 0; \
449 		do { \
450 			switch (*ibufp) { \
451 			case 'p': \
452 				gflag |= GPR; \
453 				ibufp++; \
454 				break; \
455 			case 'l': \
456 				gflag |= GLS; \
457 				ibufp++; \
458 				break; \
459 			case 'n': \
460 				gflag |= GNP; \
461 				ibufp++; \
462 				break; \
463 			default: \
464 				done++; \
465 			} \
466 		} while (!done); \
467 		if (*ibufp++ != '\n') { \
468 			seterrmsg("invalid command suffix"); \
469 			return ERR; \
470 		} \
471 	} while (0)
472 
473 /* sflags */
474 #define SGG 001		/* complement previous global substitute suffix */
475 #define SGP 002		/* complement previous print suffix */
476 #define SGR 004		/* use last regex instead of last pat */
477 #define SGF 010		/* repeat last substitution */
478 
479 int patlock = 0;	/* if set, pattern not freed by get_compiled_pattern() */
480 
481 volatile sig_atomic_t rows = 22;	/* scroll length: ws_row - 2 */
482 volatile sig_atomic_t cols = 72;	/* wrap column */
483 
484 /* exec_command: execute the next command in command buffer; return print
485    request, if any */
486 int
487 exec_command(void)
488 {
489 	extern int u_current_addr;
490 	extern int u_addr_last;
491 
492 	static pattern_t *pat = NULL;
493 	static int sgflag = 0;
494 	static int sgnum = 0;
495 
496 	pattern_t *tpat;
497 	char *fnp;
498 	int gflag = 0;
499 	int sflags = 0;
500 	int addr = 0;
501 	int n = 0;
502 	int c;
503 
504 	SKIP_BLANKS();
505 	switch ((c = *ibufp++)) {
506 	case 'a':
507 		GET_COMMAND_SUFFIX();
508 		if (!isglobal) clear_undo_stack();
509 		if (append_lines(second_addr) < 0)
510 			return ERR;
511 		break;
512 	case 'c':
513 		if (check_addr_range(current_addr, current_addr) < 0)
514 			return ERR;
515 		GET_COMMAND_SUFFIX();
516 		if (!isglobal) clear_undo_stack();
517 		if (delete_lines(first_addr, second_addr) < 0 ||
518 		    append_lines(current_addr) < 0)
519 			return ERR;
520 		break;
521 	case 'd':
522 		if (check_addr_range(current_addr, current_addr) < 0)
523 			return ERR;
524 		GET_COMMAND_SUFFIX();
525 		if (!isglobal) clear_undo_stack();
526 		if (delete_lines(first_addr, second_addr) < 0)
527 			return ERR;
528 		else if ((addr = INC_MOD(current_addr, addr_last)) != 0)
529 			current_addr = addr;
530 		break;
531 	case 'e':
532 		if (modified && !scripted)
533 			return EMOD;
534 		/* fall through */
535 	case 'E':
536 		if (addr_cnt > 0) {
537 			seterrmsg("unexpected address");
538 			return ERR;
539 		} else if (!isspace(*ibufp)) {
540 			seterrmsg("unexpected command suffix");
541 			return ERR;
542 		} else if ((fnp = get_filename()) == NULL)
543 			return ERR;
544 		GET_COMMAND_SUFFIX();
545 		if (delete_lines(1, addr_last) < 0)
546 			return ERR;
547 		clear_undo_stack();
548 		if (close_sbuf() < 0)
549 			return ERR;
550 		else if (open_sbuf() < 0)
551 			return FATAL;
552 		if (*fnp && *fnp != '!')
553 			strlcpy(old_filename, fnp, sizeof old_filename);
554 #ifdef BACKWARDS
555 		if (*fnp == '\0' && *old_filename == '\0') {
556 			seterrmsg("no current filename");
557 			return ERR;
558 		}
559 #endif
560 		if (read_file(*fnp ? fnp : old_filename, 0) < 0)
561 			return ERR;
562 		clear_undo_stack();
563 		modified = 0;
564 		u_current_addr = u_addr_last = -1;
565 		break;
566 	case 'f':
567 		if (addr_cnt > 0) {
568 			seterrmsg("unexpected address");
569 			return ERR;
570 		} else if (!isspace(*ibufp)) {
571 			seterrmsg("unexpected command suffix");
572 			return ERR;
573 		} else if ((fnp = get_filename()) == NULL)
574 			return ERR;
575 		else if (*fnp == '!') {
576 			seterrmsg("invalid redirection");
577 			return ERR;
578 		}
579 		GET_COMMAND_SUFFIX();
580 		if (*fnp)
581 			strlcpy(old_filename, fnp, sizeof old_filename);
582 		puts(strip_escapes(old_filename));
583 		break;
584 	case 'g':
585 	case 'v':
586 	case 'G':
587 	case 'V':
588 		if (isglobal) {
589 			seterrmsg("cannot nest global commands");
590 			return ERR;
591 		} else if (check_addr_range(1, addr_last) < 0)
592 			return ERR;
593 		else if (build_active_list(c == 'g' || c == 'G') < 0)
594 			return ERR;
595 		else if ((n = (c == 'G' || c == 'V')))
596 			GET_COMMAND_SUFFIX();
597 		isglobal++;
598 		if (exec_global(n, gflag) < 0)
599 			return ERR;
600 		break;
601 	case 'h':
602 		if (addr_cnt > 0) {
603 			seterrmsg("unexpected address");
604 			return ERR;
605 		}
606 		GET_COMMAND_SUFFIX();
607 		if (*errmsg) fprintf(stderr, "%s\n", errmsg);
608 		break;
609 	case 'H':
610 		if (addr_cnt > 0) {
611 			seterrmsg("unexpected address");
612 			return ERR;
613 		}
614 		GET_COMMAND_SUFFIX();
615 		if ((garrulous = 1 - garrulous) && *errmsg)
616 			fprintf(stderr, "%s\n", errmsg);
617 		break;
618 	case 'i':
619 		if (second_addr == 0) {
620 			seterrmsg("invalid address");
621 			return ERR;
622 		}
623 		GET_COMMAND_SUFFIX();
624 		if (!isglobal) clear_undo_stack();
625 		if (append_lines(second_addr - 1) < 0)
626 			return ERR;
627 		break;
628 	case 'j':
629 		if (check_addr_range(current_addr, current_addr + 1) < 0)
630 			return ERR;
631 		GET_COMMAND_SUFFIX();
632 		if (!isglobal) clear_undo_stack();
633 		if (first_addr != second_addr &&
634 		    join_lines(first_addr, second_addr) < 0)
635 			return ERR;
636 		break;
637 	case 'k':
638 		c = *ibufp++;
639 		if (second_addr == 0) {
640 			seterrmsg("invalid address");
641 			return ERR;
642 		}
643 		GET_COMMAND_SUFFIX();
644 		if (mark_line_node(get_addressed_line_node(second_addr), c) < 0)
645 			return ERR;
646 		break;
647 	case 'l':
648 		if (check_addr_range(current_addr, current_addr) < 0)
649 			return ERR;
650 		GET_COMMAND_SUFFIX();
651 		if (display_lines(first_addr, second_addr, gflag | GLS) < 0)
652 			return ERR;
653 		gflag = 0;
654 		break;
655 	case 'm':
656 		if (check_addr_range(current_addr, current_addr) < 0)
657 			return ERR;
658 		GET_THIRD_ADDR(addr);
659 		if (first_addr <= addr && addr < second_addr) {
660 			seterrmsg("invalid destination");
661 			return ERR;
662 		}
663 		GET_COMMAND_SUFFIX();
664 		if (!isglobal) clear_undo_stack();
665 		if (move_lines(addr) < 0)
666 			return ERR;
667 		break;
668 	case 'n':
669 		if (check_addr_range(current_addr, current_addr) < 0)
670 			return ERR;
671 		GET_COMMAND_SUFFIX();
672 		if (display_lines(first_addr, second_addr, gflag | GNP) < 0)
673 			return ERR;
674 		gflag = 0;
675 		break;
676 	case 'p':
677 		if (check_addr_range(current_addr, current_addr) < 0)
678 			return ERR;
679 		GET_COMMAND_SUFFIX();
680 		if (display_lines(first_addr, second_addr, gflag | GPR) < 0)
681 			return ERR;
682 		gflag = 0;
683 		break;
684 	case 'P':
685 		if (addr_cnt > 0) {
686 			seterrmsg("unexpected address");
687 			return ERR;
688 		}
689 		GET_COMMAND_SUFFIX();
690 		prompt = prompt ? NULL : optarg ? optarg : dps;
691 		break;
692 	case 'q':
693 	case 'Q':
694 		if (addr_cnt > 0) {
695 			seterrmsg("unexpected address");
696 			return ERR;
697 		}
698 		GET_COMMAND_SUFFIX();
699 		gflag =  (modified && !scripted && c == 'q') ? EMOD : EOF;
700 		break;
701 	case 'r':
702 		if (!isspace(*ibufp)) {
703 			seterrmsg("unexpected command suffix");
704 			return ERR;
705 		} else if (addr_cnt == 0)
706 			second_addr = addr_last;
707 		if ((fnp = get_filename()) == NULL)
708 			return ERR;
709 		GET_COMMAND_SUFFIX();
710 		if (!isglobal) clear_undo_stack();
711 		if (*old_filename == '\0' && *fnp != '!')
712 			strlcpy(old_filename, fnp, sizeof old_filename);
713 #ifdef BACKWARDS
714 		if (*fnp == '\0' && *old_filename == '\0') {
715 			seterrmsg("no current filename");
716 			return ERR;
717 		}
718 #endif
719 		if ((addr = read_file(*fnp ? fnp : old_filename,
720 		    second_addr)) < 0)
721 			return ERR;
722 		else if (addr && addr != addr_last)
723 			modified = 1;
724 		break;
725 	case 's':
726 		do {
727 			switch (*ibufp) {
728 			case '\n':
729 				sflags |=SGF;
730 				break;
731 			case 'g':
732 				sflags |= SGG;
733 				ibufp++;
734 				break;
735 			case 'p':
736 				sflags |= SGP;
737 				ibufp++;
738 				break;
739 			case 'r':
740 				sflags |= SGR;
741 				ibufp++;
742 				break;
743 			case '0': case '1': case '2': case '3': case '4':
744 			case '5': case '6': case '7': case '8': case '9':
745 				STRTOI(sgnum, ibufp);
746 				sflags |= SGF;
747 				sgflag &= ~GSG;		/* override GSG */
748 				break;
749 			default:
750 				if (sflags) {
751 					seterrmsg("invalid command suffix");
752 					return ERR;
753 				}
754 			}
755 		} while (sflags && *ibufp != '\n');
756 		if (sflags && !pat) {
757 			seterrmsg("no previous substitution");
758 			return ERR;
759 		} else if (sflags & SGG)
760 			sgnum = 0;		/* override numeric arg */
761 		if (*ibufp != '\n' && *(ibufp + 1) == '\n') {
762 			seterrmsg("invalid pattern delimiter");
763 			return ERR;
764 		}
765 		tpat = pat;
766 		SPL1();
767 		if ((!sflags || (sflags & SGR)) &&
768 		    (tpat = get_compiled_pattern()) == NULL) {
769 		 	SPL0();
770 			return ERR;
771 		} else if (tpat != pat) {
772 			if (pat) {
773 				regfree(pat);
774 				free(pat);
775 			}
776 			pat = tpat;
777 			patlock = 1;		/* reserve pattern */
778 		}
779 		SPL0();
780 		if (!sflags && extract_subst_tail(&sgflag, &sgnum) < 0)
781 			return ERR;
782 		else if (isglobal)
783 			sgflag |= GLB;
784 		else
785 			sgflag &= ~GLB;
786 		if (sflags & SGG)
787 			sgflag ^= GSG;
788 		if (sflags & SGP) {
789 			sgflag ^= GPR;
790 			sgflag &= ~(GLS | GNP);
791 		}
792 		do {
793 			switch (*ibufp) {
794 			case 'p':
795 				sgflag |= GPR;
796 				ibufp++;
797 				break;
798 			case 'l':
799 				sgflag |= GLS;
800 				ibufp++;
801 				break;
802 			case 'n':
803 				sgflag |= GNP;
804 				ibufp++;
805 				break;
806 			default:
807 				n++;
808 			}
809 		} while (!n);
810 		if (check_addr_range(current_addr, current_addr) < 0)
811 			return ERR;
812 		GET_COMMAND_SUFFIX();
813 		if (!isglobal) clear_undo_stack();
814 		if (search_and_replace(pat, sgflag, sgnum) < 0)
815 			return ERR;
816 		break;
817 	case 't':
818 		if (check_addr_range(current_addr, current_addr) < 0)
819 			return ERR;
820 		GET_THIRD_ADDR(addr);
821 		GET_COMMAND_SUFFIX();
822 		if (!isglobal) clear_undo_stack();
823 		if (copy_lines(addr) < 0)
824 			return ERR;
825 		break;
826 	case 'u':
827 		if (addr_cnt > 0) {
828 			seterrmsg("unexpected address");
829 			return ERR;
830 		}
831 		GET_COMMAND_SUFFIX();
832 		if (pop_undo_stack() < 0)
833 			return ERR;
834 		break;
835 	case 'w':
836 	case 'W':
837 		if ((n = *ibufp) == 'q' || n == 'Q') {
838 			gflag = EOF;
839 			ibufp++;
840 		}
841 		if (!isspace(*ibufp)) {
842 			seterrmsg("unexpected command suffix");
843 			return ERR;
844 		} else if ((fnp = get_filename()) == NULL)
845 			return ERR;
846 		if (addr_cnt == 0 && !addr_last)
847 			first_addr = second_addr = 0;
848 		else if (check_addr_range(1, addr_last) < 0)
849 			return ERR;
850 		GET_COMMAND_SUFFIX();
851 		if (*old_filename == '\0' && *fnp != '!')
852 			strlcpy(old_filename, fnp, sizeof old_filename);
853 #ifdef BACKWARDS
854 		if (*fnp == '\0' && *old_filename == '\0') {
855 			seterrmsg("no current filename");
856 			return ERR;
857 		}
858 #endif
859 		if ((addr = write_file(*fnp ? fnp : old_filename,
860 		    (c == 'W') ? "a" : "w", first_addr, second_addr)) < 0)
861 			return ERR;
862 		else if (addr == addr_last)
863 			modified = 0;
864 		else if (modified && !scripted && n == 'q')
865 			gflag = EMOD;
866 		break;
867 	case 'x':
868 		if (addr_cnt > 0) {
869 			seterrmsg("unexpected address");
870 			return ERR;
871 		}
872 		GET_COMMAND_SUFFIX();
873 #ifdef DES
874 		des = get_keyword();
875 #else
876 		seterrmsg("crypt unavailable");
877 		return ERR;
878 #endif
879 		break;
880 	case 'z':
881 		first_addr = 1;
882 #ifdef BACKWARDS
883 		if (check_addr_range(first_addr, current_addr + 1) < 0)
884 #else
885 		if (check_addr_range(first_addr, current_addr + !isglobal) < 0)
886 #endif
887 			return ERR;
888 		else if ('0' < *ibufp && *ibufp <= '9')
889 			STRTOI(rows, ibufp);
890 		GET_COMMAND_SUFFIX();
891 		if (display_lines(second_addr, min(addr_last,
892 		    second_addr + rows), gflag) < 0)
893 			return ERR;
894 		gflag = 0;
895 		break;
896 	case '=':
897 		GET_COMMAND_SUFFIX();
898 		printf("%d\n", addr_cnt ? second_addr : addr_last);
899 		break;
900 	case '!':
901 		if (addr_cnt > 0) {
902 			seterrmsg("unexpected address");
903 			return ERR;
904 		} else if ((sflags = get_shell_command()) < 0)
905 			return ERR;
906 		GET_COMMAND_SUFFIX();
907 		if (sflags) printf("%s\n", shcmd + 1);
908 		system(shcmd + 1);
909 		if (!scripted) printf("!\n");
910 		break;
911 	case '\n':
912 		first_addr = 1;
913 #ifdef BACKWARDS
914 		if (check_addr_range(first_addr, current_addr + 1) < 0
915 #else
916 		if (check_addr_range(first_addr, current_addr + !isglobal) < 0
917 #endif
918 		 || display_lines(second_addr, second_addr, 0) < 0)
919 			return ERR;
920 		break;
921 	default:
922 		seterrmsg("unknown command");
923 		return ERR;
924 	}
925 	return gflag;
926 }
927 
928 
929 /* check_addr_range: return status of address range check */
930 int
931 check_addr_range(int n, int m)
932 {
933 	if (addr_cnt == 0) {
934 		first_addr = n;
935 		second_addr = m;
936 	}
937 	if (first_addr > second_addr || 1 > first_addr ||
938 	    second_addr > addr_last) {
939 		seterrmsg("invalid address");
940 		return ERR;
941 	}
942 	return 0;
943 }
944 
945 
946 /* get_matching_node_addr: return the address of the next line matching a
947    pattern in a given direction.  wrap around begin/end of editor buffer if
948    necessary */
949 int
950 get_matching_node_addr(pattern_t *pat, int dir)
951 {
952 	char *s;
953 	int n = current_addr;
954 	line_t *lp;
955 
956 	if (!pat) return ERR;
957 	do {
958 		if ((n = dir ? INC_MOD(n, addr_last) : DEC_MOD(n, addr_last))) {
959 			lp = get_addressed_line_node(n);
960 			if ((s = get_sbuf_line(lp)) == NULL)
961 				return ERR;
962 			if (isbinary)
963 				NUL_TO_NEWLINE(s, lp->len);
964 			if (!regexec(pat, s, 0, NULL, 0))
965 				return n;
966 		}
967 	} while (n != current_addr);
968 	seterrmsg("no match");
969 	return  ERR;
970 }
971 
972 
973 /* get_filename: return pointer to copy of filename in the command buffer */
974 char *
975 get_filename(void)
976 {
977 	static char *file = NULL;
978 	static int filesz = 0;
979 	int n;
980 
981 	if (*ibufp != '\n') {
982 		SKIP_BLANKS();
983 		if (*ibufp == '\n') {
984 			seterrmsg("invalid filename");
985 			return NULL;
986 		} else if ((ibufp = get_extended_line(&n, 1)) == NULL)
987 			return NULL;
988 		else if (*ibufp == '!') {
989 			ibufp++;
990 			if ((n = get_shell_command()) < 0)
991 				return NULL;
992 			if (n) printf("%s\n", shcmd + 1);
993 			return shcmd;
994 		} else if (n >= MAXPATHLEN) {
995 			seterrmsg("filename too long");
996 			return  NULL;
997 		}
998 	}
999 #ifndef BACKWARDS
1000 	else if (*old_filename == '\0') {
1001 		seterrmsg("no current filename");
1002 		return  NULL;
1003 	}
1004 #endif
1005 	REALLOC(file, filesz, MAXPATHLEN, NULL);
1006 	for (n = 0; *ibufp != '\n';)
1007 		file[n++] = *ibufp++;
1008 	file[n] = '\0';
1009 	return is_legal_filename(file) ? file : NULL;
1010 }
1011 
1012 
1013 /* get_shell_command: read a shell command from stdin; return substitution
1014    status */
1015 int
1016 get_shell_command(void)
1017 {
1018 	static char *buf = NULL;
1019 	static int n = 0;
1020 
1021 	char *s;			/* substitution char pointer */
1022 	int i = 0;
1023 	int j = 0;
1024 
1025 	if (red) {
1026 		seterrmsg("shell access restricted");
1027 		return ERR;
1028 	} else if ((s = ibufp = get_extended_line(&j, 1)) == NULL)
1029 		return ERR;
1030 	REALLOC(buf, n, j + 1, ERR);
1031 	buf[i++] = '!';			/* prefix command w/ bang */
1032 	while (*ibufp != '\n')
1033 		switch (*ibufp) {
1034 		default:
1035 			REALLOC(buf, n, i + 2, ERR);
1036 			buf[i++] = *ibufp;
1037 			if (*ibufp++ == '\\')
1038 				buf[i++] = *ibufp++;
1039 			break;
1040 		case '!':
1041 			if (s != ibufp) {
1042 				REALLOC(buf, n, i + 1, ERR);
1043 				buf[i++] = *ibufp++;
1044 			}
1045 #ifdef BACKWARDS
1046 			else if (shcmd == NULL || *(shcmd + 1) == '\0')
1047 #else
1048 			else if (shcmd == NULL)
1049 #endif
1050 			{
1051 				seterrmsg("no previous command");
1052 				return ERR;
1053 			} else {
1054 				REALLOC(buf, n, i + shcmdi, ERR);
1055 				for (s = shcmd + 1; s < shcmd + shcmdi;)
1056 					buf[i++] = *s++;
1057 				s = ibufp++;
1058 			}
1059 			break;
1060 		case '%':
1061 			if (*old_filename  == '\0') {
1062 				seterrmsg("no current filename");
1063 				return ERR;
1064 			}
1065 			j = strlen(s = strip_escapes(old_filename));
1066 			REALLOC(buf, n, i + j, ERR);
1067 			while (j--)
1068 				buf[i++] = *s++;
1069 			s = ibufp++;
1070 			break;
1071 		}
1072 	REALLOC(shcmd, shcmdsz, i + 1, ERR);
1073 	memcpy(shcmd, buf, i);
1074 	shcmd[shcmdi = i] = '\0';
1075 	return *s == '!' || *s == '%';
1076 }
1077 
1078 
1079 /* append_lines: insert text from stdin to after line n; stop when either a
1080    single period is read or EOF; return status */
1081 int
1082 append_lines(int n)
1083 {
1084 	int l;
1085 	char *lp = ibuf;
1086 	char *eot;
1087 	undo_t *up = NULL;
1088 
1089 	for (current_addr = n;;) {
1090 		if (!isglobal) {
1091 			if ((l = get_tty_line()) < 0)
1092 				return ERR;
1093 			else if (l == 0 || ibuf[l - 1] != '\n') {
1094 				clearerr(stdin);
1095 				return  l ? EOF : 0;
1096 			}
1097 			lp = ibuf;
1098 		} else if (*(lp = ibufp) == '\0')
1099 			return 0;
1100 		else {
1101 			while (*ibufp++ != '\n')
1102 				;
1103 			l = ibufp - lp;
1104 		}
1105 		if (l == 2 && lp[0] == '.' && lp[1] == '\n') {
1106 			return 0;
1107 		}
1108 		eot = lp + l;
1109 		SPL1();
1110 		do {
1111 			if ((lp = put_sbuf_line(lp)) == NULL) {
1112 				SPL0();
1113 				return ERR;
1114 			} else if (up)
1115 				up->t = get_addressed_line_node(current_addr);
1116 			else if ((up = push_undo_stack(UADD, current_addr,
1117 			    current_addr)) == NULL) {
1118 				SPL0();
1119 				return ERR;
1120 			}
1121 		} while (lp != eot);
1122 		modified = 1;
1123 		SPL0();
1124 	}
1125 	/* NOTREACHED */
1126 }
1127 
1128 
1129 /* join_lines: replace a range of lines with the joined text of those lines */
1130 int
1131 join_lines(int from, int to)
1132 {
1133 	static char *buf = NULL;
1134 	static int n;
1135 
1136 	char *s;
1137 	int size = 0;
1138 	line_t *bp, *ep;
1139 
1140 	ep = get_addressed_line_node(INC_MOD(to, addr_last));
1141 	bp = get_addressed_line_node(from);
1142 	for (; bp != ep; bp = bp->q_forw) {
1143 		if ((s = get_sbuf_line(bp)) == NULL)
1144 			return ERR;
1145 		REALLOC(buf, n, size + bp->len, ERR);
1146 		memcpy(buf + size, s, bp->len);
1147 		size += bp->len;
1148 	}
1149 	REALLOC(buf, n, size + 2, ERR);
1150 	memcpy(buf + size, "\n", 2);
1151 	if (delete_lines(from, to) < 0)
1152 		return ERR;
1153 	current_addr = from - 1;
1154 	SPL1();
1155 	if (put_sbuf_line(buf) == NULL ||
1156 	    push_undo_stack(UADD, current_addr, current_addr) == NULL) {
1157 		SPL0();
1158 		return ERR;
1159 	}
1160 	modified = 1;
1161 	SPL0();
1162 	return 0;
1163 }
1164 
1165 
1166 /* move_lines: move a range of lines */
1167 int
1168 move_lines(int addr)
1169 {
1170 	line_t *b1, *a1, *b2, *a2;
1171 	int n = INC_MOD(second_addr, addr_last);
1172 	int p = first_addr - 1;
1173 	int done = (addr == first_addr - 1 || addr == second_addr);
1174 
1175 	SPL1();
1176 	if (done) {
1177 		a2 = get_addressed_line_node(n);
1178 		b2 = get_addressed_line_node(p);
1179 		current_addr = second_addr;
1180 	} else if (push_undo_stack(UMOV, p, n) == NULL ||
1181 	    push_undo_stack(UMOV, addr, INC_MOD(addr, addr_last)) == NULL) {
1182 		SPL0();
1183 		return ERR;
1184 	} else {
1185 		a1 = get_addressed_line_node(n);
1186 		if (addr < first_addr) {
1187 			b1 = get_addressed_line_node(p);
1188 			b2 = get_addressed_line_node(addr);
1189 					/* this get_addressed_line_node last! */
1190 		} else {
1191 			b2 = get_addressed_line_node(addr);
1192 			b1 = get_addressed_line_node(p);
1193 					/* this get_addressed_line_node last! */
1194 		}
1195 		a2 = b2->q_forw;
1196 		REQUE(b2, b1->q_forw);
1197 		REQUE(a1->q_back, a2);
1198 		REQUE(b1, a1);
1199 		current_addr = addr + ((addr < first_addr) ?
1200 		    second_addr - first_addr + 1 : 0);
1201 	}
1202 	if (isglobal)
1203 		unset_active_nodes(b2->q_forw, a2);
1204 	modified = 1;
1205 	SPL0();
1206 	return 0;
1207 }
1208 
1209 
1210 /* copy_lines: copy a range of lines; return status */
1211 int
1212 copy_lines(int addr)
1213 {
1214 	line_t *lp, *np = get_addressed_line_node(first_addr);
1215 	undo_t *up = NULL;
1216 	int n = second_addr - first_addr + 1;
1217 	int m = 0;
1218 
1219 	current_addr = addr;
1220 	if (first_addr <= addr && addr < second_addr) {
1221 		n =  addr - first_addr + 1;
1222 		m = second_addr - addr;
1223 	}
1224 	for (; n > 0; n=m, m=0, np = get_addressed_line_node(current_addr + 1))
1225 		for (; n-- > 0; np = np->q_forw) {
1226 			SPL1();
1227 			if ((lp = dup_line_node(np)) == NULL) {
1228 				SPL0();
1229 				return ERR;
1230 			}
1231 			add_line_node(lp);
1232 			if (up)
1233 				up->t = lp;
1234 			else if ((up = push_undo_stack(UADD, current_addr,
1235 			    current_addr)) == NULL) {
1236 				SPL0();
1237 				return ERR;
1238 			}
1239 			modified = 1;
1240 			SPL0();
1241 		}
1242 	return 0;
1243 }
1244 
1245 
1246 /* delete_lines: delete a range of lines */
1247 int
1248 delete_lines(int from, int to)
1249 {
1250 	line_t *n, *p;
1251 
1252 	SPL1();
1253 	if (push_undo_stack(UDEL, from, to) == NULL) {
1254 		SPL0();
1255 		return ERR;
1256 	}
1257 	n = get_addressed_line_node(INC_MOD(to, addr_last));
1258 	p = get_addressed_line_node(from - 1);
1259 					/* this get_addressed_line_node last! */
1260 	if (isglobal)
1261 		unset_active_nodes(p->q_forw, n);
1262 	REQUE(p, n);
1263 	addr_last -= to - from + 1;
1264 	current_addr = from - 1;
1265 	modified = 1;
1266 	SPL0();
1267 	return 0;
1268 }
1269 
1270 
1271 /* display_lines: print a range of lines to stdout */
1272 int
1273 display_lines(int from, int to, int gflag)
1274 {
1275 	line_t *bp;
1276 	line_t *ep;
1277 	char *s;
1278 
1279 	if (!from) {
1280 		seterrmsg("invalid address");
1281 		return ERR;
1282 	}
1283 	ep = get_addressed_line_node(INC_MOD(to, addr_last));
1284 	bp = get_addressed_line_node(from);
1285 	for (; bp != ep; bp = bp->q_forw) {
1286 		if ((s = get_sbuf_line(bp)) == NULL)
1287 			return ERR;
1288 		if (put_tty_line(s, bp->len, current_addr = from++, gflag) < 0)
1289 			return ERR;
1290 	}
1291 	return 0;
1292 }
1293 
1294 
1295 #define MAXMARK 26			/* max number of marks */
1296 
1297 line_t	*mark[MAXMARK];			/* line markers */
1298 int markno;				/* line marker count */
1299 
1300 /* mark_line_node: set a line node mark */
1301 int
1302 mark_line_node(line_t *lp, int n)
1303 {
1304 	if (!islower(n)) {
1305 		seterrmsg("invalid mark character");
1306 		return ERR;
1307 	} else if (mark[n - 'a'] == NULL)
1308 		markno++;
1309 	mark[n - 'a'] = lp;
1310 	return 0;
1311 }
1312 
1313 
1314 /* get_marked_node_addr: return address of a marked line */
1315 int
1316 get_marked_node_addr(int n)
1317 {
1318 	if (!islower(n)) {
1319 		seterrmsg("invalid mark character");
1320 		return ERR;
1321 	}
1322 	return get_line_node_addr(mark[n - 'a']);
1323 }
1324 
1325 
1326 /* unmark_line_node: clear line node mark */
1327 void
1328 unmark_line_node(line_t *lp)
1329 {
1330 	int i;
1331 
1332 	for (i = 0; markno && i < MAXMARK; i++)
1333 		if (mark[i] == lp) {
1334 			mark[i] = NULL;
1335 			markno--;
1336 		}
1337 }
1338 
1339 
1340 /* dup_line_node: return a pointer to a copy of a line node */
1341 line_t *
1342 dup_line_node(line_t *lp)
1343 {
1344 	line_t *np;
1345 
1346 	if ((np = (line_t *) malloc(sizeof(line_t))) == NULL) {
1347 		perror(NULL);
1348 		seterrmsg("out of memory");
1349 		return NULL;
1350 	}
1351 	np->seek = lp->seek;
1352 	np->len = lp->len;
1353 	return np;
1354 }
1355 
1356 
1357 /* has_trailing_escape:  return the parity of escapes preceding a character
1358    in a string */
1359 int
1360 has_trailing_escape(char *s, char *t)
1361 {
1362     return (s == t || *(t - 1) != '\\') ? 0 : !has_trailing_escape(s, t - 1);
1363 }
1364 
1365 
1366 /* strip_escapes: return copy of escaped string of at most length MAXPATHLEN */
1367 char *
1368 strip_escapes(char *s)
1369 {
1370 	static char *file = NULL;
1371 	static int filesz = 0;
1372 
1373 	int i = 0;
1374 
1375 	REALLOC(file, filesz, MAXPATHLEN, NULL);
1376 	/* assert: no trailing escape */
1377 	while ((file[i++] = (*s == '\\') ? *++s : *s) != '\0' &&
1378 	       i < MAXPATHLEN-1)
1379 		s++;
1380 	file[MAXPATHLEN-1] = '\0';
1381 	return file;
1382 }
1383 
1384 
1385 void
1386 signal_hup(int signo)
1387 {
1388 	int save_errno = errno;
1389 
1390 	if (mutex)
1391 		sigflags |= (1 << (signo - 1));
1392 	else
1393 		handle_hup(signo);
1394 	errno = save_errno;
1395 }
1396 
1397 
1398 void
1399 signal_int(int signo)
1400 {
1401 	int save_errno = errno;
1402 
1403 	if (mutex)
1404 		sigflags |= (1 << (signo - 1));
1405 	else
1406 		handle_int(signo);
1407 	errno = save_errno;
1408 }
1409 
1410 
1411 void
1412 handle_hup(int signo)
1413 {
1414 	char path[MAXPATHLEN];
1415 	char *hup = NULL;		/* hup filename */
1416 
1417 	if (!sigactive)
1418 		quit(1);
1419 	sigflags &= ~(1 << (signo - 1));
1420 	if (addr_last && write_file("ed.hup", "w", 1, addr_last) < 0 &&
1421 	    home != NULL &&
1422 	    strlen(home) + sizeof("/ed.hup") <= MAXPATHLEN) {
1423 		strlcpy(path, home, sizeof(path));
1424 		strlcat(path, "/ed.hup", sizeof(path));
1425 		write_file(hup, "w", 1, addr_last);
1426 	}
1427 	_exit(2);
1428 }
1429 
1430 
1431 void
1432 handle_int(int signo)
1433 {
1434 	if (!sigactive)
1435 		_exit(1);
1436 	sigflags &= ~(1 << (signo - 1));
1437 #ifdef _POSIX_SOURCE
1438 	siglongjmp(env, -1);
1439 #else
1440 	longjmp(env, -1);
1441 #endif
1442 }
1443 
1444 
1445 void
1446 handle_winch(int signo)
1447 {
1448 	int save_errno = errno;
1449 	struct winsize ws;		/* window size structure */
1450 
1451 	sigflags &= ~(1 << (signo - 1));
1452 	if (ioctl(0, TIOCGWINSZ, (char *) &ws) >= 0) {
1453 		if (ws.ws_row > 2)
1454 			rows = ws.ws_row - 2;
1455 		if (ws.ws_col > 8)
1456 			cols = ws.ws_col - 8;
1457 	}
1458 	errno = save_errno;
1459 }
1460 
1461 
1462 /* is_legal_filename: return a legal filename */
1463 int
1464 is_legal_filename(char *s)
1465 {
1466 	if (red && (*s == '!' || !strcmp(s, "..") || strchr(s, '/'))) {
1467 		seterrmsg("shell access restricted");
1468 		return 0;
1469 	}
1470 	return 1;
1471 }
1472