(without cluttering your history)
Hey everyone.
Sometimes the history gets too large, with many repeated commands.
I wanted to share a utility script I made to interactively clean up duplicates or specific unwanted entries from ~/.bash_history using fzf.
The Problem
Most history cleaning solutions out there either parse the file blindly (risking losing timestamps or corrupting multi-line structures) or require you to run long, slow loops. On top of that, if you try to live-reload the current terminal sessionβs memory inside a standalone script, it either fails (subshell isolation) or forces you to source the script, which ends up clogging your actual history with the scriptβs internal utility commands (awk, mktemp, cat, etc.).
The Solution
This modular approach uses a standalone physical script powered entirely by AWK for ultra-fast processing (can process 50k+ lines in milliseconds) and handles the standard Bash history timestamp formatting seamlessly.
To bypass the subshell limitation and keep your history 100% clean, it uses a tiny wrapper function in your ~/.bashrc. The function spins up the script in an isolated child process, and then syncs the changes to your active terminal window instantly.
- The Script:
~/clean-history.sh
Save this as ~/clean-history.sh and make it executable (chmod +x ~/clean-history.sh). It includes automatic detection of the system language (providing the manual in Spanish or another language, in that case it will be English).
#!/bin/bash
# Configuration files
HIST_FILE="$HOME/.bash_history"
BACKUP_FILE="$HOME/.bash_history.bak"
# Help Manual Function
mostrar_ayuda() {
if [[ "$LANG" =~ ^es_ ]]; then
echo -e "π \033[1;36mManual de Uso: Limpiador de Historial con FZF\033[0m"
echo -e "--------------------------------------------------"
echo -e "\033[1mModo de ejecuciΓ³n recomendado (vΓa funciΓ³n):\033[0m"
echo -e " cleanhist Abre fzf con todo el historial."
echo -e " cleanhist <tΓ©rmino> Filtra por una palabra (ej: cleanhist nano)."
echo -e " cleanhist -h | --help Muestra esta pantalla de ayuda."
echo ""
echo -e "\033[1mModo de ejecuciΓ³n directo (vΓa script fΓsico):\033[0m"
echo -e " $0 <parΓ‘metros>"
echo -e " \033[1;33mβ οΈ IMPORTANTE:\033[0m Si lo ejecutΓ‘s directamente como script, recordΓ‘"
echo -e " correr manualmente estos comandos al terminar para reflejar los"
echo -e " cambios en la terminal activa:"
echo -e " \033[1;35mhistory -c && history -r\033[0m"
echo ""
echo -e "\033[1mπ‘ Consejo de conveniencia:\033[0m"
echo -e " Se recomienda enfΓ‘ticamente utilizar la funciΓ³n wrapper \033[1mcleanhist\033[0m"
echo -e " en tu ~/.bashrc. De esta forma, la recarga de memoria de la terminal"
echo -e " se realiza automΓ‘ticamente al finalizar fzf, evitando comandos extra"
echo -e " y previniendo que los comandos internos del script ensucien tu historial."
echo ""
echo -e "\033[1mπ» CΓ³digo de la funciΓ³n para tu ~/.bashrc:\033[0m"
echo -e "\033[0;32mcleanhist() {"
echo -e " if [[ \"\$1\" == \"--help\" || \"\$1\" == \"-h\" ]]; then"
echo -e " \"\$HOME/clean-history.sh\" \"\$1\""
echo -e " return 0"
echo -e " fi"
echo -e " \"\$HOME/clean-history.sh\" \"\$@\""
echo -e " history -c"
echo -e " history -r"
echo -e "}\033[0m"
echo ""
echo -e "\033[1mAtajos dentro de FZF:\033[0m"
echo -e " \033[1;33mTAB\033[0m Seleccionar o deseleccionar una entrada individual."
echo -e " \033[1;33mALT + A\033[0m Seleccionar TODOS los comandos de la lista."
echo -e " \033[1;33mALT + D\033[0m Deseleccionar todas las entradas."
echo -e " \033[1;32mENTER\033[0m Confirmar y BORRAR permanentemente lo seleccionado."
echo -e " \033[1;31mESC / CTRL+C\033[0m Cancelar la operaciΓ³n sin cambios."
echo "--------------------------------------------------"
else
echo -e "π \033[1;36mUser Manual: Bash History Cleaner with FZF\033[0m"
echo -e "--------------------------------------------------"
echo -e "\033[1mRecommended Usage (via function):\033[0m"
echo -e " cleanhist Open fzf with full history."
echo -e " cleanhist <term> Filter by a word (e.g., cleanhist nano)."
echo -e " cleanhist -h | --help Show this help screen."
echo ""
echo -e "\033[1mDirect Usage (via physical script):\033[0m"
echo -e " $0 <parameters>"
echo -e " \033[1;33mβ οΈ IMPORTANT:\033[0m If run directly as a script, remember to"
echo -e " manually run these commands afterwards to apply the changes"
echo -e " to your active terminal window:"
echo -e " \033[1;35mhistory -c && history -r\033[0m"
echo ""
echo -e "\033[1mπ‘ Convenience Tip:\033[0m"
echo -e " It is strongly recommended to use the \033[1mcleanhist\033[0m wrapper function"
echo -e " in your ~/.bashrc. This ensures your terminal's memory is automatically"
echo -e " reloaded right after closing fzf, avoiding extra typing and preventing"
echo -e " the script's internal commands from cluttering your history."
echo ""
echo -e "\033[1mπ» Function Code for your ~/.bashrc:\033[0m"
echo -e "\033[0;32mcleanhist() {"
echo -e " if [[ \"\$1\" == \"--help\" || \"\$1\" == \"-h\" ]]; then"
echo -e " \"\$HOME/clean-history.sh\" \"\$1\""
echo -e " return 0"
echo -e " fi"
echo -e " \"\$HOME/clean-history.sh\" \"\$@\""
echo -e " history -c"
echo -e " history -r"
echo -e "}\033[0m"
echo ""
echo -e "\033[1mKeybindings inside FZF:\033[0m"
echo -e " \033[1;33mTAB\033[0m Select or deselect a single entry."
echo -e " \033[1;33mALT + A\033[0m Select ALL commands in the list."
echo -e " \033[1;33mALT + D\033[0m Deselect all entries."
echo -e " \033[1;32mENTER\033[0m Confirm and permanently DELETE selected items."
echo -e " \033[1;31mESC / CTRL+C\033[0m Cancel operation without changes."
echo "--------------------------------------------------"
fi
}
# Capture search parameter or help flag
FILTRO_BUSQUEDA="$1"
if [[ "$FILTRO_BUSQUEDA" == "--help" || "$FILTRO_BUSQUEDA" == "-h" ]]; then
mostrar_ayuda
exit 0
fi
# 2. Create backup
cp "$HIST_FILE" "$BACKUP_FILE"
echo "π¦ Backup created at: $BACKUP_FILE"
# Safe temporary files
PROCESADO_FILE=$(mktemp)
SELECCIONADOS_FILE=$(mktemp)
NUEVO_HISTORIAL_FILE=$(mktemp)
# 3. Join timestamps and commands seamlessly using AWK
awk '
/^#[0-9]+$/ { ts=$0; next }
{ print ts "\t" $0; ts="" }
' "$HIST_FILE" > "$PROCESADO_FILE"
# 4. Launch FZF for multiple selection (items marked will be DELETED)
fzf -m \
--header "Mark commands to DELETE (TAB: Select | ALT+A: All | ALT+D: None | ENTER: Confirm)" \
--bind "alt-a:select-all,alt-d:deselect-all" \
--delimiter "\t" \
--with-nth 2 \
--query "$FILTRO_BUSQUEDA" < "$PROCESADO_FILE" > "$SELECCIONADOS_FILE"
# If nothing was selected, exit cleanly
if [ ! -s "$SELECCIONADOS_FILE" ]; then
echo "β Operation canceled. No entries were deleted."
rm -f "$PROCESADO_FILE" "$SELECCIONADOS_FILE" "$NUEVO_HISTORIAL_FILE"
exit 0
fi
echo "π§Ή Removing selected entries..."
# 5. Reconstruct the clean history file using AWK
awk -F '\t' '
NR==FNR { eliminados[$0]; next }
!($0 in eliminados) {
if ($1 != "") print $1
print $2
}
' "$SELECCIONADOS_FILE" "$PROCESADO_FILE" > "$NUEVO_HISTORIAL_FILE"
# Apply changes permanently to the real file
cat "$NUEVO_HISTORIAL_FILE" > "$HIST_FILE"
# Cleanup temporaries
rm -f "$PROCESADO_FILE" "$SELECCIONADOS_FILE" "$NUEVO_HISTORIAL_FILE"
echo "β
Physical history file updated successfully!"
tput setaf 2 bold
if [[ "$LANG" =~ ^es_ ]]; then
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo "β Si ejecutΓ³ el script directamente en terminal β"
echo "β ejecutar 'history -c && history -r' al finalizar.β"
echo "β Si usΓ³ la funciΓ³n 'cleanhist' no hace falta. β"
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
else
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo "β If you ran the script directly in the terminal β"
echo "β run 'history -c && history -r' upon completion. β"
echo "β If you used the 'cleanhist' function, this is β"
echo "β not necessary. β"
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββ"
fi
- The Wrapper Function:
~/.bashrc
Add this snippet to the bottom of your ~/.bashrc file (adjusting the path if necessary) and run source ~/.bashrc.
cleanhist() {
# If user requests help, show it from the script and return early
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
"$HOME/clean-history.sh" "$1"
return 0
fi
# Run the script normally in a child shell environment
"$HOME/clean-history.sh" "$@"
# Instantly sync changes to the current terminal's active memory
history -c
history -r
}
Features & Keybindings:
-
Instant filtering: Run
cleanhist nanoorcleanhist gitto open FZF pre-populated with those commands. -
TAB: Toggle a single item.
-
ALT + A / ALT + D: Select all / Deselect all.
-
ENTER: Wipe out selected entries instantly.
-
Safety Net: Creates a
~/.bash_history.bakfile before making any changes.
The messages in the code are in Spanish, but itβs quite easy to understand. Also, the help for $LANG=not spanish is in English.
(I hate that all projects have comments in English, but thatβs the way the world is set upβ¦
Β‘YO HABLO ESPAΓOL ARGENTINO, CARAJO!)
Let me know what you think or if you have any suggestions to make it even more efficient!
Or if you think itβs irrelevant or redundantβ¦ Or a
.
![]()