-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathff
More file actions
executable file
·342 lines (318 loc) · 11 KB
/
ff
File metadata and controls
executable file
·342 lines (318 loc) · 11 KB
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
#!/bin/sh
# ff: a friendly wrapper around POSIX find, providing an interface similar to
# https://github.com/sharkdp/fd using only standard POSIX facilities. Install
# it by simply copying or symlinking this script into your PATH.
#
# Usage: ff [OPTIONS] [PATTERN] [PATH...]
# Run `ff --help` for more details.
# Copyright (c) 2025, Malcolm Inglis <https://minglis.id.au>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
set -eu
FF_PATH="$0"
FF_NAME="$(basename "$FF_PATH")"
FF_VERSION="0.1.0"
# Default configuration:
PATTERN=""
FIND_PATHS=""
SHOW_HIDDEN=false
FILE_TYPE=""
EXTENSION=""
MAX_DEPTH=""
EXCLUDE_PATTERN=""
FULL_PATH=false
FOLLOW_LINKS=false
COLOR="auto"
ABSOLUTE_PATH=false
# show_usage
# Prints brief documentation on the ff program's usage.
show_usage() {
cat <<EOF
Usage: $FF_NAME [OPTIONS] [PATTERN] [PATH...]
A friendly wrapper around POSIX find.
Arguments:
[PATTERN] Search pattern; glob syntax ?like*th[iI]s
[PATH...] Root directories to search (default: current directory)
Options:
-H, --hidden Search hidden files and directories
-a, --absolute-path Show absolute paths
-L, --follow Follow symbolic links
-p, --full-path Match against full path (not just filename)
-d, --max-depth NUM Maximum search depth
-t, --type TYPE Filter by type: f(ile), d(irectory), l(ink)
-e, --extension EXT Filter by file extension
-E, --exclude GLOB Exclude paths matching glob pattern
-c, --color WHEN Colorize output: always, never, auto (default: auto)
-V, --version Show version and license information
-h, --help Show this help message
Examples:
$FF_NAME config # all paths like *config* under pwd
$FF_NAME -e py script # all paths like *script*.py under pwd
$FF_NAME -t f "\.log$" /var/log # all files like *.log under /var/log
Read the source at: $FF_PATH
See version and license with: $FF_NAME --version
EOF
}
# show_version
# Prints the ff program path, version, checksum, and license.
show_version() {
cat <<EOF
$FF_NAME v$FF_VERSION [cksum: $(cksum "$FF_PATH")]
Copyright (C) 2025, Malcolm Inglis <https://minglis.id.au>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
EOF
}
# error_exit MESSAGE
# Print message to stderr and exit.
error_exit() {
echo "error: $1" >&2
exit 1
}
# require_argument OPTION [ARG]
# Exits with error message if there are fewer than two arguments.
require_argument() {
if [ $# -lt 2 ]; then
error_exit "$1 requires an argument"
fi
}
# validate_color_option OPTION
# Validates and outputs color argument for --color option.
validate_color_option() {
case "$1" in
always|never|auto) echo "$1" ;;
*) error_exit "invalid --color argument: '$1'; must be always/never/auto" ;;
esac
}
# parse_arguments [ARGS...]
# Parses ff's arguments to set the global configuration variables.
parse_arguments() {
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
show_usage
exit 0
;;
-V|--version)
show_version
exit 0
;;
-H|--hidden)
SHOW_HIDDEN=true
;;
-a|--absolute-path)
ABSOLUTE_PATH=true
;;
-L|--follow)
FOLLOW_LINKS=true
;;
-p|--full-path)
FULL_PATH=true
;;
-d|--max-depth)
require_argument "$@"
MAX_DEPTH="$2"
shift
;;
-t|--type)
require_argument "$@"
FILE_TYPE="$2"
shift
;;
-e|--extension)
require_argument "$@"
EXTENSION="$2"
shift
;;
-E|--exclude)
require_argument "$@"
EXCLUDE_PATTERN="$2"
shift
;;
-c|--color)
require_argument "$@"
COLOR="$(validate_color_option "$2")"
shift
;;
--)
shift
break
;;
-*)
error_exit "unknown option '$1'"
;;
*)
break
;;
esac
shift
done
# Setup global variables per --color option:
use_colors=false
if [ "$COLOR" = "always" ]; then
use_colors=true
elif [ "$COLOR" = "auto" ] &&
[ -t 1 ] &&
command -v tput >/dev/null 2>&1 &&
tput colors >/dev/null 2>&1; then
use_colors=true
fi
if [ "$use_colors" = "true" ]; then
RED="$(tput setaf 1)"
GREEN="$(tput setaf 2)"
BLUE="$(tput setaf 4)"
MAGENTA="$(tput setaf 5)"
CYAN="$(tput setaf 6)"
WHITE="$(tput setaf 7)"
BOLD="$(tput bold)"
RESET="$(tput sgr0)"
else
RED="" GREEN="" BLUE="" MAGENTA="" CYAN="" WHITE="" BOLD="" RESET=""
fi
# Pattern is the next argument after consuming options:
if [ $# -gt 0 ]; then
PATTERN="$1"
shift
fi
# And find paths are all arguments following that:
if [ $# -gt 0 ]; then
FIND_PATHS="$*"
else
FIND_PATHS="."
fi
}
# make_find_command
# Constructs the find command string based on the configuration.
make_find_command() {
cmd="find"
# Reflect `--follow` option to traverse symbolic links (must come before paths):
if [ "$FOLLOW_LINKS" = "true" ]; then
cmd="$cmd -L"
fi
# Add find paths; default to current directory:
if [ -z "$FIND_PATHS" ]; then
cmd="$cmd ."
else
for path in $FIND_PATHS; do
escaped_path="$(printf '%s' "$path" | sed "s/'/'\\\\''/g")"
cmd="$cmd '$escaped_path'"
done
fi
# Reflect `--max-depth` option to limit search depth:
if [ -n "$MAX_DEPTH" ]; then
cmd="$cmd -maxdepth $MAX_DEPTH"
fi
# Reflect `--type` option to control file types to search for:
if [ -n "$FILE_TYPE" ]; then
cmd="$cmd -type $FILE_TYPE"
fi
# Reflect `--hidden` option to enable searching dot-prefixed paths:
if [ "$SHOW_HIDDEN" = "false" ]; then
cmd="$cmd ! -path '*/.*'"
fi
# Reflect `--extension` option to control file extensions to search for:
if [ -n "$EXTENSION" ]; then
ext="$(printf '%s' "$EXTENSION" | sed 's/^\.//')"
cmd="$cmd -name '*.$ext'"
fi
# Reflect `--exclude` option to skip searching certain paths:
if [ -n "$EXCLUDE_PATTERN" ]; then
cmd="$cmd ! -path '$EXCLUDE_PATTERN'"
fi
# Reflect search pattern, if any:
if [ -n "$PATTERN" ]; then
if [ "$FULL_PATH" = "true" ]; then
cmd="$cmd -path '$PATTERN'"
else
cmd="$cmd -name '$PATTERN'"
fi
fi
# Output the full `find` command:
echo "$cmd"
}
# path_per_config PATH
# Outputs the path as-is, or absolute if --absolute-path option enabled.
path_per_config() {
path="$1"
if [ "$ABSOLUTE_PATH" = "true" ]; then
path_dir="$(cd -- "$(dirname "$path")" 2>/dev/null && pwd)"
path_base="$(basename -- "$path")"
printf '%s/%s' "$path_dir" "$path_base"
else
printf '%s' "$path"
fi
}
# print_path PATH
# Outputs file path with optional color, per file type and extension.
print_path() {
path="$(path_per_config "$1")"
if [ -z "$RED" ]; then
printf '%s' "$path"
else
if [ -d "$path" ]; then
printf '%s%s%s' "$BLUE$BOLD" "$path" "$RESET"
elif [ -L "$path" ]; then
printf '%s%s%s' "$CYAN" "$path" "$RESET"
elif [ -x "$path" ]; then
printf '%s%s%s' "$GREEN$BOLD" "$path" "$RESET"
else
case "$path" in
*.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tbz2|*.tar.xz|*.txz|*.zip|*.rar|*.7z)
printf '%s%s%s' "$RED" "$path" "$RESET" ;;
*.jpg|*.jpeg|*.png|*.gif|*.bmp|*.svg|*.ico)
printf '%s%s%s' "$MAGENTA" "$path" "$RESET" ;;
*.mp3|*.wav|*.ogg|*.flac|*.m4a)
printf '%s%s%s' "$CYAN" "$path" "$RESET" ;;
*.mp4|*.avi|*.mkv|*.mov|*.wmv|*.flv)
printf '%s%s%s' "$MAGENTA$BOLD" "$path" "$RESET" ;;
*.txt|*.md|*.rst|*.log)
printf '%s%s%s' "$WHITE" "$path" "$RESET" ;;
*)
printf '%s' "$path" ;;
esac
fi
fi
printf '\n'
}
# main [ARGS...]
# Main routine for the ff script.
main() {
parse_arguments "$@"
eval "$(make_find_command)" | while IFS= read -r path; do
print_path "$path"
done
}
main "$@"