/* $NetBSD: edit.c,v 1.5 2023/10/06 05:49:49 simonb Exp $ */ /* * Copyright (C) 1984-2023 Mark Nudelman * * You may distribute under the terms of either the GNU General Public * License or the Less License, as specified in the README file. * * For more information, see the README file. */ #include "less.h" #include "position.h" #if HAVE_STAT #include #endif #if HAVE_SYS_WAIT_H #include #endif /* #if OS2 XXX should add a HAVE_SIGNAL_H */ #include /* #endif XXX should add a HAVE_SIGNAL_H */ public int fd0 = 0; extern int new_file; extern int cbufs; extern char *every_first_cmd; extern int force_open; extern int is_tty; extern int sigs; extern int hshift; extern int want_filesize; extern int consecutive_nulls; extern int modelines; extern int show_preproc_error; extern IFILE curr_ifile; extern IFILE old_ifile; extern struct scrpos initial_scrpos; extern void *ml_examine; #if SPACES_IN_FILENAMES extern char openquote; extern char closequote; #endif #if LOGFILE extern int logfile; extern int force_logfile; extern char *namelogfile; #endif #if HAVE_STAT_INO public dev_t curr_dev; public ino_t curr_ino; #endif /* * Textlist functions deal with a list of words separated by spaces. * init_textlist sets up a textlist structure. * forw_textlist uses that structure to iterate thru the list of * words, returning each one as a standard null-terminated string. * back_textlist does the same, but runs thru the list backwards. */ public void init_textlist(struct textlist *tlist, char *str) { char *s; #if SPACES_IN_FILENAMES int meta_quoted = 0; int delim_quoted = 0; char *esc = get_meta_escape(); int esclen = (int) strlen(esc); #endif tlist->string = skipsp(str); tlist->endstring = tlist->string + strlen(tlist->string); for (s = str; s < tlist->endstring; s++) { #if SPACES_IN_FILENAMES if (meta_quoted) { meta_quoted = 0; } else if (esclen > 0 && s + esclen < tlist->endstring && strncmp(s, esc, esclen) == 0) { meta_quoted = 1; s += esclen - 1; } else if (delim_quoted) { if (*s == closequote) delim_quoted = 0; } else /* (!delim_quoted) */ { if (*s == openquote) delim_quoted = 1; else if (*s == ' ') *s = '\0'; } #else if (*s == ' ') *s = '\0'; #endif } } public char * forw_textlist(struct textlist *tlist, char *prev) { char *s; /* * prev == NULL means return the first word in the list. * Otherwise, return the word after "prev". */ if (prev == NULL) s = tlist->string; else s = prev + strlen(prev); if (s >= tlist->endstring) return (NULL); while (*s == '\0') s++; if (s >= tlist->endstring) return (NULL); return (s); } public char * back_textlist(struct textlist *tlist, char *prev) { char *s; /* * prev == NULL means return the last word in the list. * Otherwise, return the word before "prev". */ if (prev == NULL) s = tlist->endstring; else if (prev <= tlist->string) return (NULL); else s = prev - 1; while (*s == '\0') s--; if (s <= tlist->string) return (NULL); while (s[-1] != '\0' && s > tlist->string) s--; return (s); } /* * Parse a single option setting in a modeline. */ static void modeline_option(char *str, int opt_len) { struct mloption { char *opt_name; void (*opt_func)(char*,int); }; struct mloption options[] = { { "ts=", set_tabs }, { "tabstop=", set_tabs }, { NULL, NULL } }; struct mloption *opt; for (opt = options; opt->opt_name != NULL; opt++) { int name_len = strlen(opt->opt_name); if (opt_len > name_len && strncmp(str, opt->opt_name, name_len) == 0) { (*opt->opt_func)(str + name_len, opt_len - name_len); break; } } } /* * String length, terminated by option separator (space or colon). * Space/colon can be escaped with backspace. */ static int modeline_option_len(char *str) { int esc = FALSE; char *s; for (s = str; *s != '\0'; s++) { if (esc) esc = FALSE; else if (*s == '\\') esc = TRUE; else if (*s == ' ' || *s == ':') /* separator */ break; } return (s - str); } /* * Parse colon- or space-separated option settings in a modeline. */ static void modeline_options(char *str, char end_char) { for (;;) { int opt_len; str = skipsp(str); if (*str == '\0' || *str == end_char) break; opt_len = modeline_option_len(str); modeline_option(str, opt_len); str += opt_len; if (*str != '\0') str += 1; /* skip past the separator */ } } /* * See if there is a modeline string in a line. */ static void check_modeline(char *line) { #if HAVE_STRSTR static char *pgms[] = { "less:", "vim:", "vi:", "ex:", NULL }; char **pgm; for (pgm = pgms; *pgm != NULL; ++pgm) { char *pline = line; for (;;) { char *str; pline = strstr(pline, *pgm); if (pline == NULL) /* pgm is not in this line */ break; str = skipsp(pline + strlen(*pgm)); if (pline == line || pline[-1] == ' ') { if (strncmp(str, "set ", 4) == 0) modeline_options(str+4, ':'); else if (pgm != &pgms[0]) /* "less:" requires "set" */ modeline_options(str, '\0'); break; } /* Continue searching the rest of the line. */ pline = str; } } #endif /* HAVE_STRSTR */ } /* * Read lines from start of file and check if any are modelines. */ static void check_modelines(void) { POSITION pos = ch_zero(); int i; for (i = 0; i < modelines; i++) { char *line; int line_len; if (ABORT_SIGS()) return; pos = forw_raw_line(pos, &line, &line_len); if (pos == NULL_POSITION) break; check_modeline(line); } } /* * Close a pipe opened via popen. */ static void close_pipe(FILE *pipefd) { int status; PARG parg; if (pipefd == NULL) return; #if OS2 /* * The pclose function of OS/2 emx sometimes fails. * Send SIGINT to the piped process before closing it. */ kill(pipefd->_pid, SIGINT); #endif status = pclose(pipefd); if (status == -1) { /* An internal error in 'less', not a preprocessor error. */ parg.p_string = errno_message("pclose"); error("%s", &parg); free(parg.p_string); return; } if (!show_preproc_error) return; #if defined WIFEXITED && defined WEXITSTATUS if (WIFEXITED(status)) { int s = WEXITSTATUS(status); if (s != 0) { parg.p_int = s; error("Input preprocessor failed (status %d)", &parg); } return; } #endif #if defined WIFSIGNALED && defined WTERMSIG && HAVE_STRSIGNAL if (WIFSIGNALED(status)) { int sig = WTERMSIG(status); if (sig != SIGPIPE || ch_length() != NULL_POSITION) { parg.p_string = signal_message(sig); error("Input preprocessor terminated: %s", &parg); } return; } #endif if (status != 0) { parg.p_int = status; error("Input preprocessor exited with status %x", &parg); } } /* * Drain and close an input pipe if needed. */ public void close_altpipe(IFILE ifile) { FILE *altpipe = get_altpipe(ifile); if (altpipe != NULL && !(ch_getflags() & CH_KEEPOPEN)) { close_pipe(altpipe); set_altpipe(ifile, NULL); } } /* * Check for error status from the current altpipe. * May or may not close the pipe. */ public void check_altpipe_error(void) { if (!show_preproc_error) return; if (curr_ifile != NULL_IFILE && get_altfilename(curr_ifile) != NULL) close_altpipe(curr_ifile); } /* * Close the current input file. */ static void close_file(void) { struct scrpos scrpos; char *altfilename; if (curr_ifile == NULL_IFILE) return; /* * Save the current position so that we can return to * the same position if we edit this file again. */ get_scrpos(&scrpos, TOP); if (scrpos.pos != NULL_POSITION) { store_pos(curr_ifile, &scrpos); lastmark(); } /* * Close the file descriptor, unless it is a pipe. */ ch_close(); /* * If we opened a file using an alternate name, * do special stuff to close it. */ altfilename = get_altfilename(curr_ifile); if (altfilename != NULL) { close_altpipe(curr_ifile); close_altfile(altfilename, get_filename(curr_ifile)); set_altfilename(curr_ifile, NULL); } curr_ifile = NULL_IFILE; #if HAVE_STAT_INO curr_ino = curr_dev = 0; #endif } /* * Edit a new file (given its name). * Filename == "-" means standard input. * Filename == NULL means just close the current file. */ public int edit(char *filename) { if (filename == NULL) return (edit_ifile(NULL_IFILE)); return (edit_ifile(get_ifile(filename, curr_ifile))); } /* * Clean up what edit_ifile did before error return. */ static int edit_error(char *filename, char *alt_filename, void *altpipe, IFILE ifile, IFILE was_curr_ifile) { if (alt_filename != NULL) { close_pipe(altpipe); close_altfile(alt_filename, filename); free(alt_filename); } del_ifile(ifile); free(filename); /* * Re-open the current file. */ if (was_curr_ifile == ifile) { /* * Whoops. The "current" ifile is the one we just deleted. * Just give up. */ quit(QUIT_ERROR); } reedit_ifile(was_curr_ifile); return (1); } /* * Edit a new file (given its IFILE). * ifile == NULL means just close the current file. */ public int edit_ifile(IFILE ifile) { int f; int answer; int chflags; char *filename; char *open_filename; char *alt_filename; void *altpipe; IFILE was_curr_ifile; PARG parg; if (ifile == curr_ifile) { /* * Already have the correct file open. */ return (0); } /* * We must close the currently open file now. * This is necessary to make the open_altfile/close_altfile pairs * nest properly (or rather to avoid nesting at all). * {{ Some stupid implementations of popen() mess up if you do: * fA = popen("A"); fB = popen("B"); pclose(fA); pclose(fB); }} */ #if LOGFILE end_logfile(); #endif was_curr_ifile = save_curr_ifile(); if (curr_ifile != NULL_IFILE) { chflags = ch_getflags(); close_file(); if ((chflags & CH_HELPFILE) && held_ifile(was_curr_ifile) <= 1) { /* * Don't keep the help file in the ifile list. */ del_ifile(was_curr_ifile); was_curr_ifile = old_ifile; } } if (ifile == NULL_IFILE) { /* * No new file to open. * (Don't set old_ifile, because if you call edit_ifile(NULL), * you're supposed to have saved curr_ifile yourself, * and you'll restore it if necessary.) */ unsave_ifile(was_curr_ifile); return (0); } filename = save(get_filename(ifile)); /* * See if LESSOPEN specifies an "alternate" file to open. */ altpipe = get_altpipe(ifile); if (altpipe != NULL) { /* * File is already open. * chflags and f are not used by ch_init if ifile has * filestate which should be the case if we're here. * Set them here to avoid uninitialized variable warnings. */ chflags = 0; f = -1; alt_filename = get_altfilename(ifile); open_filename = (alt_filename != NULL) ? alt_filename : filename; } else { if (strcmp(filename, FAKE_HELPFILE) == 0 || strcmp(filename, FAKE_EMPTYFILE) == 0) alt_filename = NULL; else alt_filename = open_altfile(filename, &f, &altpipe); open_filename = (alt_filename != NULL) ? alt_filename : filename; chflags = 0; if (altpipe != NULL) { /* * The alternate "file" is actually a pipe. * f has already been set to the file descriptor of the pipe * in the call to open_altfile above. * Keep the file descriptor open because it was opened * via popen(), and pclose() wants to close it. */ chflags |= CH_POPENED; if (strcmp(filename, "-") == 0) chflags |= CH_KEEPOPEN; } else if (strcmp(filename, "-") == 0) { /* * Use standard input. * Keep the file descriptor open because we can't reopen it. */ f = fd0; chflags |= CH_KEEPOPEN; /* * Must switch stdin to BINARY mode. */ SET_BINARY(f); #if MSDOS_COMPILER==DJGPPC /* * Setting stdin to binary by default causes * Ctrl-C to not raise SIGINT. We must undo * that side-effect. */ __djgpp_set_ctrl_c(1); #endif } else if (strcmp(open_filename, FAKE_EMPTYFILE) == 0) { f = -1; chflags |= CH_NODATA; } else if (strcmp(open_filename, FAKE_HELPFILE) == 0) { f = -1; chflags |= CH_HELPFILE; } else if ((parg.p_string = bad_file(open_filename)) != NULL) { /* * It looks like a bad file. Don't try to open it. */ error("%s", &parg); free(parg.p_string); return edit_error(filename, alt_filename, altpipe, ifile, was_curr_ifile); } else if ((f = open(open_filename, OPEN_READ)) < 0) { /* * Got an error trying to open it. */ parg.p_string = errno_message(filename); error("%s", &parg); free(parg.p_string); return edit_error(filename, alt_filename, altpipe, ifile, was_curr_ifile); } else { chflags |= CH_CANSEEK; if (!force_open && !opened(ifile) && bin_file(f)) { /* * Looks like a binary file. * Ask user if we should proceed. */ parg.p_string = filename; answer = query("\"%s\" may be a binary file. See it anyway? ", &parg); if (answer != 'y' && answer != 'Y') { close(f); return edit_error(filename, alt_filename, altpipe, ifile, was_curr_ifile); } } } } if (!force_open && f >= 0 && isatty(f)) { PARG parg; parg.p_string = filename; error("%s is a terminal (use -f to open it)", &parg); return edit_error(filename, alt_filename, altpipe, ifile, was_curr_ifile); } /* * Get the new ifile. * Get the saved position for the file. */ if (was_curr_ifile != NULL_IFILE) { old_ifile = was_curr_ifile; unsave_ifile(was_curr_ifile); } curr_ifile = ifile; set_altfilename(curr_ifile, alt_filename); set_altpipe(curr_ifile, altpipe); set_open(curr_ifile); /* File has been opened */ get_pos(curr_ifile, &initial_scrpos); new_file = TRUE; ch_init(f, chflags); consecutive_nulls = 0; check_modelines(); if (!(chflags & CH_HELPFILE)) { #if LOGFILE if (namelogfile != NULL && is_tty) use_logfile(namelogfile); #endif #if HAVE_STAT_INO /* Remember the i-number and device of the opened file. */ if (strcmp(open_filename, "-") != 0) { struct stat statbuf; int r = stat(open_filename, &statbuf); if (r == 0) { curr_ino = statbuf.st_ino; curr_dev = statbuf.st_dev; } } #endif if (every_first_cmd != NULL) { ungetsc(every_first_cmd); ungetcc_back(CHAR_END_COMMAND); } } flush(); if (is_tty) { /* * Output is to a real tty. */ /* * Indicate there is nothing displayed yet. */ pos_clear(); clr_linenum(); #if HILITE_SEARCH clr_hilite(); #endif hshift = 0; if (strcmp(filename, FAKE_HELPFILE) && strcmp(filename, FAKE_EMPTYFILE)) { char *qfilename = shell_quote(filename); cmd_addhist(ml_examine, qfilename, 1); free(qfilename); } if (want_filesize) scan_eof(); } free(filename); return (0); } /* * Edit a space-separated list of files. * For each filename in the list, enter it into the ifile list. * Then edit the first one. */ public int edit_list(char *filelist) { IFILE save_ifile; char *good_filename; char *filename; char *gfilelist; char *gfilename; char *qfilename; struct textlist tl_files; struct textlist tl_gfiles; save_ifile = save_curr_ifile(); good_filename = NULL; /* * Run thru each filename in the list. * Try to glob the filename. * If it doesn't expand, just try to open the filename. * If it does expand, try to open each name in that list. */ init_textlist(&tl_files, filelist); filename = NULL; while ((filename = forw_textlist(&tl_files, filename)) != NULL) { gfilelist = lglob(filename); init_textlist(&tl_gfiles, gfilelist); gfilename = NULL; while ((gfilename = forw_textlist(&tl_gfiles, gfilename)) != NULL) { qfilename = shell_unquote(gfilename); if (edit(qfilename) == 0 && good_filename == NULL) good_filename = get_filename(curr_ifile); free(qfilename); } free(gfilelist); } /* * Edit the first valid filename in the list. */ if (good_filename == NULL) { unsave_ifile(save_ifile); return (1); } if (get_ifile(good_filename, curr_ifile) == curr_ifile) { /* * Trying to edit the current file; don't reopen it. */ unsave_ifile(save_ifile); return (0); } reedit_ifile(save_ifile); return (edit(good_filename)); } /* * Edit the first file in the command line (ifile) list. */ public int edit_first(void) { if (nifile() == 0) return (edit_stdin()); curr_ifile = NULL_IFILE; return (edit_next(1)); } /* * Edit the last file in the command line (ifile) list. */ public int edit_last(void) { curr_ifile = NULL_IFILE; return (edit_prev(1)); } /* * Edit the n-th next or previous file in the command line (ifile) list. */ static int edit_istep(IFILE h, int n, int dir) { IFILE next; /* * Skip n filenames, then try to edit each filename. */ for (;;) { next = (dir > 0) ? next_ifile(h) : prev_ifile(h); if (--n < 0) { if (edit_ifile(h) == 0) break; } if (next == NULL_IFILE) { /* * Reached end of the ifile list. */ return (1); } if (ABORT_SIGS()) { /* * Interrupt breaks out, if we're in a long * list of files that can't be opened. */ return (1); } h = next; } /* * Found a file that we can edit. */ return (0); } static int edit_inext(IFILE h, int n) { return (edit_istep(h, n, +1)); } public int edit_next(int n) { return edit_istep(curr_ifile, n, +1); } static int edit_iprev(IFILE h, int n) { return (edit_istep(h, n, -1)); } public int edit_prev(int n) { return edit_istep(curr_ifile, n, -1); } /* * Edit a specific file in the command line (ifile) list. */ public int edit_index(int n) { IFILE h; h = NULL_IFILE; do { if ((h = next_ifile(h)) == NULL_IFILE) { /* * Reached end of the list without finding it. */ return (1); } } while (get_index(h) != n); return (edit_ifile(h)); } public IFILE save_curr_ifile(void) { if (curr_ifile != NULL_IFILE) hold_ifile(curr_ifile, 1); return (curr_ifile); } public void unsave_ifile(IFILE save_ifile) { if (save_ifile != NULL_IFILE) hold_ifile(save_ifile, -1); } /* * Reedit the ifile which was previously open. */ public void reedit_ifile(IFILE save_ifile) { IFILE next; IFILE prev; /* * Try to reopen the ifile. * Note that opening it may fail (maybe the file was removed), * in which case the ifile will be deleted from the list. * So save the next and prev ifiles first. */ unsave_ifile(save_ifile); next = next_ifile(save_ifile); prev = prev_ifile(save_ifile); if (edit_ifile(save_ifile) == 0) return; /* * If can't reopen it, open the next input file in the list. */ if (next != NULL_IFILE && edit_inext(next, 0) == 0) return; /* * If can't open THAT one, open the previous input file in the list. */ if (prev != NULL_IFILE && edit_iprev(prev, 0) == 0) return; /* * If can't even open that, we're stuck. Just quit. */ quit(QUIT_ERROR); } public void reopen_curr_ifile(void) { IFILE save_ifile = save_curr_ifile(); close_file(); reedit_ifile(save_ifile); } /* * Edit standard input. */ public int edit_stdin(void) { if (isatty(fd0)) { error("Missing filename (\"less --help\" for help)", NULL_PARG); quit(QUIT_OK); } return (edit("-")); } /* * Copy a file directly to standard output. * Used if standard output is not a tty. */ public void cat_file(void) { int c; while ((c = ch_forw_get()) != EOI) putchr(c); flush(); } #if LOGFILE #define OVERWRITE_OPTIONS "Overwrite, Append, Don't log, or Quit?" /* * If the user asked for a log file and our input file * is standard input, create the log file. * We take care not to blindly overwrite an existing file. */ public void use_logfile(char *filename) { int exists; int answer; PARG parg; if (ch_getflags() & CH_CANSEEK) /* * Can't currently use a log file on a file that can seek. */ return; /* * {{ We could use access() here. }} */ exists = open(filename, OPEN_READ); if (exists >= 0) close(exists); exists = (exists >= 0); /* * Decide whether to overwrite the log file or append to it. * If it doesn't exist we "overwrite" it. */ if (!exists || force_logfile) { /* * Overwrite (or create) the log file. */ answer = 'O'; } else { /* * Ask user what to do. */ parg.p_string = filename; answer = query("Warning: \"%s\" exists; "OVERWRITE_OPTIONS" ", &parg); } loop: switch (answer) { case 'O': case 'o': /* * Overwrite: create the file. */ logfile = creat(filename, CREAT_RW); break; case 'A': case 'a': /* * Append: open the file and seek to the end. */ logfile = open(filename, OPEN_APPEND); if (lseek(logfile, (off_t)0, SEEK_END) == BAD_LSEEK) { close(logfile); logfile = -1; } break; case 'D': case 'd': /* * Don't do anything. */ return; default: /* * Eh? */ answer = query(OVERWRITE_OPTIONS" (Type \"O\", \"A\", \"D\" or \"Q\") ", NULL_PARG); goto loop; } if (logfile < 0) { /* * Error in opening logfile. */ parg.p_string = filename; error("Cannot write to \"%s\"", &parg); return; } SET_BINARY(logfile); } #endif