forked from antonbabenko/pre-commit-terraform
-
Notifications
You must be signed in to change notification settings - Fork 0
/
terraform_docs.sh
executable file
·804 lines (715 loc) · 24.4 KB
/
terraform_docs.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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
#!/usr/bin/env bash
set -e
main() {
declare argv
argv=$(getopt -o a: --long args: -- "$@") || return
eval "set -- $argv"
declare args
declare files
for argv; do
case $argv in
-a | --args)
shift
args="$1"
shift
;;
--)
shift
files="$@"
break
;;
esac
done
local hack_terraform_docs
hack_terraform_docs=$(terraform version | sed -n 1p | grep -c 0.12) || true
if [[ ! $(command -v terraform-docs) ]]; then
echo "ERROR: terraform-docs is required by terraform_docs pre-commit hook but is not installed or in the system's PATH."
exit 1
fi
local is_old_terraform_docs
is_old_terraform_docs=$(terraform-docs version | grep -o "v0.[1-7]" | tail -1)
if [[ -z "$is_old_terraform_docs" ]]; then # Using terraform-docs 0.8+ (preferred)
terraform_docs "0" "$args" "$files"
elif [[ "$hack_terraform_docs" == "1" ]]; then # Using awk script because terraform-docs is older than 0.8 and terraform 0.12 is used
if [[ ! $(command -v awk) ]]; then
echo "ERROR: awk is required for terraform-docs hack to work with Terraform 0.12."
exit 1
fi
tmp_file_awk=$(mktemp "${TMPDIR:-/tmp}/terraform-docs-XXXXXXXXXX")
terraform_docs_awk "$tmp_file_awk"
terraform_docs "$tmp_file_awk" "$args" "$files"
rm -f "$tmp_file_awk"
else # Using terraform 0.11 and no awk script is needed for that
terraform_docs "0" "$args" "$files"
fi
}
terraform_docs() {
readonly terraform_docs_awk_file="$1"
readonly args="$2"
readonly files="$3"
declare -a paths
declare -a tfvars_files
index=0
for file_with_path in $files; do
file_with_path="${file_with_path// /__REPLACED__SPACE__}"
paths[index]=$(dirname "$file_with_path")
if [[ "$file_with_path" == *".tfvars" ]]; then
tfvars_files+=("$file_with_path")
fi
((index += 1))
done
readonly tmp_file=$(mktemp)
readonly text_file="README.md"
for path_uniq in $(echo "${paths[*]}" | tr ' ' '\n' | sort -u); do
path_uniq="${path_uniq//__REPLACED__SPACE__/ }"
pushd "$path_uniq" > /dev/null
if [[ ! -f "$text_file" ]]; then
popd > /dev/null
continue
fi
if [[ "$terraform_docs_awk_file" == "0" ]]; then
# shellcheck disable=SC2086
terraform-docs md $args ./ > "$tmp_file"
else
# Can't append extension for mktemp, so renaming instead
tmp_file_docs=$(mktemp "${TMPDIR:-/tmp}/terraform-docs-XXXXXXXXXX")
mv "$tmp_file_docs" "$tmp_file_docs.tf"
tmp_file_docs_tf="$tmp_file_docs.tf"
awk -f "$terraform_docs_awk_file" ./*.tf > "$tmp_file_docs_tf"
# shellcheck disable=SC2086
terraform-docs md $args "$tmp_file_docs_tf" > "$tmp_file"
rm -f "$tmp_file_docs_tf"
fi
# Replace content between markers with the placeholder - https://stackoverflow.com/questions/1212799/how-do-i-extract-lines-between-two-line-delimiters-in-perl#1212834
perl -i -ne 'if (/BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK/../END OF PRE-COMMIT-TERRAFORM DOCS HOOK/) { print $_ if /BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK/; print "I_WANT_TO_BE_REPLACED\n$_" if /END OF PRE-COMMIT-TERRAFORM DOCS HOOK/;} else { print $_ }' "$text_file"
# Replace placeholder with the content of the file
perl -i -e 'open(F, "'"$tmp_file"'"); $f = join "", <F>; while(<>){if (/I_WANT_TO_BE_REPLACED/) {print $f} else {print $_};}' "$text_file"
rm -f "$tmp_file"
popd > /dev/null
done
}
terraform_docs_awk() {
readonly output_file=$1
cat << "EOF" > "$output_file"
# This script converts Terraform 0.12 variables/outputs to something suitable for `terraform-docs`
# As of terraform-docs v0.6.0, HCL2 is not supported. This script is a *dirty hack* to get around it.
# https://github.com/segmentio/terraform-docs/
# https://github.com/segmentio/terraform-docs/issues/62
# Script was originally found here: https://github.com/cloudposse/build-harness/blob/master/bin/terraform-docs.awk
{
if ( $0 ~ /\{/ ) {
braceCnt++
}
if ( $0 ~ /\}/ ) {
braceCnt--
}
# ----------------------------------------------------------------------------------------------
# variable|output "..." {
# ----------------------------------------------------------------------------------------------
# [END] variable/output block
if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) {
if (braceCnt == 0 && blockCnt > 0) {
blockCnt--
print $0
}
}
# [START] variable or output block started
if ($0 ~ /^[[:space:]]*(variable|output)[[:space:]][[:space:]]*"(.*?)"/) {
# Normalize the braceCnt and block (should be 1 now)
braceCnt = 1
blockCnt = 1
# [CLOSE] "default" and "type" block
blockDefaultCnt = 0
blockTypeCnt = 0
# Print variable|output line
print $0
}
# ----------------------------------------------------------------------------------------------
# default = ...
# ----------------------------------------------------------------------------------------------
# [END] multiline "default" continues/ends
if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt > 0) {
print $0
# Count opening blocks
blockDefaultCnt += gsub(/\(/, "")
blockDefaultCnt += gsub(/\[/, "")
blockDefaultCnt += gsub(/\{/, "")
# Count closing blocks
blockDefaultCnt -= gsub(/\)/, "")
blockDefaultCnt -= gsub(/\]/, "")
blockDefaultCnt -= gsub(/\}/, "")
}
# [START] multiline "default" statement started
if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) {
if ($0 ~ /^[[:space:]][[:space:]]*(default)[[:space:]][[:space:]]*=/) {
if ($3 ~ "null") {
print " default = \"null\""
} else {
print $0
# Count opening blocks
blockDefaultCnt += gsub(/\(/, "")
blockDefaultCnt += gsub(/\[/, "")
blockDefaultCnt += gsub(/\{/, "")
# Count closing blocks
blockDefaultCnt -= gsub(/\)/, "")
blockDefaultCnt -= gsub(/\]/, "")
blockDefaultCnt -= gsub(/\}/, "")
}
}
}
# ----------------------------------------------------------------------------------------------
# type = ...
# ----------------------------------------------------------------------------------------------
# [END] multiline "type" continues/ends
if (blockCnt > 0 && blockTypeCnt > 0 && blockDefaultCnt == 0) {
# The following 'print $0' would print multiline type definitions
#print $0
# Count opening blocks
blockTypeCnt += gsub(/\(/, "")
blockTypeCnt += gsub(/\[/, "")
blockTypeCnt += gsub(/\{/, "")
# Count closing blocks
blockTypeCnt -= gsub(/\)/, "")
blockTypeCnt -= gsub(/\]/, "")
blockTypeCnt -= gsub(/\}/, "")
}
# [START] multiline "type" statement started
if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) {
if ($0 ~ /^[[:space:]][[:space:]]*(type)[[:space:]][[:space:]]*=/ ) {
if ($3 ~ "object") {
print " type = \"object\""
} else {
# Convert multiline stuff into single line
if ($3 ~ /^[[:space:]]*list[[:space:]]*\([[:space:]]*$/) {
type = "list"
} else if ($3 ~ /^[[:space:]]*string[[:space:]]*\([[:space:]]*$/) {
type = "string"
} else if ($3 ~ /^[[:space:]]*map[[:space:]]*\([[:space:]]*$/) {
type = "map"
} else {
type = $3
}
# legacy quoted types: "string", "list", and "map"
if (type ~ /^[[:space:]]*"(.*?)"[[:space:]]*$/) {
print " type = " type
} else {
print " type = \"" type "\""
}
}
# Count opening blocks
blockTypeCnt += gsub(/\(/, "")
blockTypeCnt += gsub(/\[/, "")
blockTypeCnt += gsub(/\{/, "")
# Count closing blocks
blockTypeCnt -= gsub(/\)/, "")
blockTypeCnt -= gsub(/\]/, "")
blockTypeCnt -= gsub(/\}/, "")
}
}
# ----------------------------------------------------------------------------------------------
# description = ...
# ----------------------------------------------------------------------------------------------
# [PRINT] single line "description"
if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) {
if ($0 ~ /^[[:space:]][[:space:]]*description[[:space:]][[:space:]]*=/) {
print $0
}
}
# ----------------------------------------------------------------------------------------------
# value = ...
# ----------------------------------------------------------------------------------------------
## [PRINT] single line "value"
#if (blockCnt > 0 && blockTypeCnt == 0 && blockDefaultCnt == 0) {
# if ($0 ~ /^[[:space:]][[:space:]]*value[[:space:]][[:space:]]*=/) {
# print $0
# }
#}
# ----------------------------------------------------------------------------------------------
# Newlines, comments, everything else
# ----------------------------------------------------------------------------------------------
#if (blockTypeCnt == 0 && blockDefaultCnt == 0) {
# Comments with '#'
if ($0 ~ /^[[:space:]]*#/) {
print $0
}
# Comments with '//'
if ($0 ~ /^[[:space:]]*\/\//) {
print $0
}
# Newlines
if ($0 ~ /^[[:space:]]*$/) {
print $0
}
#}
}
EOF
}
getopt() {
# pure-getopt, a drop-in replacement for GNU getopt in pure Bash.
# version 1.4.3
#
# Copyright 2012-2018 Aron Griffis <aron@scampersand.com>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
_getopt_main() {
# Returns one of the following statuses:
# 0 success
# 1 error parsing parameters
# 2 error in getopt invocation
# 3 internal error
# 4 reserved for -T
#
# For statuses 0 and 1, generates normalized and shell-quoted
# "options -- parameters" on stdout.
declare parsed status
declare short long name flags
declare have_short=false
# Synopsis from getopt man-page:
#
# getopt optstring parameters
# getopt [options] [--] optstring parameters
# getopt [options] -o|--options optstring [options] [--] parameters
#
# The first form can be normalized to the third form which
# _getopt_parse() understands. The second form can be recognized after
# first parse when $short hasn't been set.
if [[ -n ${GETOPT_COMPATIBLE+isset} || $1 == [^-]* ]]; then
# Enable compatibility mode
flags=c$flags
# Normalize first to third synopsis form
set -- -o "$1" -- "${@:2}"
fi
# First parse always uses flags=p since getopt always parses its own
# arguments effectively in this mode.
parsed=$(_getopt_parse getopt ahl:n:o:qQs:TuV \
alternative,help,longoptions:,name:,options:,quiet,quiet-output,shell:,test,version \
p "$@")
status=$?
if [[ $status != 0 ]]; then
if [[ $status == 1 ]]; then
echo "Try \`getopt --help' for more information." >&2
# Since this is the first parse, convert status 1 to 2
status=2
fi
return $status
fi
eval "set -- $parsed"
while [[ $# -gt 0 ]]; do
case $1 in
-a | --alternative)
flags=a$flags
;;
-h | --help)
_getopt_help
return 2 # as does GNU getopt
;;
-l | --longoptions)
long="$long${long:+,}$2"
shift
;;
-n | --name)
name=$2
shift
;;
-o | --options)
short=$2
have_short=true
shift
;;
-q | --quiet)
flags=q$flags
;;
-Q | --quiet-output)
flags=Q$flags
;;
-s | --shell)
case $2 in
sh | bash)
flags=${flags//t/}
;;
csh | tcsh)
flags=t$flags
;;
*)
echo 'getopt: unknown shell after -s or --shell argument' >&2
echo "Try \`getopt --help' for more information." >&2
return 2
;;
esac
shift
;;
-u | --unquoted)
flags=u$flags
;;
-T | --test)
return 4
;;
-V | --version)
echo "pure-getopt 1.4.3"
return 0
;;
--)
shift
break
;;
esac
shift
done
if ! $have_short; then
# $short was declared but never set, not even to an empty string.
# This implies the second form in the synopsis.
if [[ $# == 0 ]]; then
echo 'getopt: missing optstring argument' >&2
echo "Try \`getopt --help' for more information." >&2
return 2
fi
short=$1
have_short=true
shift
fi
if [[ $short == -* ]]; then
# Leading dash means generate output in place rather than reordering,
# unless we're already in compatibility mode.
[[ $flags == *c* ]] || flags=i$flags
short=${short#?}
elif [[ $short == +* ]]; then
# Leading plus means POSIXLY_CORRECT, unless we're already in
# compatibility mode.
[[ $flags == *c* ]] || flags=p$flags
short=${short#?}
fi
# This should fire if POSIXLY_CORRECT is in the environment, even if
# it's an empty string. That's the difference between :+ and +
flags=${POSIXLY_CORRECT+p}$flags
_getopt_parse "${name:-getopt}" "$short" "$long" "$flags" "$@"
}
_getopt_parse() {
# Inner getopt parser, used for both first parse and second parse.
# Returns 0 for success, 1 for error parsing, 3 for internal error.
# In the case of status 1, still generates stdout with whatever could
# be parsed.
#
# $flags is a string of characters with the following meanings:
# a - alternative parsing mode
# c - GETOPT_COMPATIBLE
# i - generate output in place rather than reordering
# p - POSIXLY_CORRECT
# q - disable error reporting
# Q - disable normal output
# t - quote for csh/tcsh
# u - unquoted output
declare name="$1" short="$2" long="$3" flags="$4"
shift 4
# Split $long on commas, prepend double-dashes, strip colons;
# for use with _getopt_resolve_abbrev
declare -a longarr
_getopt_split longarr "$long"
longarr=("${longarr[@]/#/--}")
longarr=("${longarr[@]%:}")
longarr=("${longarr[@]%:}")
# Parse and collect options and parameters
declare -a opts params
declare o alt_recycled=false error=0
while [[ $# -gt 0 ]]; do
case $1 in
--)
params=("${params[@]}" "${@:2}")
break
;;
--*=*)
o=${1%%=*}
if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then
error=1
elif [[ ,"$long", == *,"${o#--}"::,* ]]; then
opts=("${opts[@]}" "$o" "${1#*=}")
elif [[ ,"$long", == *,"${o#--}":,* ]]; then
opts=("${opts[@]}" "$o" "${1#*=}")
elif [[ ,"$long", == *,"${o#--}",* ]]; then
if $alt_recycled; then o=${o#-}; fi
_getopt_err "$name: option '$o' doesn't allow an argument"
error=1
else
echo "getopt: assertion failed (1)" >&2
return 3
fi
alt_recycled=false
;;
--?*)
o=$1
if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then
error=1
elif [[ ,"$long", == *,"${o#--}",* ]]; then
opts=("${opts[@]}" "$o")
elif [[ ,"$long", == *,"${o#--}::",* ]]; then
opts=("${opts[@]}" "$o" '')
elif [[ ,"$long", == *,"${o#--}:",* ]]; then
if [[ $# -ge 2 ]]; then
shift
opts=("${opts[@]}" "$o" "$1")
else
if $alt_recycled; then o=${o#-}; fi
_getopt_err "$name: option '$o' requires an argument"
error=1
fi
else
echo "getopt: assertion failed (2)" >&2
return 3
fi
alt_recycled=false
;;
-*)
if [[ $flags == *a* ]]; then
# Alternative parsing mode!
# Try to handle as a long option if any of the following apply:
# 1. There's an equals sign in the mix -x=3 or -xy=3
# 2. There's 2+ letters and an abbreviated long match -xy
# 3. There's a single letter and an exact long match
# 4. There's a single letter and no short match
o=${1::2} # temp for testing #4
if [[ $1 == *=* || $1 == -?? || \
,$long, == *,"${1#-}"[:,]* || \
,$short, != *,"${o#-}"[:,]* ]]; then
o=$(_getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" 2> /dev/null)
case $? in
0)
# Unambiguous match. Let the long options parser handle
# it, with a flag to get the right error message.
set -- "-$1" "${@:2}"
alt_recycled=true
continue
;;
1)
# Ambiguous match, generate error and continue.
_getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" > /dev/null
error=1
shift
continue
;;
2)
# No match, fall through to single-character check.
true
;;
*)
echo "getopt: assertion failed (3)" >&2
return 3
;;
esac
fi
fi
o=${1::2}
if [[ "$short" == *"${o#-}"::* ]]; then
if [[ ${#1} -gt 2 ]]; then
opts=("${opts[@]}" "$o" "${1:2}")
else
opts=("${opts[@]}" "$o" '')
fi
elif [[ "$short" == *"${o#-}":* ]]; then
if [[ ${#1} -gt 2 ]]; then
opts=("${opts[@]}" "$o" "${1:2}")
elif [[ $# -ge 2 ]]; then
shift
opts=("${opts[@]}" "$o" "$1")
else
_getopt_err "$name: option requires an argument -- '${o#-}'"
error=1
fi
elif [[ "$short" == *"${o#-}"* ]]; then
opts=("${opts[@]}" "$o")
if [[ ${#1} -gt 2 ]]; then
set -- "$o" "-${1:2}" "${@:2}"
fi
else
if [[ $flags == *a* ]]; then
# Alternative parsing mode! Report on the entire failed
# option. GNU includes =value but we omit it for sanity with
# very long values.
_getopt_err "$name: unrecognized option '${1%%=*}'"
else
_getopt_err "$name: invalid option -- '${o#-}'"
if [[ ${#1} -gt 2 ]]; then
set -- "$o" "-${1:2}" "${@:2}"
fi
fi
error=1
fi
;;
*)
# GNU getopt in-place mode (leading dash on short options)
# overrides POSIXLY_CORRECT
if [[ $flags == *i* ]]; then
opts=("${opts[@]}" "$1")
elif [[ $flags == *p* ]]; then
params=("${params[@]}" "$@")
break
else
params=("${params[@]}" "$1")
fi
;;
esac
shift
done
if [[ $flags == *Q* ]]; then
true # generate no output
else
echo -n ' '
if [[ $flags == *[cu]* ]]; then
printf '%s -- %s' "${opts[*]}" "${params[*]}"
else
if [[ $flags == *t* ]]; then
_getopt_quote_csh "${opts[@]}" -- "${params[@]}"
else
_getopt_quote "${opts[@]}" -- "${params[@]}"
fi
fi
echo
fi
return $error
}
_getopt_err() {
if [[ $flags != *q* ]]; then
printf '%s\n' "$1" >&2
fi
}
_getopt_resolve_abbrev() {
# Resolves an abbrevation from a list of possibilities.
# If the abbreviation is unambiguous, echoes the expansion on stdout
# and returns 0. If the abbreviation is ambiguous, prints a message on
# stderr and returns 1. (For first parse this should convert to exit
# status 2.) If there is no match at all, prints a message on stderr
# and returns 2.
declare a q="$1"
declare -a matches
shift
for a; do
if [[ $q == "$a" ]]; then
# Exact match. Squash any other partial matches.
matches=("$a")
break
elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q" ]]; then
# Exact alternative match. Squash any other partial matches.
matches=("$a")
break
elif [[ $a == "$q"* ]]; then
# Abbreviated match.
matches=("${matches[@]}" "$a")
elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q"* ]]; then
# Abbreviated alternative match.
matches=("${matches[@]}" "${a#-}")
fi
done
case ${#matches[@]} in
0)
[[ $flags == *q* ]] ||
printf "$name: unrecognized option %s\\n" \
"$(_getopt_quote "$q")" >&2
return 2
;;
1)
printf '%s' "${matches[0]}"
return 0
;;
*)
[[ $flags == *q* ]] ||
printf "$name: option %s is ambiguous; possibilities: %s\\n" \
"$(_getopt_quote "$q")" "$(_getopt_quote "${matches[@]}")" >&2
return 1
;;
esac
}
_getopt_split() {
# Splits $2 at commas to build array specified by $1
declare IFS=,
eval "$1=( \$2 )"
}
_getopt_quote() {
# Quotes arguments with single quotes, escaping inner single quotes
declare s space q=\'
for s; do
printf "$space'%s'" "${s//$q/$q\\$q$q}"
space=' '
done
}
_getopt_quote_csh() {
# Quotes arguments with single quotes, escaping inner single quotes,
# bangs, backslashes and newlines
declare s i c space
for s; do
echo -n "$space'"
for ((i = 0; i < ${#s}; i++)); do
c=${s:i:1}
case $c in
\\ | \' | !)
echo -n "'\\$c'"
;;
$'\n')
echo -n "\\$c"
;;
*)
echo -n "$c"
;;
esac
done
echo -n \'
space=' '
done
}
_getopt_help() {
cat <<- EOT >&2
Usage:
getopt <optstring> <parameters>
getopt [options] [--] <optstring> <parameters>
getopt [options] -o|--options <optstring> [options] [--] <parameters>
Parse command options.
Options:
-a, --alternative allow long options starting with single -
-l, --longoptions <longopts> the long options to be recognized
-n, --name <progname> the name under which errors are reported
-o, --options <optstring> the short options to be recognized
-q, --quiet disable error reporting by getopt(3)
-Q, --quiet-output no normal output
-s, --shell <shell> set quoting conventions to those of <shell>
-T, --test test for getopt(1) version
-u, --unquoted do not quote the output
-h, --help display this help and exit
-V, --version output version information and exit
For more details see getopt(1).
EOT
}
_getopt_version_check() {
if [[ -z $BASH_VERSION ]]; then
echo "getopt: unknown version of bash might not be compatible" >&2
return 1
fi
# This is a lexical comparison that should be sufficient forever.
if [[ $BASH_VERSION < 2.05b ]]; then
echo "getopt: bash $BASH_VERSION might not be compatible" >&2
return 1
fi
return 0
}
_getopt_version_check
_getopt_main "$@"
declare status=$?
unset -f _getopt_main _getopt_err _getopt_parse _getopt_quote \
_getopt_quote_csh _getopt_resolve_abbrev _getopt_split _getopt_help \
_getopt_version_check
return $status
}
[[ $BASH_SOURCE != "$0" ]] || main "$@"