Cleaning Up .zsh_history File with a Bash Script

history_cleaner.sh

https://gist.github.com/ryan961/2baff64d162a946158405bb59bde1358

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/bin/bash

HISTFILE=${HISTFILE:-"$HOME/.zsh_history"}

verbose=false

print_usage() {
echo "Usage: $0 [-f HISTFILE] [-n line1,line2,...] [-o offset] [-r regexp] [-e num] [-v]"
echo "NOTE: -n, -r and -e options cannot be used together."
echo
echo "Delete specific lines from .zsh_history file."
echo " -f HISTFILE Specify history file. Default is $HISTFILE."
echo " -n line1,line2,... Delete specified lines. Line number starts from 1."
echo " -o offset Delete lines before(-) or after(+) lines specified by -n."
echo " -r regexp Delete lines match regexp pattern."
echo " -e num Delete last num lines."
echo " -v Verbose output."
echo " -h Print this help."
echo
}

# Parse flag
while getopts ":f:n:o:r:e:vh" opt; do
case $opt in
f) HISTFILE=$OPTARG ;;
n) lines_string=$OPTARG ;;
o) offset=$OPTARG ;;
r) regexp=$OPTARG ;;
e) last_lines=$OPTARG ;;
v) verbose=true ;;
h)
print_usage
exit 0
;;
\?)
echo "Invalid option: -$OPTARG" >&2
print_usage
exit 1
;;
esac
done
IFS=',' read -ra lines <<<"$lines_string"

[[ $verbose == true ]] && echo "Deleting from file: $HISTFILE"

# Check HISTFILE exists
if [[ ! -f $HISTFILE ]]; then
echo "History file $HISTFILE does not exist" >&2
exit 1
fi

# Check be used together
if [[ (${#lines[@]} -gt 0 && (-n $regexp || -n $last_lines)) || (-n $regexp && -n $last_lines) ]]; then
echo "Options -n, -r and -e cannot be used together" >&2
exit 1
fi

# Verify input offset
if [[ -n $offset && $offset != 0 ]]; then
if [[ ${#lines[@]} == 0 ]]; then
echo "You must specify line number with -n when using offset with -o." >&2
exit 1
fi

if [[ ${#lines[@]} -gt 1 ]]; then
echo "You specify line number with -n (limit 1) when using offset with -o." >&2
exit 1
fi
fi

# Delete specified lines
if [[ ${#lines[@]} -gt 0 ]]; then
if [[ -z $offset || $offset == 0 ]]; then # Delete specified lines
[[ $verbose == true ]] && echo "Deleting lines: ${lines[*]}"

# Verify line exist
for line in "${lines[@]}"; do
if [[ $(wc -l <"$HISTFILE") -lt $line ]]; then
echo "Line $line does not exist" >&2
exit 1
fi
done

for ((index = 0; index < ${#lines[@]}; index++)); do
line=$((lines[index] - index))

# Debug line content
delete_line=$(sed -n "${line}p" "$HISTFILE")
[[ $verbose == true ]] && echo "Deleting line [${line}]: $delete_line"

awk 'NR != '$line' {print}' "$HISTFILE" >tmp && mv tmp "$HISTFILE"
done

else # Delete lines in range
if [[ $offset -gt 0 ]]; then
first=${lines[0]}
last=$((first + offset))
else
first=$((lines[0] + offset))
last=${lines[0]}
fi
[[ $verbose == true ]] && echo "Deleting range: $first to $last"

for ((i = first; i <= last; i++)); do
# Debug line content
delete_line=$(sed -n "${i}p" "$HISTFILE")
[[ $verbose == true ]] && echo "Deleting line [${i}]: $delete_line"
done

awk 'NR < '"$first"' || NR > '"$last"' {print}' "$HISTFILE" >tmp && mv tmp "$HISTFILE"
fi
exit 0
fi

# Delete last n lines
if [[ -n $last_lines ]]; then
[[ $verbose == true ]] && echo "Deleting last $last_lines lines"

total_lines=$(wc -l <"$HISTFILE")
first=$((total_lines - last_lines))

for ((i = first; i < total_lines; i++)); do
delete_line=$(sed -n "${i}p" "$HISTFILE")
[[ $verbose == true ]] && echo "Deleting line [${i}]: $delete_line"
done

awk 'NR < '"$first"' || NR > '"$((total_lines-1))"' {print}' "$HISTFILE" >tmp && mv tmp "$HISTFILE"
exit 0
fi

# Delete by regex
if [[ -n $regexp ]]; then
matches=$(grep -n -E "$regexp" "$HISTFILE" | cut -d: -f1)

# shellcheck disable=SC2086
[[ $verbose == true ]] && echo "Deleting lines match '$regexp':" $matches

i=0
for line_number in $matches; do
line=$((line_number - i))

delete_line=$(sed -n "${line}p" "$HISTFILE")
[[ $verbose == true ]] && echo "Deleting line [$line_number]: $delete_line"

awk 'NR != '$line' {print}' "$HISTFILE" >tmp && mv tmp "$HISTFILE"

i=$((i + 1))
done
exit 0
fi

# Default print usage
if [[ $# -eq 0 ]]; then
print_usage
fi

This is a bash script to delete specific lines from $HISTFILE ( .zsh_history ) file.

It provides several options to specify which lines to delete:

  • -n: Delete specified line numbers
  • -o: Delete a range of lines, used together with -n
  • -r: Delete lines matching a regular expression pattern
  • -e: Delete last N lines

The script also supports:

  • Specifying a different history file with -f
  • Print verbose output with -v
  • Print help message with -h

The implementation uses awk to filter out target lines and redirect to a tmp file, then replace original file.

Some notes on usage:

  • Line numbers start from 1
  • -n, -r and -e options cannot be used together

This provides a flexible way to clean up .zsh_history by deleting unwanted lines.

Please let me know if you would like me to modify or expand the introduction.