/* Yash: yet another shell */
/* history.c: command history management */
/* (C) 2007-2013 magicant */

/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.  */


#include "common.h"
#include "history.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#if HAVE_GETTEXT
# include <libintl.h>
#endif
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>
#include "builtin.h"
#include "exec.h"
#include "job.h"
#include "option.h"
#include "path.h"
#include "redir.h"
#include "sig.h"
#include "strbuf.h"
#include "util.h"
#include "variable.h"
#include "yash.h"


/* The maximum size of history list (<= INT_MAX / 10) */
#ifndef MAX_HISTSIZE
#define MAX_HISTSIZE     1000000
#endif
#if MAX_HISTSIZE > INT_MAX / 10
#error MAX_HISTSIZE is too large
#endif
/* The minimum value of `max_number',
 * which must be at least 32768 according to POSIX. */
/* Must be a power of 10. */
#ifndef HISTORY_MIN_MAX_NUMBER
#define HISTORY_MIN_MAX_NUMBER   100000
#endif
#if HISTORY_MIN_MAX_NUMBER < 32768
#error HISTORY_MIN_MAX_NUMBER is too small
#endif
/* The default size of the history list,
 * which must be at least 128 according to POSIX. */
#ifndef DEFAULT_HISTSIZE
#define DEFAULT_HISTSIZE 500
#endif
#if DEFAULT_HISTSIZE < 128
#error DEFAULT_HISTSIZE is too small
#endif
#if DEFAULT_HISTSIZE > HISTORY_MIN_MAX_NUMBER
#error DEFAULT_HISTSIZE cannot be larger than HISTORY_MIN_MAX_NUMBER
#endif


/* The main history list. */
histlist_T histlist = {
    .link = { Histlist, Histlist, },
    .count = 0,
};
/* History entries are stored in this doubly-linked list.
 * The `Newest' (`link.prev') member points to the newest entry and the `Oldest'
 * (`link.next') member points to the oldest entry. When there's no entries,
 * `Newest' and `Oldest' point to `histlist' itself. */

/* The maximum limit of the number of an entry.
 * Must always be no less than `histsize' or `HISTORY_MIN_MAX_NUMBER'.
 * The number of any entry is not greater than this value. */
static unsigned max_number = HISTORY_MIN_MAX_NUMBER;
/* The size limit of the history list. */
static unsigned histsize = DEFAULT_HISTSIZE;
/* When a new entry is added, if there are entries that has the same value as
 * the new entry in the `histrmdup' newest entries, those entries are removed.*/
static unsigned histrmdup = 0;

/* File stream for the history file. */
static FILE *histfile = NULL;
/* The revision number of the history file. A valid revision number is
 * non-negative. */
static long histfilerev = -1;
/* The number of lines in the history file, not including the signature.
 * When this number reaches the threshold, the history file is refreshed. */
static size_t histfilelines = 0;
/* Indicates if the history file should be flushed before it is unlocked. */
static bool histneedflush = false;

/* The current time returned by `time' */
static time_t now = (time_t) -1;

/* If true, the history is locked, that is, readonly. */
static bool hist_lock = false;


struct search_result_T {
    histlink_T *prev, *next;
};

static void update_time(void);
static void set_histsize(unsigned newsize);
static histentry_T *new_entry(unsigned number, time_t time, const char *line)
    __attribute__((nonnull));
static bool need_remove_entry(unsigned number)
    __attribute__((pure));
static void remove_entry(histentry_T *e)
    __attribute__((nonnull));
static void remove_last_entry(void);
static void clear_all_entries(void);
static struct search_result_T search_entry_by_number(unsigned number)
    __attribute__((pure));
static histlink_T *get_nth_newest_entry(unsigned n)
    __attribute__((pure));
static histlink_T *search_entry_by_prefix(const char *prefix)
    __attribute__((nonnull,pure));
static bool search_result_is_newer(
	struct search_result_T sr1, struct search_result_T sr2)
    __attribute__((pure));
static bool entry_is_newer(const histentry_T *e1, const histentry_T *e2)
    __attribute__((nonnull,pure));

static void add_histfile_pid(pid_t pid);
static void remove_histfile_pid(pid_t pid);
static void clear_histfile_pids(void);
static void write_histfile_pids(void);

static FILE *open_histfile(void);
static bool lock_histfile(short type);
static bool read_line(FILE *restrict f, xwcsbuf_T *restrict buf)
    __attribute__((nonnull));
static long read_signature(void);
static void read_history_raw(void);
static void read_history(void);
static void parse_history_entry(const wchar_t *line)
    __attribute__((nonnull));
static void parse_removed_entry(const wchar_t *numstr)
    __attribute__((nonnull));
static void parse_process_id(const wchar_t *numstr)
    __attribute__((nonnull));
static void update_history(bool refresh);
static void maybe_refresh_file(void);
static int wprintf_histfile(const wchar_t *format, ...)
    __attribute__((nonnull));
static void write_signature(void);
static void write_history_entry(const histentry_T *entry)
    __attribute__((nonnull));
static void refresh_file(void);

static void add_history_line(const wchar_t *line, size_t maxlen)
    __attribute__((nonnull));
static void remove_duplicates(const char *line)
    __attribute__((nonnull));


/* Updates the value of `now'. */
void update_time(void)
{
    now = time(NULL);
}

/* Changes `histsize' to `newsize' and accordingly sets `max_number'. */
/* `histsize' is set only once in initialization. */
void set_histsize(unsigned newsize)
{
    if (newsize > MAX_HISTSIZE)
	newsize = MAX_HISTSIZE;
    if (newsize == 0)
	newsize = 1;
    histsize = newsize;

    max_number = HISTORY_MIN_MAX_NUMBER;
    while (max_number < 2 * histsize)
	max_number *= 10;
    /* `max_number' is the smallest power of 10 that is not less than
     * 2 * histsize. */
}

/* Adds a new history entry to the end of `histlist'.
 * Some oldest entries may be removed in this function if they conflict with the
 * new one or the list is full. */
histentry_T *new_entry(unsigned number, time_t time, const char *line)
{
    assert(!hist_lock);
    assert(number > 0);
    assert(number <= max_number);

    while (need_remove_entry(number))
	remove_entry(ashistentry(histlist.Oldest));

    histentry_T *new = xmallocs(sizeof *new,
	    strlen(line) + 1, sizeof *new->value);
    new->Prev = histlist.Newest;
    new->Next = Histlist;
    histlist.Newest = new->Prev->next = &new->link;
    new->number = number;
    new->time = time;
    strcpy(new->value, line);

    histlist.count++;
    assert(histlist.count <= histsize);

    return new;
}

bool need_remove_entry(unsigned number)
{
    if (histlist.count == 0)
	return false;
    if (histlist.count >= histsize)
	return true;

    unsigned oldest = ashistentry(histlist.Oldest)->number;
    unsigned newest = ashistentry(histlist.Newest)->number;
    if (oldest <= newest)
	return oldest <= number && number <= newest;
    else
	return oldest <= number || number <= newest;
}

/* Removes the specified entry from `histlist'. */
void remove_entry(histentry_T *entry)
{
    assert(!hist_lock);
    assert(&entry->link != Histlist);
    entry->Prev->next = entry->Next;
    entry->Next->prev = entry->Prev;
    histlist.count--;
    free(entry);
}

/* Removes the newest entry. */
void remove_last_entry(void)
{
    if (histlist.count > 0)
	remove_entry(ashistentry(histlist.Newest));
}

/* Renumbers all the entries in `histlist', starting from 1. */
void renumber_all_entries(void)
{
    assert(!hist_lock);

    if (histlist.count > 0) {
	unsigned num = 0;
	for (histlink_T *l = histlist.Oldest; l != Histlist; l = l->next)
	    ashistentry(l)->number = ++num;
	assert(num == histlist.count);
    }
}

/* Removes all entries in the history list. */
void clear_all_entries(void)
{
    assert(!hist_lock);

    histlink_T *l = histlist.Oldest;
    while (l != Histlist) {
	histlink_T *next = l->next;
	free(ashistentry(l));
	l = next;
    }
    histlist.Oldest = histlist.Newest = Histlist;
    histlist.count = 0;
}

/* Searches for the entry that has the specified `number'.
 * If there is such an entry in the history, the both members of the returned
 * `search_result_T' structure will be pointers to the entry. Otherwise, the
 * returned structure will contain pointers to the nearest neighbor entries.
 * (If there is not the neighbor in either direction, the pointer will be
 * `Histlist'.) */
struct search_result_T search_entry_by_number(unsigned number)
{
    struct search_result_T result;

    if (histlist.count == 0) {
	result.prev = result.next = Histlist;
	return result;
    }

    unsigned oldestnum = ashistentry(histlist.Oldest)->number;
    unsigned nnewestnum = ashistentry(histlist.Newest)->number;
    unsigned nnumber = number;
    if (nnewestnum < oldestnum) {
	if (nnumber <= nnewestnum)
	    nnumber += max_number;
	nnewestnum += max_number;
    }
    if (nnumber < oldestnum) {
	result.prev = Histlist;
	result.next = histlist.Oldest;
	return result;
    } else if (nnumber > nnewestnum) {
	result.prev = histlist.Newest;
	result.next = Histlist;
	return result;
    }

    histlink_T *l;
    if (2 * (nnumber - oldestnum) < nnewestnum - oldestnum) {
	/* search from the oldest */
	l = histlist.Oldest;
	while (number < ashistentry(l)->number)
	    l = l->next;
	while (number > ashistentry(l)->number)
	    l = l->next;
	result.next = l;
	if (number != ashistentry(l)->number)
	    l = l->prev;
	result.prev = l;
    } else {
	/* search from the newest */
	l = histlist.Newest;
	while (number > ashistentry(l)->number)
	    l = l->prev;
	while (number < ashistentry(l)->number)
	    l = l->prev;
	result.prev = l;
	if (number != ashistentry(l)->number)
	    l = l->next;
	result.next = l;
    }
    return result;
}

/* Returns the nth newest entry (or the oldest entry if `n' is too big).
 * Returns `Histlist' if `n' is zero or the history is empty. */
histlink_T *get_nth_newest_entry(unsigned n)
{
    if (histlist.count <= n)
	return histlist.Oldest;

    histlink_T *l = Histlist;
    while (n-- > 0)
	l = l->prev;
    return l;
}

/* Searches for the newest entry whose value begins with the specified `prefix'.
 * Returns `Histlist' if not found. */
histlink_T *search_entry_by_prefix(const char *prefix)
{
    histlink_T *l;
    for (l = histlist.Newest; l != Histlist; l = l->prev)
	if (matchstrprefix(ashistentry(l)->value, prefix) != NULL)
	    break;
    return l;
}

/* Returns true iff `sr1' is newer than `sr2'.
 * For each search_result_T structure, the `prev' and `next' members must not be
 * both `Histlist'. */
bool search_result_is_newer(
	struct search_result_T sr1, struct search_result_T sr2)
{
    if (sr1.prev == Histlist || sr2.next == Histlist)
	return false;
    if (sr1.next == Histlist || sr2.prev == Histlist)
	return true;

    return entry_is_newer(ashistentry(sr1.prev), ashistentry(sr2.prev))
	|| entry_is_newer(ashistentry(sr1.next), ashistentry(sr2.next));
}

/* Returns true iff `e1' is newer than `e2'. */
bool entry_is_newer(const histentry_T *e1, const histentry_T *e2)
{
    assert(histlist.count > 0);

    unsigned n1 = e1->number;
    unsigned n2 = e2->number;
    unsigned newest = ashistentry(histlist.Newest)->number;
    unsigned oldest = ashistentry(histlist.Oldest)->number;

    return (n1 <= newest && newest < oldest && oldest <= n2)
	|| (n2 <= n1 && (oldest <= n2 || n1 <= newest));
}


/********** Process ID list **********/

struct pidlist_T {
    size_t count;
    pid_t *pids;
};

/* The process IDs of processes that share the history file.
 * The `pids' member may be null when the `count' member is zero. */
static struct pidlist_T histfilepids = { 0, NULL, };

/* Adds `pid' to `histfilepids'. `pid' must be positive. */
void add_histfile_pid(pid_t pid)
{
    assert(pid > 0);

    for (size_t i = 0; i < histfilepids.count; i++)
	if (histfilepids.pids[i] == pid)
	    return;  /* don't add if already added */

    histfilepids.pids = xreallocn(histfilepids.pids,
	    histfilepids.count + 1, sizeof *histfilepids.pids);
    histfilepids.pids[histfilepids.count++] = pid;
}

/* If `pid' is non-zero, removes `pid' from `histfilepids'.
 * If `pid' is zero, removes process IDs of non-existent processes from
 * `histfilepids'. */
void remove_histfile_pid(pid_t pid)
{
    for (size_t i = 0; i < histfilepids.count; ) {
	if (pid != 0 ? histfilepids.pids[i] == pid
	             : !process_exists(histfilepids.pids[i])) {
	    memmove(&histfilepids.pids[i], &histfilepids.pids[i + 1],
		    (histfilepids.count - i - 1) * sizeof *histfilepids.pids);
	    histfilepids.count--;
	    histfilepids.pids = xreallocn(histfilepids.pids,
		    histfilepids.count, sizeof *histfilepids.pids);
	} else {
	    i++;
	}
    }
}

/* Clears `histfilepids'. */
void clear_histfile_pids(void)
{
    free(histfilepids.pids);
    histfilepids = (struct pidlist_T) { 0, NULL, };
}

/* Writes process IDs in `histfilepids' to the history file. */
/* This function does not return any error status. The caller should check
 * `ferror' for the file. */
void write_histfile_pids(void)
{
    assert(histfile != NULL);
    for (size_t i = 0; i < histfilepids.count; i++)
	wprintf_histfile(L"p%jd\n", (intmax_t) histfilepids.pids[i]);
    histfilelines += histfilepids.count;
}


/********** History file functions **********/

/***** FORMAT OF THE HISTORY FILE *****
 *
 * The first line of the history file has the following form:
 *    #$# yash history v0 rXXX
 * where `XXX' is the revision number of the file. The revision number is
 * incremented each time the file is refreshed.
 * 
 * The rest of the file consists of lines containing history data, each entry
 * per line. The type of entry is determined by the first character of the line:
 *    0-9 or A-F    history entry
 *    c             history entry cancellation
 *    d             history entry deletion
 *    p             shell process addition/elimination info
 *    others        ignored line
 *
 * A history entry has the following form:
 *    NNN:TTT COMMAND
 * where `NNN' is the entry number, `TTT' is the time of the command, and
 * `COMMAND' is the command. Both `NNN' and `TTT' are uppercase hexadecimal
 * non-negative numbers. `COMMAND' may contain any characters except newline and
 * null.
 *
 * A history entry cancellation consists of a single character `c'. It cancels
 * the previous history entry.
 *
 * A history entry deletion has the form:
 *    dNNN
 * where `NNN' is the number of the entry to be removed (hexadecimal).
 *
 * A shell process addition/elimination is in the form:
 *    pXXX
 * where `XXX' is the process id (decimal integer). For addition `XXX' is
 * positive and for elimination `XXX' is negative.
 */

/* Opens the history file.
 * Returns NULL on failure. */
FILE *open_histfile(void)
{
    const wchar_t *vhistfile = getvar(L VAR_HISTFILE);
    if (vhistfile == NULL)
	return NULL;

    char *mbshistfile = malloc_wcstombs(vhistfile);
    if (mbshistfile == NULL)
	return NULL;

    int fd = open(mbshistfile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    free(mbshistfile);
    if (fd < 0)
	return NULL;

    struct stat st;
    if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)
	    || (st.st_mode & (S_IRWXG | S_IRWXO))) {
	xclose(fd);
	return NULL;
    }

    fd = move_to_shellfd(fd);
    if (fd < 0)
	return NULL;

    FILE *f = fdopen(fd, "r+");
    if (f == NULL) {
	remove_shellfd(fd);
	xclose(fd);
    }
    return f;
}

/* Locks the history file, which must have been open.
 * `type' must be one of `F_RDLCK', `F_WRLCK' and `F_UNLCK'.
 * If `type' is `F_UNLCK', the buffer for the history file is flushed before
 * unlocking the file.
 * When another process is holding a lock for the file, this process will be
 * blocked until the lock is freed.
 * Returns true iff successful. */
bool lock_histfile(short type)
{
    if (type == F_UNLCK && histneedflush) {
	histneedflush = false;
	fflush(histfile);
	/* We only flush the history file after writing. POSIX doesn't define
	 * the behavior of flush without writing. We don't use fseek instead of
	 * fflush because fseek is less reliable than fflush. In some
	 * implementations (including glibc), fseek doesn't flush the file. */
    }

    struct flock flock = {
	.l_type   = type,
	.l_whence = SEEK_SET,
	.l_start  = 0,
	.l_len    = 0, /* to the end of file */
    };
    int fd = fileno(histfile);
    int result;

    while ((result = fcntl(fd, F_SETLKW, &flock)) == -1 && errno == EINTR);
    return result != -1;
}

/* Reads one line from file `f'.
 * The line is appended to buffer `buf', which must have been initialized.
 * The terminating newline is not left in `buf'.
 * On failure, false is returned, in which case the contents of `buf' is
 * unspecified.
 * If there is no more line in the file, false is returned. */
bool read_line(FILE *restrict f, xwcsbuf_T *restrict buf)
{
#if FGETWS_BROKEN
    wb_ensuremax(buf, LINE_MAX);
#endif
    while (fgetws(&buf->contents[buf->length],
		buf->maxlength - buf->length + 1, f)) {
	size_t len = wcslen(&buf->contents[buf->length]);
	if (len == 0)
	    return false;
	buf->length += len;
	if (buf->contents[buf->length - 1] == L'\n') {
	    wb_truncate(buf, buf->length - 1);
	    return true;
	}
	if (buf->length > LINE_MAX)
	    return false;  /* Too long line. Give up. */
	wb_ensuremax(buf, buf->length + 80);
    }
    return false;
}

/* Reads the signature of the history file (`histfile') and checks if it is a
 * valid signature.
 * If valid:
 *   - the file is positioned just after the signature,
 *   - the return value is the revision of the file (non-negative).
 * Otherwise:
 *   - the file position is undefined,
 *   - the return value is negative. */
/* The history file should be locked. */
long read_signature(void)
{
    xwcsbuf_T buf;
    long rev = -1;
    const wchar_t *s;

    assert(histfile != NULL);
    rewind(histfile);
    if (!read_line(histfile, wb_init(&buf)))
	goto end;
    
    s = matchwcsprefix(buf.contents, L"#$# yash history v0 r");
    if (s == NULL || !iswdigit(s[0]))
	goto end;

    if (!xwcstol(s, 10, &rev))
	rev = -1;
end:
    wb_destroy(&buf);
    return rev;
}

/* Reads history entries from the history file, which must have been open.
 * The file format is assumed a simple text, one entry per line.
 * The file is read from the current position.
 * The entries that were read from the file are appended to `histlist'. */
/* The file should be locked. */
/* This function does not return any error status. The caller should check
 * `ferror' and/or `feof' for the file. */
void read_history_raw(void)
{
    xwcsbuf_T buf;

    assert(histfile != NULL);
    wb_init(&buf);
    while (read_line(histfile, &buf)) {
	char *line = malloc_wcstombs(buf.contents);
	if (line != NULL) {
	    new_entry(next_history_number(), -1, line);
	    free(line);
	}
	wb_clear(&buf);
    }
    wb_destroy(&buf);
}

/* Reads history entries from the history file.
 * The file is read from the current position.
 * The entries that were read from the file are appended to `histlist'.
 * `update_time' must be called before calling this function. */
/* The file should be locked. */
/* This function does not return any error status. The caller should check
 * `ferror' and/or `feof' for the file. */
void read_history(void)
{
    xwcsbuf_T buf;

    assert(histfile != NULL);
    wb_init(&buf);
    while (read_line(histfile, &buf)) {
	histfilelines++;
	switch (buf.contents[0]) {
	    case L'0': case L'1': case L'2': case L'3': case L'4':
	    case L'5': case L'6': case L'7': case L'8': case L'9':
	    case L'A': case L'B': case L'C': case L'D': case L'E': case L'F':
		parse_history_entry(buf.contents);
		break;
	    case L'c':
		remove_last_entry();
		break;
	    case L'd':
		parse_removed_entry(buf.contents + 1);
		break;
	    case L'p':
		parse_process_id(buf.contents + 1);
		break;
	}
	wb_clear(&buf);
    }
    wb_destroy(&buf);
}

void parse_history_entry(const wchar_t *line)
{
    unsigned long num;
    time_t time;
    wchar_t *end;
    char *value;

    assert(iswxdigit(line[0]));

    errno = 0;
    num = wcstoul(line, &end, 0x10);
    if (errno || end[0] == L'\0' || num > max_number)
	return;

    if (end[0] == L':' && iswxdigit(end[1])) {
	unsigned long long t;

	errno = 0;
	t = wcstoull(&end[1], &end, 0x10);
	if (errno || end[0] == L'\0')
	    time = -1;
	else if (t > (unsigned long long) now)
	    time = now;
	else
	    time = (time_t) t;
    } else {
	time = -1;
    }

    if (!iswspace(end[0]))
	return;
    value = malloc_wcstombs(&end[1]);
    if (value != NULL) {
	new_entry((unsigned) num, time, value);
	free(value);
    }
}

void parse_removed_entry(const wchar_t *numstr)
{
    unsigned long num;
    wchar_t *end;

    if (histlist.count == 0)
	return;
    if (numstr[0] == L'\0')
	return;

    errno = 0;
    num = wcstoul(numstr, &end, 0x10);
    if (errno || (*end != L'\0' && !iswspace(*end)))
	return;
    if (num > max_number)
	return;

    struct search_result_T sr = search_entry_by_number((unsigned) num);
    if (sr.prev == sr.next)
	remove_entry(ashistentry(sr.prev));
}

void parse_process_id(const wchar_t *numstr)
{
    intmax_t num;
    wchar_t *end;

    if (numstr[0] == L'\0')
	return;

    errno = 0;
    num = wcstoimax(numstr, &end, 10);
    if (errno || (*end != L'\0' && !iswspace(*end)))
	return;
    if (num > 0)
	add_histfile_pid((pid_t) num);
    else if (num < 0)
	remove_histfile_pid((pid_t) -num);
    /* XXX: this cast and negation may be unsafe */
}

/* Re-reads history from the history file.
 * Changes that have been made to the file by other shell processes are brought
 * into this shell's history. The current data in this shell's history may be
 * changed.
 * If `refresh' is true, this function may call `refresh_file'.
 * On failure, `histfile' is closed and set to NULL.
 * `update_time' must be called before calling this function.
 * After calling this function, `histfile' must not be read without
 * repositioning. */
/* The history file should be locked (F_WRLCK if `refresh' is true or F_RDLCK if
 * `refresh' is false).
 * This function must be called just before writing to the history file. */
void update_history(bool refresh)
{
    bool posfail;
    fpos_t pos;
    long rev;

    if (histfile == NULL)
	return;
    assert(!hist_lock);

#if WIO_BROKEN
    posfail = true;
#else
    posfail = fgetpos(histfile, &pos);
#endif
    rev = read_signature();
    if (rev < 0)
	goto error;
    if (!posfail && rev == histfilerev) {
	/* The revision has not been changed. Just read new entries. */
	fsetpos(histfile, &pos);
	read_history();
    } else {
	/* The revision has been changed. Re-read everything. */
	clear_all_entries();
	clear_histfile_pids();
	add_histfile_pid(shell_pid);
	histfilerev = rev;
	histfilelines = 0;
	read_history();
    }
    if (ferror(histfile) || !feof(histfile))
	goto error;

    if (refresh)
	maybe_refresh_file();
    return;

error:
    close_history_file();
}

/* Refreshes the history file if it is time to do that.
 * `histfile' must not be NULL. */
void maybe_refresh_file(void)
{
    assert(histfile != NULL);
    if (histfilelines > 20
	    && histfilelines / 2 >= histlist.count + histfilepids.count) {
	remove_histfile_pid(0);
	refresh_file();
    }
}

/* Like `fwprintf(histfile, format, ...)', but the `histneedflush' flag is set.
 */
int wprintf_histfile(const wchar_t *format, ...)
{
    va_list ap;
    int result;

    histneedflush = true;
    va_start(ap, format);
    result = vfwprintf(histfile, format, ap);
    va_end(ap);
    return result;
}

/* Writes the signature with an incremented revision number, after emptying the
 * file. */
/* This function does not return any error status. The caller should check
 * `ferror' for the file. */
void write_signature(void)
{
    assert(histfile != NULL);
    rewind(histfile);
    while (ftruncate(fileno(histfile), 0) < 0 && errno == EINTR);

    if (histfilerev < 0 || histfilerev == LONG_MAX)
	histfilerev = 0;
    else
	histfilerev++;
    wprintf_histfile(L"#$# yash history v0 r%ld\n", histfilerev);
    histfilelines = 0;
}

/* Writes the specified entry to the history file. */
/* The file should be locked. */
/* This function does not return any error status. The caller should check
 * `ferror' for the file. */
void write_history_entry(const histentry_T *entry)
{
    assert(histfile != NULL);
    if (entry->time >= 0)
	wprintf_histfile(L"%X:%lX %s\n",
		entry->number, (unsigned long) entry->time, entry->value);
    else
	wprintf_histfile(L"%X %s\n",
		entry->number, entry->value);
    histfilelines++;
}

/* Clears and rewrites the contents of the history file.
 * The file will have a new revision number. */
/* The file should be locked. */
/* This function does not return any error status. The caller should check
 * `ferror' for the file. */
void refresh_file(void)
{
    write_signature();
    write_histfile_pids();
    for (const histlink_T *l = histlist.Oldest; l != Histlist; l = l->next)
	write_history_entry(ashistentry(l));
}


/********** External functions **********/

/* Initializes history function if not yet initialized.
 * If the shell is not interactive, history is never initialized. */
void maybe_init_history(void)
{
    static bool initialized = false;

    if (!is_interactive_now || initialized)
	return;
    initialized = true;

    /* set `histsize' */
    const wchar_t *vhistsize = getvar(L VAR_HISTSIZE);
    if (vhistsize != NULL && vhistsize[0] != L'\0') {
	unsigned long size;
	if (xwcstoul(vhistsize, 10, &size))
	    set_histsize(size);
    }

    /* set `histrmdup' */
    const wchar_t *vhistrmdup = getvar(L VAR_HISTRMDUP);
    if (vhistrmdup != NULL && vhistrmdup[0] != L'\0') {
	unsigned long rmdup;
	if (xwcstoul(vhistrmdup, 10, &rmdup))
	    histrmdup = (rmdup <= histsize) ? rmdup : histsize;
    }

    update_time();

    /* open the history file and read it */
    histfile = open_histfile();
    if (histfile != NULL) {
	lock_histfile(F_WRLCK);
	histfilerev = read_signature();
	if (histfilerev < 0) {
	    rewind(histfile);
	    read_history_raw();
	    goto refresh;
	}
	read_history();
	if (ferror(histfile) || !feof(histfile)) {
	    close_history_file();
	    return;
	}

	remove_histfile_pid(0);
	if (histfilepids.count == 0) {
	    renumber_all_entries();
refresh:
	    refresh_file();
	} else {
	    maybe_refresh_file();
	}

	add_histfile_pid(shell_pid);
	wprintf_histfile(L"p%jd\n", (intmax_t) shell_pid);
	histfilelines++;

	lock_histfile(F_UNLCK);
    }
}

/* Closes the history file after writing info on the shell process that is
 * exiting. */
void finalize_history(void)
{
    if (!is_interactive_now || histfile == NULL)
	return;

    hist_lock = false;
    lock_histfile(F_WRLCK);
    update_time();
    update_history(true);
    if (histfile != NULL) {
	wprintf_histfile(L"p%jd\n", (intmax_t) -shell_pid);
	// histfilelines++;
	close_history_file();
    }
}

/* Closes the history file if open. */
void close_history_file(void)
{
    hist_lock = false;
    if (histfile == NULL)
	return;

    /* By closing the file descriptor for the history file, the file is
     * automatically unlocked. */
    // lock_histfile(F_UNLCK);
    remove_shellfd(fileno(histfile));
    fclose(histfile);
    histfile = NULL;
}

/* Calculates the number of the next new entry. */
unsigned next_history_number(void)
{
    unsigned number;
    if (histlist.count == 0) {
	number = 1;
    } else {
	number = ashistentry(histlist.Newest)->number + 1;
	if (number > max_number)
	    number = 1;
    }
    return number;
}

/* Adds the specified `line' to the history.
 * Must not be called while the history is locked.
 * If `line' contains newlines, `line' is separated into multiple entries.
 * Only lines that contain graph-class characters are added to the history.
 * If the `shopt_histspace' option is enabled and the `line' starts with a
 * blank, the `line' is not added. */
void add_history(const wchar_t *line)
{
    if (shopt_histspace && iswblank(line[0]))
	return;

    maybe_init_history();
    assert(!hist_lock);

    if (histfile != NULL)
	lock_histfile(F_WRLCK);
    update_time();
    update_history(true);

    for (;;) {
	size_t len = wcscspn(line, L"\n");
	add_history_line(line, len);
	line += len;
	if (line[0] == L'\0')
	    break;
	line++;
    }

    if (histfile != NULL)
	lock_histfile(F_UNLCK);
}

/* Adds the specified `line' to the history.
 * If the line is longer than `maxlen' characters, only the first `maxlen'
 * characters are added.
 * The string added to the history must not contain newlines.
 * `histfile' must be locked and `update_time' and `update_history' must have
 * been called.
 * If the string does not contain any graph-class characters, it is not added
 * to the history. */
void add_history_line(const wchar_t *line, size_t maxlen)
{
    /* Check if `line' contains `graph' characters */
    for (size_t i = 0; ; i++) {
	if (i >= maxlen || line[i] == L'\0')
	    return;
	assert(line[i] != L'\n');
	if (iswgraph(line[i]))
	    break;
    }

    char *mbsline = malloc_wcsntombs(line, maxlen);
    if (mbsline != NULL) {
	histentry_T *entry;

	remove_duplicates(mbsline);
	entry = new_entry(next_history_number(), now, mbsline);
	if (histfile != NULL)
	    write_history_entry(entry);
	free(mbsline);
    }
}

/* Removes entries whose value is the same as `line' in the `histrmdup' newest
 * entries.
 * `histfile' must be locked and `update_history' must have been called. */
void remove_duplicates(const char *line)
{
    histlink_T *l = histlist.Newest;
    for (unsigned i = histrmdup; i > 0 && l != Histlist; i--) {
	histlink_T *prev = l->prev;
	histentry_T *e = ashistentry(l);
	if (strcmp(e->value, line) == 0) {
	    if (histfile != NULL) {
		wprintf_histfile(L"d%X\n", e->number);
		histfilelines++;
	    }
	    remove_entry(e);
	}
	l = prev;
    }
}

/* Returns the history entry that has the specified `number', or `Histlist' if
 * there is no such entry. */
const histlink_T *get_history_entry(unsigned number)
{
    struct search_result_T sr = search_entry_by_number((unsigned) number);
    return (sr.prev == sr.next) ? sr.prev : Histlist;
}

#if YASH_ENABLE_LINEEDIT

/* Calls `maybe_init_history' or `update_history' and locks the history. */
void start_using_history(void)
{
    if (!hist_lock) {
	if (histfile != NULL) {
	    lock_histfile(F_RDLCK);
	    update_time();
	    update_history(false);
	    if (histfile != NULL)
		lock_histfile(F_UNLCK);
	} else {
	    maybe_init_history();
	}
	hist_lock = true;
    }
}

/* Unlocks the history. */
void end_using_history(void)
{
    hist_lock = false;
}

#endif /* YASH_ENABLE_LINEEDIT */


/********** Built-ins **********/

enum fcprinttype_T {
    FC_FULL, FC_NUMBERED, FC_UNNUMBERED, FC_RAW,
};

static struct search_result_T fc_search_entry(const wchar_t *name, int number)
    __attribute__((nonnull));
static void fc_update_history(void);
static void fc_remove_last_entry(void);
static histlink_T *fc_search_entry_by_prefix(const wchar_t *prefix)
    __attribute__((nonnull));
static int fc_print_entries(
	FILE *f, const histentry_T *first, const histentry_T *last,
	bool reverse, enum fcprinttype_T type)
    __attribute__((nonnull));
static const char *fc_time_to_str(time_t time)
    __attribute__((pure));
static int fc_exec_entry(const histentry_T *entry,
	const wchar_t *old, const wchar_t *new, bool quiet)
    __attribute__((nonnull(1)));
static int fc_edit_and_exec_entries(
	const histentry_T *first, const histentry_T *last,
	bool reverse, const wchar_t *editor, bool quiet)
    __attribute__((nonnull(1,2)));
static void fc_read_history(FILE *f, bool quiet)
    __attribute__((nonnull));
static void history_clear_all(void);
static int history_delete(const wchar_t *s)
    __attribute__((nonnull));
static int history_read(const wchar_t *s)
    __attribute__((nonnull));
static int history_write(const wchar_t *s)
    __attribute__((nonnull));
static void history_refresh_file(void);

const struct xgetopt_T fc_options[] = {
    { L'e', L"editor",     OPTARG_REQUIRED, true,  NULL, },
    { L'l', L"list",       OPTARG_NONE,     true,  NULL, },
    { L'n', L"no-numbers", OPTARG_NONE,     true,  NULL, },
    { L'q', L"quiet",      OPTARG_NONE,     false, NULL, },
    { L'r', L"reverse",    OPTARG_NONE,     true,  NULL, },
    { L's', L"silent",     OPTARG_NONE,     true,  NULL, },
    { L'v', L"verbose",    OPTARG_NONE,     false, NULL, },
#if YASH_ENABLE_HELP
    { L'-', L"help",       OPTARG_NONE,     false, NULL, },
#endif
    { L'\0', NULL, 0, false, NULL, },
};

/* The "fc" built-in, which accepts the following options:
 *  -e: specify the editor to edit history
 *  -l: list history
 *  -n: don't print entry numbers
 *  -r: reverse entry order
 *  -s: execute without editing
 *  -v: print time for each entry */
int fc_builtin(int argc, void **argv)
{
    const wchar_t *editor = NULL;
    bool list = false, quiet = false, rev = false, silent = false;
    enum fcprinttype_T ptype = FC_NUMBERED;

    const struct xgetopt_T *opt;
    xoptind = 0;
    while ((opt = xgetopt(argv, fc_options, XGETOPT_DIGIT))) {
	switch (opt->shortopt) {
	    case L'e':  editor = xoptarg;        break;
	    case L'l':  list   = true;           break;
	    case L'n':  ptype  = FC_UNNUMBERED;  break;
	    case L'q':  quiet  = true;           break;
	    case L'r':  rev    = true;           break;
	    case L's':  silent = true;           break;
	    case L'v':  ptype  = FC_FULL;        break;
#if YASH_ENABLE_HELP
	    case L'-':
		return print_builtin_help(ARGV(0));
#endif
	    default:
		return Exit_ERROR;
	}
    }

    /* error checks */
    if (editor && list)
	return mutually_exclusive_option_error(L'e', L'l');
    if (editor && silent)
	return mutually_exclusive_option_error(L'e', L's');
    if (list && quiet)
	return mutually_exclusive_option_error(L'l', L'q');
    if (list && silent)
	return mutually_exclusive_option_error(L'l', L's');
    if (rev && silent)
	return mutually_exclusive_option_error(L'r', L's');
    if (ptype != FC_NUMBERED && !list) {
	xerror(0, Ngt("the -n or -v option must be used with the -l option"));
	return Exit_ERROR;
    }
    if (!validate_operand_count(argc - xoptind, 0, silent ? 1 : 2))
	return Exit_ERROR;
    if (hist_lock) {
	xerror(0, Ngt("cannot be used during line-editing"));
	return Exit_FAILURE;
    }

    maybe_init_history();
    if (list) {
	fc_update_history();
    } else {
	/* remove the entry for this "fc" command */
	fc_remove_last_entry();
    }

    if (histlist.count == 0) {
	if (list) {
	    return Exit_SUCCESS;
	} else {
	    xerror(0, Ngt("the command history is empty"));
	    return Exit_FAILURE;
	}
    }

    /* parse <old=new> */
    const wchar_t *old = NULL, *new = NULL;
    if (silent && xoptind < argc) {
	wchar_t *eq = wcschr(ARGV(xoptind), L'=');
	if (eq != NULL) {
	    eq[0] = L'\0';
	    old = ARGV(xoptind);
	    new = &eq[1];
	    xoptind++;
	}
    }

    /* parse <first> */
    const wchar_t *vfirst = (xoptind < argc) ? ARGV(xoptind++) : NULL;
    int nfirst;
    struct search_result_T lfirst;
    if (vfirst != NULL) {
	if (!xwcstoi(vfirst, 10, &nfirst) /* || nfirst == 0 */)
	    nfirst = 0;
    } else {
	nfirst = list ? -16 : -1;
    }
    lfirst = fc_search_entry(vfirst, nfirst);
    if (lfirst.prev == Histlist && lfirst.next == Histlist)
	return Exit_FAILURE;

    /* main part of our work with the -s flag */
    if (silent) {
	if (lfirst.prev != lfirst.next) {
	    assert(vfirst != NULL);
	    xerror(0, Ngt("no such history entry `%ls'"), vfirst);
	    return Exit_FAILURE;
	}
	return fc_exec_entry(ashistentry(lfirst.prev), old, new, quiet);
    }

    /* parse <last> */
    const wchar_t *vlast = (xoptind < argc) ? ARGV(xoptind++) : NULL;
    int nlast;
    struct search_result_T llast;
    if (vlast != NULL) {
	if (!xwcstoi(vlast, 10, &nlast) /* || nlast == 0 */)
	    nlast = 0;
    } else if (list) {
	nlast = -1;
    } else {
	llast = lfirst;
	goto check_rev;
    }
    llast = fc_search_entry(vlast, nlast);
    if (llast.prev == Histlist && llast.next == Histlist)
	return Exit_FAILURE;

check_rev:
    if (search_result_is_newer(lfirst, llast)) {
	struct search_result_T temp = lfirst;  lfirst = llast;  llast = temp;
	rev = !rev;
    }
    assert(lfirst.next != Histlist);
    assert(llast.prev  != Histlist);

    const histentry_T *efirst = ashistentry(lfirst.next);
    const histentry_T *elast  = ashistentry(llast.prev);
    if (list)
	return fc_print_entries(stdout, efirst, elast, rev, ptype);
    else
	return fc_edit_and_exec_entries(efirst, elast, rev, editor, quiet);
}

/* Searches for the specified entry.
 * The `prev' and `next' members of the returned structure will be `Histlist'
 * if the prefix search fails. */
struct search_result_T fc_search_entry(const wchar_t *name, int number)
{
    struct search_result_T result;

    if (number == 0) {
	result.prev = result.next = fc_search_entry_by_prefix(name);
    } else if (number > 0) {
	result = search_entry_by_number((unsigned) number);
	assert(result.prev != Histlist || result.next != Histlist);
    } else {
	number = -number;
	result.next = get_nth_newest_entry((unsigned) number);
	if ((unsigned) number > histlist.count)
	    result.prev = Histlist;
	else
	    result.prev = result.next;
	assert(result.prev != Histlist || result.next != Histlist);
    }
    return result;
}

void fc_update_history(void)
{
    if (histfile != NULL) {
	lock_histfile(F_RDLCK);
	update_time();
	update_history(false);
	if (histfile != NULL)
	    lock_histfile(F_UNLCK);
    }
}

void fc_remove_last_entry(void)
{
    if (histfile != NULL) {
	lock_histfile(F_WRLCK);
	update_time();
	update_history(true);
	remove_last_entry();
	if (histfile != NULL) {
	    wprintf_histfile(L"c\n");
	    histfilelines++;
	    lock_histfile(F_UNLCK);
	}
    } else {
	remove_last_entry();
    }
}

/* Finds the newest entry that begins with the specified prefix.
 * Prints an error message and returns `Histlist' if not found. */
histlink_T *fc_search_entry_by_prefix(const wchar_t *prefix)
{
    char *s = malloc_wcstombs(prefix);
    if (s == NULL)
	return Histlist;

    histlink_T *l = search_entry_by_prefix(s);
    free(s);
    if (l == Histlist)
	xerror(0, Ngt("no such history entry beginning with `%ls'"), prefix);
    return l;
}

/* Print history entries between `first' and `last'.
 * Only byte-oriented output functions are used for `f'.
 * An error message is printed on error. */
int fc_print_entries(
	FILE *f, const histentry_T *first, const histentry_T *last,
	bool reverse, enum fcprinttype_T type)
{
    const histentry_T *start, *end, *e;
    if (!reverse)
	start = first, end = last;
    else
	start = last, end = first;
    e = start;
    for (;;) {
	int r;
	switch (type) {
	    case FC_FULL:
		r = fprintf(f, "%u\t%s\t%s\n",
			e->number, fc_time_to_str(e->time), e->value);
		break;
	    case FC_NUMBERED:
		r = fprintf(f, "%u\t%s\n", e->number, e->value);
		break;
	    case FC_UNNUMBERED:
		r = fprintf(f, "\t%s\n", e->value);
		break;
	    case FC_RAW:
		r = fprintf(f, "%s\n", e->value);
		break;
	    default:
		assert(false);
	}
	if (r < 0) {
	    xerror(errno, Ngt("cannot print to the standard output"));
	    return Exit_FAILURE;
	}
	if (e == end)
	    break;
	e = ashistentry(!reverse ? e->Next : e->Prev);
    }
    return Exit_SUCCESS;
}

/* Converts time to a string.
 * The return value is valid until the next call to this function. */
const char *fc_time_to_str(time_t time)
{
    static char s[80];

    if (time >= 0) {
	size_t size = strftime(s, sizeof s, "%c", localtime(&time));
	if (size > 0)
	    return s;
    }
    s[0] = '?', s[1] = '\0';
    return s;
}

/* Executes the value of `entry'.
 * If `old' is not NULL, the first occurrence of `old' in the command is
 * replaced with `new' before execution (but the entry's value is unchanged).
 * If `quiet' is false, prints the command before execution. */
int fc_exec_entry(const histentry_T *entry,
	const wchar_t *old, const wchar_t *new, bool quiet)
{
    wchar_t *code = malloc_mbstowcs(entry->value);
    if (code == NULL) {
	xerror(EILSEQ, Ngt("unexpected error"));
	return Exit_ERROR;
    }

    if (old != NULL) {
	xwcsbuf_T buf;
	wchar_t *p;

	wb_initwith(&buf, code);
	p = wcsstr(buf.contents, old);
	if (p != NULL)
	    wb_replace(&buf, p - buf.contents, wcslen(old), new, SIZE_MAX);
	code = wb_towcs(&buf);
    }

    add_history(code);
    if (!quiet)
	printf("%ls\n", code);
    exec_wcs(code, "fc", false);
    free(code);
    return laststatus;
}

/* Invokes the editor to let the user edit the history entries between `first'
 * and `last' and executes the edited entries. */
int fc_edit_and_exec_entries(
	const histentry_T *first, const histentry_T *last,
	bool reverse, const wchar_t *editor, bool quiet)
{
    char *temp;
    int fd;
    FILE *f;
    pid_t cpid;
    int savelaststatus;

    fd = create_temporary_file(&temp, S_IRUSR | S_IWUSR);
    if (fd < 0) {
	xerror(errno, Ngt("cannot create a temporary file to edit history"));
	goto error1;
    }
    f = fdopen(fd, "w");
    if (f == NULL) {
	xerror(errno, Ngt("cannot open temporary file `%s'"), temp);
	xclose(fd);
	goto error2;
    }

    savelaststatus = laststatus;
    cpid = fork_and_reset(0, true, 0);
    if (cpid < 0) {  // fork failed
	xerror(0, Ngt("cannot invoke the editor to edit history"));
	fclose(f);
	if (unlink(temp) < 0)
	    xerror(errno, Ngt("failed to remove temporary file `%s'"), temp);
error2:
	free(temp);
error1:
	return Exit_FAILURE;
    } else if (cpid > 0) {  // parent process
	fclose(f);

	wchar_t **namep = wait_for_child(
		cpid,
		doing_job_control_now ? cpid : 0,
		doing_job_control_now);
	if (namep != NULL) {
	    *namep = malloc_wprintf(L"%ls %s",
		    editor ? editor : L"${FCEDIT:-ed}", temp);
	}

	if (laststatus != Exit_SUCCESS) {
	    xerror(0, Ngt("the editor returned a non-zero exit status"));
	    fd = -1;
	} else {
	    fd = move_to_shellfd(open(temp, O_RDONLY));
	    if (fd < 0)
		xerror(errno, Ngt("cannot read commands from file `%s'"), temp);
	}
	if (unlink(temp) < 0)
	    xerror(errno, Ngt("failed to remove temporary file `%s'"), temp);
	free(temp);

	if (fd < 0)
	    return Exit_FAILURE;

	f = fdopen(fd, "r");
	fc_read_history(f, quiet);
	lseek(fd, 0, SEEK_SET);
	laststatus = savelaststatus;
	exec_input(fd, "fc", XIO_SUBST_ALIAS);
	remove_shellfd(fd);
	fclose(f);
	return laststatus;
    } else {  // child process
	fc_print_entries(f, first, last, reverse, FC_RAW);
	fclose(f);

	wchar_t *command = malloc_wprintf(L"%ls %s",
		(editor != NULL) ? editor : L"${FCEDIT:-ed}", temp);
	free(temp);
	exec_wcs(command, "fc", true);
#ifndef NDEBUG
	free(command);
#endif
	assert(false);
    }
}

void fc_read_history(FILE *f, bool quiet)
{
    xwcsbuf_T buf;

    if (histfile != NULL)
	lock_histfile(F_WRLCK);
    update_time();
    update_history(false);

    wb_init(&buf);
    while (read_line(f, &buf)) {
	if (!quiet)
	    printf("%ls\n", buf.contents);
	add_history_line(buf.contents, buf.length);
	wb_clear(&buf);
    }
    wb_destroy(&buf);

    if (histfile != NULL) {
	maybe_refresh_file();
	lock_histfile(F_UNLCK);
    }
}

#if YASH_ENABLE_HELP
const char fc_help[] = Ngt(
"list or re-execute command history"
);
const char fc_syntax[] = Ngt(
"\tfc [-qr] [-e editor] [first [last]]\n"
"\tfc -s [-q] [old=new] [first]\n"
"\tfc -l [-nrv] [first [last]]\n"
);
#endif

/* Options for the "history" built-in. */
const struct xgetopt_T history_options[] = {
    { L'c', L"clear",      OPTARG_NONE,     true,  NULL, },
    { L'd', L"delete",     OPTARG_REQUIRED, true,  NULL, },
    { L'r', L"read",       OPTARG_REQUIRED, true,  NULL, },
    { L's', L"set",        OPTARG_REQUIRED, true,  NULL, },
    { L'w', L"write",      OPTARG_REQUIRED, true,  NULL, },
    { L'F', L"flush-file", OPTARG_NONE,     true,  NULL, },
#if YASH_ENABLE_HELP
    { L'-', L"help",       OPTARG_NONE,     false, NULL, },
#endif
    { L'\0', NULL, 0, false, NULL, },
};

/* The "history" built-in, which accepts the following options:
 *  -c: clear whole history
 *  -d: remove history entry
 *  -r: read history from a file
 *  -s: add history entry
 *  -w: write history into a file
 *  -F: flush history file */
int history_builtin(int argc, void **argv)
{
    if (hist_lock) {
	xerror(0, Ngt("cannot be used during line-editing"));
	return Exit_FAILURE;
    }
    maybe_init_history();

    bool hasoption = false, removedthis = false;
    int result = Exit_SUCCESS;

    /* process options */
    const struct xgetopt_T *opt;
    xoptind = 0;
    while ((opt = xgetopt(argv, history_options, 0)) != NULL) {
	hasoption = true;
	switch (opt->shortopt) {
	    case L'c':
		history_clear_all();
		break;
	    case L'd':
		result = history_delete(xoptarg);
		break;
	    case L'r':
		result = history_read(xoptarg);
		break;
	    case L's':
		if (!removedthis) {
		    fc_remove_last_entry();
		    removedthis = true;
		}
		add_history(xoptarg);
		break;
	    case L'w':
		result = history_write(xoptarg);
		break;
	    case L'F':
		history_refresh_file();
		break;
#if YASH_ENABLE_HELP
	    case L'-':
		result = print_builtin_help(ARGV(0));
		break;
#endif
	    default:
		return Exit_ERROR;
	}
	if (result != Exit_SUCCESS)
	    return result;
    }

    /* print history */
    int count;
    if (xoptind < argc) {
	if (!validate_operand_count(argc - xoptind, 0, 1))
	    return Exit_ERROR;

	if (!xwcstoi(ARGV(xoptind), 10, &count)) {
	    xerror(errno, Ngt("`%ls' is not a valid integer"), ARGV(xoptind));
	    return Exit_ERROR;
	}
	if (count <= 0)
	    return Exit_SUCCESS;
    } else if (!hasoption) {
	count = INT_MAX;
    } else {
	return Exit_SUCCESS;
    }
    fc_update_history();

    histlink_T *start = get_nth_newest_entry((unsigned) count);
    if (start == Histlist)
	return Exit_SUCCESS;
    return fc_print_entries(stdout,
	    ashistentry(start), ashistentry(histlist.Newest),
	    false, FC_NUMBERED);
}

/* Clears all the history. */
void history_clear_all(void)
{
    if (histfile != NULL) {
	lock_histfile(F_WRLCK);
	update_history(false);
    }
    clear_all_entries();
    if (histfile != NULL) {
	refresh_file();
	lock_histfile(F_UNLCK);
    }
}

/* Deletes a history entry specified by the argument string. */
int history_delete(const wchar_t *s)
{
    int n;
    histlink_T *l;

    if (histfile != NULL) {
	lock_histfile(F_WRLCK);
	update_time();
	update_history(true);
    }

    if (!xwcstoi(s, 10, &n) || n == 0) {
	l = fc_search_entry_by_prefix(s);
    } else {
	if (n >= 0) {
	    struct search_result_T sr = search_entry_by_number((unsigned) n);
	    l = (sr.prev == sr.next) ? sr.prev : Histlist;
	} else {
	    if (n != INT_MIN)
		n = -n;
	    else
		n = INT_MAX;
	    l = get_nth_newest_entry((unsigned) n);
	}
	if (l == Histlist)
	    xerror(0, Ngt("no such history entry `%ls'"), s);
    }

    if (l != Histlist) {
	histentry_T *e = ashistentry(l);
	if (histfile != NULL) {
	    wprintf_histfile(L"d%X\n", e->number);
	    histfilelines++;
	}
	remove_entry(e);
    }

    if (histfile != NULL)
	lock_histfile(F_UNLCK);

    return (yash_error_message_count == 0) ? Exit_SUCCESS : Exit_FAILURE;
}

/* Reads history from the specified file. */
int history_read(const wchar_t *s)
{
    FILE *f;

    /* The `fc_read_history' function assumes the argument stream wide-oriented.
     * We don't pass `stdin' to `fc_read_history' so that it remains
     * non-oriented. */
    if (wcscmp(s, L"-") == 0) {
	int fd = copy_as_shellfd(STDIN_FILENO);
	if (fd < 0)
	    goto error;
	f = fdopen(fd, "r");
    } else {
	char *mbsfilename = malloc_wcstombs(s);
	if (mbsfilename == NULL)
	    goto error;
	f = fopen(mbsfilename, "r");
	free(mbsfilename);
    }
    if (f == NULL)
	goto error;
    fc_read_history(f, true);
    if ((ferror(f) != 0) | (fclose(f) != 0))
	goto error;
    return Exit_SUCCESS;

error:
    xerror(0, Ngt("cannot read history from file `%ls'"), s);
    return Exit_FAILURE;
}

/* Writes history into the specified file. */
int history_write(const wchar_t *s)
{
    FILE *f;

    fc_update_history();
    if (histlist.count == 0)
	return Exit_SUCCESS;

    if (wcscmp(s, L"-") == 0) {
	f = stdout;
    } else {
	char *mbsfilename = malloc_wcstombs(s);
	if (mbsfilename == NULL)
	    goto error;
	f = fopen(mbsfilename, "w");
	free(mbsfilename);
	if (f == NULL)
	    goto error;
    }
    
    int result = fc_print_entries(f,
	    ashistentry(histlist.Oldest), ashistentry(histlist.Newest),
	    false, FC_RAW);
    if (f != stdout)
	if (fclose(f) != 0)
	    goto error;
    return result;

error:
    xerror(0, Ngt("cannot write history to file `%ls'"), s);
    return Exit_FAILURE;
}

/* Refreshes the history file. */
void history_refresh_file(void)
{
    if (histfile != NULL) {
	lock_histfile(F_WRLCK);
	update_time();
	update_history(false);
	if (histfile != NULL) {
	    remove_histfile_pid(0);
	    refresh_file();
	    lock_histfile(F_UNLCK);
	}
    }
}

#if YASH_ENABLE_HELP
const char history_help[] = Ngt(
"manage command history"
);
const char history_syntax[] = Ngt(
"\thistory [-cF] [-d entry] [-s command] [-r file] [-w file] [count]\n"
);
#endif


/* vim: set ts=8 sts=4 sw=4 noet tw=80: */
