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 "../../../shared/paths.h"
 42#include "alias.h"
 43#include "builtins.h"
 44#include "error.h"
 45#include "eval.h"
 46#include "exec.h"
 47#include "main.h"
 48#include "memalloc.h"
 49#include "mystring.h"
 50#include "options.h"
 51#include "output.h"
 52#include "parser.h"
 53#include "shell.h"
 54#include "util.h"
 55#include "var.h"
 56
 57#ifndef NO_HISTORY
 58#include "lineedit.h"
 59
 60#include <sys/param.h>
 61#include <sys/stat.h>
 62
 63#include <dirent.h>
 64#include <errno.h>
 65#include <fcntl.h>
 66#include <limits.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(
240    const char                *fs_dir,
241    const char                *user_prefix,
242    char                     **comps,
243    int                        comp_idx,
244    int                        comp_count,
245    int                        is_cmd,
246    struct redlineCompletions *lc
247)
248{
249  DIR           *dir;
250  struct dirent *de;
251  struct stat    st;
252  char           next_fs[4096];
253  char           next_user[4096];
254  size_t         len;
255
256  if (comp_idx == comp_count) {
257    /* reached the end of components, check if the path exists */
258    if (stat(fs_dir, &st) == 0) {
259      if (is_cmd && !S_ISDIR(st.st_mode) && access(fs_dir, X_OK) != 0) {
260        return;
261      }
262      snprintf(next_user, sizeof(next_user), "%s", user_prefix);
263      if (S_ISDIR(st.st_mode)) {
264        /* if it is a directory and doesnt end with
265         * slash, add slash */
266        len = strlen(next_user);
267        if (len > 0 && next_user[len - 1] != '/') {
268          strlcat(next_user, "/", sizeof(next_user));
269        }
270      } else {
271        /* file, add space */
272        strlcat(next_user, " ", sizeof(next_user));
273      }
274      redlineAddCompletion(lc, next_user);
275    }
276    return;
277  }
278
279  if (strcmp(comps[comp_idx], ".") == 0 || strcmp(comps[comp_idx], "..") == 0) {
280    /* construct filesystem path */
281    if (strcmp(fs_dir, "/") == 0) {
282      snprintf(next_fs, sizeof(next_fs), "/%s", comps[comp_idx]);
283    } else if (strcmp(fs_dir, ".") == 0) {
284      snprintf(next_fs, sizeof(next_fs), "./%s", comps[comp_idx]);
285    } else {
286      snprintf(next_fs, sizeof(next_fs), "%s/%s", fs_dir, comps[comp_idx]);
287    }
288
289    /* construct user visible path */
290    if (strcmp(user_prefix, "/") == 0) {
291      snprintf(next_user, sizeof(next_user), "/%s", comps[comp_idx]);
292    } else if (strcmp(user_prefix, "~/") == 0) {
293      snprintf(next_user, sizeof(next_user), "~/%s", comps[comp_idx]);
294    } else if (user_prefix[0] == '\0') {
295      snprintf(next_user, sizeof(next_user), "%s", comps[comp_idx]);
296    } else {
297      len = strlen(user_prefix);
298      if (user_prefix[len - 1] == '/') {
299        snprintf(next_user, sizeof(next_user), "%s%s", user_prefix, comps[comp_idx]);
300      } else {
301        snprintf(next_user, sizeof(next_user), "%s/%s", user_prefix, comps[comp_idx]);
302      }
303    }
304
305    find_completions_recurse(next_fs, next_user, comps, comp_idx + 1, comp_count, is_cmd, lc);
306    return;
307  }
308
309  dir = opendir(fs_dir);
310  if (!dir)
311    return;
312
313  while ((de = readdir(dir)) != NULL) {
314    if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
315      continue;
316
317    if (strncmp(de->d_name, comps[comp_idx], strlen(comps[comp_idx])) == 0) {
318      char *escaped_name = escape_filename(de->d_name);
319      /* construct filesystem path */
320      if (strcmp(fs_dir, "/") == 0) {
321        snprintf(next_fs, sizeof(next_fs), "/%s", de->d_name);
322      } else if (strcmp(fs_dir, ".") == 0) {
323        snprintf(next_fs, sizeof(next_fs), "./%s", de->d_name);
324      } else {
325        snprintf(next_fs, sizeof(next_fs), "%s/%s", fs_dir, de->d_name);
326      }
327
328      /* middle components must be directories */
329      if (comp_idx < comp_count - 1) {
330        if (stat(next_fs, &st) != 0 || !S_ISDIR(st.st_mode)) {
331          free(escaped_name);
332          continue;
333        }
334      }
335
336      /* construct user-visible path */
337      if (strcmp(user_prefix, "/") == 0) {
338        snprintf(next_user, sizeof(next_user), "/%s", escaped_name);
339      } else if (strcmp(user_prefix, "~/") == 0) {
340        snprintf(next_user, sizeof(next_user), "~/%s", escaped_name);
341      } else if (user_prefix[0] == '\0') {
342        snprintf(next_user, sizeof(next_user), "%s", escaped_name);
343      } else {
344        len = strlen(user_prefix);
345        if (user_prefix[len - 1] == '/') {
346          snprintf(next_user, sizeof(next_user), "%s%s", user_prefix, escaped_name);
347        } else {
348          snprintf(next_user, sizeof(next_user), "%s/%s", user_prefix, escaped_name);
349        }
350      }
351
352      find_completions_recurse(next_fs, next_user, comps, comp_idx + 1, comp_count, is_cmd, lc);
353      free(escaped_name);
354    }
355  }
356  closedir(dir);
357}
358
359/* complete matching files in the filesystem */
360static void
361complete_files(const char *word, int is_cmd, struct redlineCompletions *lc)
362{
363  char       *path_to_split;
364  const char *fs_dir;
365  const char *user_prefix;
366  const char *home;
367  char       *path_copy;
368  char       *p;
369  char       *comps[128];
370  int         comp_count;
371  char       *unescaped_word;
372
373  path_to_split  = NULL;
374  fs_dir         = ".";
375  user_prefix    = "";
376  home           = getenv("HOME");
377  comp_count     = 0;
378  unescaped_word = unescape_filename(word);
379
380  if (unescaped_word[0] == '~') {
381    if (unescaped_word[1] == '/' || unescaped_word[1] == '\0') {
382      fs_dir        = home ? home : "/";
383      user_prefix   = "~/";
384      path_to_split = (unescaped_word[1] == '\0') ? "" : (char *)(unescaped_word + 2);
385    } else {
386      /* ~username is not supported for abbreviation, fallback
387       * to home */
388      fs_dir        = home ? home : "/";
389      user_prefix   = "~/";
390      path_to_split = (char *)(unescaped_word + 1);
391    }
392  } else if (unescaped_word[0] == '/') {
393    fs_dir        = "/";
394    user_prefix   = "/";
395    path_to_split = (char *)(unescaped_word + 1);
396  } else {
397    fs_dir        = ".";
398    user_prefix   = "";
399    path_to_split = (char *)unescaped_word;
400  }
401
402  path_copy = estrdup(path_to_split);
403  p         = path_copy;
404  if (*p != '\0') {
405    comps[comp_count++] = p;
406    while (*p != '\0') {
407      if (*p == '/') {
408        *p = '\0';
409        p++;
410        while (*p == '/')
411          p++;
412        if (*p == '\0') {
413          comps[comp_count++] = p;
414          break;
415        }
416        comps[comp_count++] = p;
417      } else {
418        p++;
419      }
420    }
421  } else {
422    /* empty path_to_split (e.g. exactly ~ or exactly / or empty
423     * word) */
424    comps[comp_count++] = p;
425  }
426
427  find_completions_recurse(fs_dir, user_prefix, comps, 0, comp_count, is_cmd, lc);
428  free(path_copy);
429  free(unescaped_word);
430}
431
432/* complete matching executable commands and builtins */
433static void
434complete_commands(const char *word, struct redlineCompletions *lc)
435{
436  char                *free_path = NULL, *path;
437  const char          *dirname;
438  struct cmdentry      e;
439  const struct alias  *ap = NULL;
440  const unsigned char *bp = builtincmd;
441  const void          *a  = NULL;
442  DIR                 *dir;
443  struct dirent       *entry;
444  int                  dfd;
445  struct stat          statb;
446  char                 completed[512];
447
448  while ((ap = iteralias(ap)) != NULL) {
449    if (strncmp(ap->name, word, strlen(word)) == 0) {
450      snprintf(completed, sizeof(completed), "%s ", ap->name);
451      redlineAddCompletion(lc, completed);
452    }
453  }
454
455  while (bp && *bp != 0) {
456    if (strncmp((const char *)(bp + 2), word, strlen(word)) == 0) {
457      snprintf(completed, sizeof(completed), "%.*s ", (int)bp[0], bp + 2);
458      redlineAddCompletion(lc, completed);
459    }
460    bp += 2 + bp[0];
461  }
462
463  while ((a = itercmd(a, &e)) != NULL) {
464    if (e.cmdtype == CMDFUNCTION && strncmp(e.cmdname, word, strlen(word)) == 0) {
465      snprintf(completed, sizeof(completed), "%s ", e.cmdname);
466      redlineAddCompletion(lc, completed);
467    }
468  }
469
470  path = pathval();
471  if (path) {
472    free_path = path = estrdup(path);
473    while ((dirname = strsep(&path, ":")) != NULL) {
474      dir = opendir(dirname[0] == '\0' ? "." : dirname);
475      if (dir == NULL)
476        continue;
477      dfd = dirfd(dir);
478      if (dfd == -1) {
479        closedir(dir);
480        continue;
481      }
482      while ((entry = readdir(dir)) != NULL) {
483        if (strncmp(entry->d_name, word, strlen(word)) != 0)
484          continue;
485        if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK) {
486          if (fstatat(dfd, entry->d_name, &statb, 0) == -1)
487            continue;
488          if (!S_ISREG(statb.st_mode))
489            continue;
490        } else if (entry->d_type != DT_REG) {
491          continue;
492        }
493        snprintf(completed, sizeof(completed), "%s ", entry->d_name);
494        redlineAddCompletion(lc, completed);
495      }
496      closedir(dir);
497    }
498    free(free_path);
499  }
500}
501
502/* main completion callback called by redline library */
503static void
504sh_complete_callback(const char *buf, struct redlineCompletions *lc)
505{
506  const char               *word;
507  int                       start;
508  int                       is_cmd;
509  int                       p;
510  struct redlineCompletions temp_lc;
511  char                      line_prefix[4096];
512  char                      full_completion[4096];
513  size_t                    i;
514
515  start = strlen(buf);
516  while (start > 0) {
517    char c = buf[start - 1];
518    if (c == ' ' || c == '\t' || c == '\n' || c == '"' || c == '\'' || c == '`' || c == '@'
519        || c == '$' || c == '>' || c == '<' || c == '=' || c == ';' || c == '|' || c == '&'
520        || c == '{' || c == '(') {
521      if (start > 1 && buf[start - 2] == '\\') {
522        start -= 2;
523        continue;
524      }
525      break;
526    } else if (c == '\\') {
527      break;
528    }
529    start--;
530  }
531  word = buf + start;
532
533  is_cmd = 0;
534  if (start == 0) {
535    is_cmd = 1;
536  } else {
537    p = start;
538    while (p > 0 && (buf[p - 1] == ' ' || buf[p - 1] == '\t'))
539      p--;
540    if (p == 0 || strchr(";&|({`\n", buf[p - 1]) != NULL)
541      is_cmd = 1;
542  }
543
544  if (start >= (int)sizeof(line_prefix))
545    return;
546  snprintf(line_prefix, sizeof(line_prefix), "%.*s", start, buf);
547
548  temp_lc.len  = 0;
549  temp_lc.cvec = NULL;
550
551  if (word[0] == '$') {
552    complete_variables(word, &temp_lc);
553  } else if (word[0] == '~' && strchr(word, '/') == NULL) {
554    complete_tildes(word, &temp_lc);
555  } else if (is_cmd && strchr(word, '/') == NULL && word[0] != '~' && word[0] != '.') {
556    complete_commands(word, &temp_lc);
557  } else {
558    complete_files(word, is_cmd, &temp_lc);
559  }
560
561  for (i = 0; i < temp_lc.len; i++) {
562    snprintf(full_completion, sizeof(full_completion), "%s%s", line_prefix, temp_lc.cvec[i]);
563    redlineAddCompletion(lc, full_completion);
564  }
565
566  for (i = 0; i < temp_lc.len; i++) {
567    free(temp_lc.cvec[i]);
568  }
569  free(temp_lc.cvec);
570}
571
572void
573histedit(void)
574{
575  sh_history_enabled = (iflag && (Eflag || Vflag));
576  if (sh_history_enabled) {
577    redlineSetCompletionCallback(sh_complete_callback);
578    redlineSetMultiLine(1);
579  }
580}
581
582void
583sethistsize(const char *hs)
584{
585  int histsize;
586
587  if (hs == NULL || !is_number(hs))
588    histsize = 128;
589  else
590    histsize = atoi(hs);
591  redlineHistorySetMaxLen(histsize);
592}
593
594void
595setterm(const char *term __unused)
596{
597}
598
599int
600histcmd(int argc, char **argv __unused)
601{
602  const char    *editor = NULL;
603  int            lflg = 0, nflg = 0, rflg = 0, sflg = 0;
604  int            i;
605  const char    *firststr, *laststr;
606  int            first, last;
607  char          *pat = NULL, *repl = NULL;
608  static int     active = 0;
609  struct jmploc  jmploc;
610  struct jmploc *savehandler;
611  char           editfilestr[PATH_MAX];
612  char *volatile editfile;
613  FILE *efp = NULL;
614  int   dir;
615
616  if (redlineHistoryLen() == 0)
617    error("history not active");
618
619  if (argc == 1)
620    error("missing history argument");
621
622  while (not_fcnumber(*argptr))
623    do {
624      switch (nextopt("e:lnrs")) {
625        case 'e':
626          editor = shoptarg;
627          break;
628        case 'l':
629          lflg = 1;
630          break;
631        case 'n':
632          nflg = 1;
633          break;
634        case 'r':
635          rflg = 1;
636          break;
637        case 's':
638          sflg = 1;
639          break;
640        case '\0':
641          goto operands;
642      }
643    } while (nextopt_optptr != NULL);
644operands:
645  savehandler = handler;
646  if (lflg == 0 || editor || sflg) {
647    lflg     = 0;
648    editfile = NULL;
649    if (setjmp(jmploc.loc)) {
650      active = 0;
651      if (editfile)
652        unlink(editfile);
653      handler = savehandler;
654      longjmp(handler->loc, 1);
655    }
656    handler = &jmploc;
657    if (++active > MAXHISTLOOPS) {
658      active      = 0;
659      displayhist = 0;
660      error("called recursively too many times");
661    }
662    if (sflg == 0) {
663      if (editor == NULL && (editor = bltinlookup("FCEDIT", 1)) == NULL
664          && (editor = bltinlookup("EDITOR", 1)) == NULL)
665        editor = DEFEDITOR;
666      if (editor[0] == '-' && editor[1] == '\0') {
667        sflg   = 1;
668        editor = NULL;
669      }
670    }
671  }
672
673  if (lflg == 0 && *argptr != NULL && ((repl = strchr(*argptr, '=')) != NULL)) {
674    pat     = *argptr;
675    *repl++ = '\0';
676    argptr++;
677  }
678
679  if (*argptr == NULL) {
680    firststr = lflg ? "-16" : "-1";
681    laststr  = "-1";
682  } else if (argptr[1] == NULL) {
683    firststr = argptr[0];
684    laststr  = lflg ? "-1" : argptr[0];
685  } else if (argptr[2] == NULL) {
686    firststr = argptr[0];
687    laststr  = argptr[1];
688  } else {
689    error("too many arguments");
690  }
691
692  first = str_to_event(firststr, 0);
693  last  = str_to_event(laststr, 1);
694
695  if (rflg) {
696    i     = last;
697    last  = first;
698    first = i;
699  }
700
701  if (editor) {
702    int fd;
703    INTOFF;
704    sprintf(editfilestr, "%s/_shXXXXXX", ARUU_PATH_TMP);
705    if ((fd = mkstemp(editfilestr)) < 0)
706      error("can't create temporary file %s", editfile);
707    editfile = editfilestr;
708    if ((efp = fdopen(fd, "w")) == NULL) {
709      close(fd);
710      error("Out of space");
711    }
712  }
713
714  dir = (first <= last) ? 1 : -1;
715  for (i = first;; i += dir) {
716    if (i < 1 || i > redlineHistoryLen())
717      continue;
718    const char *hstr = redlineHistoryGet(i - 1);
719    if (lflg) {
720      if (!nflg)
721        out1fmt("%5d ", i);
722      out1fmt("%s\n", hstr);
723    } else {
724      const char *s = pat ? fc_replace(hstr, pat, repl) : hstr;
725      if (sflg) {
726        if (displayhist) {
727          out2fmt_flush("%s\n", s);
728        }
729        evalstring(s, 0);
730        if (displayhist) {
731          redlineHistoryAdd(s);
732        }
733      } else {
734        fprintf(efp, "%s\n", s);
735      }
736    }
737    if (i == last)
738      break;
739  }
740
741  if (editor) {
742    char *editcmd;
743
744    fclose(efp);
745    INTON;
746    editcmd = stalloc(strlen(editor) + strlen(editfile) + 2);
747    sprintf(editcmd, "%s %s", editor, editfile);
748    evalstring(editcmd, 0);
749    readcmdfile(editfile, 0);
750    unlink(editfile);
751  }
752
753  if (lflg == 0 && active > 0)
754    --active;
755  if (displayhist)
756    displayhist = 0;
757  handler = savehandler;
758  return 0;
759}
760
761static char *
762fc_replace(const char *s, char *p, char *r)
763{
764  char *dest;
765  int   plen = strlen(p);
766
767  STARTSTACKSTR(dest);
768  while (*s) {
769    if (*s == *p && strncmp(s, p, plen) == 0) {
770      STPUTS(r, dest);
771      s += plen;
772      *p = '\0';
773    } else
774      STPUTC(*s++, dest);
775  }
776  STPUTC('\0', dest);
777  dest = grabstackstr(dest);
778
779  return (dest);
780}
781
782static int
783not_fcnumber(const char *s)
784{
785  if (s == NULL)
786    return (0);
787  if (*s == '-')
788    s++;
789  return (!is_number(s));
790}
791
792static int
793str_to_event(const char *str, int last_fallback)
794{
795  int         relative = 0;
796  int         i;
797  const char *s = str;
798
799  if (s == NULL) {
800    return last_fallback ? redlineHistoryLen()
801                         : (redlineHistoryLen() > 16 ? redlineHistoryLen() - 15 : 1);
802  }
803
804  switch (*s) {
805    case '-':
806      relative = 1;
807      s++;
808      break;
809    case '+':
810      s++;
811      break;
812  }
813
814  if (is_number(s)) {
815    i = atoi(s);
816    if (relative) {
817      return redlineHistoryLen() - i;
818    }
819    return i;
820  }
821
822  for (i = redlineHistoryLen() - 1; i >= 0; i--) {
823    if (strncmp(redlineHistoryGet(i), str, strlen(str)) == 0) {
824      return i + 1;
825    }
826  }
827  error("history pattern not found: %s", str);
828  return 0;
829}
830
831int
832bindcmd(int argc __unused, char **argv __unused)
833{
834  error("not compiled with line editing support");
835  return (0);
836}
837
838#else
839
840int
841histcmd(int argc __unused, char **argv __unused)
842{
843  error("not compiled with history support");
844  return (0);
845}
846
847int
848bindcmd(int argc __unused, char **argv __unused)
849{
850  error("not compiled with line editing support");
851  return (0);
852}
853#endif