-
-
Save ramonpin/b11e5d38330dae271e879f53714fb8ad to your computer and use it in GitHub Desktop.
mo script for moustache on bash
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# | |
#/ Mo is a mustache template rendering software written in bash. It inserts | |
#/ environment variables into templates. | |
#/ | |
#/ Simply put, mo will change {{VARIABLE}} into the value of that | |
#/ environment variable. You can use {{#VARIABLE}}content{{/VARIABLE}} to | |
#/ conditionally display content or iterate over the values of an array. | |
#/ | |
#/ Learn more about mustache templates at https://mustache.github.io/ | |
#/ | |
#/ Simple usage: | |
#/ | |
#/ mo [--false] [--help] [--source=FILE] filenames... | |
#/ | |
#/ --fail-not-set - Fail upon expansion of an unset variable. | |
#/ --false - Treat the string "false" as empty for conditionals. | |
#/ --help - This message. | |
#/ --source=FILE - Load FILE into the environment before processing templates. | |
# | |
# Mo is under a MIT style licence with an additional non-advertising clause. | |
# See LICENSE.md for the full text. | |
# | |
# This is open source! Please feel free to contribute. | |
# | |
# https://github.com/tests-always-included/mo | |
# Public: Template parser function. Writes templates to stdout. | |
# | |
# $0 - Name of the mo file, used for getting the help message. | |
# --allow-function-arguments | |
# - Permit functions in templates to be called with additional | |
# arguments. This puts template data directly in to the path | |
# of an eval statement. Use with caution. | |
# --fail-not-set - Fail upon expansion of an unset variable. Default behavior | |
# is to silently ignore and expand into empty string. | |
# --false - Treat "false" as an empty value. You may set the | |
# MO_FALSE_IS_EMPTY environment variable instead to a non-empty | |
# value to enable this behavior. | |
# --help - Display a help message. | |
# --source=FILE - Source a file into the environment before processint | |
# template files. | |
# -- - Used to indicate the end of options. You may optionally | |
# use this when filenames may start with two hyphens. | |
# $@ - Filenames to parse. | |
# | |
# Mo uses the following environment variables: | |
# | |
# MO_ALLOW_FUNCTION_ARGUMENTS | |
# - When set to a non-empty value, this allows functions | |
# referenced in templates to receive additional | |
# options and arguments. This puts the content from the | |
# template directly into an eval statement. Use with | |
# extreme care. | |
# MO_FAIL_ON_UNSET - When set to a non-empty value, expansion of an unset | |
# env variable will be aborted with an error. | |
# MO_FALSE_IS_EMPTY - When set to a non-empty value, the string "false" | |
# will be treated as an empty value for the purposes | |
# of conditionals. | |
# MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate | |
# a help message. | |
# | |
# Returns nothing. | |
mo() ( | |
# This function executes in a subshell so IFS is reset. | |
# Namespace this variable so we don't conflict with desired values. | |
local moContent f2source files doubleHyphens | |
IFS=$' \n\t' | |
files=() | |
doubleHyphens=false | |
if [[ $# -gt 0 ]]; then | |
for arg in "$@"; do | |
if $doubleHyphens; then | |
# After we encounter two hyphens together, all the rest | |
# of the arguments are files. | |
files=("${files[@]}" "$arg") | |
else | |
case "$arg" in | |
-h|--h|--he|--hel|--help|-\?) | |
moUsage "$0" | |
exit 0 | |
;; | |
--allow-function-arguments) | |
# shellcheck disable=SC2030 | |
MO_ALLOW_FUNCTION_ARGUMENTS=true | |
;; | |
--fail-not-set) | |
# shellcheck disable=SC2030 | |
MO_FAIL_ON_UNSET=true | |
;; | |
--false) | |
# shellcheck disable=SC2030 | |
MO_FALSE_IS_EMPTY=true | |
;; | |
--source=*) | |
f2source="${arg#--source=}" | |
if [[ -f "$f2source" ]]; then | |
# shellcheck disable=SC1090 | |
. "$f2source" | |
else | |
echo "No such file: $f2source" >&2 | |
exit 1 | |
fi | |
;; | |
--) | |
# Set a flag indicating we've encountered double hyphens | |
doubleHyphens=true | |
;; | |
*) | |
# Every arg that is not a flag or a option should be a file | |
files=(${files[@]+"${files[@]}"} "$arg") | |
;; | |
esac | |
fi | |
done | |
fi | |
moGetContent moContent "${files[@]}" || return 1 | |
moParse "$moContent" "" true | |
) | |
# Internal: Call a function. | |
# | |
# $1 - Function to call | |
# $2 - Content to pass | |
# $3 - Additional arguments as a single string | |
# | |
# This can be dangerous, especially if you are using tags like | |
# {{someFunction ; rm -rf / }} | |
# | |
# Returns nothing. | |
moCallFunction() { | |
local moArgs | |
moArgs=() | |
# shellcheck disable=SC2031 | |
if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then | |
moArgs=$3 | |
fi | |
echo -n "$2" | eval "$1" "$moArgs" | |
} | |
# Internal: Scan content until the right end tag is found. Creates an array | |
# with the following members: | |
# | |
# [0] = Content before end tag | |
# [1] = End tag (complete tag) | |
# [2] = Content after end tag | |
# | |
# Everything using this function uses the "standalone tags" logic. | |
# | |
# $1 - Name of variable for the array | |
# $2 - Content | |
# $3 - Name of end tag | |
# $4 - If -z, do standalone tag processing before finishing | |
# | |
# Returns nothing. | |
moFindEndTag() { | |
local content remaining scanned standaloneBytes tag | |
#: Find open tags | |
scanned="" | |
moSplit content "$2" '{{' '}}' | |
while [[ "${#content[@]}" -gt 1 ]]; do | |
moTrimWhitespace tag "${content[1]}" | |
#: Restore content[1] before we start using it | |
content[1]='{{'"${content[1]}"'}}' | |
case $tag in | |
'#'* | '^'*) | |
#: Start another block | |
scanned="${scanned}${content[0]}${content[1]}" | |
moTrimWhitespace tag "${tag:1}" | |
moFindEndTag content "${content[2]}" "$tag" "loop" | |
scanned="${scanned}${content[0]}${content[1]}" | |
remaining=${content[2]} | |
;; | |
'/'*) | |
#: End a block - could be ours | |
moTrimWhitespace tag "${tag:1}" | |
scanned="$scanned${content[0]}" | |
if [[ "$tag" == "$3" ]]; then | |
#: Found our end tag | |
if [[ -z "${4-}" ]] && moIsStandalone standaloneBytes "$scanned" "${content[2]}" true; then | |
#: This is also a standalone tag - clean up whitespace | |
#: and move those whitespace bytes to the "tag" element | |
standaloneBytes=( $standaloneBytes ) | |
content[1]="${scanned:${standaloneBytes[0]}}${content[1]}${content[2]:0:${standaloneBytes[1]}}" | |
scanned="${scanned:0:${standaloneBytes[0]}}" | |
content[2]="${content[2]:${standaloneBytes[1]}}" | |
fi | |
local "$1" && moIndirectArray "$1" "$scanned" "${content[1]}" "${content[2]}" | |
return 0 | |
fi | |
scanned="$scanned${content[1]}" | |
remaining=${content[2]} | |
;; | |
*) | |
#: Ignore all other tags | |
scanned="${scanned}${content[0]}${content[1]}" | |
remaining=${content[2]} | |
;; | |
esac | |
moSplit content "$remaining" '{{' '}}' | |
done | |
#: Did not find our closing tag | |
scanned="$scanned${content[0]}" | |
local "$1" && moIndirectArray "$1" "${scanned}" "" "" | |
} | |
# Internal: Find the first index of a substring. If not found, sets the | |
# index to -1. | |
# | |
# $1 - Destination variable for the index | |
# $2 - Haystack | |
# $3 - Needle | |
# | |
# Returns nothing. | |
moFindString() { | |
local pos string | |
string=${2%%$3*} | |
[[ "$string" == "$2" ]] && pos=-1 || pos=${#string} | |
local "$1" && moIndirect "$1" "$pos" | |
} | |
# Internal: Generate a dotted name based on current context and target name. | |
# | |
# $1 - Target variable to store results | |
# $2 - Context name | |
# $3 - Desired variable name | |
# | |
# Returns nothing. | |
moFullTagName() { | |
if [[ -z "${2-}" ]] || [[ "$2" == *.* ]]; then | |
local "$1" && moIndirect "$1" "$3" | |
else | |
local "$1" && moIndirect "$1" "${2}.${3}" | |
fi | |
} | |
# Internal: Fetches the content to parse into a variable. Can be a list of | |
# partials for files or the content from stdin. | |
# | |
# $1 - Variable name to assign this content back as | |
# $2-@ - File names (optional) | |
# | |
# Returns nothing. | |
moGetContent() { | |
local content filename target | |
target=$1 | |
shift | |
if [[ "${#@}" -gt 0 ]]; then | |
content="" | |
for filename in "$@"; do | |
#: This is so relative paths work from inside template files | |
content="$content"'{{>'"$filename"'}}' | |
done | |
else | |
moLoadFile content /dev/stdin || return 1 | |
fi | |
local "$target" && moIndirect "$target" "$content" | |
} | |
# Internal: Indent a string, placing the indent at the beginning of every | |
# line that has any content. | |
# | |
# $1 - Name of destination variable to get an array of lines | |
# $2 - The indent string | |
# $3 - The string to reindent | |
# | |
# Returns nothing. | |
moIndentLines() { | |
local content fragment len posN posR result trimmed | |
result="" | |
#: Remove the period from the end of the string. | |
len=$((${#3} - 1)) | |
content=${3:0:$len} | |
if [[ -z "${2-}" ]]; then | |
local "$1" && moIndirect "$1" "$content" | |
return 0 | |
fi | |
moFindString posN "$content" $'\n' | |
moFindString posR "$content" $'\r' | |
while [[ "$posN" -gt -1 ]] || [[ "$posR" -gt -1 ]]; do | |
if [[ "$posN" -gt -1 ]]; then | |
fragment="${content:0:$posN + 1}" | |
content=${content:$posN + 1} | |
else | |
fragment="${content:0:$posR + 1}" | |
content=${content:$posR + 1} | |
fi | |
moTrimChars trimmed "$fragment" false true " " $'\t' $'\n' $'\r' | |
if [[ -n "$trimmed" ]]; then | |
fragment="$2$fragment" | |
fi | |
result="$result$fragment" | |
moFindString posN "$content" $'\n' | |
moFindString posR "$content" $'\r' | |
# If the content ends in a newline, do not indent. | |
if [[ "$posN" -eq ${#content} ]]; then | |
# Special clause for \r\n | |
if [[ "$posR" -eq "$((posN - 1))" ]]; then | |
posR=-1 | |
fi | |
posN=-1 | |
fi | |
if [[ "$posR" -eq ${#content} ]]; then | |
posR=-1 | |
fi | |
done | |
moTrimChars trimmed "$content" false true " " $'\t' | |
if [[ -n "$trimmed" ]]; then | |
content="$2$content" | |
fi | |
result="$result$content" | |
local "$1" && moIndirect "$1" "$result" | |
} | |
# Internal: Send a variable up to the parent of the caller of this function. | |
# | |
# $1 - Variable name | |
# $2 - Value | |
# | |
# Examples | |
# | |
# callFunc () { | |
# local "$1" && moIndirect "$1" "the value" | |
# } | |
# callFunc dest | |
# echo "$dest" # writes "the value" | |
# | |
# Returns nothing. | |
moIndirect() { | |
unset -v "$1" | |
printf -v "$1" '%s' "$2" | |
} | |
# Internal: Send an array as a variable up to caller of a function | |
# | |
# $1 - Variable name | |
# $2-@ - Array elements | |
# | |
# Examples | |
# | |
# callFunc () { | |
# local myArray=(one two three) | |
# local "$1" && moIndirectArray "$1" "${myArray[@]}" | |
# } | |
# callFunc dest | |
# echo "${dest[@]}" # writes "one two three" | |
# | |
# Returns nothing. | |
moIndirectArray() { | |
unset -v "$1" | |
# IFS must be set to a string containing space or unset in order for | |
# the array slicing to work regardless of the current IFS setting on | |
# bash 3. This is detailed further at | |
# https://github.com/fidian/gg-core/pull/7 | |
eval "$(printf "IFS= %s=(\"\${@:2}\") IFS=%q" "$1" "$IFS")" | |
} | |
# Internal: Determine if a given environment variable exists and if it is | |
# an array. | |
# | |
# $1 - Name of environment variable | |
# | |
# Be extremely careful. Even if strict mode is enabled, it is not honored | |
# in newer versions of Bash. Any errors that crop up here will not be | |
# caught automatically. | |
# | |
# Examples | |
# | |
# var=(abc) | |
# if moIsArray var; then | |
# echo "This is an array" | |
# echo "Make sure you don't accidentally use \$var" | |
# fi | |
# | |
# Returns 0 if the name is not empty, 1 otherwise. | |
moIsArray() { | |
# Namespace this variable so we don't conflict with what we're testing. | |
local moTestResult | |
moTestResult=$(declare -p "$1" 2>/dev/null) || return 1 | |
[[ "${moTestResult:0:10}" == "declare -a" ]] && return 0 | |
[[ "${moTestResult:0:10}" == "declare -A" ]] && return 0 | |
return 1 | |
} | |
# Internal: Determine if the given name is a defined function. | |
# | |
# $1 - Function name to check | |
# | |
# Be extremely careful. Even if strict mode is enabled, it is not honored | |
# in newer versions of Bash. Any errors that crop up here will not be | |
# caught automatically. | |
# | |
# Examples | |
# | |
# moo () { | |
# echo "This is a function" | |
# } | |
# if moIsFunction moo; then | |
# echo "moo is a defined function" | |
# fi | |
# | |
# Returns 0 if the name is a function, 1 otherwise. | |
moIsFunction() { | |
local functionList functionName | |
functionList=$(declare -F) | |
functionList=( ${functionList//declare -f /} ) | |
for functionName in "${functionList[@]}"; do | |
if [[ "$functionName" == "$1" ]]; then | |
return 0 | |
fi | |
done | |
return 1 | |
} | |
# Internal: Determine if the tag is a standalone tag based on whitespace | |
# before and after the tag. | |
# | |
# Passes back a string containing two numbers in the format "BEFORE AFTER" | |
# like "27 10". It indicates the number of bytes remaining in the "before" | |
# string (27) and the number of bytes to trim in the "after" string (10). | |
# Useful for string manipulation: | |
# | |
# $1 - Variable to set for passing data back | |
# $2 - Content before the tag | |
# $3 - Content after the tag | |
# $4 - true/false: is this the beginning of the content? | |
# | |
# Examples | |
# | |
# moIsStandalone RESULT "$before" "$after" false || return 0 | |
# RESULT_ARRAY=( $RESULT ) | |
# echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}" | |
# | |
# Returns nothing. | |
moIsStandalone() { | |
local afterTrimmed beforeTrimmed char | |
moTrimChars beforeTrimmed "$2" false true " " $'\t' | |
moTrimChars afterTrimmed "$3" true false " " $'\t' | |
char=$((${#beforeTrimmed} - 1)) | |
char=${beforeTrimmed:$char} | |
# If the content before didn't end in a newline | |
if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]]; then | |
# and there was content or this didn't start the file | |
if [[ -n "$char" ]] || ! $4; then | |
# then this is not a standalone tag. | |
return 1 | |
fi | |
fi | |
char=${afterTrimmed:0:1} | |
# If the content after doesn't start with a newline and it is something | |
if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]] && [[ -n "$char" ]]; then | |
# then this is not a standalone tag. | |
return 2 | |
fi | |
if [[ "$char" == $'\r' ]] && [[ "${afterTrimmed:1:1}" == $'\n' ]]; then | |
char="$char"$'\n' | |
fi | |
local "$1" && moIndirect "$1" "$((${#beforeTrimmed})) $((${#3} + ${#char} - ${#afterTrimmed}))" | |
} | |
# Internal: Join / implode an array | |
# | |
# $1 - Variable name to receive the joined content | |
# $2 - Joiner | |
# $3-$* - Elements to join | |
# | |
# Returns nothing. | |
moJoin() { | |
local joiner part result target | |
target=$1 | |
joiner=$2 | |
result=$3 | |
shift 3 | |
for part in "$@"; do | |
result="$result$joiner$part" | |
done | |
local "$target" && moIndirect "$target" "$result" | |
} | |
# Internal: Read a file into a variable. | |
# | |
# $1 - Variable name to receive the file's content | |
# $2 - Filename to load | |
# | |
# Returns nothing. | |
moLoadFile() { | |
local content len | |
# The subshell removes any trailing newlines. We forcibly add | |
# a dot to the content to preserve all newlines. | |
# As a future optimization, it would be worth considering removing | |
# cat and replacing this with a read loop. | |
content=$(cat -- "$2" && echo '.') || return 1 | |
len=$((${#content} - 1)) | |
content=${content:0:$len} # Remove last dot | |
local "$1" && moIndirect "$1" "$content" | |
} | |
# Internal: Process a chunk of content some number of times. Writes output | |
# to stdout. | |
# | |
# $1 - Content to parse repeatedly | |
# $2 - Tag prefix (context name) | |
# $3-@ - Names to insert into the parsed content | |
# | |
# Returns nothing. | |
moLoop() { | |
local content context contextBase | |
content=$1 | |
contextBase=$2 | |
shift 2 | |
while [[ "${#@}" -gt 0 ]]; do | |
moFullTagName context "$contextBase" "$1" | |
moParse "$content" "$context" false | |
shift | |
done | |
} | |
# Internal: Parse a block of text, writing the result to stdout. | |
# | |
# $1 - Block of text to change | |
# $2 - Current name (the variable NAME for what {{.}} means) | |
# $3 - true when no content before this, false otherwise | |
# | |
# Returns nothing. | |
moParse() { | |
# Keep naming variables mo* here to not overwrite needed variables | |
# used in the string replacements | |
local moArgs moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag | |
moCurrent=$2 | |
moIsBeginning=$3 | |
# Find open tags | |
moSplit moContent "$1" '{{' '}}' | |
while [[ "${#moContent[@]}" -gt 1 ]]; do | |
moTrimWhitespace moTag "${moContent[1]}" | |
moNextIsBeginning=false | |
case $moTag in | |
'#'*) | |
# Loop, if/then, or pass content through function | |
# Sets context | |
moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" | |
moTrimWhitespace moTag "${moTag:1}" | |
# Split arguments from the tag name. Arguments are passed to | |
# functions. | |
moArgs=$moTag | |
moTag=${moTag%% *} | |
moTag=${moTag%%$'\t'*} | |
moArgs=${moArgs:${#moTag}} | |
moFindEndTag moBlock "$moContent" "$moTag" | |
moFullTagName moTag "$moCurrent" "$moTag" | |
if moTest "$moTag"; then | |
# Show / loop / pass through function | |
if moIsFunction "$moTag"; then | |
#: Consider piping the output to moGetContent | |
#: so the lambda does not execute in a subshell? | |
moContent=$(moCallFunction "$moTag" "${moBlock[0]}" "$moArgs") | |
moParse "$moContent" "$moCurrent" false | |
moContent="${moBlock[2]}" | |
elif moIsArray "$moTag"; then | |
eval "moLoop \"\${moBlock[0]}\" \"$moTag\" \"\${!${moTag}[@]}\"" | |
else | |
moParse "${moBlock[0]}" "$moCurrent" true | |
fi | |
fi | |
moContent="${moBlock[2]}" | |
;; | |
'>'*) | |
# Load partial - get name of file relative to cwd | |
moPartial moContent "${moContent[@]}" "$moIsBeginning" "$moCurrent" | |
moNextIsBeginning=${moContent[1]} | |
moContent=${moContent[0]} | |
;; | |
'/'*) | |
# Closing tag - If hit in this loop, we simply ignore | |
# Matching tags are found in moFindEndTag | |
moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" | |
;; | |
'^'*) | |
# Display section if named thing does not exist | |
moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" | |
moTrimWhitespace moTag "${moTag:1}" | |
moFindEndTag moBlock "$moContent" "$moTag" | |
moFullTagName moTag "$moCurrent" "$moTag" | |
if ! moTest "$moTag"; then | |
moParse "${moBlock[0]}" "$moCurrent" false "$moCurrent" | |
fi | |
moContent="${moBlock[2]}" | |
;; | |
'!'*) | |
# Comment - ignore the tag content entirely | |
# Trim spaces/tabs before the comment | |
moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" | |
;; | |
.) | |
# Current content (environment variable or function) | |
moStandaloneDenied moContent "${moContent[@]}" | |
moShow "$moCurrent" "$moCurrent" | |
;; | |
'=') | |
# Change delimiters | |
# Any two non-whitespace sequences separated by whitespace. | |
# This tag is ignored. | |
moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" | |
;; | |
'{'*) | |
# Unescaped - split on }}} not }} | |
moStandaloneDenied moContent "${moContent[@]}" | |
moContent="${moTag:1}"'}}'"$moContent" | |
moSplit moContent "$moContent" '}}}' | |
moTrimWhitespace moTag "${moContent[0]}" | |
moArgs=$moTag | |
moTag=${moTag%% *} | |
moTag=${moTag%%$'\t'*} | |
moArgs=${moArgs:${#moTag}} | |
moFullTagName moTag "$moCurrent" "$moTag" | |
moContent=${moContent[1]} | |
# Now show the value | |
# Quote moArgs here, do not quote it later. | |
moShow "$moTag" "$moCurrent" "$moArgs" | |
;; | |
'&'*) | |
# Unescaped | |
moStandaloneDenied moContent "${moContent[@]}" | |
moTrimWhitespace moTag "${moTag:1}" | |
moFullTagName moTag "$moCurrent" "$moTag" | |
moShow "$moTag" "$moCurrent" | |
;; | |
*) | |
# Normal environment variable or function call | |
moStandaloneDenied moContent "${moContent[@]}" | |
moArgs=$moTag | |
moTag=${moTag%% *} | |
moTag=${moTag%%$'\t'*} | |
moArgs=${moArgs:${#moTag}} | |
moFullTagName moTag "$moCurrent" "$moTag" | |
# Quote moArgs here, do not quote it later. | |
moShow "$moTag" "$moCurrent" "$moArgs" | |
;; | |
esac | |
moIsBeginning=$moNextIsBeginning | |
moSplit moContent "$moContent" '{{' '}}' | |
done | |
echo -n "${moContent[0]}" | |
} | |
# Internal: Process a partial. | |
# | |
# Indentation should be applied to the entire partial. | |
# | |
# This sends back the "is beginning" flag because the newline after a | |
# standalone partial is consumed. That newline is very important in the middle | |
# of content. We send back this flag to reset the processing loop's | |
# `moIsBeginning` variable, so the software thinks we are back at the | |
# beginning of a file and standalone processing continues to work. | |
# | |
# Prefix all variables. | |
# | |
# $1 - Name of destination variable. Element [0] is the content, [1] is the | |
# true/false flag indicating if we are at the beginning of content. | |
# $2 - Content before the tag that was not yet written | |
# $3 - Tag content | |
# $4 - Content after the tag | |
# $5 - true/false: is this the beginning of the content? | |
# $6 - Current context name | |
# | |
# Returns nothing. | |
moPartial() { | |
# Namespace variables here to prevent conflicts. | |
local moContent moFilename moIndent moIsBeginning moPartial moStandalone moUnindented | |
if moIsStandalone moStandalone "$2" "$4" "$5"; then | |
moStandalone=( $moStandalone ) | |
echo -n "${2:0:${moStandalone[0]}}" | |
moIndent=${2:${moStandalone[0]}} | |
moContent=${4:${moStandalone[1]}} | |
moIsBeginning=true | |
else | |
moIndent="" | |
echo -n "$2" | |
moContent=$4 | |
moIsBeginning=$5 | |
fi | |
moTrimWhitespace moFilename "${3:1}" | |
# Execute in subshell to preserve current cwd and environment | |
( | |
# It would be nice to remove `dirname` and use a function instead, | |
# but that's difficult when you're only given filenames. | |
cd "$(dirname -- "$moFilename")" || exit 1 | |
moUnindented="$( | |
moLoadFile moPartial "${moFilename##*/}" || exit 1 | |
moParse "${moPartial}" "$6" true | |
# Fix bash handling of subshells and keep trailing whitespace. | |
# This is removed in moIndentLines. | |
echo -n "." | |
)" || exit 1 | |
moIndentLines moPartial "$moIndent" "$moUnindented" | |
echo -n "$moPartial" | |
) || exit 1 | |
# If this is a standalone tag, the trailing newline after the tag is | |
# removed and the contents of the partial are added, which typically | |
# contain a newline. We need to send a signal back to the processing | |
# loop that the moIsBeginning flag needs to be turned on again. | |
# | |
# [0] is the content, [1] is that flag. | |
local "$1" && moIndirectArray "$1" "$moContent" "$moIsBeginning" | |
} | |
# Internal: Show an environment variable or the output of a function to | |
# stdout. | |
# | |
# Limit/prefix any variables used. | |
# | |
# $1 - Name of environment variable or function | |
# $2 - Current context | |
# $3 - Arguments string if $1 is a function | |
# | |
# Returns nothing. | |
moShow() { | |
# Namespace these variables | |
local moJoined moNameParts | |
if moIsFunction "$1"; then | |
CONTENT=$(moCallFunction "$1" "" "$3") | |
moParse "$CONTENT" "$2" false | |
return 0 | |
fi | |
moSplit moNameParts "$1" "." | |
if [[ -z "${moNameParts[1]-}" ]]; then | |
if moIsArray "$1"; then | |
eval moJoin moJoined "," "\${$1[@]}" | |
echo -n "$moJoined" | |
else | |
# shellcheck disable=SC2031 | |
if moTestVarSet "$1"; then | |
echo -n "${!1}" | |
elif [[ -n "${MO_FAIL_ON_UNSET-}" ]]; then | |
echo "Env variable not set: $1" >&2 | |
exit 1 | |
fi | |
fi | |
else | |
# Further subindexes are disallowed | |
eval "echo -n \"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\"" | |
fi | |
} | |
# Internal: Split a larger string into an array. | |
# | |
# $1 - Destination variable | |
# $2 - String to split | |
# $3 - Starting delimiter | |
# $4 - Ending delimiter (optional) | |
# | |
# Returns nothing. | |
moSplit() { | |
local pos result | |
result=( "$2" ) | |
moFindString pos "${result[0]}" "$3" | |
if [[ "$pos" -ne -1 ]]; then | |
# The first delimiter was found | |
result[1]=${result[0]:$pos + ${#3}} | |
result[0]=${result[0]:0:$pos} | |
if [[ -n "${4-}" ]]; then | |
moFindString pos "${result[1]}" "$4" | |
if [[ "$pos" -ne -1 ]]; then | |
# The second delimiter was found | |
result[2]="${result[1]:$pos + ${#4}}" | |
result[1]="${result[1]:0:$pos}" | |
fi | |
fi | |
fi | |
local "$1" && moIndirectArray "$1" "${result[@]}" | |
} | |
# Internal: Handle the content for a standalone tag. This means removing | |
# whitespace (not newlines) before a tag and whitespace and a newline after | |
# a tag. That is, assuming, that the line is otherwise empty. | |
# | |
# $1 - Name of destination "content" variable. | |
# $2 - Content before the tag that was not yet written | |
# $3 - Tag content (not used) | |
# $4 - Content after the tag | |
# $5 - true/false: is this the beginning of the content? | |
# | |
# Returns nothing. | |
moStandaloneAllowed() { | |
local bytes | |
if moIsStandalone bytes "$2" "$4" "$5"; then | |
bytes=( $bytes ) | |
echo -n "${2:0:${bytes[0]}}" | |
local "$1" && moIndirect "$1" "${4:${bytes[1]}}" | |
else | |
echo -n "$2" | |
local "$1" && moIndirect "$1" "$4" | |
fi | |
} | |
# Internal: Handle the content for a tag that is never "standalone". No | |
# adjustments are made for newlines and whitespace. | |
# | |
# $1 - Name of destination "content" variable. | |
# $2 - Content before the tag that was not yet written | |
# $3 - Tag content (not used) | |
# $4 - Content after the tag | |
# | |
# Returns nothing. | |
moStandaloneDenied() { | |
echo -n "$2" | |
local "$1" && moIndirect "$1" "$4" | |
} | |
# Internal: Determines if the named thing is a function or if it is a | |
# non-empty environment variable. When MO_FALSE_IS_EMPTY is set to a | |
# non-empty value, then "false" is also treated is an empty value. | |
# | |
# Do not use variables without prefixes here if possible as this needs to | |
# check if any name exists in the environment | |
# | |
# $1 - Name of environment variable or function | |
# $2 - Current value (our context) | |
# MO_FALSE_IS_EMPTY - When set to a non-empty value, this will say the | |
# string value "false" is empty. | |
# | |
# Returns 0 if the name is not empty, 1 otherwise. When MO_FALSE_IS_EMPTY | |
# is set, this returns 1 if the name is "false". | |
moTest() { | |
# Test for functions | |
moIsFunction "$1" && return 0 | |
if moIsArray "$1"; then | |
# Arrays must have at least 1 element | |
eval "[[ \"\${#${1}[@]}\" -gt 0 ]]" && return 0 | |
else | |
# If MO_FALSE_IS_EMPTY is set, then return 1 if the value of | |
# the variable is "false". | |
# shellcheck disable=SC2031 | |
[[ -n "${MO_FALSE_IS_EMPTY-}" ]] && [[ "${!1-}" == "false" ]] && return 1 | |
# Environment variables must not be empty | |
[[ -n "${!1-}" ]] && return 0 | |
fi | |
return 1 | |
} | |
# Internal: Determine if a variable is assigned, even if it is assigned an empty | |
# value. | |
# | |
# $1 - Variable name to check. | |
# | |
# Returns true (0) if the variable is set, 1 if the variable is unset. | |
moTestVarSet() { | |
[[ "${!1-a}" == "${!1-b}" ]] | |
} | |
# Internal: Trim the leading whitespace only. | |
# | |
# $1 - Name of destination variable | |
# $2 - The string | |
# $3 - true/false - trim front? | |
# $4 - true/false - trim end? | |
# $5-@ - Characters to trim | |
# | |
# Returns nothing. | |
moTrimChars() { | |
local back current front last target varName | |
target=$1 | |
current=$2 | |
front=$3 | |
back=$4 | |
last="" | |
shift 4 # Remove target, string, trim front flag, trim end flag | |
while [[ "$current" != "$last" ]]; do | |
last=$current | |
for varName in "$@"; do | |
$front && current="${current/#$varName}" | |
$back && current="${current/%$varName}" | |
done | |
done | |
local "$target" && moIndirect "$target" "$current" | |
} | |
# Internal: Trim leading and trailing whitespace from a string. | |
# | |
# $1 - Name of variable to store trimmed string | |
# $2 - The string | |
# | |
# Returns nothing. | |
moTrimWhitespace() { | |
local result | |
moTrimChars result "$2" true true $'\r' $'\n' $'\t' " " | |
local "$1" && moIndirect "$1" "$result" | |
} | |
# Internal: Displays the usage for mo. Pulls this from the file that | |
# contained the `mo` function. Can only work when the right filename | |
# comes is the one argument, and that only happens when `mo` is called | |
# with `$0` set to this file. | |
# | |
# $1 - Filename that has the help message | |
# | |
# Returns nothing. | |
moUsage() { | |
grep '^#/' "${MO_ORIGINAL_COMMAND}" | cut -c 4- | |
echo "" | |
set | grep ^MO_VERSION= | |
} | |
# Save the original command's path for usage later | |
MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}" | |
MO_VERSION="2.0.4" | |
# If sourced, load all functions. | |
# If executed, perform the actions as expected. | |
if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then | |
mo "$@" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you want to download this script into your machine:
This is intended to use behind proxies which disable access to raw data on github.