xref: /freebsd/contrib/less/cmdbuf.c (revision 252d6dde57d5dd0184929d1f8fb65e7713f51c6d)
1 /*
2  * Copyright (C) 1984-2025  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 /*
12  * Functions which manipulate the command buffer.
13  * Used only by command() and related functions.
14  */
15 
16 #include "less.h"
17 #include "cmd.h"
18 #include "charset.h"
19 #if HAVE_STAT
20 #include <sys/stat.h>
21 #endif
22 
23 extern int sc_width;
24 extern int utf_mode;
25 extern int no_hist_dups;
26 extern int marks_modified;
27 extern int no_paste;
28 public lbool pasting = FALSE;
29 
30 static char cmdbuf[CMDBUF_SIZE]; /* Buffer for holding a multi-char command */
31 static int cmd_col;              /* Current column of the cursor */
32 static int prompt_col;           /* Column of cursor just after prompt */
33 static char *cp;                 /* Pointer into cmdbuf */
34 static int cmd_offset;           /* Index into cmdbuf of first displayed char */
35 static lbool literal;            /* Next input char should not be interpreted */
36 static size_t updown_match;      /* Prefix length in up/down movement */
37 static lbool have_updown_match = FALSE;
38 
39 static int cmd_complete(int action);
40 /*
41  * These variables are statics used by cmd_complete.
42  */
43 static lbool in_completion = FALSE;
44 static char *tk_text;
45 static char *tk_original;
46 static constant char *tk_ipoint;
47 static constant char *tk_trial = NULL;
48 static struct textlist tk_tlist;
49 
50 static int cmd_left();
51 static int cmd_right();
52 
53 #if SPACES_IN_FILENAMES
54 public char openquote = '"';
55 public char closequote = '"';
56 #endif
57 
58 #if CMD_HISTORY
59 
60 /* History file */
61 #define HISTFILE_FIRST_LINE      ".less-history-file:"
62 #define HISTFILE_SEARCH_SECTION  ".search"
63 #define HISTFILE_SHELL_SECTION   ".shell"
64 #define HISTFILE_MARK_SECTION    ".mark"
65 
66 /*
67  * A mlist structure represents a command history.
68  */
69 struct mlist
70 {
71 	struct mlist *next;
72 	struct mlist *prev;
73 	struct mlist *curr_mp;
74 	char *string;
75 	lbool modified;
76 };
77 
78 /*
79  * These are the various command histories that exist.
80  */
81 struct mlist mlist_search =
82 	{ &mlist_search,  &mlist_search,  &mlist_search,  NULL, 0 };
83 public void *ml_search = (void *) &mlist_search;
84 
85 struct mlist mlist_examine =
86 	{ &mlist_examine, &mlist_examine, &mlist_examine, NULL, 0 };
87 public void *ml_examine = (void *) &mlist_examine;
88 
89 #if SHELL_ESCAPE || PIPEC
90 struct mlist mlist_shell =
91 	{ &mlist_shell,   &mlist_shell,   &mlist_shell,   NULL, 0 };
92 public void *ml_shell = (void *) &mlist_shell;
93 #endif
94 
95 #else /* CMD_HISTORY */
96 
97 /* If CMD_HISTORY is off, these are just flags. */
98 public void *ml_search = (void *)1;
99 public void *ml_examine = (void *)2;
100 #if SHELL_ESCAPE || PIPEC
101 public void *ml_shell = (void *)3;
102 #endif
103 
104 #endif /* CMD_HISTORY */
105 
106 /*
107  * History for the current command.
108  */
109 static struct mlist *curr_mlist = NULL;
110 static int curr_cmdflags;
111 
112 static char cmd_mbc_buf[MAX_UTF_CHAR_LEN];
113 static int cmd_mbc_buf_len;
114 static int cmd_mbc_buf_index;
115 
116 
117 /*
118  * Reset command buffer (to empty).
119  */
cmd_reset(void)120 public void cmd_reset(void)
121 {
122 	cp = cmdbuf;
123 	*cp = '\0';
124 	cmd_col = 0;
125 	cmd_offset = 0;
126 	literal = FALSE;
127 	cmd_mbc_buf_len = 0;
128 	have_updown_match = FALSE;
129 }
130 
131 /*
132  * Clear command line.
133  */
clear_cmd(void)134 public void clear_cmd(void)
135 {
136 	cmd_col = prompt_col = 0;
137 	cmd_mbc_buf_len = 0;
138 	have_updown_match = FALSE;
139 }
140 
141 /*
142  * Display a string, usually as a prompt for input into the command buffer.
143  */
cmd_putstr(constant char * s)144 public void cmd_putstr(constant char *s)
145 {
146 	LWCHAR prev_ch = 0;
147 	LWCHAR ch;
148 	constant char *endline = s + strlen(s);
149 	while (*s != '\0')
150 	{
151 		constant char *os = s;
152 		int width;
153 		ch = step_charc(&s, +1, endline);
154 		while (os < s)
155 			putchr(*os++);
156 		if (!utf_mode)
157 			width = 1;
158 		else if (is_composing_char(ch) || is_combining_char(prev_ch, ch))
159 			width = 0;
160 		else
161 			width = is_wide_char(ch) ? 2 : 1;
162 		cmd_col += width;
163 		prompt_col += width;
164 		prev_ch = ch;
165 	}
166 }
167 
168 /*
169  * How many characters are in the command buffer?
170  */
len_cmdbuf(void)171 public int len_cmdbuf(void)
172 {
173 	constant char *s = cmdbuf;
174 	constant char *endline = s + strlen(s);
175 	int len = 0;
176 
177 	while (*s != '\0')
178 	{
179 		step_charc(&s, +1, endline);
180 		len++;
181 	}
182 	return (len);
183 }
184 
185 /*
186  * Is the command buffer empty?
187  * It is considered nonempty if there is any text in it,
188  * or if a multibyte command is being entered but not yet complete.
189  */
cmdbuf_empty(void)190 public lbool cmdbuf_empty(void)
191 {
192 	return cp == cmdbuf && cmd_mbc_buf_len == 0;
193 }
194 
195 /*
196  * Common part of cmd_step_right() and cmd_step_left().
197  * {{ Returning pwidth and bswidth separately is a historical artifact
198  *    since they're always the same. Maybe clean this up someday. }}
199  */
cmd_step_common(char * p,LWCHAR ch,size_t len,int * pwidth,int * bswidth)200 static constant char * cmd_step_common(char *p, LWCHAR ch, size_t len, int *pwidth, int *bswidth)
201 {
202 	constant char *pr;
203 	int width;
204 
205 	if (len == 1)
206 	{
207 		pr = prchar(ch);
208 		width = (int) strlen(pr);
209 	} else
210 	{
211 		pr = prutfchar(ch);
212 		if (is_composing_char(ch))
213 			width = 0;
214 		else if (is_ubin_char(ch))
215 			width = (int) strlen(pr);
216 		else
217 		{
218 			LWCHAR prev_ch = step_char(&p, -1, cmdbuf);
219 			if (is_combining_char(prev_ch, ch))
220 				width = 0;
221 			else
222 				width = is_wide_char(ch) ? 2 : 1;
223 		}
224 	}
225 	if (pwidth != NULL)
226 		*pwidth = width;
227 	if (bswidth != NULL)
228 		*bswidth = width;
229 	return (pr);
230 }
231 
232 /*
233  * Step a pointer one character right in the command buffer.
234  */
cmd_step_right(char ** pp,int * pwidth,int * bswidth)235 static constant char * cmd_step_right(char **pp, int *pwidth, int *bswidth)
236 {
237 	char *p = *pp;
238 	LWCHAR ch = step_char(pp, +1, p + strlen(p));
239 
240 	return cmd_step_common(p, ch, ptr_diff(*pp, p), pwidth, bswidth);
241 }
242 
243 /*
244  * Step a pointer one character left in the command buffer.
245  */
cmd_step_left(char ** pp,int * pwidth,int * bswidth)246 static constant char * cmd_step_left(char **pp, int *pwidth, int *bswidth)
247 {
248 	char *p = *pp;
249 	LWCHAR ch = step_char(pp, -1, cmdbuf);
250 
251 	return cmd_step_common(*pp, ch, ptr_diff(p, *pp), pwidth, bswidth);
252 }
253 
254 /*
255  * Put the cursor at "home" (just after the prompt),
256  * and set cp to the corresponding char in cmdbuf.
257  */
cmd_home(void)258 static void cmd_home(void)
259 {
260 	while (cmd_col > prompt_col)
261 	{
262 		int width, bswidth;
263 
264 		cmd_step_left(&cp, &width, &bswidth);
265 		while (bswidth-- > 0)
266 			putbs();
267 		cmd_col -= width;
268 	}
269 
270 	cp = &cmdbuf[cmd_offset];
271 }
272 
273 /*
274  * Repaint the line from cp onwards.
275  * Then position the cursor just after the char old_cp (a pointer into cmdbuf).
276  */
cmd_repaint(constant char * old_cp)277 public void cmd_repaint(constant char *old_cp)
278 {
279 	/*
280 	 * Repaint the line from the current position.
281 	 */
282 	if (old_cp == NULL)
283 	{
284 		old_cp = cp;
285 		cmd_home();
286 	}
287 	clear_eol();
288 	while (*cp != '\0')
289 	{
290 		char *np = cp;
291 		int width;
292 		constant char *pr = cmd_step_right(&np, &width, NULL);
293 		if (cmd_col + width >= sc_width)
294 			break;
295 		cp = np;
296 		putstr(pr);
297 		cmd_col += width;
298 	}
299 	while (*cp != '\0')
300 	{
301 		char *np = cp;
302 		int width;
303 		constant char *pr = cmd_step_right(&np, &width, NULL);
304 		if (width > 0)
305 			break;
306 		cp = np;
307 		putstr(pr);
308 	}
309 
310 	/*
311 	 * Back up the cursor to the correct position.
312 	 */
313 	while (cp > old_cp)
314 		cmd_left();
315 }
316 
317 /*
318  * Repaint the entire line, without moving the cursor.
319  */
cmd_repaint_curr(void)320 static void cmd_repaint_curr(void)
321 {
322 	char *save_cp = cp;
323 	cmd_home();
324 	cmd_repaint(save_cp);
325 }
326 
327 /*
328  * Shift the cmdbuf display left a half-screen.
329  */
cmd_lshift(void)330 static void cmd_lshift(void)
331 {
332 	char *s;
333 	char *save_cp;
334 	int cols;
335 
336 	/*
337 	 * Start at the first displayed char, count how far to the
338 	 * right we'd have to move to reach the center of the screen.
339 	 */
340 	s = cmdbuf + cmd_offset;
341 	cols = 0;
342 	while (cols < (sc_width - prompt_col) / 2 && *s != '\0')
343 	{
344 		int width;
345 		cmd_step_right(&s, &width, NULL);
346 		cols += width;
347 	}
348 	while (*s != '\0')
349 	{
350 		int width;
351 		char *ns = s;
352 		cmd_step_right(&ns, &width, NULL);
353 		if (width > 0)
354 			break;
355 		s = ns;
356 	}
357 
358 	cmd_offset = (int) (s - cmdbuf);
359 	save_cp = cp;
360 	cmd_home();
361 	cmd_repaint(save_cp);
362 }
363 
364 /*
365  * Shift the cmdbuf display right a half-screen.
366  */
cmd_rshift(void)367 static void cmd_rshift(void)
368 {
369 	char *s;
370 	char *save_cp;
371 	int cols;
372 
373 	/*
374 	 * Start at the first displayed char, count how far to the
375 	 * left we'd have to move to traverse a half-screen width
376 	 * of displayed characters.
377 	 */
378 	s = cmdbuf + cmd_offset;
379 	cols = 0;
380 	while (cols < (sc_width - prompt_col) / 2 && s > cmdbuf)
381 	{
382 		int width;
383 		cmd_step_left(&s, &width, NULL);
384 		cols += width;
385 	}
386 
387 	cmd_offset = (int) (s - cmdbuf);
388 	save_cp = cp;
389 	cmd_home();
390 	cmd_repaint(save_cp);
391 }
392 
393 /*
394  * Move cursor right one character.
395  */
cmd_right(void)396 static int cmd_right(void)
397 {
398 	constant char *pr;
399 	char *ncp;
400 	int width;
401 
402 	if (*cp == '\0')
403 	{
404 		/* Already at the end of the line. */
405 		return (CC_OK);
406 	}
407 	ncp = cp;
408 	pr = cmd_step_right(&ncp, &width, NULL);
409 	if (cmd_col + width >= sc_width)
410 		cmd_lshift();
411 	else if (cmd_col + width == sc_width - 1 && cp[1] != '\0')
412 		cmd_lshift();
413 	cp = ncp;
414 	cmd_col += width;
415 	putstr(pr);
416 	while (*cp != '\0')
417 	{
418 		pr = cmd_step_right(&ncp, &width, NULL);
419 		if (width > 0)
420 			break;
421 		putstr(pr);
422 		cp = ncp;
423 	}
424 	return (CC_OK);
425 }
426 
427 /*
428  * Move cursor left one character.
429  */
cmd_left(void)430 static int cmd_left(void)
431 {
432 	char *ncp;
433 	int width = 0;
434 	int bswidth = 0;
435 
436 	if (cp <= cmdbuf)
437 	{
438 		/* Already at the beginning of the line */
439 		return (CC_OK);
440 	}
441 	ncp = cp;
442 	while (ncp > cmdbuf)
443 	{
444 		cmd_step_left(&ncp, &width, &bswidth);
445 		if (width > 0)
446 			break;
447 	}
448 	if (cmd_col < prompt_col + width)
449 		cmd_rshift();
450 	cp = ncp;
451 	cmd_col -= width;
452 	while (bswidth-- > 0)
453 		putbs();
454 	return (CC_OK);
455 }
456 
457 /*
458  * Insert a char into the command buffer, at the current position.
459  */
cmd_ichar(constant char * cs,size_t clen)460 static int cmd_ichar(constant char *cs, size_t clen)
461 {
462 	char *s;
463 
464 	if (strlen(cmdbuf) + clen >= sizeof(cmdbuf)-1)
465 	{
466 		/* No room in the command buffer for another char. */
467 		bell();
468 		return (CC_ERROR);
469 	}
470 
471 	/*
472 	 * Make room for the new character (shift the tail of the buffer right).
473 	 */
474 	for (s = &cmdbuf[strlen(cmdbuf)];  s >= cp;  s--)
475 		s[clen] = s[0];
476 	/*
477 	 * Insert the character into the buffer.
478 	 */
479 	for (s = cp;  s < cp + clen;  s++)
480 		*s = *cs++;
481 	/*
482 	 * Reprint the tail of the line from the inserted char.
483 	 */
484 	have_updown_match = FALSE;
485 	cmd_repaint(cp);
486 	cmd_right();
487 	return (CC_OK);
488 }
489 
490 /*
491  * Backspace in the command buffer.
492  * Delete the char to the left of the cursor.
493  */
cmd_erase(void)494 static int cmd_erase(void)
495 {
496 	char *s;
497 	int clen;
498 
499 	if (cp == cmdbuf)
500 	{
501 		/*
502 		 * Backspace past beginning of the buffer:
503 		 * this usually means abort the command.
504 		 */
505 		return (CC_QUIT);
506 	}
507 	/*
508 	 * Move cursor left (to the char being erased).
509 	 */
510 	s = cp;
511 	cmd_left();
512 	clen = (int) (s - cp);
513 
514 	/*
515 	 * Remove the char from the buffer (shift the buffer left).
516 	 */
517 	for (s = cp;  ;  s++)
518 	{
519 		s[0] = s[clen];
520 		if (s[0] == '\0')
521 			break;
522 	}
523 
524 	/*
525 	 * Repaint the buffer after the erased char.
526 	 */
527 	have_updown_match = FALSE;
528 	cmd_repaint(cp);
529 
530 	/*
531 	 * We say that erasing the entire command string causes us
532 	 * to abort the current command, if CF_QUIT_ON_ERASE is set.
533 	 */
534 	if ((curr_cmdflags & CF_QUIT_ON_ERASE) && cp == cmdbuf && *cp == '\0')
535 		return (CC_QUIT);
536 	return (CC_OK);
537 }
538 
539 /*
540  * Delete the char under the cursor.
541  */
cmd_delete(void)542 static int cmd_delete(void)
543 {
544 	if (*cp == '\0')
545 	{
546 		/* At end of string; there is no char under the cursor. */
547 		return (CC_OK);
548 	}
549 	/*
550 	 * Move right, then use cmd_erase.
551 	 */
552 	cmd_right();
553 	cmd_erase();
554 	return (CC_OK);
555 }
556 
557 /*
558  * Delete the "word" to the left of the cursor.
559  */
cmd_werase(void)560 static int cmd_werase(void)
561 {
562 	if (cp > cmdbuf && cp[-1] == ' ')
563 	{
564 		/*
565 		 * If the char left of cursor is a space,
566 		 * erase all the spaces left of cursor (to the first non-space).
567 		 */
568 		while (cp > cmdbuf && cp[-1] == ' ')
569 			(void) cmd_erase();
570 	} else
571 	{
572 		/*
573 		 * If the char left of cursor is not a space,
574 		 * erase all the nonspaces left of cursor (the whole "word").
575 		 */
576 		while (cp > cmdbuf && cp[-1] != ' ')
577 			(void) cmd_erase();
578 	}
579 	return (CC_OK);
580 }
581 
582 /*
583  * Delete the "word" under the cursor.
584  */
cmd_wdelete(void)585 static int cmd_wdelete(void)
586 {
587 	if (*cp == ' ')
588 	{
589 		/*
590 		 * If the char under the cursor is a space,
591 		 * delete it and all the spaces right of cursor.
592 		 */
593 		while (*cp == ' ')
594 			(void) cmd_delete();
595 	} else
596 	{
597 		/*
598 		 * If the char under the cursor is not a space,
599 		 * delete it and all nonspaces right of cursor (the whole word).
600 		 */
601 		while (*cp != ' ' && *cp != '\0')
602 			(void) cmd_delete();
603 	}
604 	return (CC_OK);
605 }
606 
607 /*
608  * Delete all chars in the command buffer.
609  */
cmd_kill(void)610 static int cmd_kill(void)
611 {
612 	if (cmdbuf[0] == '\0')
613 	{
614 		/* Buffer is already empty; abort the current command. */
615 		return (CC_QUIT);
616 	}
617 	cmd_offset = 0;
618 	cmd_home();
619 	*cp = '\0';
620 	have_updown_match = FALSE;
621 	cmd_repaint(cp);
622 
623 	/*
624 	 * We say that erasing the entire command string causes us
625 	 * to abort the current command, if CF_QUIT_ON_ERASE is set.
626 	 */
627 	if (curr_cmdflags & CF_QUIT_ON_ERASE)
628 		return (CC_QUIT);
629 	return (CC_OK);
630 }
631 
632 /*
633  * Select an mlist structure to be the current command history.
634  */
set_mlist(void * mlist,int cmdflags)635 public void set_mlist(void *mlist, int cmdflags)
636 {
637 #if CMD_HISTORY
638 	curr_mlist = (struct mlist *) mlist;
639 	curr_cmdflags = cmdflags;
640 
641 	/* Make sure the next up-arrow moves to the last string in the mlist. */
642 	if (curr_mlist != NULL)
643 		curr_mlist->curr_mp = curr_mlist;
644 #endif
645 }
646 
647 #if CMD_HISTORY
648 /*
649  * Move up or down in the currently selected command history list.
650  * Only consider entries whose first updown_match chars are equal to
651  * cmdbuf's corresponding chars.
652  */
cmd_updown(int action)653 static int cmd_updown(int action)
654 {
655 	constant char *s;
656 	struct mlist *ml;
657 
658 	if (curr_mlist == NULL)
659 	{
660 		/*
661 		 * The current command has no history list.
662 		 */
663 		bell();
664 		return (CC_OK);
665 	}
666 
667 	if (!have_updown_match)
668 	{
669 		updown_match = ptr_diff(cp, cmdbuf);
670 		have_updown_match = TRUE;
671 	}
672 
673 	/*
674 	 * Find the next history entry which matches.
675 	 */
676 	for (ml = curr_mlist->curr_mp;;)
677 	{
678 		ml = (action == EC_UP) ? ml->prev : ml->next;
679 		if (ml == curr_mlist)
680 		{
681 			/*
682 			 * We reached the end (or beginning) of the list.
683 			 */
684 			break;
685 		}
686 		if (strncmp(cmdbuf, ml->string, updown_match) == 0)
687 		{
688 			/*
689 			 * This entry matches; stop here.
690 			 * Copy the entry into cmdbuf and echo it on the screen.
691 			 */
692 			curr_mlist->curr_mp = ml;
693 			s = ml->string;
694 			if (s == NULL)
695 				s = "";
696 			cmd_offset = 0;
697 			cmd_home();
698 			clear_eol();
699 			strcpy(cmdbuf, s);
700 			for (cp = cmdbuf;  *cp != '\0';  )
701 				cmd_right();
702 			return (CC_OK);
703 		}
704 	}
705 	/*
706 	 * We didn't find a history entry that matches.
707 	 */
708 	bell();
709 	return (CC_OK);
710 }
711 
712 /*
713  * Yet another lesson in the evils of global variables.
714  */
save_updown_match(void)715 public ssize_t save_updown_match(void)
716 {
717 	if (!have_updown_match)
718 		return (ssize_t)(-1);
719 	return (ssize_t) updown_match;
720 }
721 
restore_updown_match(ssize_t udm)722 public void restore_updown_match(ssize_t udm)
723 {
724 	updown_match = udm;
725 	have_updown_match = (udm != (ssize_t)(-1));
726 }
727 #endif /* CMD_HISTORY */
728 
729 /*
730  *
731  */
ml_link(struct mlist * mlist,struct mlist * ml)732 static void ml_link(struct mlist *mlist, struct mlist *ml)
733 {
734 	ml->next = mlist;
735 	ml->prev = mlist->prev;
736 	mlist->prev->next = ml;
737 	mlist->prev = ml;
738 }
739 
740 /*
741  *
742  */
ml_unlink(struct mlist * ml)743 static void ml_unlink(struct mlist *ml)
744 {
745 	ml->prev->next = ml->next;
746 	ml->next->prev = ml->prev;
747 }
748 
749 /*
750  * Add a string to an mlist.
751  */
cmd_addhist(struct mlist * mlist,constant char * cmd,lbool modified)752 public void cmd_addhist(struct mlist *mlist, constant char *cmd, lbool modified)
753 {
754 #if CMD_HISTORY
755 	struct mlist *ml;
756 
757 	/*
758 	 * Don't save a trivial command.
759 	 */
760 	if (strlen(cmd) == 0)
761 		return;
762 
763 	if (no_hist_dups)
764 	{
765 		struct mlist *next = NULL;
766 		for (ml = mlist->next;  ml->string != NULL;  ml = next)
767 		{
768 			next = ml->next;
769 			if (strcmp(ml->string, cmd) == 0)
770 			{
771 				ml_unlink(ml);
772 				free(ml->string);
773 				free(ml);
774 			}
775 		}
776 	}
777 
778 	/*
779 	 * Save the command unless it's a duplicate of the
780 	 * last command in the history.
781 	 */
782 	ml = mlist->prev;
783 	if (ml == mlist || strcmp(ml->string, cmd) != 0)
784 	{
785 		/*
786 		 * Did not find command in history.
787 		 * Save the command and put it at the end of the history list.
788 		 */
789 		ml = (struct mlist *) ecalloc(1, sizeof(struct mlist));
790 		ml->string = save(cmd);
791 		ml->modified = modified;
792 		ml_link(mlist, ml);
793 	}
794 	/*
795 	 * Point to the cmd just after the just-accepted command.
796 	 * Thus, an UPARROW will always retrieve the previous command.
797 	 */
798 	mlist->curr_mp = ml->next;
799 #endif
800 }
801 
802 /*
803  * Accept the command in the command buffer.
804  * Add it to the currently selected history list.
805  */
cmd_accept(void)806 public void cmd_accept(void)
807 {
808 #if CMD_HISTORY
809 	/*
810 	 * Nothing to do if there is no currently selected history list.
811 	 */
812 	if (curr_mlist == NULL || curr_mlist == ml_examine)
813 		return;
814 	cmd_addhist(curr_mlist, cmdbuf, TRUE);
815 	curr_mlist->modified = TRUE;
816 #endif
817 }
818 
819 /*
820  * Try to perform a line-edit function on the command buffer,
821  * using a specified char as a line-editing command.
822  * Returns:
823  *      CC_PASS The char does not invoke a line edit function.
824  *      CC_OK   Line edit function done.
825  *      CC_QUIT The char requests the current command to be aborted.
826  */
cmd_edit(char c,lbool stay_in_completion)827 static int cmd_edit(char c, lbool stay_in_completion)
828 {
829 	int action;
830 	int flags;
831 
832 #define not_in_completion()     do { if (!stay_in_completion) in_completion = FALSE; } while(0)
833 
834 	/*
835 	 * See if the char is indeed a line-editing command.
836 	 */
837 	flags = 0;
838 #if CMD_HISTORY
839 	if (curr_mlist == NULL)
840 		/*
841 		 * No current history; don't accept history manipulation cmds.
842 		 */
843 		flags |= ECF_NOHISTORY;
844 #endif
845 
846 	/*
847 	 * Don't accept completion cmds in contexts
848 	 * such as search pattern, digits, etc.
849 	 */
850 	if ((curr_mlist == NULL && (curr_cmdflags & CF_OPTION))
851 #if TAB_COMPLETE_FILENAME
852 		|| curr_mlist == ml_examine || curr_mlist == ml_shell
853 #endif
854 		)
855 		; /* allow completion */
856 	else
857 		flags |= ECF_NOCOMPLETE;
858 
859 	action = editchar(c, flags);
860 	if (is_ignoring_input(action))
861 		return (CC_OK);
862 
863 	switch (action)
864 	{
865 	case A_NOACTION:
866 		return (CC_OK);
867 	case EC_START_PASTE:
868 		if (no_paste)
869 			pasting = TRUE;
870 		return (CC_OK);
871 	case EC_END_PASTE:
872 		stop_ignoring_input();
873 		return (CC_OK);
874 	case EC_RIGHT:
875 		not_in_completion();
876 		return (cmd_right());
877 	case EC_LEFT:
878 		not_in_completion();
879 		return (cmd_left());
880 	case EC_W_RIGHT:
881 		not_in_completion();
882 		while (*cp != '\0' && *cp != ' ')
883 			cmd_right();
884 		while (*cp == ' ')
885 			cmd_right();
886 		return (CC_OK);
887 	case EC_W_LEFT:
888 		not_in_completion();
889 		while (cp > cmdbuf && cp[-1] == ' ')
890 			cmd_left();
891 		while (cp > cmdbuf && cp[-1] != ' ')
892 			cmd_left();
893 		return (CC_OK);
894 	case EC_HOME:
895 		not_in_completion();
896 		cmd_offset = 0;
897 		cmd_home();
898 		cmd_repaint(cp);
899 		return (CC_OK);
900 	case EC_END:
901 		not_in_completion();
902 		while (*cp != '\0')
903 			cmd_right();
904 		return (CC_OK);
905 	case EC_INSERT:
906 		not_in_completion();
907 		return (CC_OK);
908 	case EC_BACKSPACE:
909 		not_in_completion();
910 		return (cmd_erase());
911 	case EC_LINEKILL:
912 		not_in_completion();
913 		return (cmd_kill());
914 	case EC_ABORT:
915 		not_in_completion();
916 		(void) cmd_kill();
917 		return (CC_QUIT);
918 	case EC_W_BACKSPACE:
919 		not_in_completion();
920 		return (cmd_werase());
921 	case EC_DELETE:
922 		not_in_completion();
923 		return (cmd_delete());
924 	case EC_W_DELETE:
925 		not_in_completion();
926 		return (cmd_wdelete());
927 	case EC_LITERAL:
928 		literal = TRUE;
929 		return (CC_OK);
930 #if CMD_HISTORY
931 	case EC_UP:
932 	case EC_DOWN:
933 		not_in_completion();
934 		return (cmd_updown(action));
935 #endif
936 	case EC_F_COMPLETE:
937 	case EC_B_COMPLETE:
938 	case EC_EXPAND:
939 		return (cmd_complete(action));
940 	default:
941 		not_in_completion();
942 		return (CC_PASS);
943 	}
944 }
945 
946 /*
947  * Insert a string into the command buffer, at the current position.
948  */
cmd_istr(constant char * str)949 static int cmd_istr(constant char *str)
950 {
951 	constant char *endline = str + strlen(str);
952 	constant char *s;
953 	int action;
954 
955 	for (s = str;  *s != '\0';  )
956 	{
957 		constant char *os = s;
958 		step_charc(&s, +1, endline);
959 		action = cmd_ichar(os, ptr_diff(s, os));
960 		if (action != CC_OK)
961 			return (action);
962 	}
963 	return (CC_OK);
964 }
965 
966 /*
967  * Set tk_original to word.
968  */
set_tk_original(constant char * word)969 static void set_tk_original(constant char *word)
970 {
971 	if (tk_original != NULL)
972 		free(tk_original);
973 	tk_original = (char *) ecalloc(ptr_diff(cp,word)+1, sizeof(char));
974 	strncpy(tk_original, word, ptr_diff(cp,word));
975 }
976 
977 #if TAB_COMPLETE_FILENAME
978 /*
979  * Find the beginning and end of the "current" word.
980  * This is the word which the cursor (cp) is inside or at the end of.
981  * Return pointer to the beginning of the word and put the
982  * cursor at the end of the word.
983  */
delimit_word(void)984 static char * delimit_word(void)
985 {
986 	char *word;
987 #if SPACES_IN_FILENAMES
988 	char *p;
989 	int delim_quoted = FALSE;
990 	int meta_quoted = FALSE;
991 	constant char *esc = get_meta_escape();
992 	size_t esclen = strlen(esc);
993 #endif
994 
995 	/*
996 	 * Move cursor to end of word.
997 	 */
998 	if (*cp != ' ' && *cp != '\0')
999 	{
1000 		/*
1001 		 * Cursor is on a nonspace.
1002 		 * Move cursor right to the next space.
1003 		 */
1004 		while (*cp != ' ' && *cp != '\0')
1005 			cmd_right();
1006 	} else if (cp > cmdbuf && cp[-1] != ' ')
1007 	{
1008 		/*
1009 		 * Cursor is on a space, and char to the left is a nonspace.
1010 		 * We're already at the end of the word.
1011 		 */
1012 		;
1013 #if 0
1014 	} else
1015 	{
1016 		/*
1017 		 * Cursor is on a space and char to the left is a space.
1018 		 * Huh? There's no word here.
1019 		 */
1020 		return (NULL);
1021 #endif
1022 	}
1023 	/*
1024 	 * Find the beginning of the word which the cursor is in.
1025 	 */
1026 	if (cp == cmdbuf)
1027 		return (NULL);
1028 #if SPACES_IN_FILENAMES
1029 	/*
1030 	 * If we have an unbalanced quote (that is, an open quote
1031 	 * without a corresponding close quote), we return everything
1032 	 * from the open quote, including spaces.
1033 	 */
1034 	for (word = cmdbuf;  word < cp;  word++)
1035 		if (*word != ' ')
1036 			break;
1037 	if (word >= cp)
1038 		return (cp);
1039 	for (p = cmdbuf;  p < cp;  p++)
1040 	{
1041 		if (meta_quoted)
1042 		{
1043 			meta_quoted = FALSE;
1044 		} else if (esclen > 0 && p + esclen < cp &&
1045 		           strncmp(p, esc, esclen) == 0)
1046 		{
1047 			meta_quoted = TRUE;
1048 			p += esclen - 1;
1049 		} else if (delim_quoted)
1050 		{
1051 			if (*p == closequote)
1052 				delim_quoted = FALSE;
1053 		} else /* (!delim_quoted) */
1054 		{
1055 			if (*p == openquote)
1056 				delim_quoted = TRUE;
1057 			else if (*p == ' ')
1058 				word = p+1;
1059 		}
1060 	}
1061 #endif
1062 	return (word);
1063 }
1064 
1065 /*
1066  * Set things up to enter file completion mode.
1067  * Expand the word under the cursor into a list of filenames
1068  * which start with that word, and set tk_text to that list.
1069  */
init_file_compl(void)1070 static void init_file_compl(void)
1071 {
1072 	char *word;
1073 	char c;
1074 
1075 	/*
1076 	 * Find the original (uncompleted) word in the command buffer.
1077 	 */
1078 	word = delimit_word();
1079 	if (word == NULL)
1080 		return;
1081 	/*
1082 	 * Set the insertion point to the point in the command buffer
1083 	 * where the original (uncompleted) word now sits.
1084 	 */
1085 	tk_ipoint = word;
1086 	set_tk_original(word);
1087 	/*
1088 	 * Get the expanded filename.
1089 	 * This may result in a single filename, or
1090 	 * a blank-separated list of filenames.
1091 	 */
1092 	c = *cp;
1093 	*cp = '\0';
1094 	if (*word != openquote)
1095 	{
1096 		tk_text = fcomplete(word);
1097 	} else
1098 	{
1099 #if MSDOS_COMPILER
1100 		char *qword = NULL;
1101 #else
1102 		char *qword = shell_quote(word+1);
1103 #endif
1104 		if (qword == NULL)
1105 			tk_text = fcomplete(word+1);
1106 		else
1107 		{
1108 			tk_text = fcomplete(qword);
1109 			free(qword);
1110 		}
1111 	}
1112 	*cp = c;
1113 }
1114 #endif /* TAB_COMPLETE_FILENAME */
1115 
1116 /*
1117  * Set things up to enter option completion mode.
1118  */
init_opt_compl(void)1119 static void init_opt_compl(void)
1120 {
1121 	tk_ipoint = cmdbuf;
1122 	set_tk_original(cmdbuf);
1123 	tk_text = findopts_name(cmdbuf);
1124 }
1125 
1126 /*
1127  * Return the next word in the current completion list.
1128  */
next_compl(int action,constant char * prev)1129 static constant char * next_compl(int action, constant char *prev)
1130 {
1131 	switch (action)
1132 	{
1133 	case EC_F_COMPLETE:
1134 		return (forw_textlist(&tk_tlist, prev));
1135 	case EC_B_COMPLETE:
1136 		return (back_textlist(&tk_tlist, prev));
1137 	}
1138 	/* Cannot happen */
1139 	return ("?");
1140 }
1141 
1142 /*
1143  * Complete the filename before (or under) the cursor.
1144  * cmd_complete may be called multiple times.  The global in_completion
1145  * remembers whether this call is the first time (create the list),
1146  * or a subsequent time (step thru the list).
1147  */
cmd_complete(int action)1148 static int cmd_complete(int action)
1149 {
1150 	constant char *s;
1151 
1152 	if (!in_completion || action == EC_EXPAND)
1153 	{
1154 		/*
1155 		 * Expand the word under the cursor and
1156 		 * use the first word in the expansion
1157 		 * (or the entire expansion if we're doing EC_EXPAND).
1158 		 */
1159 		if (tk_text != NULL)
1160 		{
1161 			free(tk_text);
1162 			tk_text = NULL;
1163 		}
1164 		if (curr_cmdflags & CF_OPTION)
1165 			init_opt_compl();
1166 		else
1167 #if TAB_COMPLETE_FILENAME
1168 			init_file_compl();
1169 #else
1170 			quit(QUIT_ERROR); /* cannot happen */
1171 #endif /* TAB_COMPLETE_FILENAME */
1172 		if (tk_text == NULL)
1173 		{
1174 			bell();
1175 			return (CC_OK);
1176 		}
1177 		if (action == EC_EXPAND)
1178 		{
1179 			/*
1180 			 * Use the whole list.
1181 			 */
1182 			tk_trial = tk_text;
1183 		} else
1184 		{
1185 			/*
1186 			 * Use the first filename in the list.
1187 			 */
1188 			in_completion = TRUE;
1189 			init_textlist(&tk_tlist, tk_text);
1190 			tk_trial = next_compl(action, (char*)NULL);
1191 		}
1192 	} else
1193 	{
1194 		/*
1195 		 * We already have a completion list.
1196 		 * Use the next/previous filename from the list.
1197 		 */
1198 		tk_trial = next_compl(action, tk_trial);
1199 	}
1200 
1201 	/*
1202 	 * Remove the original word, or the previous trial completion.
1203 	 */
1204 	while (cp > tk_ipoint)
1205 		(void) cmd_erase();
1206 
1207 	if (tk_trial == NULL)
1208 	{
1209 		/*
1210 		 * There are no more trial completions.
1211 		 * Insert the original (uncompleted) filename.
1212 		 */
1213 		in_completion = FALSE;
1214 		if (cmd_istr(tk_original) != CC_OK)
1215 			goto fail;
1216 	} else
1217 	{
1218 		/*
1219 		 * Insert trial completion.
1220 		 */
1221 		if (cmd_istr(tk_trial) != CC_OK)
1222 			goto fail;
1223 		/*
1224 		 * If it is a directory, append a slash.
1225 		 */
1226 		if (is_dir(tk_trial))
1227 		{
1228 			if (cp > cmdbuf && cp[-1] == closequote)
1229 				(void) cmd_erase();
1230 			s = lgetenv("LESSSEPARATOR");
1231 			if (s == NULL)
1232 				s = PATHNAME_SEP;
1233 			if (cmd_istr(s) != CC_OK)
1234 				goto fail;
1235 		}
1236 	}
1237 
1238 	return (CC_OK);
1239 
1240 fail:
1241 	in_completion = FALSE;
1242 	bell();
1243 	return (CC_OK);
1244 }
1245 
1246 /*
1247  * Build a UTF-8 char in cmd_mbc_buf.
1248  * Returns:
1249  *      CC_OK    Char has been stored but we don't have a complete UTF-8 sequence yet.
1250  *      CC_ERROR This is an invalid UTF-8 sequence.
1251  *      CC_PASS  There is a complete UTF-8 sequence in cmd_mbc_buf.
1252  *               The length of the complete sequence is returned in *plen.
1253  */
cmd_uchar(char c,size_t * plen)1254 static int cmd_uchar(char c, size_t *plen)
1255 {
1256 	if (!utf_mode)
1257 	{
1258 		cmd_mbc_buf[0] = c;
1259 		*plen = 1;
1260 	} else
1261 	{
1262 		/* Perform strict validation in all possible cases.  */
1263 		if (cmd_mbc_buf_len == 0)
1264 		{
1265 		 retry:
1266 			cmd_mbc_buf_index = 1;
1267 			*cmd_mbc_buf = c;
1268 			if (IS_ASCII_OCTET(c))
1269 				cmd_mbc_buf_len = 1;
1270 #if MSDOS_COMPILER || OS2
1271 			else if (c == '\340' && IS_ASCII_OCTET(peekcc()))
1272 			{
1273 				/* Assume a special key. */
1274 				cmd_mbc_buf_len = 1;
1275 			}
1276 #endif
1277 			else if (IS_UTF8_LEAD(c))
1278 			{
1279 				cmd_mbc_buf_len = utf_len(c);
1280 				return (CC_OK);
1281 			} else
1282 			{
1283 				/* UTF8_INVALID or stray UTF8_TRAIL */
1284 				bell();
1285 				return (CC_ERROR);
1286 			}
1287 		} else if (IS_UTF8_TRAIL(c))
1288 		{
1289 			cmd_mbc_buf[cmd_mbc_buf_index++] = c;
1290 			if (cmd_mbc_buf_index < cmd_mbc_buf_len)
1291 				return (CC_OK);
1292 			if (!is_utf8_well_formed(cmd_mbc_buf, cmd_mbc_buf_index))
1293 			{
1294 				/* complete, but not well formed (non-shortest form), sequence */
1295 				cmd_mbc_buf_len = 0;
1296 				bell();
1297 				return (CC_ERROR);
1298 			}
1299 		} else
1300 		{
1301 			/* Flush incomplete (truncated) sequence.  */
1302 			cmd_mbc_buf_len = 0;
1303 			bell();
1304 			/* Handle new char.  */
1305 			goto retry;
1306 		}
1307 
1308 		*plen = (size_t) cmd_mbc_buf_len; /*{{type-issue}}*/
1309 		cmd_mbc_buf_len = 0;
1310 	}
1311 	return (CC_PASS);
1312 }
1313 
1314 /*
1315  * Process a single character of a multi-character command, such as
1316  * a number, or the pattern of a search command.
1317  * Returns:
1318  *      CC_OK           The char was accepted.
1319  *      CC_QUIT         The char requests the command to be aborted.
1320  *      CC_ERROR        The char could not be accepted due to an error.
1321  */
cmd_char2(char c,lbool stay_in_completion)1322 static int cmd_char2(char c, lbool stay_in_completion)
1323 {
1324 	size_t len;
1325 	int action = cmd_uchar(c, &len);
1326 	if (action != CC_PASS)
1327 		return (action);
1328 
1329 	if (literal)
1330 	{
1331 		/*
1332 		 * Insert the char, even if it is a line-editing char.
1333 		 */
1334 		literal = FALSE;
1335 		return (cmd_ichar(cmd_mbc_buf, len));
1336 	}
1337 
1338 	/*
1339 	 * See if it is a line-editing character.
1340 	 */
1341 	if (in_mca() && len == 1)
1342 	{
1343 		action = cmd_edit(c, stay_in_completion);
1344 		switch (action)
1345 		{
1346 		case CC_OK:
1347 		case CC_QUIT:
1348 			return (action);
1349 		case CC_PASS:
1350 			break;
1351 		}
1352 	}
1353 
1354 	/*
1355 	 * Insert the char into the command buffer.
1356 	 */
1357 	return (cmd_ichar(cmd_mbc_buf, len));
1358 }
1359 
cmd_char(char c)1360 public int cmd_char(char c)
1361 {
1362 	return cmd_char2(c, FALSE);
1363 }
1364 
1365 /*
1366  * Copy an ASCII string to the command buffer.
1367  */
cmd_setstring(constant char * s,lbool uc)1368 public int cmd_setstring(constant char *s, lbool uc)
1369 {
1370 	while (*s != '\0')
1371 	{
1372 		int action;
1373 		char c = *s++;
1374 		if (uc && ASCII_IS_LOWER(c))
1375 			c = ASCII_TO_UPPER(c);
1376 		action = cmd_char2(c, TRUE);
1377 		if (action != CC_OK)
1378 			return (action);
1379 	}
1380 	cmd_repaint_curr();
1381 	return (CC_OK);
1382 }
1383 
1384 /*
1385  * Return the number currently in the command buffer.
1386  */
cmd_int(mutable long * frac)1387 public LINENUM cmd_int(mutable long *frac)
1388 {
1389 	constant char *p;
1390 	LINENUM n = 0;
1391 	lbool err;
1392 
1393 	for (p = cmdbuf;  *p >= '0' && *p <= '9';  p++)
1394 	{
1395 		if (ckd_mul(&n, n, 10) || ckd_add(&n, n, *p - '0'))
1396 		{
1397 			error("Integer is too big", NULL_PARG);
1398 			return (0);
1399 		}
1400 	}
1401 	*frac = 0;
1402 	if (*p++ == '.')
1403 	{
1404 		*frac = getfraction(&p, NULL, &err);
1405 		/* {{ do something if err is set? }} */
1406 	}
1407 	return (n);
1408 }
1409 
1410 /*
1411  * Return a pointer to the command buffer.
1412  */
get_cmdbuf(void)1413 public constant char * get_cmdbuf(void)
1414 {
1415 	if (cmd_mbc_buf_index < cmd_mbc_buf_len)
1416 		/* Don't return buffer containing an incomplete multibyte char. */
1417 		return (NULL);
1418 	return (cmdbuf);
1419 }
1420 
1421 #if CMD_HISTORY
1422 /*
1423  * Return the last (most recent) string in the current command history.
1424  */
cmd_lastpattern(void)1425 public constant char * cmd_lastpattern(void)
1426 {
1427 	if (curr_mlist == NULL)
1428 		return (NULL);
1429 	return (curr_mlist->curr_mp->prev->string);
1430 }
1431 #endif
1432 
1433 #if CMD_HISTORY
1434 /*
1435  */
mlist_size(struct mlist * ml)1436 static int mlist_size(struct mlist *ml)
1437 {
1438 	int size = 0;
1439 	for (ml = ml->next;  ml->string != NULL;  ml = ml->next)
1440 		++size;
1441 	return size;
1442 }
1443 
1444 /*
1445  * Get the name of the history file.
1446  */
histfile_find(lbool must_exist)1447 static char * histfile_find(lbool must_exist)
1448 {
1449 	constant char *home = lgetenv("HOME");
1450 	char *name = NULL;
1451 
1452 	/* Try in $XDG_STATE_HOME, then in $HOME/.local/state, then in $XDG_DATA_HOME, then in $HOME. */
1453 #if OS2
1454 	if (isnullenv(home))
1455 		home = lgetenv("INIT");
1456 #endif
1457 	name = dirfile(lgetenv("XDG_STATE_HOME"), &LESSHISTFILE[1], must_exist);
1458 	if (name == NULL)
1459 	{
1460 		char *dir = dirfile(home, ".local/state", 1);
1461 		if (dir != NULL)
1462 		{
1463 			name = dirfile(dir, &LESSHISTFILE[1], must_exist);
1464 			free(dir);
1465 		}
1466 	}
1467 	if (name == NULL)
1468 		name = dirfile(lgetenv("XDG_DATA_HOME"), &LESSHISTFILE[1], must_exist);
1469 	if (name == NULL)
1470 		name = dirfile(home, LESSHISTFILE, must_exist);
1471 	return (name);
1472 }
1473 
histfile_name(lbool must_exist)1474 static char * histfile_name(lbool must_exist)
1475 {
1476 	constant char *name;
1477 	char *wname;
1478 
1479 	/* See if filename is explicitly specified by $LESSHISTFILE. */
1480 	name = lgetenv("LESSHISTFILE");
1481 	if (!isnullenv(name))
1482 	{
1483 		if (strcmp(name, "-") == 0 || strcmp(name, "/dev/null") == 0)
1484 			/* $LESSHISTFILE == "-" means don't use a history file. */
1485 			return (NULL);
1486 		return (save(name));
1487 	}
1488 
1489 	/* See if history file is disabled in the build. */
1490 	if (strcmp(LESSHISTFILE, "") == 0 || strcmp(LESSHISTFILE, "-") == 0)
1491 		return (NULL);
1492 
1493 	wname = NULL;
1494 	if (!must_exist)
1495 	{
1496 	 	/* If we're writing the file and the file already exists, use it. */
1497 		wname = histfile_find(TRUE);
1498 	}
1499 	if (wname == NULL)
1500 		wname = histfile_find(must_exist);
1501 	return (wname);
1502 }
1503 
1504 /*
1505  * Read a .lesshst file and call a callback for each line in the file.
1506  */
read_cmdhist2(void (* action)(void *,struct mlist *,constant char *),void * uparam,int skip_search,int skip_shell)1507 static void read_cmdhist2(void (*action)(void*,struct mlist*,constant char*), void *uparam, int skip_search, int skip_shell)
1508 {
1509 	struct mlist *ml = NULL;
1510 	char line[CMDBUF_SIZE];
1511 	char *filename;
1512 	FILE *f;
1513 	int *skip = NULL;
1514 
1515 	filename = histfile_name(TRUE);
1516 	if (filename == NULL)
1517 		return;
1518 	f = fopen(filename, "r");
1519 	free(filename);
1520 	if (f == NULL)
1521 		return;
1522 	if (fgets(line, sizeof(line), f) == NULL ||
1523 	    strncmp(line, HISTFILE_FIRST_LINE, strlen(HISTFILE_FIRST_LINE)) != 0)
1524 	{
1525 		fclose(f);
1526 		return;
1527 	}
1528 	while (fgets(line, sizeof(line), f) != NULL)
1529 	{
1530 		char *p;
1531 		for (p = line;  *p != '\0';  p++)
1532 		{
1533 			if (*p == '\n' || *p == '\r')
1534 			{
1535 				*p = '\0';
1536 				break;
1537 			}
1538 		}
1539 		if (strcmp(line, HISTFILE_SEARCH_SECTION) == 0)
1540 		{
1541 			ml = &mlist_search;
1542 			skip = &skip_search;
1543 		} else if (strcmp(line, HISTFILE_SHELL_SECTION) == 0)
1544 		{
1545 #if SHELL_ESCAPE || PIPEC
1546 			ml = &mlist_shell;
1547 			skip = &skip_shell;
1548 #else
1549 			ml = NULL;
1550 			skip = NULL;
1551 			(void) skip_shell;
1552 #endif
1553 		} else if (strcmp(line, HISTFILE_MARK_SECTION) == 0)
1554 		{
1555 			ml = NULL;
1556 		} else if (*line == '"')
1557 		{
1558 			if (ml != NULL)
1559 			{
1560 				if (skip != NULL && *skip > 0)
1561 					--(*skip);
1562 				else
1563 					(*action)(uparam, ml, line+1);
1564 			}
1565 		} else if (*line == 'm')
1566 		{
1567 			(*action)(uparam, NULL, line);
1568 		}
1569 	}
1570 	fclose(f);
1571 }
1572 
read_cmdhist(void (* action)(void *,struct mlist *,constant char *),void * uparam,lbool skip_search,lbool skip_shell)1573 static void read_cmdhist(void (*action)(void*,struct mlist*,constant char*), void *uparam, lbool skip_search, lbool skip_shell)
1574 {
1575 	if (!secure_allow(SF_HISTORY))
1576 		return;
1577 	read_cmdhist2(action, uparam, skip_search, skip_shell);
1578 	(*action)(uparam, NULL, NULL); /* signal end of file */
1579 }
1580 
addhist_init(void * uparam,struct mlist * ml,constant char * string)1581 static void addhist_init(void *uparam, struct mlist *ml, constant char *string)
1582 {
1583 	(void) uparam;
1584 	if (ml != NULL)
1585 		cmd_addhist(ml, string, 0);
1586 	else if (string != NULL)
1587 		restore_mark(string);
1588 }
1589 #endif /* CMD_HISTORY */
1590 
1591 /*
1592  * Initialize history from a .lesshist file.
1593  */
init_cmdhist(void)1594 public void init_cmdhist(void)
1595 {
1596 #if CMD_HISTORY
1597 	read_cmdhist(&addhist_init, NULL, 0, 0);
1598 #endif /* CMD_HISTORY */
1599 }
1600 
1601 /*
1602  * Write the header for a section of the history file.
1603  */
1604 #if CMD_HISTORY
write_mlist_header(struct mlist * ml,FILE * f)1605 static void write_mlist_header(struct mlist *ml, FILE *f)
1606 {
1607 	if (ml == &mlist_search)
1608 		fprintf(f, "%s\n", HISTFILE_SEARCH_SECTION);
1609 #if SHELL_ESCAPE || PIPEC
1610 	else if (ml == &mlist_shell)
1611 		fprintf(f, "%s\n", HISTFILE_SHELL_SECTION);
1612 #endif
1613 }
1614 
1615 /*
1616  * Write all modified entries in an mlist to the history file.
1617  */
write_mlist(struct mlist * ml,FILE * f)1618 static void write_mlist(struct mlist *ml, FILE *f)
1619 {
1620 	for (ml = ml->next;  ml->string != NULL;  ml = ml->next)
1621 	{
1622 		if (!ml->modified)
1623 			continue;
1624 		fprintf(f, "\"%s\n", ml->string);
1625 		ml->modified = FALSE;
1626 	}
1627 	ml->modified = FALSE; /* entire mlist is now unmodified */
1628 }
1629 
1630 /*
1631  * Make a temp name in the same directory as filename.
1632  */
make_tempname(constant char * filename)1633 static char * make_tempname(constant char *filename)
1634 {
1635 	char lastch;
1636 	char *tempname = ecalloc(1, strlen(filename)+1);
1637 	strcpy(tempname, filename);
1638 	lastch = tempname[strlen(tempname)-1];
1639 	tempname[strlen(tempname)-1] = (lastch == 'Q') ? 'Z' : 'Q';
1640 	return tempname;
1641 }
1642 
1643 struct save_ctx
1644 {
1645 	struct mlist *mlist;
1646 	FILE *fout;
1647 };
1648 
1649 /*
1650  * Copy entries from the saved history file to a new file.
1651  * At the end of each mlist, append any new entries
1652  * created during this session.
1653  */
copy_hist(void * uparam,struct mlist * ml,constant char * string)1654 static void copy_hist(void *uparam, struct mlist *ml, constant char *string)
1655 {
1656 	struct save_ctx *ctx = (struct save_ctx *) uparam;
1657 
1658 	if (ml != NULL && ml != ctx->mlist) {
1659 		/* We're changing mlists. */
1660 		if (ctx->mlist)
1661 			/* Append any new entries to the end of the current mlist. */
1662 			write_mlist(ctx->mlist, ctx->fout);
1663 		/* Write the header for the new mlist. */
1664 		ctx->mlist = ml;
1665 		write_mlist_header(ctx->mlist, ctx->fout);
1666 	}
1667 
1668 	if (string == NULL) /* End of file */
1669 	{
1670 		/* Write any sections that were not in the original file. */
1671 		if (mlist_search.modified)
1672 		{
1673 			write_mlist_header(&mlist_search, ctx->fout);
1674 			write_mlist(&mlist_search, ctx->fout);
1675 		}
1676 #if SHELL_ESCAPE || PIPEC
1677 		if (mlist_shell.modified)
1678 		{
1679 			write_mlist_header(&mlist_shell, ctx->fout);
1680 			write_mlist(&mlist_shell, ctx->fout);
1681 		}
1682 #endif
1683 	} else if (ml != NULL)
1684 	{
1685 		/* Copy mlist entry. */
1686 		fprintf(ctx->fout, "\"%s\n", string);
1687 	}
1688 	/* Skip marks */
1689 }
1690 #endif /* CMD_HISTORY */
1691 
1692 /*
1693  * Make a file readable only by its owner.
1694  */
make_file_private(FILE * f)1695 static void make_file_private(FILE *f)
1696 {
1697 #if HAVE_FCHMOD
1698 	lbool do_chmod = TRUE;
1699 #if HAVE_STAT
1700 	struct stat statbuf;
1701 	int r = fstat(fileno(f), &statbuf);
1702 	if (r < 0 || !S_ISREG(statbuf.st_mode))
1703 		/* Don't chmod if not a regular file. */
1704 		do_chmod = FALSE;
1705 #endif
1706 	if (do_chmod)
1707 		fchmod(fileno(f), 0600);
1708 #endif
1709 }
1710 
1711 /*
1712  * Does the history file need to be updated?
1713  */
1714 #if CMD_HISTORY
histfile_modified(void)1715 static lbool histfile_modified(void)
1716 {
1717 	if (mlist_search.modified)
1718 		return TRUE;
1719 #if SHELL_ESCAPE || PIPEC
1720 	if (mlist_shell.modified)
1721 		return TRUE;
1722 #endif
1723 	if (marks_modified)
1724 		return TRUE;
1725 	return FALSE;
1726 }
1727 #endif
1728 
1729 /*
1730  * Update the .lesshst file.
1731  */
save_cmdhist(void)1732 public void save_cmdhist(void)
1733 {
1734 #if CMD_HISTORY
1735 	char *histname;
1736 	char *tempname;
1737 	int skip_search;
1738 	int skip_shell;
1739 	struct save_ctx ctx;
1740 	constant char *s;
1741 	FILE *fout = NULL;
1742 	int histsize = 0;
1743 
1744 	if (!secure_allow(SF_HISTORY) || !histfile_modified())
1745 		return;
1746 	histname = histfile_name(0);
1747 	if (histname == NULL)
1748 		return;
1749 	tempname = make_tempname(histname);
1750 	fout = fopen(tempname, "w");
1751 	if (fout != NULL)
1752 	{
1753 		make_file_private(fout);
1754 		s = lgetenv("LESSHISTSIZE");
1755 		if (s != NULL)
1756 			histsize = atoi(s);
1757 		if (histsize <= 0)
1758 			histsize = 100;
1759 		skip_search = mlist_size(&mlist_search) - histsize;
1760 #if SHELL_ESCAPE || PIPEC
1761 		skip_shell = mlist_size(&mlist_shell) - histsize;
1762 #else
1763 		skip_shell = 0; /* not actually used */
1764 #endif
1765 		fprintf(fout, "%s\n", HISTFILE_FIRST_LINE);
1766 		ctx.fout = fout;
1767 		ctx.mlist = NULL;
1768 		read_cmdhist(&copy_hist, &ctx, skip_search, skip_shell);
1769 		save_marks(fout, HISTFILE_MARK_SECTION);
1770 		fclose(fout);
1771 #if MSDOS_COMPILER==WIN32C
1772 		/*
1773 		 * Windows rename doesn't remove an existing file,
1774 		 * making it useless for atomic operations. Sigh.
1775 		 */
1776 		remove(histname);
1777 #endif
1778 		rename(tempname, histname);
1779 	}
1780 	free(tempname);
1781 	free(histname);
1782 #endif /* CMD_HISTORY */
1783 }
1784