-
Notifications
You must be signed in to change notification settings - Fork 2
/
commando.sh
executable file
·351 lines (290 loc) · 6.81 KB
/
commando.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
#!/usr/bin/env bash
#
# Commando script.
#
# Reads configuration from a few locations:
#
# Commands - $basedir/.$progname/commands/*.sh
# Project - $basedir/.$progname/config.sh
# User - $basedir/$progname.rc
#
# NOTE: $progname resolves to whatever the name of the commando script is (sans .sh suffix)
#
set -o errexit
set -o nounset
# compatibility check
if [ "$BASH_VERSINFO" -lt '4' ]; then
echo "ERROR: Incompatible Bash detected: $BASH $BASH_VERSINFO $BASH_VERSION"
exit 2
fi
#
# Output
#
function __output_helpers {
declare -g font_bold=''
declare -g font_normal=''
declare -g font_underline=''
declare -g font_standout=''
if [ -t 1 ]; then
local ncolors=$(tput colors)
if [ -n "$ncolors" -a "$ncolors" -ge 8 ]; then
font_normal=$(tput sgr0)
font_bold=$(tput bold)
font_underline=$(tput smul)
font_standout=$(tput smso)
fi
fi
function BOLD {
printf "${font_bold}$*${font_normal}"
}
function UL {
printf "${font_underline}$*${font_normal}"
}
# display fatal message and exit
function die {
printf "$(BOLD FATAL): $*\n" >&2
exit 1
}
# TODO: consider log_{error|warn|info|verbose|debug} naming
# display error message
function error {
printf "$(BOLD ERROR): $*\n" >&2
}
# display warning message
function warn {
printf "$(BOLD WARN): $*\n" >&2
}
# display message
function info {
printf "$(BOLD INFO): $*\n" >&2
}
# display verbose message if enabled
declare -g verbose='false'
function log {
if [ ${verbose} = 'true' ]; then
printf "$(BOLD VERBOSE): $*\n" >&2
fi
}
# display debug message if enabled
declare -g debug='false'
function debug {
if [ ${debug} = 'true' ]; then
printf "$(BOLD DEBUG): $*\n" >&2
fi
}
# wrap output of command with snip markers
function snip_output {
echo '----8<----'
"${@}"
echo '---->8----'
}
}
#
# Modules
#
function __module_system {
declare -gA defined_modules
declare -gA loaded_modules
declare -gA prepared_modules
function define_module {
local fn="$1"
local module_name="$2"
debug "Define module: $module_name -> $fn"
defined_modules[$module_name]=${fn}
}
function load_modules {
for source in "$@"; do
if [ -d "$source" ]; then
for script in ${source}/*.sh; do
load_module "$script"
done
elif [ -f "$source" ]; then
load_module "$source"
fi
done
debug "Loaded modules: ${!loaded_modules[@]}"
}
function load_module {
local script="$1"
if [ -f "$script" ]; then
local module_name="$(basename $script)"
debug "Load module: $module_name -> $script"
source "$script" "$module_name"
loaded_modules[$module_name]="$script"
else
warn "Missing: $script"
fi
}
function prepare_modules {
for module_name in ${!loaded_modules[@]}; do
prepare_module ${module_name}
done
log "Prepared modules: ${!prepared_modules[@]}"
}
function prepare_module {
local module_name=${1}
debug "Prepare module: $module_name"
# resolve and invoke module initializer
local fn=${defined_modules[$module_name]}
${fn}
prepared_modules[${module_name}]=${fn}
}
function require_module {
local module_name="$1"
debug "Require module: $module_name"
# skip if module has already been prepared
set +o nounset
if [ -n "${prepared_modules[${module_name}]}" ]; then
return
fi
set -o nounset
prepare_module $module_name
}
}
#
# Commands
#
function __command_system {
declare -gA defined_commands
function define_command {
local name=$1
local fn=$2
debug "Define command: $name -> $fn"
# ensure given function is actually a function
if [ "$(type -t $fn)" != 'function' ]; then
die "Invalid command: $name -> $fn"
fi
defined_commands[$name]="$fn"
}
# run a named command with optional arguments
function run_command {
local command="$1"
shift
if ${verbose}; then
log "Command: '$command'; ${#@} arguments"
for arg in "${@}"; do
log " '$arg'"
done
fi
# resolve command function
set +o nounset
local fn="${defined_commands[$command]}"
set -o nounset
if [ -z "$fn" ]; then
die "Invalid command: $command"
fi
# handle default options
local -a arguments
for opt in "$@"; do
case $opt in
# give all commands help options
-h|--help)
run_command help "$command"
return
;;
esac
done
${fn} "$@"
}
}
#
# Main
#
function __main {
# resolve this script name
declare -g basename=$(basename $0)
declare -g progname=$(basename -s .sh ${basename})
# determine fully-qualified base directory
declare -g basedir=$(dirname $0)
basedir=$(cd "$basedir" && pwd)
# re-run self with arguments
function self {
log "Running: $0 $*"
"$0" "$@"
}
# display usage and exit
function usage {
printf "\nusage: $basename [options] <command> [command-options]
options:
-h,--help Show usage
-v,--verbose Enable VERBOSE output
--debug Enable DEBUG output
-- Stop processing options
To see available commands:
$(BOLD ${basename} help)\n\n"
exit 2
}
cd "$basedir"
__module_system
__command_system
# parse options and build command-line (command + command-options)
local -a command_line
for opt in "$@"; do
local consume_remaining='false'
case $opt in
-h|--help)
usage
;;
-v|--verbose)
verbose='true'
shift
;;
--debug)
debug='true'
shift
;;
-*)
die "Unknown option: $opt"
;;
--)
shift
consume_remaining='true'
;;
*)
consume_remaining='true'
;;
esac
if [ "$consume_remaining" = 'true' ]; then
for extra in "$@"; do
command_line+=("$1")
shift
done
break
fi
done
# check if we have a command-line or not
set +o nounset
local have_command=false
if [ ${#command_line[@]} != 0 ]; then
have_command=true
fi
set -o nounset
if ${verbose}; then
log "Bash: $BASH $BASH_VERSINFO $BASH_VERSION"
log "Base name: $basename"
log "Program name: $progname"
log "Base directory: $basedir"
# explain command-line
if ${have_command}; then
log "Command line; ${#command_line[@]} arguments"
for arg in ${!command_line[@]}; do
log " '${command_line[$arg]}'"
done
fi
fi
load_modules ".$progname/library" ".$progname/config.sh"
prepare_modules
# load user customizations
source "$progname.rc"
# display usage if no arguments, else execute command
if ${have_command}; then
run_command "${command_line[@]}"
else
usage
fi
}
#
# Bootstrap
#
__output_helpers
__main "$@"