-
Notifications
You must be signed in to change notification settings - Fork 8
/
history.sh
428 lines (385 loc) · 11.5 KB
/
history.sh
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# history
# persistent shell history with advanced search
#
# Copyright (C) 2013 Mara Kim, Kris McGary
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see http://www.gnu.org/licenses/.
### USAGE ###
# Source this file in your shell's .*rc file
### SETTINGS ###
if [ -z "$ALL_HISTORY_FILE" ]
then
ALL_HISTORY_FILE=~/.bash_all_history
fi
### END SETTINGS ###
# Check dependencies
if ! command -v gawk &> /dev/null; then
printf '%s requires gawk\n' $BASH_SOURCE
fi
if date --date today &> /dev/null; then
__BASH_HISTORY_GDATE=date
elif gdate --date today &> /dev/null; then
__BASH_HISTORY_GDATE=gdate
else
printf '%s requires GNU date\n' $BASH_SOURCE
fi
#set up history logging of commands
export HISTTIMEFORMAT=' %F %T '
export HISTCONTROL='ignorespace'
PROMPT_COMMAND="_log_history;${PROMPT_COMMAND}"
_HISTNUM=""
_LAST_COMMAND=""
declare -a _PWD
# logging function
function _log_history {
local directory="$(pwd -P)"
local histnum="$(history 1 | sed 's/ *\([0-9]*\).*/\1/')"
if [ -z "$_HISTNUM" ]
then
_HISTNUM="$histnum"
elif [ "$histnum" != "$_HISTNUM" ]
then
if [ "$directory" != "$_PWD" ]
then
local match
local i
for i in {1..8}
do if [ "$directory" = "${_PWD[$i]}" ]
then unset _PWD[$i]
match="true"
fi
done
if [ -z "$match" ]
then unset _PWD[9]
fi
_PWD=( "$directory" "${_PWD[@]}" )
local directory="${_PWD[1]}"
fi
local command="$(cat <(history 1 | head -1 | sed 's/[^ ]* //') <(history 1 | tail -n +2))"
printf '%q\t%q\t%s\n\x00' "$USER@$HOSTNAME" "$directory" "$command" >> "$ALL_HISTORY_FILE"
if [ "$_LAST_COMMAND" = "$command" ]
then
history -d "$histnum"
else
_HISTNUM="$histnum"
_LAST_COMMAND="$command"
fi
fi
}
# history
h () {
gawk_history_interactive "/" 1 "$@"
}
# history of commands run in this directory and subdirectories (with grep)
dh () {
gawk_history_interactive "$(printf '%b' "$(pwd -P)")" 1 "$@"
}
# history of commands run in this directory only (with grep)
ldh () {
gawk_history_interactive "$(printf '%b' "$(pwd -P)")" 0 "$@"
}
# select from history
h! () {
select_history "/" 1 "$@"
}
# select from history
dh! () {
select_history "$(printf '%b' "$(pwd -P)")" 1 "$@"
}
# select from history
ldh! () {
select_history "$(printf '%b' "$(pwd -P)")" 0 "$@"
}
# select from working directory history
cd! () {
local histline
local history
local item
local line
if [ -z "$*" ]
then history=( "${_PWD[@]}" )
else while read -r -d '' histline
do history+=( "$histline" )
done < <( gawk_directory_history "/" 1 "$@" )
fi
select item in "${history[@]}"
do
if [ -z "$item" ]
then break
fi
# Read user edited command
read -er -i "$item" -p '$ ' item
while bash -n <<<$item 2>&1 | grep 'unexpected end of file' > /dev/null || [ -z "${item%%*\\}" ]
do
read -r -p '> ' line
item="$item"$'\n'"$line"
done
# Add command to history and run
history -s "cd $item"
cd "$item"
return $?
done
}
# bash completions
complete -cf h
complete -cf dh
complete -cf ldh
complete -cf h!
complete -cf dh!
complete -cf ldh!
# select history implementation
select_history () {
local histline
local history
local item
local line
while \read -r -d '' histline
do
history+=( "$histline" )
done < <( gawk_history "$@" |
gawk 'BEGIN { RS="\0"; FS="\t"; }
{ for(i = 5; i <= NF; i++) $4 = $4 "\t" $i}
{ a[$4] = NR }
END { PROCINFO["sorted_in"] = "@val_num_desc";
num = 0;
for (i in a) { printf "%s\0", i; num++; if (num == 10) break } }' )
select item in "${history[@]}"
do
if [ -z "$item" ]
then break
fi
# Read user edited command
read -er -i "$item" -p '$ ' item
while bash -n <<<$item 2>&1 | grep 'unexpected end of file' > /dev/null || [ -z "${item%%*\\}" ]
do
read -r -p '> ' line
item="$item"$'\n'"$line"
done
# Add command to history and run
history -s "$item"
eval "$item"
return $?
done
}
# directory history implementation
gawk_directory_history () {
gawk_history "$@" |
gawk 'BEGIN { RS="\0"; FS="\t"; }
{ a[$2] = NR }
END { PROCINFO["sorted_in"] = "@val_num_desc";
num = 0;
for (i in a) { printf "%s\0", i; num++; if (num == 10) break } }'
}
# interative gawk history implementation
gawk_history_interactive () {
# read arguments
local state="first"
for arg in "$@"
do
if [ "$state" = "first" ]
then
state="second"
elif [ "$state" = "second" ]
then
state=""
elif [ "$arg" = "-h" -o "$arg" = "--help" ]
then
printf 'Usage: [[l]d]h[!] [CONTEXT] [TIMESPEC] [--] [SEARCH]
Search command history.
SEARCH is a regular expression understood by `gawk`
used to match the executed command.
TIMESPEC is an argument of the form "[START..END]",
where START and END are strings understood by `date`.
A single day may be specified by "[DATE]".
CONTEXT is an argument of the form "USER@HOST:DIRECTORY"
or "USER@HOST::DIRECTORY", where each field is optional.
"@" is used to specify user or host filters.
":" is used to specify a directory filter.
"::" may be used instead to exclude subdirectories.
Select from the 10 most recent matching entries
adding `!` to the command (ex. `h!`).
The selected command may be edited before execution.
'
return 0
fi
done
gawk_history "$@" | tr -d '\000' | less -FX +G
}
# core gawk history implementation
gawk_history () {
# read arguments
local state="first"
local search
local timespec
local user
local host
local argdir
local directory
local recursive_dir
for arg in "$@"
do
if [ "$state" = "first" ]
then
directory="$arg"
state="second"
elif [ "$state" = "second" ]
then
recursive_dir="$arg"
state=""
elif [ "$state" = "input" ]
then
search+="${arg}.*"
elif [ "$state" = "time" ]
then
timespec="$timespec $arg"
if [ -z "${arg/*]/}" ]
then state=""
fi
elif [ "$arg" = "--" ]
then state="input"
elif [ -z "${arg/\[*/}" -a ! "$timespec" ]
then
timespec="$arg"
if [ "${arg/*]/}" ]
then state="time"
fi
elif [ -z "${arg/*@*/}" -o -z "${arg/*:*/}" ]
then
if [ -z "${arg/*@*/}" ]
then
if [ "${arg%%@*}" -a ! "$user" ]
then
user="${arg%%@*}"
fi
if [ "${arg#*@}" -a ! "$host" ]
then
host="${arg#*@}"
host="${host%%:*}"
fi
fi
if [ -z "${arg/*::*/}" ]
then
if [ "${arg#*::}" -a ! "$argdir" ]
then
argdir="${arg#*::}"
recursive_dir=0
fi
elif [ -z "${arg/*:*/}" ]
then
if [ "${arg#*:}" -a ! "$argdir" ]
then
argdir="${arg#*:}"
recursive_dir=1
fi
fi
else
search+="${arg}.*"
fi
done
if [ "$argdir" ]
then
if [ -z "${argdir##~*}" ]
then directory="$(readlink -m -- "$HOME${argdir#\~}")"
else directory="$(readlink -m -- "$argdir")"
fi
fi
local start_time
local end_time
if [ "${timespec/*..*/}" ]
then
timespec="${timespec#[}"
timespec="$($__BASH_HISTORY_GDATE -d "${timespec%]}" '+%F')"
start_time="$timespec"
end_time="$timespec + 1day"
else
start_time="${timespec%..*}"
start_time="${start_time#[}"
end_time="${timespec#*..}"
end_time="${end_time%]}"
fi
if [ "$start_time" ]
then
start_time="$($__BASH_HISTORY_GDATE -d "$start_time" '+%F %T')"
if [ -z "$start_time" ]
then
return 1
fi
fi
if [ "$end_time" ]
then
end_time="$($__BASH_HISTORY_GDATE -d "$end_time" '+%F %T')"
if [ -z "$end_time" ]
then
return 1
fi
fi
if [ "$recursive_dir" = 0 ]
then gawk -vdirectory="$directory" -vstart_time="$start_time" -vend_time="$end_time" -vsearch="$search" -vhost="$host" -vuser="$user" \
'BEGIN { RS="\0"; FS="\t"; user_matcher="^"user"(@|$)"; host_matcher="[^@]*@"host;}
{ for(i = 5; i <= NF; i++) $4 = $4 "\t" $i}
index($2,directory) == 1 && length($2) == length(directory) {
if((length(start_time) == 0 || $3 >= start_time) &&
(length(end_time) == 0 || $3 <= end_time) &&
(length(user) == 0 || $1 ~ user_matcher ) &&
(length(host) == 0 || $1 ~ host_matcher ) &&
(length(search) == 0 || $4 ~ search )) printf "%s\t%s\t%s\t%s\0", $1,$2,$3,$4}' "$ALL_HISTORY_FILE"
elif [ "$directory" = "/" ]
then gawk -vdirectory="$directory" -vstart_time="$start_time" -vend_time="$end_time" -vsearch="$search" -vhost="$host" -vuser="$user" \
'BEGIN { RS="\0"; FS="\t"; user_matcher="^"user"(@|$)"; host_matcher="[^@]*@"host;}
{ for(i = 5; i <= NF; i++) $4 = $4 "\t" $i}
{ if((length(start_time) == 0 || $3 >= start_time) &&
(length(end_time) == 0 || $3 <= end_time) &&
(length(user) == 0 || $1 ~ user_matcher ) &&
(length(host) == 0 || $1 ~ host_matcher ) &&
(length(search) == 0 || $4 ~ search )) printf "%s\t%s\t%s\t%s\0", $1,$2,$3,$4}' "$ALL_HISTORY_FILE"
else gawk -vdirectory="$directory" -vstart_time="$start_time" -vend_time="$end_time" -vsearch="$search" -vhost="$host" -vuser="$user" \
'BEGIN { RS="\0"; FS="\t"; user_matcher="^"user"(@|$)"; host_matcher="[^@]*@"host;}
{ for(i = 5; i <= NF; i++) $4 = $4 "\t" $i}
index($2,directory) == 1 {
if((length(start_time) == 0 || $3 >= start_time) &&
(length(end_time) == 0 || $3 <= end_time) &&
(length(user) == 0 || $1 ~ user_matcher ) &&
(length(host) == 0 || $1 ~ host_matcher ) &&
(length(search) == 0 || $4 ~ search )) printf "%s\t%s\t%s\t%s\0", $1,$2,$3,$4}' "$ALL_HISTORY_FILE"
fi
}
_init_log_history () {
local histline
while read -r -d '' histline
do _PWD+=( "$histline" )
done < <( gawk 'BEGIN { RS="\0"; FS="\t"; }
{ a[$2] = NR }
END { PROCINFO["sorted_in"] = "@val_num_desc";
num = 0;
for (i in a) { printf "%s\0", i; num++; if (num == 10) break } }' \
"$ALL_HISTORY_FILE" )
local directory="$(pwd -P)"
if [ "$directory" != "$_PWD" ]
then
local match
local i
for i in {1..8}
do if [ "$directory" = "${_PWD[$i]}" ]
then unset _PWD[$i]
match="true"
fi
done
if [ -z "$match" ]
then unset _PWD[9]
fi
_PWD=( "$directory" "${_PWD[@]}" )
local directory="${_PWD[1]}"
fi
}
_init_log_history