Clean duplicate shell commands from shell history files, while preserving the most recent occurrence of each command.
You can either download the latest binary from releases page.
Or clone the repo, then build it with the Zig compiler (zig >= 0.16)
git clone https://github.com/ABDsheikho/histclean.git
cd histclean
zig build installIf that's your first time running histclean, don't forget to pass --backup
option to backup your history file:
histclean -bZig compiler >= 0.16
Note
Since Zig is pre 1.0 release, histclean will always follow the latest Zig release.
First clone the repo:
git clone https://github.com/ABDsheikho/histclean.git
cd histcleanThen build it using the Zig compiler:
zig build installOr with a custom prefix:
zig build --prefix /usr/localIf you downloaded the latest binary from releases, then you probably need to do the following commands:
mv histclean-v* histclean # Clean the name
chmod +x histclean # Make it an executable
eval "$(histclean --completion bash)" # Or zshhistclean is available on the AUR. So you can install it using yay or any other AUR-helper
yay -S histclean-bin
Then you need to move the binary to your $PATH.
histclean [options]When run without any options, histclean cleans the default history file on
the system.
histclean determines the default history file from the HISTFILE environment
variable. If HISTFILE is not set, histclean exits with an error.
| Option | Description |
|---|---|
-h, --help |
Show help message and exit |
-v, --version |
Show version and exit |
-d, --dry-run |
Print the resulted output to stdout without modifying anything |
-b, --backup |
Create a .backup copy before modifying the file |
-w, --which-file |
Print the path to the detected history file and exit |
-i, --input <FILE> |
Read history from the specified file |
-o, --output <FILE> |
Write resulted output to the specified file |
-s, --shell <SHELL> |
Override the shell type instead of auto-detecting from $SHELL. See Supported shells below. |
-c, --completion <SHELL> |
Generate completion script for the specified shell. See Supported shells below. |
# Deduplicate the default history file in-place
histclean
# Preview what would be removed
histclean --dry-run
# Clean a specific history file with a backup
histclean --input ~/.zsh_history --backup
# Write cleaned output to a new file
histclean --input ~/.bash_history --output ~/cleaned_historyhistclean scans the history file backwards, keeping only the most recent
occurrence of each unique command line. Timestamp lines (prefixed with #)
are preserved for their associated commands, and orphaned consecutive
timestamps are collapsed.
- bash
- zsh
- x86_64-linux
- aarch64-macos
- x86_64-macos
- aarch64-freebsd
- x86_64-freebsd
zig build # Debug build
zig build test # Run unit and integration tests
zig build man # Generate man page (requires scdoc)Optimization modes:
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
zig build -Doptimize=ReleaseSmallShell completion is available in completions/ directory.
# Source it (per session)
source completions/histclean.bash
# Or install system-wide (requires bash-completion v2.x)
sudo cp completions/histclean.bash /usr/share/bash-completion/completions/histcleanYou can also generate the completion script using the --completion option by
providing the associated shell (bash, zsh).
eval "$(histclean --completion zsh)"Doesn't this tool can be done in a one-liner POSIX shell?
Yes, a core logic can be done in one line as:tac "$HISTFILE" | awk '!seen[$0]++' | tacIf you prefer this kind of quick hacks, then go for it.
Why 1k LoC in Zig when you can write it in a much smaller Python or JS?
Shorter != better. LoC is a cost, not a virtue. What matters is what ships. Python and JS add a runtime. histclean is a statically-linked binary meant to be installed once and forgotten.
This tool doesn't have --watch
option, what's good about it?
--watch
turns a millisecond task into a daemon. That's what your shell's
HISTCONTROL
settings are for. histclean is for bulk cleanup, and you can
run it from cron or a hook if you need to.
Not every CLI needs to sit in your terminal.
I downloaded the binary but can't run it. What's wrong?
You probably need to chmod +x
it, then move it to a directory in your $PATH
like ~/.local/bin
or /usr/local/bin.
I ran histclean but my history file wasn't modified. Why?
Most likely HISTFILE
isn't set, or histclean is targeting a different file
than you expect. To confirm that it's pointing at the right file, run:
histclean --which-file
Running histclean
broke my history file. What happened?
histclean only keeps the most recent occurrence of each command and preserves timestamp lines, and it doesn't invent or reorder lines. If your history file has entries that seem wrong, they were already in your file.
Restore your backup if you used --backup.
If you didn't, consider file-recovery tools if necessary,
but the data was already in that state before histclean ran.
Do I need Zig on my system to have this command?
If your OS/Architecture is listed on
supported platforms,
then no. The releases page provides a prebuilt binary. Download it,
chmod +x
it, done.
But if you're on a different OS/architecture,
you'll need the Zig compiler to build from source.
Does histclean work with fish shell?
No. Fish already handles this natively, and it never stores duplicate history entries to begin with. You don't need histclean for that, and that's why we currently don't support it.
Can I ask to support my shell?
Yes, just fill an issue and provide a history format or history sample, and I'll work on it.
Does histclean handle history files larger than X?
Nothing you're likely to hit. A history file with 100k lines (~10MB) would use maybe 20-30MB of RAM when loaded fully into memory, which is well within range. Extremely large files (hundreds of MB) would be constrained by available memory.
Can I use histclean with a custom/non-standard history file path?
Yes. By default histclean reads $HISTFILE. If yours is non-standard,
just make sure it's exported. Or use --input
to bypass env detection entirely and target any file.
I use bash as my shell, but I passed --input
to a zsh history file and nothing changed. Why?
By default, histclean detects your shell (and its parsing mechanism) from
$SHELL, not from the file you pass with --input. So a zsh file gets parsed
with bash rules. Pass --shell zsh
alongside --input
to override.
MIT