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