xref: /freebsd/contrib/less/edit.c (revision 8ddb146abcdf061be9f2c0db7e391697dafad85c)
1 /*
2  * Copyright (C) 1984-2021  Mark Nudelman
3  *
4  * You may distribute under the terms of either the GNU General Public
5  * License or the Less License, as specified in the README file.
6  *
7  * For more information, see the README file.
8  */
9 
10 
11 #include "less.h"
12 #include "position.h"
13 #if HAVE_STAT
14 #include <sys/stat.h>
15 #endif
16 #if OS2
17 #include <signal.h>
18 #endif
19 
20 public int fd0 = 0;
21 
22 extern int new_file;
23 extern int cbufs;
24 extern char *every_first_cmd;
25 extern int force_open;
26 extern int is_tty;
27 extern int sigs;
28 extern int hshift;
29 extern int want_filesize;
30 extern IFILE curr_ifile;
31 extern IFILE old_ifile;
32 extern struct scrpos initial_scrpos;
33 extern void *ml_examine;
34 #if SPACES_IN_FILENAMES
35 extern char openquote;
36 extern char closequote;
37 #endif
38 
39 #if LOGFILE
40 extern int logfile;
41 extern int force_logfile;
42 extern char *namelogfile;
43 #endif
44 
45 #if HAVE_STAT_INO
46 public dev_t curr_dev;
47 public ino_t curr_ino;
48 #endif
49 
50 /*
51  * Textlist functions deal with a list of words separated by spaces.
52  * init_textlist sets up a textlist structure.
53  * forw_textlist uses that structure to iterate thru the list of
54  * words, returning each one as a standard null-terminated string.
55  * back_textlist does the same, but runs thru the list backwards.
56  */
57 	public void
58 init_textlist(tlist, str)
59 	struct textlist *tlist;
60 	char *str;
61 {
62 	char *s;
63 #if SPACES_IN_FILENAMES
64 	int meta_quoted = 0;
65 	int delim_quoted = 0;
66 	char *esc = get_meta_escape();
67 	int esclen = (int) strlen(esc);
68 #endif
69 
70 	tlist->string = skipsp(str);
71 	tlist->endstring = tlist->string + strlen(tlist->string);
72 	for (s = str;  s < tlist->endstring;  s++)
73 	{
74 #if SPACES_IN_FILENAMES
75 		if (meta_quoted)
76 		{
77 			meta_quoted = 0;
78 		} else if (esclen > 0 && s + esclen < tlist->endstring &&
79 		           strncmp(s, esc, esclen) == 0)
80 		{
81 			meta_quoted = 1;
82 			s += esclen - 1;
83 		} else if (delim_quoted)
84 		{
85 			if (*s == closequote)
86 				delim_quoted = 0;
87 		} else /* (!delim_quoted) */
88 		{
89 			if (*s == openquote)
90 				delim_quoted = 1;
91 			else if (*s == ' ')
92 				*s = '\0';
93 		}
94 #else
95 		if (*s == ' ')
96 			*s = '\0';
97 #endif
98 	}
99 }
100 
101 	public char *
102 forw_textlist(tlist, prev)
103 	struct textlist *tlist;
104 	char *prev;
105 {
106 	char *s;
107 
108 	/*
109 	 * prev == NULL means return the first word in the list.
110 	 * Otherwise, return the word after "prev".
111 	 */
112 	if (prev == NULL)
113 		s = tlist->string;
114 	else
115 		s = prev + strlen(prev);
116 	if (s >= tlist->endstring)
117 		return (NULL);
118 	while (*s == '\0')
119 		s++;
120 	if (s >= tlist->endstring)
121 		return (NULL);
122 	return (s);
123 }
124 
125 	public char *
126 back_textlist(tlist, prev)
127 	struct textlist *tlist;
128 	char *prev;
129 {
130 	char *s;
131 
132 	/*
133 	 * prev == NULL means return the last word in the list.
134 	 * Otherwise, return the word before "prev".
135 	 */
136 	if (prev == NULL)
137 		s = tlist->endstring;
138 	else if (prev <= tlist->string)
139 		return (NULL);
140 	else
141 		s = prev - 1;
142 	while (*s == '\0')
143 		s--;
144 	if (s <= tlist->string)
145 		return (NULL);
146 	while (s[-1] != '\0' && s > tlist->string)
147 		s--;
148 	return (s);
149 }
150 
151 /*
152  * Close a pipe opened via popen.
153  */
154 	static void
155 close_pipe(FILE *pipefd)
156 {
157 	if (pipefd == NULL)
158 		return;
159 #if OS2
160 	/*
161 	 * The pclose function of OS/2 emx sometimes fails.
162 	 * Send SIGINT to the piped process before closing it.
163 	 */
164 	kill(pipefd->_pid, SIGINT);
165 #endif
166 	pclose(pipefd);
167 }
168 
169 /*
170  * Close the current input file.
171  */
172 	static void
173 close_file(VOID_PARAM)
174 {
175 	struct scrpos scrpos;
176 	int chflags;
177 	FILE *altpipe;
178 	char *altfilename;
179 
180 	if (curr_ifile == NULL_IFILE)
181 		return;
182 
183 	/*
184 	 * Save the current position so that we can return to
185 	 * the same position if we edit this file again.
186 	 */
187 	get_scrpos(&scrpos, TOP);
188 	if (scrpos.pos != NULL_POSITION)
189 	{
190 		store_pos(curr_ifile, &scrpos);
191 		lastmark();
192 	}
193 	/*
194 	 * Close the file descriptor, unless it is a pipe.
195 	 */
196 	chflags = ch_getflags();
197 	ch_close();
198 	/*
199 	 * If we opened a file using an alternate name,
200 	 * do special stuff to close it.
201 	 */
202 	altfilename = get_altfilename(curr_ifile);
203 	if (altfilename != NULL)
204 	{
205 		altpipe = get_altpipe(curr_ifile);
206 		if (altpipe != NULL && !(chflags & CH_KEEPOPEN))
207 		{
208 			close_pipe(altpipe);
209 			set_altpipe(curr_ifile, NULL);
210 		}
211 		close_altfile(altfilename, get_filename(curr_ifile));
212 		set_altfilename(curr_ifile, NULL);
213 	}
214 	curr_ifile = NULL_IFILE;
215 #if HAVE_STAT_INO
216 	curr_ino = curr_dev = 0;
217 #endif
218 }
219 
220 /*
221  * Edit a new file (given its name).
222  * Filename == "-" means standard input.
223  * Filename == NULL means just close the current file.
224  */
225 	public int
226 edit(filename)
227 	char *filename;
228 {
229 	if (filename == NULL)
230 		return (edit_ifile(NULL_IFILE));
231 	return (edit_ifile(get_ifile(filename, curr_ifile)));
232 }
233 
234 /*
235  * Edit a new file (given its IFILE).
236  * ifile == NULL means just close the current file.
237  */
238 	public int
239 edit_ifile(ifile)
240 	IFILE ifile;
241 {
242 	int f;
243 	int answer;
244 	int chflags;
245 	char *filename;
246 	char *open_filename;
247 	char *alt_filename;
248 	void *altpipe;
249 	IFILE was_curr_ifile;
250 	PARG parg;
251 
252 	if (ifile == curr_ifile)
253 	{
254 		/*
255 		 * Already have the correct file open.
256 		 */
257 		return (0);
258 	}
259 
260 	/*
261 	 * We must close the currently open file now.
262 	 * This is necessary to make the open_altfile/close_altfile pairs
263 	 * nest properly (or rather to avoid nesting at all).
264 	 * {{ Some stupid implementations of popen() mess up if you do:
265 	 *    fA = popen("A"); fB = popen("B"); pclose(fA); pclose(fB); }}
266 	 */
267 #if LOGFILE
268 	end_logfile();
269 #endif
270 	was_curr_ifile = save_curr_ifile();
271 	if (curr_ifile != NULL_IFILE)
272 	{
273 		chflags = ch_getflags();
274 		close_file();
275 		if ((chflags & CH_HELPFILE) && held_ifile(was_curr_ifile) <= 1)
276 		{
277 			/*
278 			 * Don't keep the help file in the ifile list.
279 			 */
280 			del_ifile(was_curr_ifile);
281 			was_curr_ifile = old_ifile;
282 		}
283 	}
284 
285 	if (ifile == NULL_IFILE)
286 	{
287 		/*
288 		 * No new file to open.
289 		 * (Don't set old_ifile, because if you call edit_ifile(NULL),
290 		 *  you're supposed to have saved curr_ifile yourself,
291 		 *  and you'll restore it if necessary.)
292 		 */
293 		unsave_ifile(was_curr_ifile);
294 		return (0);
295 	}
296 
297 	filename = save(get_filename(ifile));
298 
299 	/*
300 	 * See if LESSOPEN specifies an "alternate" file to open.
301 	 */
302 	altpipe = get_altpipe(ifile);
303 	if (altpipe != NULL)
304 	{
305 		/*
306 		 * File is already open.
307 		 * chflags and f are not used by ch_init if ifile has
308 		 * filestate which should be the case if we're here.
309 		 * Set them here to avoid uninitialized variable warnings.
310 		 */
311 		chflags = 0;
312 		f = -1;
313 		alt_filename = get_altfilename(ifile);
314 		open_filename = (alt_filename != NULL) ? alt_filename : filename;
315 	} else
316 	{
317 		if (strcmp(filename, FAKE_HELPFILE) == 0 ||
318 			 strcmp(filename, FAKE_EMPTYFILE) == 0)
319 			alt_filename = NULL;
320 		else
321 			alt_filename = open_altfile(filename, &f, &altpipe);
322 
323 		open_filename = (alt_filename != NULL) ? alt_filename : filename;
324 
325 		chflags = 0;
326 		if (altpipe != NULL)
327 		{
328 			/*
329 			 * The alternate "file" is actually a pipe.
330 			 * f has already been set to the file descriptor of the pipe
331 			 * in the call to open_altfile above.
332 			 * Keep the file descriptor open because it was opened
333 			 * via popen(), and pclose() wants to close it.
334 			 */
335 			chflags |= CH_POPENED;
336 			if (strcmp(filename, "-") == 0)
337 				chflags |= CH_KEEPOPEN;
338 		} else if (strcmp(filename, "-") == 0)
339 		{
340 			/*
341 			 * Use standard input.
342 			 * Keep the file descriptor open because we can't reopen it.
343 			 */
344 			f = fd0;
345 			chflags |= CH_KEEPOPEN;
346 			/*
347 			 * Must switch stdin to BINARY mode.
348 			 */
349 			SET_BINARY(f);
350 #if MSDOS_COMPILER==DJGPPC
351 			/*
352 			 * Setting stdin to binary by default causes
353 			 * Ctrl-C to not raise SIGINT.  We must undo
354 			 * that side-effect.
355 			 */
356 			__djgpp_set_ctrl_c(1);
357 #endif
358 		} else if (strcmp(open_filename, FAKE_EMPTYFILE) == 0)
359 		{
360 			f = -1;
361 			chflags |= CH_NODATA;
362 		} else if (strcmp(open_filename, FAKE_HELPFILE) == 0)
363 		{
364 			f = -1;
365 			chflags |= CH_HELPFILE;
366 		} else if ((parg.p_string = bad_file(open_filename)) != NULL)
367 		{
368 			/*
369 			 * It looks like a bad file.  Don't try to open it.
370 			 */
371 			error("%s", &parg);
372 			free(parg.p_string);
373 			err1:
374 			if (alt_filename != NULL)
375 			{
376 				close_pipe(altpipe);
377 				close_altfile(alt_filename, filename);
378 				free(alt_filename);
379 			}
380 			del_ifile(ifile);
381 			free(filename);
382 			/*
383 			 * Re-open the current file.
384 			 */
385 			if (was_curr_ifile == ifile)
386 			{
387 				/*
388 				 * Whoops.  The "current" ifile is the one we just deleted.
389 				 * Just give up.
390 				 */
391 				quit(QUIT_ERROR);
392 			}
393 			reedit_ifile(was_curr_ifile);
394 			return (1);
395 		} else if ((f = open(open_filename, OPEN_READ)) < 0)
396 		{
397 			/*
398 			 * Got an error trying to open it.
399 			 */
400 			parg.p_string = errno_message(filename);
401 			error("%s", &parg);
402 			free(parg.p_string);
403 				goto err1;
404 		} else
405 		{
406 			chflags |= CH_CANSEEK;
407 			if (!force_open && !opened(ifile) && bin_file(f))
408 			{
409 				/*
410 				 * Looks like a binary file.
411 				 * Ask user if we should proceed.
412 				 */
413 				parg.p_string = filename;
414 				answer = query("\"%s\" may be a binary file.  See it anyway? ",
415 					&parg);
416 				if (answer != 'y' && answer != 'Y')
417 				{
418 					close(f);
419 					goto err1;
420 				}
421 			}
422 		}
423 	}
424 
425 	/*
426 	 * Get the new ifile.
427 	 * Get the saved position for the file.
428 	 */
429 	if (was_curr_ifile != NULL_IFILE)
430 	{
431 		old_ifile = was_curr_ifile;
432 		unsave_ifile(was_curr_ifile);
433 	}
434 	curr_ifile = ifile;
435 	set_altfilename(curr_ifile, alt_filename);
436 	set_altpipe(curr_ifile, altpipe);
437 	set_open(curr_ifile); /* File has been opened */
438 	get_pos(curr_ifile, &initial_scrpos);
439 	new_file = TRUE;
440 	ch_init(f, chflags);
441 
442 	if (!(chflags & CH_HELPFILE))
443 	{
444 #if LOGFILE
445 		if (namelogfile != NULL && is_tty)
446 			use_logfile(namelogfile);
447 #endif
448 #if HAVE_STAT_INO
449 		/* Remember the i-number and device of the opened file. */
450 		if (strcmp(open_filename, "-") != 0)
451 		{
452 			struct stat statbuf;
453 			int r = stat(open_filename, &statbuf);
454 			if (r == 0)
455 			{
456 				curr_ino = statbuf.st_ino;
457 				curr_dev = statbuf.st_dev;
458 			}
459 		}
460 #endif
461 		if (every_first_cmd != NULL)
462 		{
463 			ungetsc(every_first_cmd);
464 			ungetcc_back(CHAR_END_COMMAND);
465 		}
466 	}
467 
468 	flush();
469 
470 	if (is_tty)
471 	{
472 		/*
473 		 * Output is to a real tty.
474 		 */
475 
476 		/*
477 		 * Indicate there is nothing displayed yet.
478 		 */
479 		pos_clear();
480 		clr_linenum();
481 #if HILITE_SEARCH
482 		clr_hilite();
483 #endif
484 		hshift = 0;
485 		if (strcmp(filename, FAKE_HELPFILE) && strcmp(filename, FAKE_EMPTYFILE))
486 		{
487 			char *qfilename = shell_quote(filename);
488 			cmd_addhist(ml_examine, qfilename, 1);
489 			free(qfilename);
490 		}
491 		if (want_filesize)
492 			scan_eof();
493 	}
494 	free(filename);
495 	return (0);
496 }
497 
498 /*
499  * Edit a space-separated list of files.
500  * For each filename in the list, enter it into the ifile list.
501  * Then edit the first one.
502  */
503 	public int
504 edit_list(filelist)
505 	char *filelist;
506 {
507 	IFILE save_ifile;
508 	char *good_filename;
509 	char *filename;
510 	char *gfilelist;
511 	char *gfilename;
512 	char *qfilename;
513 	struct textlist tl_files;
514 	struct textlist tl_gfiles;
515 
516 	save_ifile = save_curr_ifile();
517 	good_filename = NULL;
518 
519 	/*
520 	 * Run thru each filename in the list.
521 	 * Try to glob the filename.
522 	 * If it doesn't expand, just try to open the filename.
523 	 * If it does expand, try to open each name in that list.
524 	 */
525 	init_textlist(&tl_files, filelist);
526 	filename = NULL;
527 	while ((filename = forw_textlist(&tl_files, filename)) != NULL)
528 	{
529 		gfilelist = lglob(filename);
530 		init_textlist(&tl_gfiles, gfilelist);
531 		gfilename = NULL;
532 		while ((gfilename = forw_textlist(&tl_gfiles, gfilename)) != NULL)
533 		{
534 			qfilename = shell_unquote(gfilename);
535 			if (edit(qfilename) == 0 && good_filename == NULL)
536 				good_filename = get_filename(curr_ifile);
537 			free(qfilename);
538 		}
539 		free(gfilelist);
540 	}
541 	/*
542 	 * Edit the first valid filename in the list.
543 	 */
544 	if (good_filename == NULL)
545 	{
546 		unsave_ifile(save_ifile);
547 		return (1);
548 	}
549 	if (get_ifile(good_filename, curr_ifile) == curr_ifile)
550 	{
551 		/*
552 		 * Trying to edit the current file; don't reopen it.
553 		 */
554 		unsave_ifile(save_ifile);
555 		return (0);
556 	}
557 	reedit_ifile(save_ifile);
558 	return (edit(good_filename));
559 }
560 
561 /*
562  * Edit the first file in the command line (ifile) list.
563  */
564 	public int
565 edit_first(VOID_PARAM)
566 {
567 	if (nifile() == 0)
568 		return (edit_stdin());
569 	curr_ifile = NULL_IFILE;
570 	return (edit_next(1));
571 }
572 
573 /*
574  * Edit the last file in the command line (ifile) list.
575  */
576 	public int
577 edit_last(VOID_PARAM)
578 {
579 	curr_ifile = NULL_IFILE;
580 	return (edit_prev(1));
581 }
582 
583 
584 /*
585  * Edit the n-th next or previous file in the command line (ifile) list.
586  */
587 	static int
588 edit_istep(h, n, dir)
589 	IFILE h;
590 	int n;
591 	int dir;
592 {
593 	IFILE next;
594 
595 	/*
596 	 * Skip n filenames, then try to edit each filename.
597 	 */
598 	for (;;)
599 	{
600 		next = (dir > 0) ? next_ifile(h) : prev_ifile(h);
601 		if (--n < 0)
602 		{
603 			if (edit_ifile(h) == 0)
604 				break;
605 		}
606 		if (next == NULL_IFILE)
607 		{
608 			/*
609 			 * Reached end of the ifile list.
610 			 */
611 			return (1);
612 		}
613 		if (ABORT_SIGS())
614 		{
615 			/*
616 			 * Interrupt breaks out, if we're in a long
617 			 * list of files that can't be opened.
618 			 */
619 			return (1);
620 		}
621 		h = next;
622 	}
623 	/*
624 	 * Found a file that we can edit.
625 	 */
626 	return (0);
627 }
628 
629 	static int
630 edit_inext(h, n)
631 	IFILE h;
632 	int n;
633 {
634 	return (edit_istep(h, n, +1));
635 }
636 
637 	public int
638 edit_next(n)
639 	int n;
640 {
641 	return edit_istep(curr_ifile, n, +1);
642 }
643 
644 	static int
645 edit_iprev(h, n)
646 	IFILE h;
647 	int n;
648 {
649 	return (edit_istep(h, n, -1));
650 }
651 
652 	public int
653 edit_prev(n)
654 	int n;
655 {
656 	return edit_istep(curr_ifile, n, -1);
657 }
658 
659 /*
660  * Edit a specific file in the command line (ifile) list.
661  */
662 	public int
663 edit_index(n)
664 	int n;
665 {
666 	IFILE h;
667 
668 	h = NULL_IFILE;
669 	do
670 	{
671 		if ((h = next_ifile(h)) == NULL_IFILE)
672 		{
673 			/*
674 			 * Reached end of the list without finding it.
675 			 */
676 			return (1);
677 		}
678 	} while (get_index(h) != n);
679 
680 	return (edit_ifile(h));
681 }
682 
683 	public IFILE
684 save_curr_ifile(VOID_PARAM)
685 {
686 	if (curr_ifile != NULL_IFILE)
687 		hold_ifile(curr_ifile, 1);
688 	return (curr_ifile);
689 }
690 
691 	public void
692 unsave_ifile(save_ifile)
693 	IFILE save_ifile;
694 {
695 	if (save_ifile != NULL_IFILE)
696 		hold_ifile(save_ifile, -1);
697 }
698 
699 /*
700  * Reedit the ifile which was previously open.
701  */
702 	public void
703 reedit_ifile(save_ifile)
704 	IFILE save_ifile;
705 {
706 	IFILE next;
707 	IFILE prev;
708 
709 	/*
710 	 * Try to reopen the ifile.
711 	 * Note that opening it may fail (maybe the file was removed),
712 	 * in which case the ifile will be deleted from the list.
713 	 * So save the next and prev ifiles first.
714 	 */
715 	unsave_ifile(save_ifile);
716 	next = next_ifile(save_ifile);
717 	prev = prev_ifile(save_ifile);
718 	if (edit_ifile(save_ifile) == 0)
719 		return;
720 	/*
721 	 * If can't reopen it, open the next input file in the list.
722 	 */
723 	if (next != NULL_IFILE && edit_inext(next, 0) == 0)
724 		return;
725 	/*
726 	 * If can't open THAT one, open the previous input file in the list.
727 	 */
728 	if (prev != NULL_IFILE && edit_iprev(prev, 0) == 0)
729 		return;
730 	/*
731 	 * If can't even open that, we're stuck.  Just quit.
732 	 */
733 	quit(QUIT_ERROR);
734 }
735 
736 	public void
737 reopen_curr_ifile(VOID_PARAM)
738 {
739 	IFILE save_ifile = save_curr_ifile();
740 	close_file();
741 	reedit_ifile(save_ifile);
742 }
743 
744 /*
745  * Edit standard input.
746  */
747 	public int
748 edit_stdin(VOID_PARAM)
749 {
750 	if (isatty(fd0))
751 	{
752 		error("Missing filename (\"less --help\" for help)", NULL_PARG);
753 		quit(QUIT_OK);
754 	}
755 	return (edit("-"));
756 }
757 
758 /*
759  * Copy a file directly to standard output.
760  * Used if standard output is not a tty.
761  */
762 	public void
763 cat_file(VOID_PARAM)
764 {
765 	int c;
766 
767 	while ((c = ch_forw_get()) != EOI)
768 		putchr(c);
769 	flush();
770 }
771 
772 #if LOGFILE
773 
774 #define OVERWRITE_OPTIONS "Overwrite, Append, Don't log, or Quit?"
775 
776 /*
777  * If the user asked for a log file and our input file
778  * is standard input, create the log file.
779  * We take care not to blindly overwrite an existing file.
780  */
781 	public void
782 use_logfile(filename)
783 	char *filename;
784 {
785 	int exists;
786 	int answer;
787 	PARG parg;
788 
789 	if (ch_getflags() & CH_CANSEEK)
790 		/*
791 		 * Can't currently use a log file on a file that can seek.
792 		 */
793 		return;
794 
795 	/*
796 	 * {{ We could use access() here. }}
797 	 */
798 	exists = open(filename, OPEN_READ);
799 	if (exists >= 0)
800 		close(exists);
801 	exists = (exists >= 0);
802 
803 	/*
804 	 * Decide whether to overwrite the log file or append to it.
805 	 * If it doesn't exist we "overwrite" it.
806 	 */
807 	if (!exists || force_logfile)
808 	{
809 		/*
810 		 * Overwrite (or create) the log file.
811 		 */
812 		answer = 'O';
813 	} else
814 	{
815 		/*
816 		 * Ask user what to do.
817 		 */
818 		parg.p_string = filename;
819 		answer = query("Warning: \"%s\" exists; "OVERWRITE_OPTIONS" ", &parg);
820 	}
821 
822 loop:
823 	switch (answer)
824 	{
825 	case 'O': case 'o':
826 		/*
827 		 * Overwrite: create the file.
828 		 */
829 		logfile = creat(filename, 0644);
830 		break;
831 	case 'A': case 'a':
832 		/*
833 		 * Append: open the file and seek to the end.
834 		 */
835 		logfile = open(filename, OPEN_APPEND);
836 		if (lseek(logfile, (off_t)0, SEEK_END) == BAD_LSEEK)
837 		{
838 			close(logfile);
839 			logfile = -1;
840 		}
841 		break;
842 	case 'D': case 'd':
843 		/*
844 		 * Don't do anything.
845 		 */
846 		return;
847 	default:
848 		/*
849 		 * Eh?
850 		 */
851 
852 		answer = query(OVERWRITE_OPTIONS" (Type \"O\", \"A\", \"D\" or \"Q\") ", NULL_PARG);
853 		goto loop;
854 	}
855 
856 	if (logfile < 0)
857 	{
858 		/*
859 		 * Error in opening logfile.
860 		 */
861 		parg.p_string = filename;
862 		error("Cannot write to \"%s\"", &parg);
863 		return;
864 	}
865 	SET_BINARY(logfile);
866 }
867 
868 #endif
869