| |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # `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. |
| _mustache_sed | _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 |
| |
| } |
| |
| # Paper over different versions of cat. |
| _mustache_cat() { |
| set +e |
| cat -A <"/dev/null" >"/dev/null" 2>&1 |
| _M_STATUS="$?" |
| set -e |
| if [ "$_M_STATUS" -eq 1 ] |
| then cat -e |
| else cat -A |
| fi |
| } |
| |
| # 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 |
| } |
| |
| # Paper over differences between GNU sed and BSD sed |
| _mustache_sed() { |
| _M_NEWLINE=" |
| " |
| set +e |
| sed -r <"/dev/null" >"/dev/null" 2>&1 |
| _M_STATUS="$?" |
| set -e |
| if [ "$_M_STATUS" -eq 1 ] |
| then sed -E "s/./&\\$_M_NEWLINE/g; s/\\\\/\\\\\\\\/g" |
| else sed -r "s/./&\\n/g; s/\\\\/\\\\\\\\/g" |
| fi |
| } |
| |
| # 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" | _mustache_cat >&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 |
| } |