-
Notifications
You must be signed in to change notification settings - Fork 0
/
mustache.sh
230 lines (194 loc) · 6.85 KB
/
mustache.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
# `mustache.sh`, Mustache in POSIX shell.
set -e
# File descriptor 3 is commandeered for debug output, which may end up being
# forwarded to standard error.
[ -z "$MUSTACHE_DEBUG" ] && exec 3>/dev/null || exec 3>&2
# File descriptor 4 is commandeered for use as a sink for literal and
# variable output of (inverted) sections that are not destined for standard
# output because their condition is not met.
exec 4>/dev/null
# File descriptor 5 is commandeered for capturing input for list processing.
exec 5>/dev/null
# Consume standard input one character at a time to render `mustache`(5)
# templates with data from the environment.
mustache() {
# Initialize the file descriptor to be used to emit characters. At
# times this value will be 4 to send output to `/dev/null`.
_M_FD=1
# IFS must only contain '\n' so as to be able to read space and tab
# characters from standard input one-at-a-time. The easiest way to
# convince it to actually contain the correct byte, and only the
# correct byte, is to use a single-quoted literal newline.
IFS='
'
# Consuming standard input one character at a time is quite a feat
# within the confines of POSIX shell. Bash's `read` builtin has
# `-n` for limiting the number of characters consumed. Here it is
# faked using `sed`(1) to place each character on its own line.
# The subtlety is that real newline characters are chomped so they
# must be indirectly detected by checking for zero-length
# characters, which is done as the character is emitted.
sed -r "
s/./&\\n/g
s/\\\\/\\\\\\\\/g
" | _mustache
# TODO Replace the original value of IFS. Be careful if it's unset.
}
# Process the one-character-per-line stream from `sed` via a state machine.
# This function will be called recursively in subshell environments to
# isolate nested section tags from the outside environment.
_mustache() {
# Always start by assuming a character is a literal.
_M_STATE="literal"
# The `read` builtin consumes one line at a time but by now each line
# contains only a single character.
while read _M_C
do
echo " _M_C: $_M_C (${#_M_C}), _M_STATE: $_M_STATE" >&3
echo "$_M_C" >&5
case "$_M_STATE" in
# Consume a single character literal. In the event this
# character and the previous character have been opening
# braces, progress to the "tag" state and initialize the
# tag name to the empty string (this invariant is relied
# on by the "tag" state). If this is the first opening
# brace, wait and see. Otherwise, emit this character.
"literal")
if [ -z "$_M_PREV_C" ]
then
case "$_M_C" in
"{") ;;
"") echo;;
*) printf "%s" "$_M_C";;
esac
else
case "$_M_PREV_C$_M_C" in
"{{") _M_STATE="tag" _M_TAG="";;
?"{") ;;
*)
[ "$_M_PREV_C" = "{" ] && printf "%s" "{"
[ -z "$_M_C" ] && echo || printf "%s" "$_M_C";;
esac
fi >&$_M_FD;;
# Consume the tag type and tag.
"tag")
case "$_M_PREV_C$_M_C" in
# A third opening brace in a row could be treated as
# a literal and the beginning of tag, as it is here,
# or as the beginning of a tag which begins with an
# opening brace.
"{{") printf "{" >&$_M_FD;;
# Note the type of this tag, defaulting to "variable".
"{#"|"{^"|"{/"|"{!"|"{>") _M_TAG_TYPE="$_M_C" _M_TAG="";;
# A variable tag must note the first character of the
# variable name. Since it's possible that an opening
# brace comes in the middle of the tag, check that
# this is indeed the beginning of the tag.
"{"?)
if [ -z "$_M_TAG" ]
then
_M_TAG_TYPE="variable" _M_TAG="$_M_C"
fi;;
# Two closing braces in a row closes the tag. The
# state resets to "literal" and the tag is processed,
# possibly in a subshell.
"}}")
_M_STATE="literal"
_mustache_tag;;
# A single closing brace is ignored at first.
?"}") ;;
# If the variable continues, the closing brace becomes
# part of the variable name.
"}"?) _M_TAG="$_M_TAG}";;
# Any other character becomes part of the variable name.
*) _M_TAG="$_M_TAG$_M_C";;
esac;;
esac
# This character becomes the previous character.
_M_PREV_C="$_M_C"
done
}
# Execute a tag surrounded by backticks. Remove the backticks first.
_mustache_cmd() {
_M_CMD="$*"
_M_CMD="${_M_CMD#"\`"}"
_M_CMD="${_M_CMD%"\`"}"
sh -c "$_M_CMD"
}
# Print an error message and GTFO. The message is the concatenation
# of all the arguments to this function.
_mustache_die() {
echo "mustache.sh: $*" >&2
exit 1
}
# Process a complete tag. Variables are emitted, sections are recursed
# into, comments are ignored, and (for now) partials raise an error.
_mustache_tag() {
case "$_M_TAG_TYPE" in
# Variable tags expand to the value of an environment variable
# or the empty string if the environment variable is unset.
#
# If the tag is surrounded by backticks, execute it as a shell
# command, instead, using standard output as its value.
#
# Since the variable tag has been completely consumed, return
# to the assumption that everything's a literal until proven
# otherwise for this character.
"variable")
case "$_M_TAG" in
"\`"*"\`") _mustache_cmd "$_M_TAG";;
*) eval printf "%s" "\"\$$_M_TAG\"";;
esac >&$_M_FD;;
# Section tags expand to the expanded value of the section's
# literals and tags if and only if the section tag is in the
# environment and non-empty. Inverted section tags expand
# if the section tag is empty or unset in the environment.
#
# If the tag is surrounded by backticks, execute it as a shell
# command, instead, and process the section once for each line
# of standard output (made available as `_M_LINE`).
#
# Sections not being expanded are redirected to `/dev/null`.
"#"|"^")
echo " # _M_TAG: $_M_TAG" >&3
_M_TAG_V="$(eval printf "%s" "\"\$$_M_TAG\"")"
case "$_M_TAG_TYPE" in
"#") [ -z "$_M_TAG_V" ] && _M_FD=4;;
"^") [ -n "$_M_TAG_V" ] && _M_FD=4;;
esac
case "$_M_TAG" in
"\`"*"\`")
_M_CAPTURE="$(_M_SECTION_TAG="$_M_TAG" _mustache 5>&1 >&4)"
echo " _M_CAPTURE: $_M_CAPTURE" | cat -A >&3
_mustache_cmd "$_M_TAG" | while read _M_LINE
do
echo " _M_LINE: $_M_LINE" >&3
(
_M_SECTION_TAG="$_M_TAG"
echo "$_M_CAPTURE" | _mustache
)
done;;
*)
(
_M_SECTION_TAG="$_M_TAG"
_mustache
);;
esac
_M_FD=1;;
# Closing tags for (inverted) sections must match the expected
# tag name. Any redirections made when the (inverted) section
# opened are reset when the section closes.
"/")
echo " / _M_TAG: $_M_TAG, _M_SECTION_TAG: $_M_SECTION_TAG" >&3
if [ "$_M_TAG" != "$_M_SECTION_TAG" ]
then
_mustache_die "mismatched closing tag $_M_TAG," \
"expected $_M_SECTION_TAG"
fi
exit;;
# Comments do nothing.
"!") ;;
# TODO Partials.
">") _mustache_die "{{>$_M_TAG}} syntax not implemented";;
esac
}