sqlite-history.sh
sqlite-history.sh is a handcrafted complete replacer for builtin shell fc, history, and Ctrl+R interactive search.
Why use sqlite-history.sh?
sqlite-history.sh is a ground-up reimagining of how shell history can, and arguably should function in the modern day. Most popular shells have been around since the 1970s and 1980s, and their history mechanics are based on the typical use case for the time.
sqlite-history.sh is designed with the knowledge that these days people rarely ever have just a single shell open. Most people will have multiple shells open simultaneously; whether in separate windows, multiple terminal tabs, or a terminal multiplexer such as tmux or screen.
In order to support this, sqlite-history.sh makes use of a sqlite database file which each terminal writes to as commands are run.
Pros & Cons (compared to builtin shell history)
Pros
- Commands are added to the database as you run them, instead of waiting for shell exit and clobbering the history file.
- Old commands aren't dropped from history as new ones are added due to arbitrary size limits.
- All shells configured to use sqlite-history.sh can search the database via Ctrl+R and read/execute commands that were run in other shells without needing to reload history and lose their live history order.
- There is a distinction between live history and database history ignore lists, as opposed to a single HISTIGNORE which applies to both, allowing you to maintain new commands in live history but not in the database, and vice-versa.
- Re-running a given command increments the run count in the database instead of adding a new entry, meaning for large history files the equivalent database is smaller, and more unique commands can be loaded into a new shell.
- A given command's last run time is always saved regardless of whether HISTTIMEFORMAT is specified, giving more meaningful output when running history when HISTTIMEFORMAT is set, but was not for some commands.
- Advanced match expressions are available for various operations, including but not limited to history ignore entries.
- Each shell configured to use sqlite-history.sh effectively shares history.
Cons
- Requires sqlite3 to be installed.
- Slightly slower than shell history; most operations still take less than 20 milliseconds on the high end.
- For small shell histories, the sqlite3 database takes more space than a plaintext file.
- Requires more memory than builtin history to store live history information.
Installation
Prerequisites
sqlite-history.sh requires sqlite3 version 3.24.0 or higher, and shell support for: both normal & associative arrays, coproc, and EPOCHREALTIME.
The installation script requires curl and either git or jq.
Quickstart
To install sqlite-history.sh, you can copy the command below and paste it into your console.
Once the installer has run, you will need to open your shell's rc file (e.g. .bashrc for bash, .zshrc for zsh, etc.) and add the following:
If you customized the target installation directory via the --install-dir argument to the installation script, make sure to use the correct path to the installation in your rc file.
Alternate
If you don't want to run the install script, you can also simply clone the repository:
Advanced
If you want to customize your installation, you can pass additional arguments to the installer by adding -s -- after bash and then the arguments you want to specify, e.g.:
Some arguments you may want to customize are as follows.
- -i, --install-dir
- Default Value: ~/.tools/sqlite-history.sh/
- The folder to install sqlite-history.sh in.
- -t, --target
- Default Value: master
- The target branch, tag, or commit to install.
- -C, --prefer-curl
- Default Value: false
- Use curl instead of git for installation.
- -R, --skip-readme
- Default Value: false on fresh installation, otherwise true
- Do not display the README when done installing.
- -S, --sqlite-path
- Default Value: $(command -v sqlite3)
- Specify an alternate location for sqlite3.
Setting Up sqlite-history.sh
Basic Setup
By default, sqlite-history.sh will inherit from your existing HISTIGNORE and HISTSIZE variables.
sqlite-history.sh's defaults can be further customized by changing the following variables before sourcing the script:
- SQLH_HISTIGNORE
- Defaults to the expanded value of HISTIGNORE, or an empty array if unset.
- An array of commands to omit from the history database. These commands will still show up in live history unless also specified in SQLH_LIVE_HISTIGNORE.
- SQLH_LIVE_HISTIGNORE
- Defaults to the expanded value of HISTIGNORE, or an empty array if unset.
- An array of commands to omit from live history. These commands will still show up in the database unless also specified in SQLH_HISTIGNORE.
- SQLH_HISTSIZE
- Defaults to HISTSIZE, or 500 if unset.
- An array of commands to omit from the history database. These commands will still show up in live history unless also specified in SQLH_HISTIGNORE.
Additional variables you can use to tweak behavior are documented in the Advanced Usage section.
Using sqlite-history.sh
sqlite-history.sh provides the sqlh function, which has subcommands to access most of the functionality of the script, and sqlite-i-search, which replaces your shell's builtin Ctrl+R search.
sqlh Subcommands
sqlh init
Initializes sqlite-history.sh. Takes no additional arguments at this time.
sqlh fc
Runs sqlite-history.sh's replacement of builtin fc. Understands all arguments to builtin fc, as well as the following long-form versions:
- -e, --editor
- Specify an editor to use. If not specified, defaults to FCEDIT or EDITOR.
- -l, --list
- Prints history entries to the console rather than invoking the editor.
- -n, --no-numbers
- Omits history indices from list output.
- -r, --reverse
- Reverses list output.
- -s, --substitute
- Performs substitution on a command.
sqlh fc also understands -h and --help and will print a help message to the console, unlike builtin fc.
sqlh history
Runs sqlite-history.sh's replacement of builtin history. Understands most arguments to builtin history, as well as the following long-form versions:
- -a, --append
- Append live history to the history file. Ignored by sqlite-history.sh because it is not necessary.
- -c, --clear
- Clears live history.
- -d, --delete
- Deletes entries from live history.
- -n, --read-new
- Reads new entries from the history file into live history.
- -p, --substitute
- Performs substitution on the given command without adding it to live history, printing the result to the console.
- -r, --reload
- Loads from the history file into live history.
- -w, --write
- Write live history to the history file, overwriting any previous contents. Ignored by sqlite-history.sh because it is not necessary.
sqlh history also understands -h and --help and will print a help message to the console, unlike builtin history.
The following entirely new options are added in addition to the above:
- -f, --filter
- Takes an advanced match expression and filters output to only those entries matching the expression. May be used in conjunction with -o.
- -F, --prune-from
- Takes a comma-separated list of prune targets consisting of live or db, followed by a series of advanced match expressions. Prunes entries matching the given expression(s) from live or database history, as specified.
- -o, --omit
- Takes an advanced match expression and filters output to only those entries not matching the expression. May be used in conjunction with -f.
- -P, --pager
- Outputs history to the system pager instead of printing to the console.
- -R, --prune
- Takes a series of advanced match expressions. Prunes entries matching the given expression(s) from both live and database history.
sqlh update and sqlh update-check
Runs the installer script with pre-filled arguments for your current installation.
sqlh version
Reports the version of sqlite-history.sh to the console.
sqlite-i-search (Ctrl+R search)
sqlite-i-search is a drop-in replacement for shell builtin Ctrl+R search (reverse-i-search in bash, reverse-history-search in zsh), but instead of only being able to search the local shell's live history, it also searches in the database, pulling in any new commands run in other shells.
Usage is nearly identical to that of bash's reverse-i-search, with the following key bindings:
- Escape
- Exit sqlite-i-search and set the current command buffer to whatever the current selection was.
- Up/Down Arrow Keys
- Attempts to find the index of the currently selected command in live history and navigates through live history from that point, selecting the previous/next result. Exits sqlite-i-search.
- Left/Right Arrow Keys
- Selects the current command and exits sqlite-i-search, then moves the cursor either to the left or the right of the beginning of the first instance of matched text in the command; e.g.: if searching for "hello" and the command is echo "hello world", the left arrow key would move the cursor to the first double quote, and the right arrow key would move the cursor to the 'e' in "Hello".
- Ctrl+R
- Selects the next oldest (previous) matched entry, unless there are no more matches.
- Ctrl+N
- Selects the next newest matched entry, unless you are at the first (most recent) match.
- This key binding is different from reverse-i-search's binding of Ctrl+S because that keystroke is normally swallowed by the terminal.
- Home
- Selects the current command and exits sqlite-i-search, then moves the cursor to the beginning of the line.
- End
- Selects the current command and exits sqlite-i-search, then moves the cursor to the end of the line.
- Enter/Return
- Selects the current command and exits sqlite-i-search, then moves the cursor to the end of the line.
- This currently does not attempt to automatically run the selected command like reverse-i-search would for consistency across shells and operating systems, because we have been unable to find a truly cross-platform of sending an extra keystroke to the parent terminal.
- Tab
- Selects the current command and exits sqlite-i-search, then moves the cursor to the first instance of matched text in the command.
All other "special" keystrokes are ignored at this time.
By default, sqlite-i-search is configured to be run in place of the standard Ctrl+R search, but the keybind to start it can be customized.
Advanced Usage
Advanced Match Expressions
sqlh fc, sqlh history, and sqlite-history.sh's live/database history ignore arrays all allow usage of advanced match expressions, on top of standard shell history ignnore patterns. Advanced match expressions consist of a standard shell history ignore pattern preceded by one of the following special characters:
- = for exact matching of an entire command
- =l[ls] will match either ll or ls, but not ls -l.
- ^ for the start of a command
- ^ls will match any of the following: ls, ls -l, ls ~, etc.
- $ for the end of a command
- $| less will match any command piped to less.
- / for substring matching anywhere in a command
- /kill will match commands containing kill, pkill, skill, etc.
- ! for exact word matching anywhere in a command
- !kill will only match commands containing the exact word kill, and not commands containing other words that include "kill" like pkill, skill, etc.
Not having a special character, or escaping one of the above special characters with a backslash (\) will cause the match expression to use default behavior as defined by the value of SQLH_IMPLICIT_STARTSWITH as detailed below.
Environment Variables
Core Behavior
- NO_COLOR
- No Default Value
- Disables ANSI color codes in output, following the no-color.org standard.
- SQLH_SQLITE
- Default Value: $(command -v sqlite3)
- The path to the sqlite3 executable you wish to use. Will also be eventually used to allow using sqlcipher.
- SQLH_HISTFILE
- Default Value: ~/history.sqlite
- The path sqlite-history.sh's history file.
- SQLH_NO_CLOBBER_BUILTINS
- No Default Value
- Prevent clobbering (shadowing) shell builtins fc and history with shell functions, only providing sqlh fc and sqlh history.
- SQLH_IMPLICIT_STARTSWITH
- No Default Value
- Use startswith matching to determine if a given shell command should be ignored, instead of mimicing default shell behavior of matching the full command.
- SQLH_BETA_OPT_IN
- No Default Value
- Opt in to beta features available on the develop branch of the repository instead of waiting for them to reach an official release.
- SQLH_UPDATE_CHECK_INTERVAL
- Default Value: "1d"
- A duration string describing how often to check for updates. Supports discrete units from seconds to weeks.
- SQLH_UPDATE_MSG_QUEUE
- Default Value: "precmd"
- The name of the deferred action queue to use for printing "updates are available" messages. Valid values are currently precmd and preprompt.
Miscellaneous Behavior
The following variables are intended primarily for debugging, and have no default values.
- SQLH_ALLOW_DEBUG_LOGS
- If set to any value other than false, enables debug logging. Otherwise, debug logging is a no-op to keep sqlite-history.sh as fast as possible.
- SQLH_DEBUG_LEVEL
- An integer value between 0 and 3 (inclusive) which controls the level of debug logging verbosity. Has no effect if SQLH_ALLOW_DEBUG_LOGS is set to false.
- SQLH_LOG_FILE
- The path of a log file to write to.
- SQLH_SUPPRESS_LOGS_IN_TERM
- Suppresses log messages in the terminal, printing them only to the log file if explicitly set to anything other than false.
Custom Keybinds
You can customize several key bindings for sqlite-history.sh via the following additional environment variables:
- SQLH_REVERSE_I_SEARCH_KEYBIND
- Ctrl+R
- Initiates sqlite-i-search.
- SQLH_PREV_HIST_ENTRY_KEYBIND
- Up Arrow
- Navigates to previous history entry when not in sqlite-i-search.
- SQLH_NEXT_HIST_ENTRY_KEYBIND
- Down Arrow
- Navigates to next history entry when not in sqlite-i-search.
Custom Colors
If you have not opted to disable colorization entirely by enabling NO_COLOR above, you can customize the colors sqlite-history.sh uses in various situations by changing these environment variables:
- SQLH_INTERACTIVE_SUCCESS_COLOR
- Used in sqlite-i-search for successful search indication; default value: \e[36m (ANSI Cyan)
- SQLH_INTERACTIVE_FAILURE_COLOR
- Used in sqlite-i-search for failed search indication; default value: \e[31m (ANSI Red)
- SQLH_INTERACTIVE_TERM_COLOR
- Used in sqlite-i-search for search term; default value: \e[93m (ANSI Bright Yellow)
- SQLH_INTERACTIVE_RESULT_COLOR
- Used in sqlite-i-search for search result; default value: \e[94m (ANSI Bright Blue)
- SQLH_LOGGING_HIGHLIGHT_COLOR
- Used in logging to highlight [sqlite-history.sh]; default value: \e[36m (ANSI Cyan)
- SQLH_LOGGING_ERROR_COLOR
- Used in error logs; default value: \e[31m (ANSI Red)
- SQLH_LOGGING_WARN_COLOR
- Used in warning logs; default value: \e[33m (ANSI Yellow)
- SQLH_LOGGING_INFO_COLOR
- Used in info logs; default value: \e[93m (ANSI Bright Yellow)
- SQLH_LOGGING_DEBUG_COLOR
- Used in debug logs; default value: \e[37m (ANSI White)
- SQLH_HYPERLINK_COLOR
- Used in Markdown conversion; no default value
- SQLH_ASIDE_COLOR
- Used in Markdown conversion; default value: \e[90m (ANSI Bright Black)
Handcrafted Code
sqlite-history.sh and its maintainers are proud of the fact that 100% of the repository has been written without AI.
Our Promise
We will do everything in our power to keep sqlite-history.sh AI-free forever - that is to say, we will do everything we reasonably can to ensure no code is merged that was created using AI "contributions" or "assistance", and we will never introduce any sort of AI assistant to sqlite-history.sh itself.
The Reason
AI companies are currently gobbling up all of the computing hardware in the world, crowding out the individual consumer, preventing normal people from being able to afford a computer. Whether this is by design or not isn't important - we believe that everyone should be able to do what they want on their computer without everything being funneled through the cloud or some AI agent.
We also believe that the shell in particular should be a place that never be able to mislead the user. AI "assistants" will always hallucinate fake information, regardless of how well they're trained on real data.
This is not to say that we believe AI is useless. We just believe that jamming AI into anything and everything possible is the peak of stupidity, and we would like to leave users with at least one tool that isn't trying to be smarter than it should be.
Keeping AI out of the mix also helps us better tune the performance of sqlite-history.sh. AI is trained on "good" coding patterns, which for shell scripts almost always means using subshells. This kills the performance of sqlite-history.sh in critical areas such as the Ctrl+R search, which needs to be able to re-render itself in realtime as the user types.
The Benefits
You don't need to upgrade your device in order to run sqlite-history.sh.
- It typically requires less than 1 megabyte of additional RAM per shell, and doesn't require a GPU (much less an NPU) at all.
-
It reliably initializes in less than 100 milliseconds on a 10+ year old PC running in Cygwin on Windows 10 even when debug logs are enabled (i.e. extra work is being done).
- sqlite-i-search re-renders itself within 10 milliseconds of both user input and reading results from the database