master xplshn/aruu / cmd / posix / sh / lineedit.c
  1/*-
  2 * SPDX-License-Identifier: BSD-3-Clause
  3 *
  4 * Copyright (c) 1993
  5 *	The Regents of the University of California.  All rights reserved.
  6 *
  7 * This code is derived from software contributed to Berkeley by
  8 * Kenneth Almquist.
  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 * 3. Neither the name of the University nor the names of its contributors
 19 *    may be used to promote products derived from this software
 20 *    without specific prior written permission.
 21 *
 22 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 23 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 25 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 26 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 27 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 28 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 29 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 31 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 32 * SUCH DAMAGE.
 33 */
 34
 35#if !FEATURE_SH_HISTEDIT
 36#ifndef NO_HISTORY
 37#define NO_HISTORY
 38#endif
 39#endif
 40
 41#include "util.h"
 42#include "shell.h"
 43#include "alias.h"
 44#include "builtins.h"
 45#include "error.h"
 46#include "eval.h"
 47#include "exec.h"
 48#include "main.h"
 49#include "memalloc.h"
 50#include "mystring.h"
 51#include "options.h"
 52#include "output.h"
 53#include "parser.h"
 54#include "var.h"
 55
 56#ifndef NO_HISTORY
 57#include "lineedit.h"
 58
 59#include <sys/param.h>
 60#include <sys/stat.h>
 61
 62#include <dirent.h>
 63#include <errno.h>
 64#include <fcntl.h>
 65#include <limits.h>
 66#include <paths.h>
 67#include <pwd.h>
 68#include <stdio.h>
 69#include <stdlib.h>
 70#include <string.h>
 71#include <unistd.h>
 72
 73#define MAXHISTLOOPS	4
 74#define DEFEDITOR	"ed"
 75#define VTABSIZE 39
 76
 77extern struct var *vartab[VTABSIZE];
 78
 79int sh_history_enabled = 0;
 80int displayhist = 0;
 81static int savehist = 0;
 82
 83static char *fc_replace(const char *, char *, char *);
 84static int not_fcnumber(const char *);
 85static int str_to_event(const char *, int);
 86
 87static char *
 88escape_filename(const char *filename)
 89{
 90	size_t len;
 91	size_t i;
 92	size_t j;
 93	char *escaped;
 94	const char *special;
 95
 96	len = 0;
 97	special = " \t\n\"'\\$&|;<>()*?[]!{}";
 98	for (i = 0; filename[i] != '\0'; i++) {
 99		if (strchr(special, filename[i]) != NULL)
100			len += 2;
101		else
102			len += 1;
103	}
104
105	escaped = malloc(len + 1);
106	j = 0;
107	for (i = 0; filename[i] != '\0'; i++) {
108		if (strchr(special, filename[i]) != NULL) {
109			escaped[j++] = '\\';
110			escaped[j++] = filename[i];
111		} else {
112			escaped[j++] = filename[i];
113		}
114	}
115	escaped[j] = '\0';
116	return escaped;
117}
118
119static char *
120unescape_filename(const char *filename)
121{
122	size_t len;
123	char *unescaped;
124	size_t i;
125	size_t j;
126
127	len = strlen(filename);
128	unescaped = malloc(len + 1);
129	i = 0;
130	j = 0;
131	while (i < len) {
132		if (filename[i] == '\\' && i + 1 < len) {
133			unescaped[j++] = filename[i+1];
134			i += 2;
135		} else {
136			unescaped[j++] = filename[i];
137			i++;
138		}
139	}
140	unescaped[j] = '\0';
141	return unescaped;
142}
143
144static void
145complete_tildes(const char *word, struct redlineCompletions *lc)
146{
147	struct passwd *pw;
148	char completed[512];
149	const char *user_prefix;
150	size_t prefix_len;
151
152	user_prefix = word + 1;
153	prefix_len = strlen(user_prefix);
154
155	setpwent();
156	while ((pw = getpwent()) != NULL) {
157		if (strncmp(pw->pw_name, user_prefix, prefix_len) == 0) {
158			snprintf(completed, sizeof(completed), "~%s/", pw->pw_name);
159			redlineAddCompletion(lc, completed);
160		}
161	}
162	endpwent();
163}
164
165static void
166complete_variables(const char *word, struct redlineCompletions *lc)
167{
168	struct var **vpp;
169	struct var *vp;
170	char name[256];
171	char completed[512];
172	const char *var_prefix;
173	size_t prefix_len;
174	char *eq;
175	size_t name_len;
176
177	var_prefix = word + 1;
178	prefix_len = strlen(var_prefix);
179
180	for (vpp = vartab; vpp < vartab + VTABSIZE; vpp++) {
181		for (vp = *vpp; vp; vp = vp->next) {
182			if (!(vp->flags & VUNSET)) {
183				eq = strchr(vp->text, '=');
184				if (eq) {
185					name_len = eq - vp->text;
186					if (name_len < sizeof(name)) {
187						memcpy(name, vp->text, name_len);
188						name[name_len] = '\0';
189						if (strncmp(name, var_prefix, prefix_len) == 0) {
190							snprintf(completed, sizeof(completed), "$%s ", name);
191							redlineAddCompletion(lc, completed);
192						}
193					}
194				}
195			}
196		}
197	}
198}
199
200static const char *
201get_histfile(void)
202{
203	const char *histfile;
204
205	if (!strcmp(histsizeval(), "0"))
206		return (NULL);
207	histfile = expandstr("${HISTFILE-${HOME-}/.sh_history}");
208
209	if (histfile[0] == '\0')
210		return (NULL);
211	return (histfile);
212}
213
214void
215histsave(void)
216{
217	const char *histfile;
218
219	if (!savehist || (histfile = get_histfile()) == NULL)
220		return;
221	INTOFF;
222	redlineHistorySave(histfile);
223	INTON;
224}
225
226void
227histload(void)
228{
229	const char *histfile;
230
231	if ((histfile = get_histfile()) == NULL)
232		return;
233	errno = 0;
234	if (redlineHistoryLoad(histfile) != -1 || errno == ENOENT)
235		savehist = 1;
236}
237
238static void
239find_completions_recurse(const char *fs_dir, const char *user_prefix,
240    char **comps, int comp_idx, int comp_count, int is_cmd, struct redlineCompletions *lc)
241{
242	DIR *dir;
243	struct dirent *de;
244	struct stat st;
245	char next_fs[4096];
246	char next_user[4096];
247	size_t len;
248
249	if (comp_idx == comp_count) {
250		/* reached the end of components, check if the path exists */
251		if (stat(fs_dir, &st) == 0) {
252			if (is_cmd && !S_ISDIR(st.st_mode) && access(fs_dir, X_OK) != 0) {
253				return;
254			}
255			snprintf(next_user, sizeof(next_user), "%s", user_prefix);
256			if (S_ISDIR(st.st_mode)) {
257				/* if it is a directory and doesnt end with slash, add slash */
258				len = strlen(next_user);
259				if (len > 0 && next_user[len - 1] != '/') {
260					strlcat(next_user, "/", sizeof(next_user));
261				}
262			} else {
263				/* file, add space */
264				strlcat(next_user, " ", sizeof(next_user));
265			}
266			redlineAddCompletion(lc, next_user);
267		}
268		return;
269	}
270
271	if (strcmp(comps[comp_idx], ".") == 0 || strcmp(comps[comp_idx], "..") == 0) {
272		/* construct filesystem path */
273		if (strcmp(fs_dir, "/") == 0) {
274			snprintf(next_fs, sizeof(next_fs), "/%s", comps[comp_idx]);
275		} else if (strcmp(fs_dir, ".") == 0) {
276			snprintf(next_fs, sizeof(next_fs), "./%s", comps[comp_idx]);
277		} else {
278			snprintf(next_fs, sizeof(next_fs), "%s/%s", fs_dir, comps[comp_idx]);
279		}
280
281		/* construct user visible path */
282		if (strcmp(user_prefix, "/") == 0) {
283			snprintf(next_user, sizeof(next_user), "/%s", comps[comp_idx]);
284		} else if (strcmp(user_prefix, "~/") == 0) {
285			snprintf(next_user, sizeof(next_user), "~/%s", comps[comp_idx]);
286		} else if (user_prefix[0] == '\0') {
287			snprintf(next_user, sizeof(next_user), "%s", comps[comp_idx]);
288		} else {
289			len = strlen(user_prefix);
290			if (user_prefix[len - 1] == '/') {
291				snprintf(next_user, sizeof(next_user), "%s%s", user_prefix, comps[comp_idx]);
292			} else {
293				snprintf(next_user, sizeof(next_user), "%s/%s", user_prefix, comps[comp_idx]);
294			}
295		}
296
297		find_completions_recurse(next_fs, next_user, comps, comp_idx + 1, comp_count, is_cmd, lc);
298		return;
299	}
300
301	dir = opendir(fs_dir);
302	if (!dir)
303		return;
304
305	while ((de = readdir(dir)) != NULL) {
306		if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
307			continue;
308
309		if (strncmp(de->d_name, comps[comp_idx], strlen(comps[comp_idx])) == 0) {
310			char *escaped_name = escape_filename(de->d_name);
311			/* construct filesystem path */
312			if (strcmp(fs_dir, "/") == 0) {
313				snprintf(next_fs, sizeof(next_fs), "/%s", de->d_name);
314			} else if (strcmp(fs_dir, ".") == 0) {
315				snprintf(next_fs, sizeof(next_fs), "./%s", de->d_name);
316			} else {
317				snprintf(next_fs, sizeof(next_fs), "%s/%s", fs_dir, de->d_name);
318			}
319
320			/* middle components must be directories */
321			if (comp_idx < comp_count - 1) {
322				if (stat(next_fs, &st) != 0 || !S_ISDIR(st.st_mode)) {
323					free(escaped_name);
324					continue;
325				}
326			}
327
328			/* construct user-visible path */
329			if (strcmp(user_prefix, "/") == 0) {
330				snprintf(next_user, sizeof(next_user), "/%s", escaped_name);
331			} else if (strcmp(user_prefix, "~/") == 0) {
332				snprintf(next_user, sizeof(next_user), "~/%s", escaped_name);
333			} else if (user_prefix[0] == '\0') {
334				snprintf(next_user, sizeof(next_user), "%s", escaped_name);
335			} else {
336				len = strlen(user_prefix);
337				if (user_prefix[len - 1] == '/') {
338					snprintf(next_user, sizeof(next_user), "%s%s", user_prefix, escaped_name);
339				} else {
340					snprintf(next_user, sizeof(next_user), "%s/%s", user_prefix, escaped_name);
341				}
342			}
343
344			find_completions_recurse(next_fs, next_user, comps, comp_idx + 1, comp_count, is_cmd, lc);
345			free(escaped_name);
346		}
347	}
348	closedir(dir);
349}
350
351/* complete matching files in the filesystem */
352static void
353complete_files(const char *word, int is_cmd, struct redlineCompletions *lc)
354{
355	char *path_to_split;
356	const char *fs_dir;
357	const char *user_prefix;
358	const char *home;
359	char *path_copy;
360	char *p;
361	char *comps[128];
362	int comp_count;
363	char *unescaped_word;
364
365	path_to_split = NULL;
366	fs_dir = ".";
367	user_prefix = "";
368	home = getenv("HOME");
369	comp_count = 0;
370	unescaped_word = unescape_filename(word);
371
372	if (unescaped_word[0] == '~') {
373		if (unescaped_word[1] == '/' || unescaped_word[1] == '\0') {
374			fs_dir = home ? home : "/";
375			user_prefix = "~/";
376			path_to_split = (unescaped_word[1] == '\0') ? "" : (char *)(unescaped_word + 2);
377		} else {
378			/* ~username is not supported for abbreviation, fallback to home */
379			fs_dir = home ? home : "/";
380			user_prefix = "~/";
381			path_to_split = (char *)(unescaped_word + 1);
382		}
383	} else if (unescaped_word[0] == '/') {
384		fs_dir = "/";
385		user_prefix = "/";
386		path_to_split = (char *)(unescaped_word + 1);
387	} else {
388		fs_dir = ".";
389		user_prefix = "";
390		path_to_split = (char *)unescaped_word;
391	}
392
393	path_copy = estrdup(path_to_split);
394	p = path_copy;
395	if (*p != '\0') {
396		comps[comp_count++] = p;
397		while (*p != '\0') {
398			if (*p == '/') {
399				*p = '\0';
400				p++;
401				while (*p == '/')
402					p++;
403				if (*p == '\0') {
404					comps[comp_count++] = p;
405					break;
406				}
407				comps[comp_count++] = p;
408			} else {
409				p++;
410			}
411		}
412	} else {
413		/* empty path_to_split (e.g. exactly ~ or exactly / or empty word) */
414		comps[comp_count++] = p;
415	}
416
417	find_completions_recurse(fs_dir, user_prefix, comps, 0, comp_count, is_cmd, lc);
418	free(path_copy);
419	free(unescaped_word);
420}
421
422/* complete matching executable commands and builtins */
423static void
424complete_commands(const char *word, struct redlineCompletions *lc)
425{
426	char *free_path = NULL, *path;
427	const char *dirname;
428	struct cmdentry e;
429	const struct alias *ap = NULL;
430	const unsigned char *bp = builtincmd;
431	const void *a = NULL;
432	DIR *dir;
433	struct dirent *entry;
434	int dfd;
435	struct stat statb;
436	char completed[512];
437
438	while ((ap = iteralias(ap)) != NULL) {
439		if (strncmp(ap->name, word, strlen(word)) == 0) {
440			snprintf(completed, sizeof(completed), "%s ", ap->name);
441			redlineAddCompletion(lc, completed);
442		}
443	}
444
445	while (bp && *bp != 0) {
446		if (strncmp((const char *)(bp + 2), word, strlen(word)) == 0) {
447			snprintf(completed, sizeof(completed), "%.*s ", (int)bp[0], bp + 2);
448			redlineAddCompletion(lc, completed);
449		}
450		bp += 2 + bp[0];
451	}
452
453	while ((a = itercmd(a, &e)) != NULL) {
454		if (e.cmdtype == CMDFUNCTION && strncmp(e.cmdname, word, strlen(word)) == 0) {
455			snprintf(completed, sizeof(completed), "%s ", e.cmdname);
456			redlineAddCompletion(lc, completed);
457		}
458	}
459
460	path = pathval();
461	if (path) {
462		free_path = path = estrdup(path);
463		while ((dirname = strsep(&path, ":")) != NULL) {
464			dir = opendir(dirname[0] == '\0' ? "." : dirname);
465			if (dir == NULL)
466				continue;
467			dfd = dirfd(dir);
468			if (dfd == -1) {
469				closedir(dir);
470				continue;
471			}
472			while ((entry = readdir(dir)) != NULL) {
473				if (strncmp(entry->d_name, word, strlen(word)) != 0)
474					continue;
475				if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK) {
476					if (fstatat(dfd, entry->d_name, &statb, 0) == -1)
477						continue;
478					if (!S_ISREG(statb.st_mode))
479						continue;
480				} else if (entry->d_type != DT_REG) {
481					continue;
482				}
483				snprintf(completed, sizeof(completed), "%s ", entry->d_name);
484				redlineAddCompletion(lc, completed);
485			}
486			closedir(dir);
487		}
488		free(free_path);
489	}
490}
491
492/* main completion callback called by redline library */
493static void
494sh_complete_callback(const char *buf, struct redlineCompletions *lc)
495{
496	const char *word;
497	int start;
498	int is_cmd;
499	int p;
500	struct redlineCompletions temp_lc;
501	char line_prefix[4096];
502	char full_completion[4096];
503	size_t i;
504
505	start = strlen(buf);
506	while (start > 0) {
507		char c = buf[start - 1];
508		if (c == ' ' || c == '\t' || c == '\n' || c == '"' || c == '\'' || c == '`' ||
509		    c == '@' || c == '$' || c == '>' || c == '<' || c == '=' || c == ';' ||
510		    c == '|' || c == '&' || c == '{' || c == '(') {
511			if (start > 1 && buf[start - 2] == '\\') {
512				start -= 2;
513				continue;
514			}
515			break;
516		} else if (c == '\\') {
517			break;
518		}
519		start--;
520	}
521	word = buf + start;
522
523	is_cmd = 0;
524	if (start == 0) {
525		is_cmd = 1;
526	} else {
527		p = start;
528		while (p > 0 && (buf[p - 1] == ' ' || buf[p - 1] == '\t'))
529			p--;
530		if (p == 0 || strchr(";&|({`\n", buf[p - 1]) != NULL)
531			is_cmd = 1;
532	}
533
534	if (start >= (int)sizeof(line_prefix))
535		return;
536	snprintf(line_prefix, sizeof(line_prefix), "%.*s", start, buf);
537
538	temp_lc.len = 0;
539	temp_lc.cvec = NULL;
540
541	if (word[0] == '$') {
542		complete_variables(word, &temp_lc);
543	} else if (word[0] == '~' && strchr(word, '/') == NULL) {
544		complete_tildes(word, &temp_lc);
545	} else if (is_cmd && strchr(word, '/') == NULL && word[0] != '~' && word[0] != '.') {
546		complete_commands(word, &temp_lc);
547	} else {
548		complete_files(word, is_cmd, &temp_lc);
549	}
550
551	for (i = 0; i < temp_lc.len; i++) {
552		snprintf(full_completion, sizeof(full_completion), "%s%s", line_prefix, temp_lc.cvec[i]);
553		redlineAddCompletion(lc, full_completion);
554	}
555
556	for (i = 0; i < temp_lc.len; i++) {
557		free(temp_lc.cvec[i]);
558	}
559	free(temp_lc.cvec);
560}
561
562void
563histedit(void)
564{
565	sh_history_enabled = (iflag && (Eflag || Vflag));
566	if (sh_history_enabled) {
567		redlineSetCompletionCallback(sh_complete_callback);
568		redlineSetMultiLine(1);
569	}
570}
571
572void
573sethistsize(const char *hs)
574{
575	int histsize;
576
577	if (hs == NULL || !is_number(hs))
578		histsize = 128;
579	else
580		histsize = atoi(hs);
581	redlineHistorySetMaxLen(histsize);
582}
583
584void
585setterm(const char *term __unused)
586{
587}
588
589int
590histcmd(int argc, char **argv __unused)
591{
592	const char *editor = NULL;
593	int lflg = 0, nflg = 0, rflg = 0, sflg = 0;
594	int i;
595	const char *firststr, *laststr;
596	int first, last;
597	char *pat = NULL, *repl = NULL;
598	static int active = 0;
599	struct jmploc jmploc;
600	struct jmploc *savehandler;
601	char editfilestr[PATH_MAX];
602	char *volatile editfile;
603	FILE *efp = NULL;
604	int dir;
605
606	if (redlineHistoryLen() == 0)
607		error("history not active");
608
609	if (argc == 1)
610		error("missing history argument");
611
612	while (not_fcnumber(*argptr))
613		do {
614			switch (nextopt("e:lnrs")) {
615			case 'e':
616				editor = shoptarg;
617				break;
618			case 'l':
619				lflg = 1;
620				break;
621			case 'n':
622				nflg = 1;
623				break;
624			case 'r':
625				rflg = 1;
626				break;
627			case 's':
628				sflg = 1;
629				break;
630			case '\0':
631				goto operands;
632			}
633		} while (nextopt_optptr != NULL);
634operands:
635	savehandler = handler;
636	if (lflg == 0 || editor || sflg) {
637		lflg = 0;
638		editfile = NULL;
639		if (setjmp(jmploc.loc)) {
640			active = 0;
641			if (editfile)
642				unlink(editfile);
643			handler = savehandler;
644			longjmp(handler->loc, 1);
645		}
646		handler = &jmploc;
647		if (++active > MAXHISTLOOPS) {
648			active = 0;
649			displayhist = 0;
650			error("called recursively too many times");
651		}
652		if (sflg == 0) {
653			if (editor == NULL &&
654			    (editor = bltinlookup("FCEDIT", 1)) == NULL &&
655			    (editor = bltinlookup("EDITOR", 1)) == NULL)
656				editor = DEFEDITOR;
657			if (editor[0] == '-' && editor[1] == '\0') {
658				sflg = 1;
659				editor = NULL;
660			}
661		}
662	}
663
664	if (lflg == 0 && *argptr != NULL &&
665	     ((repl = strchr(*argptr, '=')) != NULL)) {
666		pat = *argptr;
667		*repl++ = '\0';
668		argptr++;
669	}
670
671	if (*argptr == NULL) {
672		firststr = lflg ? "-16" : "-1";
673		laststr = "-1";
674	} else if (argptr[1] == NULL) {
675		firststr = argptr[0];
676		laststr = lflg ? "-1" : argptr[0];
677	} else if (argptr[2] == NULL) {
678		firststr = argptr[0];
679		laststr = argptr[1];
680	} else {
681		error("too many arguments");
682	}
683
684	first = str_to_event(firststr, 0);
685	last = str_to_event(laststr, 1);
686
687	if (rflg) {
688		i = last;
689		last = first;
690		first = i;
691	}
692
693	if (editor) {
694		int fd;
695		INTOFF;
696		sprintf(editfilestr, "%s/_shXXXXXX", _PATH_TMP);
697		if ((fd = mkstemp(editfilestr)) < 0)
698			error("can't create temporary file %s", editfile);
699		editfile = editfilestr;
700		if ((efp = fdopen(fd, "w")) == NULL) {
701			close(fd);
702			error("Out of space");
703		}
704	}
705
706	dir = (first <= last) ? 1 : -1;
707	for (i = first; ; i += dir) {
708		if (i < 1 || i > redlineHistoryLen())
709			continue;
710		const char *hstr = redlineHistoryGet(i - 1);
711		if (lflg) {
712			if (!nflg)
713				out1fmt("%5d ", i);
714			out1fmt("%s\n", hstr);
715		} else {
716			const char *s = pat ? fc_replace(hstr, pat, repl) : hstr;
717			if (sflg) {
718				if (displayhist) {
719					out2fmt_flush("%s\n", s);
720				}
721				evalstring(s, 0);
722				if (displayhist) {
723					redlineHistoryAdd(s);
724				}
725			} else {
726				fprintf(efp, "%s\n", s);
727			}
728		}
729		if (i == last)
730			break;
731	}
732
733	if (editor) {
734		char *editcmd;
735
736		fclose(efp);
737		INTON;
738		editcmd = stalloc(strlen(editor) + strlen(editfile) + 2);
739		sprintf(editcmd, "%s %s", editor, editfile);
740		evalstring(editcmd, 0);
741		readcmdfile(editfile, 0);
742		unlink(editfile);
743	}
744
745	if (lflg == 0 && active > 0)
746		--active;
747	if (displayhist)
748		displayhist = 0;
749	handler = savehandler;
750	return 0;
751}
752
753static char *
754fc_replace(const char *s, char *p, char *r)
755{
756	char *dest;
757	int plen = strlen(p);
758
759	STARTSTACKSTR(dest);
760	while (*s) {
761		if (*s == *p && strncmp(s, p, plen) == 0) {
762			STPUTS(r, dest);
763			s += plen;
764			*p = '\0';
765		} else
766			STPUTC(*s++, dest);
767	}
768	STPUTC('\0', dest);
769	dest = grabstackstr(dest);
770
771	return (dest);
772}
773
774static int
775not_fcnumber(const char *s)
776{
777	if (s == NULL)
778		return (0);
779	if (*s == '-')
780		s++;
781	return (!is_number(s));
782}
783
784static int
785str_to_event(const char *str, int last_fallback)
786{
787	int relative = 0;
788	int i;
789	const char *s = str;
790
791	if (s == NULL) {
792		return last_fallback ? redlineHistoryLen() : (redlineHistoryLen() > 16 ? redlineHistoryLen() - 15 : 1);
793	}
794
795	switch (*s) {
796	case '-':
797		relative = 1;
798		s++;
799		break;
800	case '+':
801		s++;
802		break;
803	}
804
805	if (is_number(s)) {
806		i = atoi(s);
807		if (relative) {
808			return redlineHistoryLen() - i;
809		}
810		return i;
811	}
812
813	for (i = redlineHistoryLen() - 1; i >= 0; i--) {
814		if (strncmp(redlineHistoryGet(i), str, strlen(str)) == 0) {
815			return i + 1;
816		}
817	}
818	error("history pattern not found: %s", str);
819	return 0;
820}
821
822int
823bindcmd(int argc __unused, char **argv __unused)
824{
825	error("not compiled with line editing support");
826	return (0);
827}
828
829#else
830
831int
832histcmd(int argc __unused, char **argv __unused)
833{
834	error("not compiled with history support");
835	return (0);
836}
837
838int
839bindcmd(int argc __unused, char **argv __unused)
840{
841	error("not compiled with line editing support");
842	return (0);
843}
844#endif