-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsstr
executable file
·418 lines (376 loc) · 10.2 KB
/
sstr
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
#!/bin/bash
#
# @(#) Utility to search strings in files interactively.
#
# Copyright (c) 2010 Shinya Ishida. All rights reserved.
#
# Search a keyword in files located in a directory and its subdirectories.
# The search result is printed with file names and line numbers. Additional
# actions to the search result (extra searches, viewing/editing a file, etc)
# can be applied interactively.
readonly me="$0"
readonly my_base="${me##*/}"
default_dir=$(pwd)
readonly default_dir
readonly tmp_prefix="/tmp/${my_base}.$$"
readonly tmp_init="${tmp_prefix}_init"
readonly tmp_current="${tmp_prefix}_current"
readonly tmp_previous="${tmp_prefix}_previous"
# search options
search_dir="$default_dir"
grep_options='-Inr'
ignore_cases=
quick_undo=
debug_mode=
# variables to store search history
pattern_history=()
ignore_cases_history=()
invert_history=()
last_epoch=-1 # note: can be removed after bash on macOS migrates to version 4
function __clean {
rm -f "$tmp_prefix"* &>/dev/null
}
function __exit_with {
local -r msg="${2:-unexpected error}"
__clean
case "$1" in
0)
exit 0;;
*)
echo -e "\033[31mERROR:\033[m $msg" 1>&2
exit "$1";;
esac
}
trap '__exit_with 127 ctrl-c' 1 2 3 15
function __toggle_debug_mode {
if [ "$debug_mode" ]; then
debug_mode=
set +x
else
debug_mode=y
set -x
fi
}
function __get_first_available_command {
local first
for c in "$@"; do
which "$c" &>/dev/null && first="$c" && break
done
echo -n "$first"
}
function __print_usage {
cat <<EOF
usage:
$my_base [options] <pattern>
options:
-c case-sensitive search
-d dir search pattern in the directory
-D debug mode
-h print this help
-x file exclude file from pattern search
-X dir skip pattern search in the directory
note:
- Binary files are ignored.
- Multiple '-x' and '-X' options are accepted.
EOF
__exit_with 0
}
function __toggle_case_sensitivity {
ignore_cases=$([ "$ignore_cases" ] && echo '' || echo '-i')
}
function __set_search_dir {
search_dir="${search_dir:-$1}"
}
function __exclude_file {
grep_options="$grep_options${1:+ --exclude=$1}"
}
function __exclude_directory {
grep_options="$grep_options${1:+ --exclude-dir=$1}"
}
function __enable_debug_mode {
__toggle_debug_mode
}
function __get_hit_count {
local wc_output
read -ra wc_output <<< "$(wc -l "$tmp_current" 2>/dev/null)"
echo "${wc_output[0]}"
}
function __highlight_found_patterns {
local epoch="$last_epoch"
while [ "${invert_history[epoch]}" ] && [ "$epoch" -gt 0 ]; do
((epoch-=1))
done
# shellcheck disable=SC2086
grep --no-filename --color=always ${ignore_cases_history[epoch]} \
"${pattern_history[epoch]}" "$tmp_current"
}
pager=$(__get_first_available_command more less)
readonly pager
case "$pager" in
more|less)
function __print_found_patterns {
__highlight_found_patterns | $pager -NR
};;
*)
function __print_found_patterns {
__highlight_found_patterns | cat -ne
};;
esac
function __conduct_first_search {
local pattern="$1"
((last_epoch+=1))
pattern_history[last_epoch]="$pattern"
ignore_cases_history[last_epoch]="$ignore_cases"
invert_history[last_epoch]=''
# shellcheck disable=SC2086
grep $grep_options $ignore_cases "$pattern" "$search_dir" > "$tmp_init" || \
__exit_with 1 'no matches'
cp -pf "$tmp_init" "$tmp_current"
}
function __search_entries {
local pattern
pattern=$(__prompt_input 'pattern')
((last_epoch+=1))
pattern_history[last_epoch]="$pattern"
ignore_cases_history[last_epoch]="$ignore_cases"
invert_history[last_epoch]="$*"
cp -pf "$tmp_current" "$tmp_previous"
quick_undo=yes
# shellcheck disable=SC2086
grep "$@" $ignore_cases "$pattern" "$tmp_previous" > "$tmp_current"
}
function __select_entry {
local -r hit_count=$(__get_hit_count)
if [ "$hit_count" -gt 0 ]; then
local number
number=$(__prompt_input "number (1-$hit_count)")
[[ "$number" =~ ^[1-9][0-9]*$ ]] && \
[ "$number" -le "$hit_count" ] && \
head "-$number" "$tmp_current" | tail -1
fi
}
function __print_search_result {
local -r count=$(__get_hit_count)
[ "$count" -gt 0 ] && __print_found_patterns
echo -e "\033[32m$count line(s) matched\033[m"
}
function __print_command_list {
echo "$command_help"
}
function __prompt_input {
local input
echo -en "\033[33m$1: \033[m" >&2 && read -r input
echo "$input"
}
function __get_command_function {
local IFS=''
grep "^$1" <<< "$command_map" | cut -d ' ' -f 2
}
function __execute_command {
local -r cmd=$(__prompt_input 'command')
local -r cmd_func=$(__get_command_function "$cmd")
if [ "$cmd_func" ]; then
$cmd_func
else
__print_command_list
fi
}
# Command functions
#
# To add a new command, the function must have a comment in the following
# format. This special comment is parsed by this script itself and registered.
# '@func' is a tag to specify the function's name. '@key' is a tag to indicate
# an alphabetical letter assigned to call the function via the prompt. '@desc'
# is a tag for a brief description of the function. This description is
# printed in the command help. The note may contain scripts; i.e., variables,
# $var, and command/function calls, $(command...). Instead, to print letters
# and strings, which have a special meaning for bash script, as they are, they
# must be quoted with single quotation, like '(' and ')'.
#
# Format:
# # @func FUNCTION_NAME
# # @key K
# # @desc A BRIEF DESCRIPTION OF THE FUNCTION IN A LINE
#
# Note:
# - All tags are required, in the order above, for a function to register.
# - Only a tag can be in a line.
# - A tag line must start with '#'.
# - Only spaces, ' ', may be between the '#' and a tag.
# @func ToggleCaseSensitivity
# @key c
# @desc toggle case sensitivity
function ToggleCaseSensitivity {
__toggle_case_sensitivity
if [ "$ignore_cases" ]; then
echo 'ignore cases'
else
echo 'consider cases'
fi
}
# @func ToggleDebugMode
# @key D
# @desc toggle debug mode
function ToggleDebugMode {
__toggle_debug_mode
if [ "$debug_mode" ]; then
echo 'debug mode enabled'
else
echo 'debug mode disabled'
fi
}
# @func EditFile
# @key e
# @desc open a file in editor '('${editor-not available}')'
editor=$(__get_first_available_command vim vi)
readonly editor
case "$editor" in
vim|vi)
function EditFile {
read -r file_name line_num rest < <(__select_entry | tr ':' ' ') && \
$editor "+$line_num" "$file_name"
};;
*)
function EditFile {
echo 'no editors: cannot use this command in your environment'
};;
esac
# @func ShowHelp
# @key h
# @desc show this help
function ShowHelp {
__print_command_list
}
# @func ListFiles
# @key l
# @desc list files in the current search result
function ListFiles {
cut -d : -f 1 "$tmp_current" | sort -u
}
# @func SearchEntriesContainingPattern
# @key m
# @desc get lines containing substring matching a specified pattern
function SearchEntriesContainingPattern {
__search_entries
__print_search_result
}
# @func SearchEntriesNotContainingPattern
# @key n
# @desc get lines not containing substring matching a specified pattern
function SearchEntriesNotContainingPattern {
__search_entries -v
__print_search_result
}
# @func PrintSearchResult
# @key p
# @desc print search result
function PrintSearchResult {
__print_search_result
}
# @func Quit
# @key q
# @desc quit search
function Quit {
__exit_with 0
}
# @func RefreshSearch
# @key r
# @desc refresh search result
function RefreshSearch {
__rerun_searches
__print_search_result
}
# @func UndoSearch
# @key u
# @desc undo last search
function UndoSearch {
if [ "$last_epoch" -gt 0 ]; then
unset 'invert_history[last_epoch]'
unset 'ignore_cases_history[last_epoch]'
unset 'pattern_history[last_epoch]'
((last_epoch-=1))
if [ "${quick_undo-}" ]; then
[ -f "$tmp_previous" ] && cp -pf "$tmp_previous" "$tmp_current"
quick_undo=
else
cp -pf "$tmp_init" "$tmp_current"
__rerun_searches
fi
fi
__print_search_result
}
# @func ViewFile
# @key v
# @desc open a file in viewer '('${viewer-not available}')'
viewer=$(__get_first_available_command vim vi less more)
readonly viewer
case "$viewer" in
vim|vi)
function ViewFile {
read -r file_name line_num rest < <(__select_entry | tr ':' ' ') && \
$viewer -R "+$line_num" '+set nu' "$file_name"
};;
less|more)
function ViewFile {
read -r file_name line_num rest < <(__select_entry | tr ':' ' ') && \
$viewer "+$line_num" -N "$file_name"
};;
*)
function ViewFile {
local -r range=10
read -r file_name line_num rest < <(__select_entry | tr ':' ' ') && \
cat -n "$file_name" | head -$((line_num + range)) | tail -$((range * 2 + 1))
};;
esac
function __build_command_list {
# Note: Using associative arrays seems a better choice to implement this
# function. However, it is supported from bash version 4. The MacOS's
# default bash (/bin/bash) is version 3.
local cmd_docs
cmd_docs=$(grep '^# *@' "$me" | tr '\n' ' ')
cmd_docs=${cmd_docs//'# '/}
local IFS=$'\n'
# shellcheck disable=SC2206
cmd_docs=(${cmd_docs//@func /$'\n'})
IFS=' '
local i=0
while [ $i -lt "${#cmd_docs[@]}" ]; do
read -r func_name at_key key at_desc desc <<< "${cmd_docs[i]}"
[ "$func_name" ] && [ "$desc" ] && [ "$at_key" = '@key' ] && \
[ "$at_desc" = '@desc' ] && [[ "$key" =~ ^[a-zA-Z]$ ]] && {
command_map="${command_map+${command_map}$'\n'}$key $func_name"
command_help="${command_help+${command_help}$'\n'} $key: $desc"
}
((i+=1))
done
readonly command_map="$command_map"
readonly command_help="$command_help"
}
##### Main routine
__build_command_list || __exit_with 2 'failed initialization'
while getopts 'cd:x:X:hD' flag; do
case $flag in
c)
__toggle_case_sensitivity;;
d)
__set_search_dir "$OPTARG";;
x)
__exclude_file "$OPTARG";;
X)
__exclude_directory "$OPTARG";;
D)
__enable_debug_mode;;
h)
__print_usage;;
*)
__exit_with 5 "see help by '-h' option";;
esac
done
shift $((OPTIND - 1))
[ -d "$search_dir" ] || __exit_with 3 "directory '$search_dir' not found"
[ "$1" ] || __exit_with 4 'pattern not specified'
__conduct_first_search "$1"
__print_search_result
while true; do
__execute_command
done