diff --git a/bashhist.c b/bashhist.c index 9979f99a..d411eb12 100644 --- a/bashhist.c +++ b/bashhist.c @@ -22,6 +22,10 @@ #if defined (HISTORY) +#if defined (HAVE_SQLITE3) && defined (HAVE_SQLITE3_H) +# include "bashhist_sqlite.c" +#else + #if defined (HAVE_UNISTD_H) # ifdef _MINIX # include @@ -975,4 +979,5 @@ history_should_ignore (line) return match; } +#endif /* !HAVE_SQLITE3 */ #endif /* HISTORY */ diff --git a/config.h.in b/config.h.in index a5ad9e72..656ad16d 100644 --- a/config.h.in +++ b/config.h.in @@ -703,6 +703,9 @@ /* Define if you have the killpg function. */ #undef HAVE_KILLPG +/* Define if sqlite3 library is available. */ +#undef HAVE_SQLITE3 + /* Define if you have the lstat function. */ #undef HAVE_LSTAT @@ -985,6 +988,9 @@ /* Define if you have the header file. */ #undef HAVE_REGEX_H +/* Define if you have the header file. */ +#undef HAVE_SQLITE3_H + /* Define if you have the header file. */ #undef HAVE_STDLIB_H diff --git a/configure.ac b/configure.ac index ce4e9b60..7140edf9 100644 --- a/configure.ac +++ b/configure.ac @@ -862,6 +862,13 @@ AC_CHECK_LIB(dl, dlopen) AC_CHECK_FUNCS(dlopen dlclose dlsym) fi +dnl check for sqlite3 (used for history storage) +AC_CHECK_LIB([sqlite3], [sqlite3_open_v2], [ + AC_DEFINE([HAVE_SQLITE3], [1], [Define if sqlite3 library is available]) + LIBS="-lsqlite3 $LIBS" +]) +AC_CHECK_HEADERS([sqlite3.h]) + dnl this defines HAVE_DECL_SYS_SIGLIST AC_DECL_SYS_SIGLIST --- /dev/null 2026-04-01 12:42:23.952423856 -0400 +++ bashhist_sqlite.c 2026-04-01 22:30:04.382338607 -0400 @@ -0,0 +1,998 @@ +/* bashhist_sqlite.c -- SQLite-backed history for bash. + * + * Drop-in replacement for the body of bashhist.c. Included (not compiled + * separately) when HAVE_SQLITE3 && HAVE_SQLITE3_H are both defined. + * + * Differences from the stock file: + * - History is stored in $HISTFILE.db (SQLite, WAL mode) instead of the + * flat $HISTFILE text file. + * - Every command is written to the DB immediately on add, so history + * survives shell crashes without needing HISTAPPEND / HISTFILESIZE tricks. + * - HISTSIZE still limits the readline in-memory list; the DB keeps everything. + * - Timestamps, CWD, hostname, and PID are stored per-entry. + * - erasedups (HISTCONTROL=erasedups) removes duplicates from the DB too, + * across all sessions. + * - history -c clears the DB. + * - history -a / exit are no-ops for file I/O (already persisted). + * - history -r / -w / -n still operate on the plain text file for + * compatibility / export use. + */ + +#include +#include + +#if defined (HAVE_UNISTD_H) +# ifdef _MINIX +# include +# endif +# include +#endif + +#include "bashtypes.h" +#include +#include +#include "bashansi.h" +#include "posixstat.h" +#include "filecntl.h" + +#include "bashintl.h" + +#if defined (SYSLOG_HISTORY) +# include +#endif + +#include "shell.h" +#include "flags.h" +#include "input.h" +#include "parser.h" +#include "pathexp.h" +#include "bashhist.h" +#include "builtins/common.h" + +#include +#include +#include + +#if defined (READLINE) +# include "bashline.h" +extern int rl_done, rl_dispatching; +#endif + +#if !defined (errno) +extern int errno; +#endif + +/* ------------------------------------------------------------------ */ +/* SQLite state */ +/* ------------------------------------------------------------------ */ + +static sqlite3 *hist_db = NULL; +static int sqlite_pending_erasedups = 0; + +/* Forward declaration — sqlite_history_add calls this. */ +static void sqlite_history_erasedups __P((const char *)); + +static char * +sqlite_history_dbpath (const char *histfile) +{ + char *p; + size_t len; + + if (histfile == NULL || *histfile == '\0') + { + const char *home = get_string_value ("HOME"); + if (home == NULL) return NULL; + len = strlen (home) + 20; + p = xmalloc (len); + snprintf (p, len, "%s/.bash_history.db", home); + return p; + } + + len = strlen (histfile) + 4; + p = xmalloc (len); + snprintf (p, len, "%s.db", histfile); + return p; +} + +static int +sqlite_history_open (const char *histfile) +{ + char *dbpath; + int rc; + + if (hist_db != NULL) + return 0; + + dbpath = sqlite_history_dbpath (histfile); + if (dbpath == NULL) + return -1; + + rc = sqlite3_open (dbpath, &hist_db); + xfree (dbpath); + + if (rc != SQLITE_OK) + { + hist_db = NULL; + return -1; + } + + rc = sqlite3_exec (hist_db, + "PRAGMA journal_mode=WAL;" + "PRAGMA synchronous=NORMAL;" + "CREATE TABLE IF NOT EXISTS history (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " command TEXT NOT NULL," + " timestamp INTEGER NOT NULL DEFAULT (strftime('%s','now'))," + " session INTEGER," + " hostname TEXT," + " cwd TEXT" + ");" + "CREATE INDEX IF NOT EXISTS idx_hist_cmd ON history(command);", + NULL, NULL, NULL); + + if (rc != SQLITE_OK) + { + sqlite3_close (hist_db); + hist_db = NULL; + return -1; + } + + return 0; +} + +/* Load the most-recent MAX_ENTRIES rows into readline's in-memory list. + Pass max_entries <= 0 to load everything. */ +static int +sqlite_history_load (int max_entries) +{ + sqlite3_stmt *stmt; + int rc, count; + char sql[512]; + + if (hist_db == NULL) + return -1; + + if (max_entries > 0) + snprintf (sql, sizeof (sql), + "SELECT id, command, timestamp FROM (" + " SELECT id, command, timestamp FROM history ORDER BY id DESC LIMIT %d" + ") ORDER BY id ASC", + max_entries); + else + snprintf (sql, sizeof (sql), + "SELECT id, command, timestamp FROM history ORDER BY id ASC"); + + rc = sqlite3_prepare_v2 (hist_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) + return -1; + + count = 0; + while (sqlite3_step (stmt) == SQLITE_ROW) + { + const char *cmd = (const char *) sqlite3_column_text (stmt, 1); + sqlite3_int64 ts = sqlite3_column_int64 (stmt, 2); + + if (cmd && *cmd) + { + add_history (cmd); + if (ts > 0) + { + char tsbuf[32]; + snprintf (tsbuf, sizeof (tsbuf), "#%lld", (long long) ts); + add_history_time (tsbuf); + } + count++; + } + } + + sqlite3_finalize (stmt); + history_lines_read_from_file = count; + return count; +} + +static void +sqlite_history_add (const char *line) +{ + sqlite3_stmt *stmt; + time_t now; + char cwd[4096]; + const char *hostname; + + if (hist_db == NULL) + return; + + time (&now); + hostname = get_string_value ("HOSTNAME"); + if (getcwd (cwd, sizeof (cwd)) == NULL) + cwd[0] = '\0'; + + if (sqlite3_prepare_v2 (hist_db, + "INSERT INTO history (command, timestamp, session, hostname, cwd)" + " VALUES (?, ?, ?, ?, ?)", + -1, &stmt, NULL) != SQLITE_OK) + return; + + sqlite3_bind_text (stmt, 1, line, -1, SQLITE_STATIC); + sqlite3_bind_int64 (stmt, 2, (sqlite3_int64) now); + sqlite3_bind_int (stmt, 3, (int) getpid ()); + sqlite3_bind_text (stmt, 4, hostname ? hostname : "", -1, SQLITE_STATIC); + sqlite3_bind_text (stmt, 5, cwd, -1, SQLITE_STATIC); + + sqlite3_step (stmt); + sqlite3_finalize (stmt); + + /* hc_erasedups() already removed in-memory duplicates and set the flag; + now remove DB duplicates after the insert so the new row has the MAX id + and the DELETE keeps it. */ + if (sqlite_pending_erasedups) + { + sqlite_pending_erasedups = 0; + sqlite_history_erasedups (line); + } +} + +/* Keep only the most recent occurrence of LINE in the DB. */ +static void +sqlite_history_erasedups (const char *line) +{ + sqlite3_stmt *stmt; + + if (hist_db == NULL) + return; + + if (sqlite3_prepare_v2 (hist_db, + "DELETE FROM history WHERE command = ?" + " AND id != (SELECT MAX(id) FROM history WHERE command = ?)", + -1, &stmt, NULL) != SQLITE_OK) + return; + + sqlite3_bind_text (stmt, 1, line, -1, SQLITE_STATIC); + sqlite3_bind_text (stmt, 2, line, -1, SQLITE_STATIC); + sqlite3_step (stmt); + sqlite3_finalize (stmt); +} + +static void +sqlite_history_clear () +{ + if (hist_db == NULL) + return; + sqlite3_exec (hist_db, "DELETE FROM history", NULL, NULL, NULL); +} + +/* ------------------------------------------------------------------ */ +/* Boilerplate identical to bashhist.c */ +/* ------------------------------------------------------------------ */ + +static int histignore_item_func __P((struct ign *)); +static int check_history_control __P((char *)); +static void hc_erasedups __P((char *)); +static void really_add_history __P((char *)); + +static struct ignorevar histignore = +{ + "HISTIGNORE", + (struct ign *)0, + 0, + (char *)0, + (sh_iv_item_func_t *)histignore_item_func, +}; + +#define HIGN_EXPAND 0x01 + +int remember_on_history = 0; +int enable_history_list = 0; +int history_lines_this_session; +int history_lines_in_file; + +#if defined (BANG_HISTORY) +int history_expansion_inhibited; +int double_quotes_inhibit_history_expansion = 0; +#endif + +int command_oriented_history = 1; +int current_command_first_line_saved = 0; +int literal_history; +int force_append_history; +int history_control; +int hist_last_line_added; +int hist_last_line_pushed; + +#if defined (READLINE) +int history_reediting; +int hist_verify; +#endif + +int dont_save_function_defs; + +extern int current_command_line_count; +extern struct dstack dstack; +extern int parser_state; + +#if defined (BANG_HISTORY) +static int bash_history_inhibit_expansion __P((char *, int)); +#endif +#if defined (READLINE) +static void re_edit __P((char *)); +#endif +static int history_expansion_p __P((char *)); +static int shell_comment __P((char *)); +static int should_expand __P((char *)); +static HIST_ENTRY *last_history_entry __P((void)); +static char *expand_histignore_pattern __P((char *)); +static int history_should_ignore __P((char *)); + +#if defined (BANG_HISTORY) +static int +bash_history_inhibit_expansion (string, i) + char *string; + int i; +{ + int t; + char hx[2]; + + hx[0] = history_expansion_char; + hx[1] = '\0'; + + if (i > 0 && (string[i - 1] == '[') && member (']', string + i + 1)) + return (1); + else if (i > 1 && string[i - 1] == '{' && string[i - 2] == '$' && + member ('}', string + i + 1)) + return (1); + else if (i > 1 && string[i - 1] == '$' && string[i] == '!') + return (1); +#if defined (EXTENDED_GLOB) + else if (extended_glob && i > 1 && string[i+1] == '(' && member (')', string + i + 2)) + return (1); +#endif + else if ((t = skip_to_histexp (string, 0, hx, SD_NOJMP|SD_HISTEXP)) > 0) + { + while (t < i) + { + t = skip_to_histexp (string, t+1, hx, SD_NOJMP|SD_HISTEXP); + if (t <= 0) + return 0; + } + return (t > i); + } + else + return (0); +} +#endif + +void +bash_initialize_history () +{ + history_quotes_inhibit_expansion = 1; + history_search_delimiter_chars = ";&()|<>"; +#if defined (BANG_HISTORY) + history_inhibit_expansion_function = bash_history_inhibit_expansion; + sv_histchars ("histchars"); +#endif +} + +void +bash_history_reinit (interact) + int interact; +{ +#if defined (BANG_HISTORY) + history_expansion = interact != 0; + history_expansion_inhibited = 1; + history_inhibit_expansion_function = bash_history_inhibit_expansion; +#endif + remember_on_history = enable_history_list; +} + +void +bash_history_disable () +{ + remember_on_history = 0; +#if defined (BANG_HISTORY) + history_expansion_inhibited = 1; +#endif +} + +void +bash_history_enable () +{ + remember_on_history = enable_history_list = 1; +#if defined (BANG_HISTORY) + history_expansion_inhibited = 0; + history_inhibit_expansion_function = bash_history_inhibit_expansion; +#endif + sv_history_control ("HISTCONTROL"); + sv_histignore ("HISTIGNORE"); +} + +/* ------------------------------------------------------------------ */ +/* Modified functions */ +/* ------------------------------------------------------------------ */ + +void +load_history () +{ + char *hf; + int hsval, nread; + + set_if_not ("HISTSIZE", "500"); + sv_histsize ("HISTSIZE"); + + hf = get_string_value ("HISTFILE"); + + if (sqlite_history_open (hf) == 0) + { + char *hs = get_string_value ("HISTSIZE"); + hsval = (hs && *hs) ? atoi (hs) : 500; + if (hsval <= 0) hsval = 500; + + nread = sqlite_history_load (hsval); + if (nread >= 0) + { + history_lines_in_file = nread; + using_history (); + return; + } + /* SQLite open succeeded but load failed; fall through to plain text. */ + } + + set_if_not ("HISTFILESIZE", get_string_value ("HISTSIZE")); + sv_histsize ("HISTFILESIZE"); + + if (hf && *hf && file_exists (hf)) + { + read_history (hf); + history_lines_in_file = history_lines_read_from_file; + using_history (); + } +} + +void +bash_clear_history () +{ + clear_history (); + history_lines_this_session = 0; + sqlite_history_clear (); +} + +int +bash_delete_histent (i) + int i; +{ + HIST_ENTRY *discard; + + discard = remove_history (i); + if (discard) + free_history_entry (discard); + history_lines_this_session--; + + return 1; +} + +int +bash_delete_last_history () +{ + register int i; + HIST_ENTRY **hlist, *histent; + int r; + + hlist = history_list (); + if (hlist == NULL) + return 0; + + for (i = 0; hlist[i]; i++) + ; + i--; + + histent = history_get (history_base + i); + if (histent == NULL) + return 0; + + r = bash_delete_histent (i); + + if (where_history () > history_length) + history_set_pos (history_length); + + return r; +} + +#ifdef INCLUDE_UNUSED +void +save_history () +{ + char *hf; + int r; + + hf = get_string_value ("HISTFILE"); + if (hf && *hf && file_exists (hf)) + { + using_history (); + if (history_lines_this_session <= where_history () || force_append_history) + r = append_history (history_lines_this_session, hf); + else + r = write_history (hf); + sv_histsize ("HISTFILESIZE"); + } +} +#endif + +int +maybe_append_history (filename) + char *filename; +{ + /* Commands are written to the DB on every add; nothing to append. */ + if (hist_db != NULL) + { + history_lines_this_session = 0; + return EXECUTION_SUCCESS; + } + + /* Fallback: plain-text path (SQLite unavailable). */ + int fd, result; + struct stat buf; + + result = EXECUTION_SUCCESS; + if (history_lines_this_session > 0 && (history_lines_this_session <= where_history ())) + { + if (stat (filename, &buf) == -1 && errno == ENOENT) + { + fd = open (filename, O_WRONLY|O_CREAT, 0600); + if (fd < 0) + { + builtin_error (_("%s: cannot create: %s"), filename, strerror (errno)); + return (EXECUTION_FAILURE); + } + close (fd); + } + result = append_history (history_lines_this_session, filename); + history_lines_in_file += history_lines_this_session; + history_lines_this_session = 0; + } + else + history_lines_this_session = 0; + + return (result); +} + +int +maybe_save_shell_history () +{ + int result; + char *hf; + + /* Commands are written to the DB on every add; nothing to do at exit. */ + if (hist_db != NULL) + { + history_lines_this_session = 0; + return 0; + } + + /* Fallback: plain-text path (SQLite unavailable). */ + result = 0; + if (history_lines_this_session > 0) + { + hf = get_string_value ("HISTFILE"); + if (hf && *hf) + { + if (file_exists (hf) == 0) + { + int file; + file = open (hf, O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (file != -1) + close (file); + } + using_history (); + if (history_lines_this_session <= where_history () || force_append_history) + { + result = append_history (history_lines_this_session, hf); + history_lines_in_file += history_lines_this_session; + } + else + { + result = write_history (hf); + history_lines_in_file = history_lines_written_to_file; + } + history_lines_this_session = 0; + sv_histsize ("HISTFILESIZE"); + } + } + return (result); +} + +#if defined (READLINE) +static void +re_edit (text) + char *text; +{ + if (bash_input.type == st_stdin) + bash_re_edit (text); +} +#endif + +static int +history_expansion_p (line) + char *line; +{ + register char *s; + + for (s = line; *s; s++) + if (*s == history_expansion_char || *s == history_subst_char) + return 1; + return 0; +} + +char * +pre_process_line (line, print_changes, addit) + char *line; + int print_changes, addit; +{ + char *history_value; + char *return_value; + int expanded; + + return_value = line; + expanded = 0; + +# if defined (BANG_HISTORY) + if (!history_expansion_inhibited && history_expansion && history_expansion_p (line)) + { + expanded = history_expand (line, &history_value); + + if (expanded) + { + if (print_changes) + { + if (expanded < 0) + internal_error ("%s", history_value); +#if defined (READLINE) + else if (hist_verify == 0 || expanded == 2) +#else + else +#endif + fprintf (stderr, "%s\n", history_value); + } + + if (expanded < 0 || expanded == 2) + { +# if defined (READLINE) + if (expanded == 2 && rl_dispatching == 0 && *history_value) +# else + if (expanded == 2 && *history_value) +# endif + maybe_add_history (history_value); + + free (history_value); + +# if defined (READLINE) + if (history_reediting && expanded < 0 && rl_done) + re_edit (line); +# endif + return ((char *)NULL); + } + +# if defined (READLINE) + if (hist_verify && expanded == 1) + { + re_edit (history_value); + free (history_value); + return ((char *)NULL); + } +# endif + } + + expanded = 1; + return_value = history_value; + } +# endif /* BANG_HISTORY */ + + if (addit && remember_on_history && *return_value) + maybe_add_history (return_value); + + return (return_value); +} + +static int +shell_comment (line) + char *line; +{ + char *p; + + for (p = line; p && *p && whitespace (*p); p++) + ; + return (p && *p == '#'); +} + +static int +check_history_control (line) + char *line; +{ + HIST_ENTRY *temp; + int r; + + if (history_control == 0) + return 1; + + if ((history_control & HC_IGNSPACE) && *line == ' ') + return 0; + + if (history_control & HC_IGNDUPS) + { + using_history (); + temp = previous_history (); + r = (temp == 0 || STREQ (temp->line, line) == 0); + using_history (); + if (r == 0) + return r; + } + + return 1; +} + +static void +hc_erasedups (line) + char *line; +{ + HIST_ENTRY *temp; + int r; + + using_history (); + while (temp = previous_history ()) + { + if (STREQ (temp->line, line)) + { + r = where_history (); + temp = remove_history (r); + if (temp) + free_history_entry (temp); + } + } + using_history (); + /* Signal sqlite_history_add to erase DB duplicates after the insert. */ + sqlite_pending_erasedups = 1; +} + +void +maybe_add_history (line) + char *line; +{ + hist_last_line_added = 0; + + if (current_command_line_count > 1) + { + if (current_command_first_line_saved && + ((parser_state & PST_HEREDOC) || literal_history || dstack.delimiter_depth != 0 || shell_comment (line) == 0)) + bash_add_history (line); + return; + } + + current_command_first_line_saved = check_add_history (line, 0); +} + +int +check_add_history (line, force) + char *line; + int force; +{ + if (check_history_control (line) && history_should_ignore (line) == 0) + { + if (history_control & HC_ERASEDUPS) + hc_erasedups (line); + + if (force) + { + really_add_history (line); + using_history (); + } + else + bash_add_history (line); + return 1; + } + return 0; +} + +#if defined (SYSLOG_HISTORY) +#define SYSLOG_MAXLEN 600 + +extern char *shell_name; + +#ifndef OPENLOG_OPTS +#define OPENLOG_OPTS 0 +#endif + +void +bash_syslog_history (line) + const char *line; +{ + char trunc[SYSLOG_MAXLEN]; + static int first = 1; + + if (first) + { + openlog (shell_name, OPENLOG_OPTS, SYSLOG_FACILITY); + first = 0; + } + + if (strlen(line) < SYSLOG_MAXLEN) + syslog (SYSLOG_FACILITY|SYSLOG_LEVEL, "HISTORY: PID=%d UID=%d %s", getpid(), current_user.uid, line); + else + { + strncpy (trunc, line, SYSLOG_MAXLEN); + trunc[SYSLOG_MAXLEN - 1] = '\0'; + syslog (SYSLOG_FACILITY|SYSLOG_LEVEL, "HISTORY (TRUNCATED): PID=%d UID=%d %s", getpid(), current_user.uid, trunc); + } +} +#endif + +void +bash_add_history (line) + char *line; +{ + int add_it, offset, curlen; + HIST_ENTRY *current, *old; + char *chars_to_add, *new_line; + + add_it = 1; + if (command_oriented_history && current_command_line_count > 1) + { + if ((parser_state & PST_HEREDOC) && literal_history && current_command_line_count > 2 && line[strlen (line) - 1] == '\n') + chars_to_add = ""; + else + chars_to_add = literal_history ? "\n" : history_delimiting_chars (line); + + using_history (); + current = previous_history (); + + if (current) + { + curlen = strlen (current->line); + + if (dstack.delimiter_depth == 0 && current->line[curlen - 1] == '\\' && + current->line[curlen - 2] != '\\') + { + current->line[curlen - 1] = '\0'; + curlen--; + chars_to_add = ""; + } + + if (dstack.delimiter_depth == 0 && current->line[curlen - 1] == '\n' && *chars_to_add == ';') + chars_to_add++; + + new_line = (char *)xmalloc (1 + curlen + strlen (line) + strlen (chars_to_add)); + sprintf (new_line, "%s%s%s", current->line, chars_to_add, line); + offset = where_history (); + old = replace_history_entry (offset, new_line, current->data); + free (new_line); + + if (old) + free_history_entry (old); + + add_it = 0; + } + } + + if (add_it) + really_add_history (line); + +#if defined (SYSLOG_HISTORY) + bash_syslog_history (line); +#endif + + using_history (); +} + +static void +really_add_history (line) + char *line; +{ + hist_last_line_added = 1; + hist_last_line_pushed = 0; + add_history (line); + history_lines_this_session++; + sqlite_history_add (line); +} + +int +history_number () +{ + using_history (); + return (remember_on_history ? history_base + where_history () : 1); +} + +static int +should_expand (s) + char *s; +{ + char *p; + + for (p = s; p && *p; p++) + { + if (*p == '\\') + p++; + else if (*p == '&') + return 1; + } + return 0; +} + +static int +histignore_item_func (ign) + struct ign *ign; +{ + if (should_expand (ign->val)) + ign->flags |= HIGN_EXPAND; + return (0); +} + +void +setup_history_ignore (varname) + char *varname; +{ + setup_ignore_patterns (&histignore); +} + +static HIST_ENTRY * +last_history_entry () +{ + HIST_ENTRY *he; + + using_history (); + he = previous_history (); + using_history (); + return he; +} + +char * +last_history_line () +{ + HIST_ENTRY *he; + + he = last_history_entry (); + if (he == 0) + return ((char *)NULL); + return he->line; +} + +static char * +expand_histignore_pattern (pat) + char *pat; +{ + HIST_ENTRY *phe; + char *ret; + + phe = last_history_entry (); + + if (phe == (HIST_ENTRY *)0) + return (savestring (pat)); + + ret = strcreplace (pat, '&', phe->line, 1); + + return ret; +} + +static int +history_should_ignore (line) + char *line; +{ + register int i, match; + char *npat; + + if (histignore.num_ignores == 0) + return 0; + + for (i = match = 0; i < histignore.num_ignores; i++) + { + if (histignore.ignores[i].flags & HIGN_EXPAND) + npat = expand_histignore_pattern (histignore.ignores[i].val); + else + npat = histignore.ignores[i].val; + + match = strmatch (npat, line, FNMATCH_EXTFLAG) != FNM_NOMATCH; + + if (histignore.ignores[i].flags & HIGN_EXPAND) + free (npat); + + if (match) + break; + } + + return match; +}