Compare commits

...

44 Commits

Author SHA1 Message Date
Tyler Akins
7e86c1a5f5
Detect when variables are declared and not set
It is possible to declare a variable but not assign a value to it using
`export x`. When you do this, `declare -p x` shows the variable but does
not show an "=" nor any value afterwards.

When running another command, this variable will not be added to the
environment variables even though it is flagged as exported. Most likely
it's because the value is not a string and can't be easily converted to
a string; this is the same behavior as arrays.

Using `[[ -v x ]]` is also inadequate and I believe its because there
are false positives when trying to access data, which goes on to break
tests and the new braces and parenthesis indirection. Perhaps it could
be reviewed and made to work.

The best solution so far is to combine `declare -p` with `[[ -v` to see
if the variable is declared and if a value is set.

Closes #75.
2024-07-24 12:22:03 -05:00
Tyler Akins
b595ad26b7
Documenting that parents are not supported 2024-06-21 20:44:16 -05:00
Tyler Akins
5a49fe9900
Releasing 3.0.6 2024-06-16 21:13:43 -05:00
Tyler Akins
8056ee6961
Only cut strings once
It's faster to loop through the string and check the character at an
index than it is to trim single characters from the end in a loop.
Trimming multiple characters in the loop is surprisingly slower than
trimming one.

Addresses more of the speed problem reported in #73.
2024-06-16 21:11:45 -05:00
Tyler Akins
5db34e55d3
Caching function name lookups
Faster than dumping functions each time.
Related to #73.
2024-06-16 21:11:02 -05:00
Tyler Akins
26ca5059d8
Fix slowness with larger templates
Closes #69 and #71
Released 3.0.5
2024-03-27 21:57:11 -05:00
Tyler Akins
6e57510ba9
Making it more clear that sourced files are shell
Addresses part of the concerns from #69
2023-11-16 16:04:52 -06:00
Tyler Akins
54b2184b70
Bumping version 2023-09-10 08:11:54 -05:00
Tyler Akins
84d17268c9
Work with symbolic links 2023-09-10 08:10:51 -05:00
Tyler Akins
68306c4c6d
More Bash 4.x compatibility issues found and fixed 2023-05-12 08:17:57 -05:00
Tyler Akins
6cc72acde0
Fix #67 - unbound variable error
Release 3.0.1
2023-05-12 07:44:48 -05:00
Tyler Akins
0ce41dedb1
Releasing 3.0.0 2023-05-07 21:18:00 -05:00
Tyler Akins
0617b2ed35
Various bugfixes
Specs: 181 total, 119 pass (with 7 overridden), 55 fail, 7 skip
2023-04-23 10:04:59 -05:00
Tyler Akins
1d4e186486
All tests pass.
Specs: 181 total, 111 pass (with 7 overridden), 63 fail, 7 skip.
2023-04-23 09:24:36 -05:00
Tyler Akins
0f150ccb19
Fixing standalone detection with blocks
27 tests pass, 18 still fail
2023-04-22 08:16:49 -05:00
Tyler Akins
00af7d49f0
Bugfixes 2023-04-21 21:18:55 -05:00
Tyler Akins
0896e2b966
Parser rewritten again 2023-04-21 20:59:31 -05:00
Tyler Akins
511a3187e8
Changing how parsed/unparsed content is tracked
Use global variables instead of passing to every function.

112 specs pass, 7 overridden, 62 fail, 7 skipped.
2023-04-10 21:07:45 -05:00
Tyler Akins
d32b912472
Moving delimiters and standalone to global vars 2023-04-10 13:24:06 -05:00
Tyler Akins
3a58ee390e
More bugfixes and shellcheck tweaks 2023-04-10 11:39:47 -05:00
Tyler Akins
7604ce3054
Pass shellcheck, more specs are handled, preserve function whitespace
This closes #49.
2023-04-10 08:10:14 -05:00
Tyler Akins
e0e9189355
Bugfixes, pretty printing, new tests added
Spec result: 108 specs pass, 6 skipped, 67 fail
2023-04-09 18:53:55 -05:00
Tyler Akins
a1e4398547
Overriding some tests so they work with mo + bugfix 2023-04-09 10:20:45 -05:00
Tyler Akins
47e10012ee
Splitting parse function, fixing variable bug 2023-04-09 09:52:18 -05:00
Tyler Akins
a237d23a2d
Fix standalone tag detection 2023-04-08 19:06:04 -05:00
Tyler Akins
72a0e426c7
Starting support for skipping/altering tests
Bash doesn't support advanced data structures, like objects. The goal is
to switch simple objects to associative arrays automatically, then
override other tests so they don't use deeply-created structures.
2023-04-08 12:41:46 -05:00
Tyler Akins
501ca2144f
Need to trim whitespace in the tag 2023-04-08 12:41:31 -05:00
Tyler Akins
2085dc2792
Rewriting how specs run 2023-04-08 11:52:22 -05:00
Tyler Akins
d997ad0e0a
Passes all of the tests 2023-04-07 23:10:47 -05:00
Tyler Akins
febd3467c8
Attempting to address shortcomings and whitespace issues 2023-04-07 19:35:25 -05:00
Tyler Akins
8e9fd680d4
Fixing a test, release 2.4.1 2023-04-05 21:14:19 -05:00
Tyler Akins
24e80aedaa
Bumping version
2.3.0 was already tagged
2023-04-05 20:55:16 -05:00
Tyler Akins
4311f29879
Fixing the build, some doc cleanup, update version 2023-04-05 20:54:27 -05:00
Tyler Akins
d6794de1e2
Merge pull request #57 from Swivelgames/loop-accessors
Feature: Key and Index Accessors Inside Loops
2023-04-05 20:51:13 -05:00
Joseph Dalrymple
b22baa9776 Added Functions/Helpers to README 2023-04-05 20:19:42 -05:00
Joseph Dalrymple
c28bffe708 Added @key loop accessor 2023-04-05 20:19:29 -05:00
Tyler Akins
b01fc43580
Merge pull request #60 from Swivelgames/portability
Improved portability by using /usr/bin/env pragma
2023-04-04 09:50:21 -05:00
Joseph Dalrymple
53e3208ba0 Improved portability by using /usr/bin/env pragma 2023-04-04 02:00:31 -05:00
Tyler Akins
6dc284f05a
Updating URLs
Closes #53
2023-04-03 12:57:14 -05:00
Tyler Akins
a62541fc98
Adding missing character, reported by brainchild0 2022-01-26 19:07:12 -06:00
Tyler Akins
b31a97cfb1
Merge pull request #51 from brainchild0/docker
improve docker image
2022-01-26 10:00:28 -06:00
Eric Levy
71f85fa4f2 Use latest Alpine image as Docker base 2022-01-25 17:34:56 -05:00
Eric Levy
14003ba24a Introduce general improvements for Docker image 2022-01-24 20:51:27 -05:00
Tyler Akins
54195a6c6e
Fixing test script to return 0 on success 2021-11-25 16:02:21 -06:00
151 changed files with 4154 additions and 2117 deletions

168
.eslintrc.yaml Normal file
View File

@ -0,0 +1,168 @@
parserOptions:
ecmaVersion: latest
sourceType: module
env:
es6: true
jasmine: true
node: true
extends: eslint:recommended
rules:
accessor-pairs: error
array-bracket-spacing:
- error
- never
array-callback-return: error
block-spacing:
- error
- never
brace-style: error
comma-dangle: error
comma-spacing: error
comma-style: error
complexity:
- error
- 10
computed-property-spacing: error
consistent-return: error
consistent-this: error
constructor-super: error
curly: error
default-case: error
dot-notation: error
eol-last: error
eqeqeq: error
generator-star-spacing: error
global-require: off
guard-for-in: error
jsx-quotes: error
key-spacing: error
keyword-spacing: error
linebreak-style: error
lines-around-comment:
- error
-
allowBlockStart: true
allowObjectStart: true
allowArrayStart: true
max-statements-per-line: error
new-cap: error
new-parens: error
no-array-constructor: error
no-bitwise: error
no-caller: error
no-case-declarations: error
no-catch-shadow: error
no-class-assign: error
no-cond-assign: error
no-confusing-arrow: error
no-console: off
no-const-assign: error
no-constant-condition: error
no-continue: error
no-delete-var: error
no-dupe-args: error
no-dupe-class-members: error
no-dupe-keys: error
no-duplicate-case: error
no-duplicate-imports: error
no-empty: off
no-empty-character-class: error
no-empty-pattern: error
no-eq-null: error
no-eval: error
no-extend-native: error
no-extra-bind: error
no-extra-boolean-cast: error
no-extra-label: error
no-extra-semi: error
no-fallthrough: error
no-func-assign: error
no-implied-eval: error
no-inner-declarations: error
no-invalid-this: error
no-invalid-regexp: error
no-irregular-whitespace: error
no-iterator: error
no-label-var: error
no-labels: error
no-lone-blocks: error
no-lonely-if: error
no-loop-func: error
no-mixed-spaces-and-tabs: error
no-multi-spaces: error
no-multi-str: error
no-multiple-empty-lines:
- error
-
max: 2
no-native-reassign: error
no-negated-condition: error
no-nested-ternary: error
no-new: error
no-new-func: error
no-new-object: error
no-new-symbol: error
no-new-wrappers: error
no-obj-calls: error
no-octal: error
no-octal-escape: error
no-path-concat: error
no-plusplus: error
no-proto: error
no-redeclare: error
no-regex-spaces: error
no-restricted-globals: error
no-return-assign: error
no-script-url: error
no-self-assign: error
no-self-compare: error
no-sequences: error
no-shadow: error
no-shadow-restricted-names: error
no-spaced-func: error
no-sparse-arrays: error
no-this-before-super: error
no-throw-literal: error
no-trailing-spaces: error
no-undef: error
no-undef-init: error
no-unexpected-multiline: error
no-unmodified-loop-condition: error
no-unneeded-ternary: error
no-unreachable: error
no-unsafe-finally: error
no-unused-expressions: error
no-unused-labels: error
no-unused-vars: error
no-useless-call: error
no-useless-computed-key: error
no-useless-concat: error
no-useless-constructor: error
no-useless-escape: error
no-void: error
no-warning-comments: warn
no-whitespace-before-property: error
no-with: error
operator-assignment: error
padded-blocks:
- error
- never
prefer-const: error
quote-props:
- error
- as-needed
radix: error
require-yield: error
semi: error
semi-spacing: error
space-before-blocks: error
space-in-parens: error
space-infix-ops:
- error
-
int32Hint: false
space-unary-ops: error
spaced-comment: error
use-isnan: error
valid-typeof: error
yield-star-spacing: error

783
API.md
View File

@ -12,132 +12,87 @@ Public: Template parser function. Writes templates to stdout.
* $0 - Name of the mo file, used for getting the help message.
* $@ - Filenames to parse.
Options:
Returns nothing.
--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. Not listed in the help because it only makes sense when mo is sourced.
`mo::debug()`
-------------
-u, --fail-not-set
Internal: Show a debug message
Fail upon expansion of an unset variable. Default behavior is to silently ignore and expand into empty string.
-x, --fail-on-function
Fail when a function used by a template returns an error status code. Alternately, ou may set the MO_FAIL_ON_FUNCTION environment variable to a non-empty value to enable this behavior.
-e, --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.
-h, --help
Display a help message.
-s=FILE, --source=FILE
Source a file into the environment before processing template files. This can be used multiple times.
--
Used to indicate the end of options. You may optionally use this when filenames may start with two hyphens.
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_FUNCTION_ARGS - Arguments passed to the function
* MO_FAIL_ON_FUNCTION - If a function returns a non-zero status code, abort with an error.
* 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.
* $1 - The debug message to show
Returns nothing.
`moCallFunction()`
------------------
`mo::debugShowState()`
----------------------
Internal: Call a function.
Internal: Show a debug message and internal state information
* $1 - Variable for output
* $2 - Function to call
* $3 - Content to pass
* $4 - Additional arguments as a single string
This can be dangerous, especially if you are using tags like {{someFunction ; rm -rf / }}
No arguments
Returns nothing.
`moFindEndTag()`
----------------
`mo::error()`
-------------
Internal: Scan content until the right end tag is found. Creates an array with the following members:
Internal: Show an error message and exit
[0] = Content before end tag
[1] = End tag (complete tag)
[2] = Content after end tag
* $1 - The error message to show
* $2 - Error code
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.
Returns nothing. Exits the program.
`moFindString()`
----------------
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.
`moFullTagName()`
`mo::errorNear()`
-----------------
Internal: Generate a dotted name based on current context and target name.
Internal: Show an error message with a snippet of context and exit
* $1 - Target variable to store results
* $2 - Context name
* $3 - Desired variable name
* $1 - The error message to show
* $2 - The starting point
* $3 - Error code
Returns nothing. Exits the program.
`mo::usage()`
-------------
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.
`moGetContent()`
`mo::content()`
---------------
Internal: Fetches the content to parse into MO_UNPARSED. Can be a list of partials for files or the content from stdin.
* $1 - Destination variable name
* $2-@ - File names (optional), read from stdin otherwise
Returns nothing.
`mo::contentFile()`
-------------------
Internal: Read a file into MO_UNPARSED.
* $1 - Destination variable name.
* $2 - Filename to load - if empty, defaults to /dev/stdin
Returns nothing.
`mo::indirect()`
----------------
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.
`moIndentLines()`
-----------------
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.
`moIndirect()`
--------------
Internal: Send a variable up to the parent of the caller of this function.
* $1 - Variable name
@ -146,7 +101,7 @@ Internal: Send a variable up to the parent of the caller of this function.
Examples
callFunc () {
local "$1" && moIndirect "$1" "the value"
local "$1" && mo::indirect "$1" "the value"
}
callFunc dest
echo "$dest" # writes "the value"
@ -154,19 +109,19 @@ Examples
Returns nothing.
`moIndirectArray()`
-------------------
`mo::indirectArray()`
---------------------
Internal: Send an array as a variable up to caller of a function
* $1 - Variable name
* $1 - Variable name
* $2-@ - Array elements
Examples
callFunc () {
local myArray=(one two three)
local "$1" && moIndirectArray "$1" "${myArray[@]}"
local "$1" && mo::indirectArray "$1" "${myArray[@]}"
}
callFunc dest
echo "${dest[@]}" # writes "one two three"
@ -174,9 +129,156 @@ Examples
Returns nothing.
`moIsArray()`
`mo::trimUnparsed()`
--------------------
Internal: Trim leading characters from MO_UNPARSED
Returns nothing.
`mo::chomp()`
-------------
Internal: Remove whitespace and content after whitespace
* $1 - Name of the destination variable
* $2 - The string to chomp
Returns nothing.
`mo::parse()`
-------------
Public: Parses text, interpolates mustache tags. Utilizes the current value of MO_OPEN_DELIMITER, MO_CLOSE_DELIMITER, and MO_STANDALONE_CONTENT. Those three variables shouldn't be changed by user-defined functions.
* $1 - Destination variable name - where to store the finished content
* $2 - Content to parse
* $3 - Preserve standalone status/content - truthy if not empty. When set to a value, that becomes the standalone content value
Returns nothing.
`mo::parseInternal()`
---------------------
Internal: Parse MO_UNPARSED, writing content to MO_PARSED. Interpolates mustache tags.
No arguments
Returns nothing.
`mo::parseBlock()`
------------------
Internal: Handle parsing a block
* $1 - Invert condition ("true" or "false")
Returns nothing
`mo::parseBlockFunction()`
--------------------------
Internal: Handle parsing a block whose first argument is a function
* $1 - Invert condition ("true" or "false")
* $2-@ - The parsed tokens from inside the block tags
Returns nothing
`mo::parseBlockArray()`
-----------------------
Internal: Handle parsing a block whose first argument is an array
* $1 - Invert condition ("true" or "false")
* $2-@ - The parsed tokens from inside the block tags
Returns nothing
`mo::parseBlockValue()`
-----------------------
Internal: Handle parsing a block whose first argument is a value
* $1 - Invert condition ("true" or "false")
* $2-@ - The parsed tokens from inside the block tags
Returns nothing
`mo::parsePartial()`
--------------------
Internal: Handle parsing a partial
No arguments.
Indentation will be applied to the entire partial's contents before parsing. This indentation is based on the whitespace that ends the previously parsed content.
Returns nothing
`mo::parseComment()`
--------------------
Internal: Handle parsing a comment
No arguments.
Returns nothing
`mo::parseDelimiter()`
----------------------
Internal: Handle parsing the change of delimiters
No arguments.
Returns nothing
`mo::parseValue()`
------------------
Internal: Handle parsing value or function call
No arguments.
Returns nothing
`mo::isFunction()`
------------------
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 mo::isFunction moo; then
echo "moo is a defined function"
fi
Returns 0 if the name is a function, 1 otherwise.
`mo::isArray()`
---------------
Internal: Determine if a given environment variable exists and if it is an array.
* $1 - Name of environment variable
@ -194,231 +296,312 @@ Examples
Returns 0 if the name is not empty, 1 otherwise.
`moIsFunction()`
----------------
`mo::isArrayIndexValid()`
-------------------------
Internal: Determine if the given name is a defined function.
Internal: Determine if an array index exists.
* $1 - Function name to check
* $1 - Variable name to check
* $2 - The index 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.
Has to check if the variable is an array and if the index is valid for that type of array.
Examples
Returns true (0) if everything was ok, 1 if there's any condition that fails.
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.
`moIsStandalone()`
------------------
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.
`moJoin()`
----------
Internal: Join / implode an array
* $1 - Variable name to receive the joined content
* $2 - Joiner
* $3-$* - Elements to join
Returns nothing.
`moLoadFile()`
--------------
Internal: Read a file into a variable.
* $1 - Variable name to receive the file's content
* $2 - Filename to load - if empty, defaults to /dev/stdin
Returns nothing.
`moLoop()`
----------
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.
`moParse()`
-----------
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.
`moArgs`
--------
Split arguments from the tag name. Arguments are passed to functions.
`moPartial()`
-------------
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.
`moShow()`
----------
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.
`moSplit()`
-----------
Internal: Split a larger string into an array.
* $1 - Destination variable
* $2 - String to split
* $3 - Starting delimiter
* $4 - Ending delimiter (optional)
Returns nothing.
`moStandaloneAllowed()`
-----------------------
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.
`moStandaloneDenied()`
----------------------
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.
`moTest()`
----------
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".
`moTestVarSet()`
`mo::isVarSet()`
----------------
Internal: Determine if a variable is assigned, even if it is assigned an empty value.
* $1 - Variable name to check.
Can not use logic like this in case invalid variable names are passed. [[ "${!1-a}" == "${!1-b}" ]]
Returns true (0) if the variable is set, 1 if the variable is unset.
`moTrimChars()`
---------------
`mo::isTruthy()`
----------------
Internal: Trim the leading whitespace only.
Internal: Determine if a value is considered truthy.
* $1 - Name of destination variable
* $2 - The string
* $3 - true/false - trim front?
* $4 - true/false - trim end?
* $5-@ - Characters to trim
* $1 - The value to test
* $2 - Invert the value, either "true" or "false"
Returns true (0) if truthy, 1 otherwise.
`mo::evaluate()`
----------------
Internal: Convert token list to values
* $1 - Destination variable name
* $2-@ - Tokens to convert
Sample call:
mo::evaluate dest NAME username VALUE abc123 PAREN 2
Returns nothing.
`moTrimWhitespace()`
--------------------
`mo::evaluateListOfSingles()`
-----------------------------
Internal: Trim leading and trailing whitespace from a string.
Internal: Convert an argument list to individual values.
* $1 - Name of variable to store trimmed string
* $2 - The string
* $1 - Destination variable name
* $2-@ - A list of argument types and argument name/value.
This assumes each value is separate from the rest. In contrast, mo::evaluate will pass all arguments to a function if the first value is a function.
Sample call:
mo::evaluateListOfSingles dest NAME username VALUE abc123
Returns nothing.
`moUsage()`
-----------
`mo::evaluateSingle()`
----------------------
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.
Internal: Evaluate a single argument
* $1 - Filename that has the help message
* $1 - Name of variable for result
* $2 - Type of argument, either NAME or VALUE
* $3 - Argument
Returns nothing
`mo::evaluateKey()`
-------------------
Internal: Return the value for @key based on current's name
* $1 - Name of variable for result
Returns nothing
`mo::evaluateVariable()`
------------------------
Internal: Handle a variable name
* $1 - Destination variable name
* $2 - Variable name
Returns nothing.
`mo::findVariableName()`
------------------------
Internal: Find the name of a variable to use
* $1 - Destination variable name, receives an array
* $2 - Variable name from the template
The array contains the following values
* [0] - Variable name
* [1] - Array index, or empty string
Example variables a="a"
b="b"
c=("c.0" "c.1")
d=([b]="d.b" [d]="d.d")
Given these inputs (function input, current value), produce these outputs a c => a
a c.0 => a
b d => d.b
b d.d => d.b
a d => d.a
a d.d => d.a
c.0 d => c.0
d.b d => d.b
'' c => c
'' c.0 => c.0
Returns nothing.
`mo::join()`
------------
Internal: Join / implode an array
* $1 - Variable name to receive the joined content
* $2 - Joiner
* $3-@ - Elements to join
Returns nothing.
`mo::evaluateFunction()`
------------------------
Internal: Call a function.
* $1 - Variable for output
* $2 - Content to pass
* $3 - Function to call
* $4-@ - Additional arguments as list of type, value/name
Returns nothing.
`mo::standaloneCheck()`
-----------------------
Internal: Check if a tag appears to have only whitespace before it and after it on a line. There must be a new line before and there must be a newline after or the end of a string
No arguments.
Returns 0 if this is a standalone tag, 1 otherwise.
`mo::standaloneProcess()`
-------------------------
Internal: Process content before and after a tag. Remove prior whitespace up to the previous newline. Remove following whitespace up to and including the next newline.
No arguments.
Returns nothing.
`mo::indentLines()`
-------------------
Internal: Apply indentation before any line that has content in MO_UNPARSED.
* $1 - Destination variable name.
* $2 - The indentation string.
* $3 - The content that needs the indentation string prepended on each line.
Returns nothing.
`mo::escape()`
--------------
Internal: Escape a value
* $1 - Destination variable name
* $2 - Value to escape
Returns nothing
`mo::getContentUntilClose()`
----------------------------
Internal: Get the content up to the end of the block by minimally parsing and balancing blocks. Returns the content before the end tag to the caller and removes the content + the end tag from MO_UNPARSED. This can change the delimiters, adjusting MO_OPEN_DELIMITER and MO_CLOSE_DELIMITER.
* $1 - Destination variable name
* $2 - Token string to match for a closing tag
Returns nothing.
`mo::tokensToString()`
----------------------
Internal: Convert a list of tokens to a string
* $1 - Destination variable for the string
* $2-$@ - Token list
Returns nothing.
`mo::getContentTrim()`
----------------------
Internal: Trims content from MO_UNPARSED, returns trimmed content.
* $1 - Destination variable
Returns nothing.
`mo::getContentComment()`
-------------------------
Get the content up to and including a close tag
* $1 - Destination variable
Returns nothing.
`mo::getContentDelimiter()`
---------------------------
Get the content up to and including a close tag. First two non-whitespace tokens become the new open and close tag.
* $1 - Destination variable
Returns nothing.
`mo::getContentWithinTag()`
---------------------------
Get the content up to and including a close tag. First two non-whitespace tokens become the new open and close tag.
* $1 - Destination variable, an array
* $2 - Terminator string
The array contents: [0] The raw content within the tag
[1] The parsed tokens as a single string
Returns nothing.
`mo::tokenizeTagContents()`
---------------------------
Internal: Parse MO_UNPARSED and retrieve the content within the tag delimiters. Converts everything into an array of string values.
* $1 - Destination variable for the array of contents.
* $2 - Stop processing when this content is found.
The list of tokens are in RPN form. The first item in the resulting array is the number of actual tokens (after combining command tokens) in the list.
Given: a 'bc' "de\"\n" (f {g 'h'}) Result: ([0]=4 [1]=NAME [2]=a [3]=VALUE [4]=bc [5]=VALUE [6]=$'de\"\n' [7]=NAME [8]=f [9]=NAME [10]=g [11]=VALUE [12]=h [13]=BRACE [14]=2 [15]=PAREN [16]=2
Returns nothing
`mo::tokenizeTagContentsName()`
-------------------------------
Internal: Get the contents of a variable name.
* $1 - Destination variable name for the token list (array of strings)
Returns nothing
`mo::tokenizeTagContentsDoubleQuote()`
--------------------------------------
Internal: Get the contents of a tag in double quotes. Parses the backslash sequences.
* $1 - Destination variable name for the token list (array of strings)
Returns nothing.
`mo::tokenizeTagContentsSingleQuote()`
--------------------------------------
Internal: Get the contents of a tag in single quotes. Only gets the raw value.
* $1 - Destination variable name for the token list (array of strings)
Returns nothing.

View File

@ -1,11 +1,7 @@
FROM alpine:3.13.4
FROM alpine
COPY ./mo .
RUN apk add --update bash && \
chmod +x mo &&\
mv mo /usr/local/bin/mo && \
rm -rf /var/cache/apk/*
RUN apk add --no-cache bash
ADD mo /usr/local/bin/mo
RUN chmod +x /usr/local/bin/mo
WORKDIR /opt
ENTRYPOINT ["/usr/local/bin/mo"]
ENTRYPOINT /usr/local/bin/mo

182
README.md
View File

@ -41,7 +41,7 @@ There are a few ways you can install this tool. How you install it depends on h
You can install this file in `/usr/local/bin/` or `/usr/bin/` by simply downloading it, changing the permissions, then moving it to the right location. Double check that your system's PATH includes the destination folder, otherwise users may have a hard time starting the command.
# Download
curl -sSL https://git.io/get-mo -o mo
curl -sSL https://raw.githubusercontent.com/tests-always-included/mo/master/mo -o mo
# Make executable
chmod +x mo
@ -58,7 +58,7 @@ You can install this file in `/usr/local/bin/` or `/usr/bin/` by simply download
This is very similar to installing it globally but it does not require root privileges. It is very important that your PATH includes the destination folder otherwise it won't work. Some local folders that are typically used are `~/bin/` and `~/.local/bin/`.
# Download
curl -sSL https://git.io/get-mo -o mo
curl -sSL https://raw.githubusercontent.com/tests-always-included/mo/master/mo -o mo
# Make executable
chmod +x mo
@ -78,7 +78,7 @@ This is very similar to installing it globally but it does not require root priv
Bash scripts can source `mo` to include the functionality in their own routines. This usage typically would have `mo` saved to a `lib/` folder in an application and your other scripts would use `. lib/mo` to bring it into your project.
# Download
curl -sSL https://git.io/get-mo -o mo
curl -sSL https://raw.githubusercontent.com/tests-always-included/mo/master/mo -o mo
# Move into your project folder
mv mo ~/projects/amazing-things/lib/
@ -91,7 +91,7 @@ How to Use
If you only plan using strings and numbers, nothing could be simpler. In your shell script you can choose to export the variables. The below script is [`demo/using-strings`](demo/using-strings).
#!/bin/bash
#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory
export TEST="This is a test"
echo "Your message: {{TEST}}" | ../mo
@ -100,7 +100,7 @@ The result? "Your message: This is a test".
Using arrays adds a slight level of complexity. *You must source `mo`.* Look at [`demo/using-arrays`](demo/using-arrays).
#!/bin/bash
#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory
export ARRAY=( one two "three three three" four five )
. ../mo # This loads the "mo" function
@ -117,6 +117,141 @@ There are more scripts available in the [demos directory](demo/) that could help
There are additional features that the program supports. Try using `mo --help` to see what is available.
Please note that this command is written in Bash and pulls data from either the environment or (when using `--source`) from a text file that will be sourced and loaded into the environment, which means you will need to have Bash-style variables defined. Please see the examples in `demo/` for different ways you can use `mo`.
Enhancements
------------
In addition to many of the features built-in to Mustache, `mo` includes a number of unique features that make it a bit more powerful.
### Loop @key
`mo` implements Handlebar's `@key` references for outputting the key inside of a loop:
Env:
```bash
myarr=( foo bar )
# Bash v4+
declare -A myassoc
myassoc[hello]="mo"
myassoc[world]="is great"
```
Template:
```handlebars
{{#myarr}}
- {{@key}} {{.}}
{{/myarr}}
{{#myassoc}}
* {{@key}} {{.}}
{{/myassoc}}
```
Output:
```markdown
- 0 foo
- 1 bar
* hello mo
* world is great
```
### Helpers / Function Arguments
Function Arguments are not a part of the official Mustache implementation, and are more often associated with Handlebar's Helper functionality.
`mo` allows for passing strings to functions.
```handlebars
{{myfunc foo bar}}
```
For security reasons, these arguments are not immediately available to function calls without a flag.
#### with `--allow-function-arguments`
```bash
myfunc() {
# Outputs "foo, bar"
echo "$1, $2";
}
```
#### Using `$MO_FUNCTION_ARGS`
```bash
myfunc() {
# Outputs "foo, bar"
echo "${MO_FUNCTION_ARGS[0]}, ${MO_FUNCTION_ARGS[1]}";
}
```
### Triple Mustache, Parenthesis, and Quotes
Normally, triple mustache syntax, such as `{{{var}}}` will avoid HTML escaping of the variable. Because HTML escaping is not supported in `mo`, this is now used differently. Anything within braces will be looked up and the values will be concatenated together and the result will be treated as a value. Anything in parenthesis will be looked up, concatenated, and treated as a name. Also, anything in single quotes is passed as a value; double quoted things first are unescaped and then passed as a value.
```
# Example input
var=abc
user=admin
admin=Administrator
u=user
abc=([0]=zero [1]=one [2]=two)
```
| Mustache syntax | Resulting output | Notes |
|-----------------|------------------|-------|
| `{{var}}` | `abc` | Normal behavior |
| `{{var us}}` | `abcus` | Concatenation |
| `{{'var'}}` | `var` | Passing as a value |
| `{{"a\tb"}}` | `a b` | There was an escaped tab in the value |
| `{{u}}` | `user` | Normal behavior |
| `{{{u}}}` | `user` | Look up "$u", treat as the value `{{'user'}}` |
| `{{(u)}}` | `admin` | Look up "$u", treat as the name `{{user}}` |
| `{{var user}}` | `abcuser` | Concatenation |
| `{{(var '.1')}}` | `one` | Look up "$var", treat as "abc", then concatenate ".1" and look up `{{abc.1}}` |
In double-quoted strings, the following escape sequences are defined.
* `\"` - Quote
* `\b` - Bell
* `\e` - Escape (note that Bash typically uses $'\E' for the same thing)
* `\f` - Form feed
* `\n` - Newline
* `\r` - Carriage return
* `\t` - Tab
* `\v` - Vertical tab
* Anything else will skip the `\` and place the next character. However, this implementation is allowed to change in the future if a different escape character mapping becomes commonplace.
Environment Variables and Functions
-----------------------------------
There are several functions and variables used to process templates. `mo` reserves variables that start with `MO_` for variables exposing data or configuration, functions starting with `mo::`, and local variables starting with `mo[A-Z]`. You are welcome to use internal functions, though only ones that are marked as "Public" should not change their interface. Scripts may also read any of the variables.
Functions are all executed in a subshell, with another subshell for lambdas. Thus, your lambda can't affect the parsing of a template. There's more information about lambdas when talking about tests that fail.
* `MO_ALLOW_FUNCTION_ARGUMENTS` - When set to a non-empty value, this allows functions referenced in templates to receive additional options and arguments.
* `MO_CLOSE_DELIMITER` - The string used when closing a tag. Defaults to "}}". Used internally.
* `MO_CLOSE_DELIMITER_DEFAULT` - The default value of `MO_CLOSE_DELIMITER`. Used when resetting the close delimiter, such as when parsing a partial.
* `MO_CURRENT` - Variable name to use for ".".
* `MO_DEBUG` - When set to a non-empty value, additional debug information is written to stderr.
* `MO_FUNCTION_ARGS` - Arguments passed to the function.
* `MO_FAIL_ON_FILE` - If a filename from the command-line is missing or a partial does not exist, abort with an error.
* `MO_FAIL_ON_FUNCTION` - If a function returns a non-zero status code, abort with an error.
* `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_OPEN_DELIMITER` - The string used when opening a tag. Defaults to "{{". Used internally.
* `MO_OPEN_DELIMITER_DEFAULT` - The default value of MO_OPEN_DELIMITER. Used when resetting the open delimiter, such as when parsing a partial.
* `MO_ORIGINAL_COMMAND` - Used to find the `mo` program in order to generate a help message.
* `MO_PARSED` - Content that has made it through the template engine.
* `MO_STANDALONE_CONTENT` - The unparsed content that preceeded the current tag. When a standalone tag is encountered, this is checked to see if it only contains whitespace. If this and the whitespace condition after a tag is met, then this will be reset to $'\n'.
* `MO_UNPARSED` - Template content yet to make it through the parser.
Concessions
-----------
@ -129,9 +264,9 @@ Pull requests to solve the following issues would be helpful.
### Mustache Syntax
* Dotted names are supported but only for associative arrays (Bash 4). See [`demo/associative-arrays`](demo/associative-arrays) for an example.
* There's no "top level" object, so `echo '{.}' | ./mo` does not do anything useful. In other languages you can say the data for the template is a string and in `mo` the data is always the environment. Luckily this type of usage is rare and `{.}` works great when iterating over an array.
* There's no "top level" object, so `echo '{{.}}' | ./mo` does not do anything useful. In other languages you can say the data for the template is a string and in `mo` the data is always the environment. Luckily this type of usage is rare and `{{.}}` works great when iterating over an array.
* [Parents](https://mustache.github.io/mustache.5.html#Parents), where a template can override chunks of a partial, are not supported.
* HTML encoding is not built into `mo`. `{{{var}}}`, `{{&var}}` and `{{var}}` all do the same thing. `echo '{{TEST}}' | TEST='<b>' mo` will give you "`<b>`" instead of "`&gt;b&lt;`".
* You can not change the delimiters.
### General Scripting Issues
@ -153,16 +288,41 @@ When submitting patches, make sure to run them past [ShellCheck] and ensure no p
### Porting and Backporting
There is a diagnostic script that is aimed to help at making sure the internal functions all work when testing the script out on a version of bash or in an environment that is causing issues. When adding new functions, please make sure this script gets updated to properly test your code.
In case of problems, setting MO_DEBUG to a non-empty value will give you LOTS of output.
MO_DEBUG=1 ./mo my-template
### Failed Specs
It is acceptable for some of the official spec tests to fail. Anything dealing with multiple levels of objects (eg. `{{a.b.c}}`) and changing the delimiters (`{{= | | =}}`) will fail. Other than that, this bash implementation of the mustache spec should pass tests.
It is acceptable for some of the official spec tests to fail. The spec runner has specific exclusions and overrides to test similar functionality that avoid the following issues.
Specific issues:
* Interpolation - Multiple Calls: This fails because lambdas execute in a subshell so their output can be captured. This is flagged as a TODO in the code.
* Using `{{.}}` outside of a loop - In order to access any variable, you must use its name. In a loop, `{{.}}` will refer to the current value, but outside the loop you are unable to use this dot notation because there is no current value.
* Deeply nested data - Bash doesn't support complex data structure. Basically, just strings and arrays of strings.
* Interpolation; Multiple Calls: This fails because lambdas execute in a subshell so their output can be captured. If you want state to be preserved, you will need to write it outside of the current environment and load it again later.
* HTML Escaping - Since bash is not often executed in a web server context, it makes no sense to have the output escaped as HTML. Performing shell escaping of variables may be an option in the future if there's a demand.
* Lambdas - Function results are *not* automatically interpreted again. If you want to parse the results as Mustache content, use `mo::parse`. When they use `mo::parse`, it will use the current delimiters.
For lambdas, these examples may help.
```bash
# Retrieve content into a variable.
content=$(cat)
# Retrieve all content and do not trim newlines at the end.
content=$(cat; echo -n '.')
content=${content%.}
# Parse content using the current delimiters
mo::parse results "This is my content. Hello, {{username}}"
echo -n "$results"
# Parse content using the default delimiters
MO_OPEN_DELIMITER=$MO_OPEN_DELIMITER_DEFAULT
MO_CLOSE_DELIMITER=$MO_CLOSE_DELIMITER_DEFAULT
mo::parse results "This is my content. Hello, {{username}}"
echo -n "$results"
```
### Future Enhancements

View File

@ -1,9 +1,9 @@
#!/bin/bash
#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory
declare -A DATA
DATA=([one]=111 [two]=222)
export DATA=([one]=111 [two]=222)
. ../mo
cat <<EOF | mo

View File

@ -1,6 +1,7 @@
#!/bin/bash
#!/usr/bin/env bash
#
# This embeds a template in the script without using strange `cat` syntax.
# shellcheck disable=SC1083 disable=SC1010 disable=SC1054 disable=SC1073 disable=SC1072 disable=SC1056 disable=SC1009
cd "$(dirname "$0")" # Go to the script's directory

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# This sources a simple script with the env. variables needed for the template.
@ -10,20 +10,22 @@ export ARRAY=( AAA BBB CCC )
# Include an external template
INCLUDE() {
cat "$MO_FUNCTION_ARGS"
# shellcheck disable=SC2031
cat "${MO_FUNCTION_ARGS[0]}"
}
# Print section title
TITLE() {
echo "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"
echo "$MO_FUNCTION_ARGS"
# shellcheck disable=SC2031
echo "${MO_FUNCTION_ARGS[0]}"
echo "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"
}
cat <<EOF | mo -u
{{TITLE Part 1}}
{{INCLUDE function-args-part1}}
{{TITLE 'Part 1'}}
{{INCLUDE 'function-args-part1'}}
{{TITLE Part 2}}
{{INCLUDE function-args-part2}}
{{TITLE 'Part 2'}}
{{INCLUDE 'function-args-part2'}}
EOF

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory

View File

@ -9,6 +9,8 @@ foreach() {
foreachContent=$(cat)
local x
x=("${@}")
if [[ "$2" != "as" && "$2" != "in" ]]; then
echo "Invalid foreach - bad format."
elif [[ "$(declare -p "$1")" != "declare -"[aA]* ]]; then
@ -41,8 +43,8 @@ links=(resque hub rip)
# Process the template
cat <<EOF | mo --allow-function-arguments
Here are your links:
{{#foreach links as link}}
{{#foreach 'links' 'as' 'link'}}
* [{{link.name}}]({{link.url}})
{{/foreach}}
{{/foreach 'links' 'as' 'link'}}
EOF

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cd "$(dirname "$0")"/..

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# This sources a simple script with the env. variables needed for the template.

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
cd "$(dirname "$0")" # Go to the script's directory
export ARRAY=( one two "three three three" four five )
. ../mo

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# This example does not source `mo` and is intentionally restricted to
# variables that are not arrays.

View File

@ -1,393 +0,0 @@
#!/usr/bin/env bash
#
# Test individual functions in mo and make sure they are performing their
# tasks correctly. This can be used to help determine what needs to get
# fixed when making mo work on another version of bash, or another shell
# entirely.
#
# Functions are tested from the most primitive to the most advanced.
# Errors, once found, halt the program. This way we can more easily
# diagnose what's not working and fix those low-level functions first.
PARENT_PID=$$
cd "$(dirname "$0")" || exit 1
rm -f diagnostic.test
rm -f diagnostic.partial
# Load mo's functions
# shellcheck disable=SC1091
. ./mo
fail() {
echo "$1"
kill $PARENT_PID
exit 1
}
# No dependencies
echo -n "moIndirect ... "
(
a() {
local V
V="a"
b
echo -n "$V"
}
b() {
local V
V="b"
c V
echo -n "$V"
}
c() {
local "$1" && moIndirect "$1" c
}
[[ "$(a)" != "ca" ]] && fail "Did not assign or scope bled '$RESULT'"
)
echo "ok"
echo -n "moIndirectArray ... "
(
a() {
local V
V=( "a" )
b
echo -n "${#V[@]}"
}
b() {
local V
V=( "b" "b" )
c V
echo -n "${#V[@]}"
}
c() {
local "$1" && moIndirectArray "$1" c c c
}
[[ "$(a)" != "31" ]] && fail "Did not assign or scope bled '$RESULT'"
)
echo "ok"
echo -n "moIsArray ... "
(
export TEST_NUM=1
moIsArray TEST_NUM && fail "Wrongly said number was an array"
)
(
export TEST_ARR=()
moIsArray TEST_ARR || fail "Wrongly said array was not an array"
)
(
export TEST_EMPTY=""
moIsArray TEST_EMPTY && fail "Wrongly said string was an array"
)
(
unset TEST_UNSET
moIsArray TEST_UNSET && fail "Wrongly said undefined was an array"
)
echo "ok"
echo -n "moIsFunction ... "
(
aa() { echo "hi"; }
moIsFunction aa || fail "Did not find a function"
moIsFunction dd && fail "Wrongly found a function"
)
echo "ok"
echo -n "moFindString ... "
(
moFindString POS "abcdefg" "ab"
[[ "$POS" == "0" ]] || fail "Did not find at beginning of string"
moFindString POS "abcdefg" "fg"
[[ "$POS" == "5" ]] || fail "Did not find at end of string"
moFindString POS "abcdefg" "de"
[[ "$POS" == "3" ]] || fail "Did not find at middle of string"
moFindString POS "abcdefg" "CD"
[[ "$POS" == "-1" ]] || fail "Did not correctly return a miss"
)
echo "ok"
echo -n "moFullTagName ... "
(
moFullTagName TAG "abc" "def"
[[ "$TAG" == "abc.def" ]] || fail "Did not work with a context"
moFullTagName TAG "" "one"
[[ "$TAG" == "one" ]] || fail "Did not work without a context"
)
echo "ok"
echo -n "moLoadFile ... "
(
# TODO - find a way to test reading from stdin that is not painful.
touch diagnostic.test
moLoadFile RESULT diagnostic.test
[[ "${#RESULT}" == "0" ]] || fail "Did not read from empty file '$RESULT'"
echo "abc" >> diagnostic.test
moLoadFile RESULT diagnostic.test
[[ "${#RESULT}" == "4" ]] || fail "Did not read from file '$RESULT'"
echo "" >> diagnostic.test
moLoadFile RESULT diagnostic.test
[[ "${#RESULT}" == "5" ]] || fail "Trimmed newline from file '$RESULT'"
rm diagnostic.test
)
echo "ok"
echo -n "moStandaloneDenied ... "
(
moStandaloneDenied RESULT before tag after > diagnostic.test
[[ "$(cat diagnostic.test)" == "before" ]] || fail "Did not output content before tag '$(cat diagnostic.test)'"
[[ "$RESULT" == "after" ]] || fail "Did not set the remaining content '$RESULT'"
rm diagnostic.test
)
echo "ok"
echo -n "moTest ... "
# shellcheck disable=SC2034
(
aa() { echo "hi"; }
bb="bb"
cc=3
dd=( dd )
xx=()
unset yy
zz=""
moTest aa || fail "Did not detect a function"
moTest bb || fail "Did not detect a non-empty string"
moTest cc || fail "Did not detect a number"
moTest dd || fail "Did not detect a populated array"
moTest xx && fail "Erroneously flagged an empty array"
moTest yy && fail "Erroneously flagged an undefined value"
moTest zz && fail "Erroneously flagged an empty string"
MO_FALSE_IF_EMPTY=true moTest zz && fail "Erroneously flagged an empty value as non-false when empty should be false"
)
echo "ok"
echo -n "moTrimChars ... "
(
moTrimChars RESULT "abcdabc" true true a b c
[[ "$RESULT" == "d" ]] || fail "Did not trim multiple characters '$RESULT'"
moTrimChars RESULT "abc" true false a c
[[ "$RESULT" == "bc" ]] || fail "Did not trim only from the front '$RESULT'"
moTrimChars RESULT "abc" false true a c
[[ "$RESULT" == "ab" ]] || fail "Did not trim only from the end '$RESULT'"
)
echo "ok"
echo -n "moGetContent ... "
(
# TODO - find a way to test reading from stdin that is not painful.
# Until then, mock it
moLoadFile() { local "$1" && moIndirect "$1" "STDIN"; }
moGetContent RESULT a
[[ "$RESULT" == "{{>a}}" ]] || fail "Did not construct 1 partial correctly '$RESULT'"
moGetContent RESULT a b c
[[ "$RESULT" == "{{>a}}{{>b}}{{>c}}" ]] || fail "Did not construct 3 partials correctly '$RESULT'"
moGetContent RESULT
[[ "$RESULT" == "STDIN" ]] || fail "Did not call moLoadFile correctly"
)
echo "ok"
echo -n "moIndentLines ... "
(
CR=$'\r'
LF=$'\n'
# shellcheck disable=SC2034
CRLF="$CR$LF"
# CAUTION
# This must have a dot at the end of the input string
# That is part of how moPartial calls this function
for NEWLINE in "CR" "LF" "CRLF"; do
NL="${!NEWLINE}"
moIndentLines RESULT "" "has${NL}${NEWLINE}${NL}."
printf -v QUOTED '%q' "$RESULT"
[[ "$RESULT" == "has${NL}${NEWLINE}${NL}" ]] || fail "Should not have changed string $QUOTED"
moIndentLines RESULT "" "without${NL}trailing${NL}${NEWLINE}."
printf -v QUOTED '%q' "$RESULT"
[[ "$RESULT" == "without${NL}trailing${NL}${NEWLINE}" ]] || fail "Should not have changed string $QUOTED"
moIndentLines RESULT "_-_" "has${NL}${NL}${NEWLINE}${NL}."
printf -v QUOTED '%q' "$RESULT"
[[ "$RESULT" == "_-_has${NL}${NL}_-_${NEWLINE}${NL}" ]] || fail "Should have indented $QUOTED"
moIndentLines RESULT "_-_" "without${NL}${NL}trailing${NL}${NEWLINE}."
printf -v QUOTED '%q' "$RESULT"
[[ "$RESULT" == "_-_without${NL}${NL}_-_trailing${NL}_-_${NEWLINE}" ]] || fail "Should have indented $QUOTED"
done
)
echo "ok"
echo -n "moIsStandalone ... "
(
CR=$'\r'
LF=$'\n'
TAB=$'\t'
moIsStandalone RESULT "" "" false && fail "Needs newline before or beginning of content flag"
moIsStandalone RESULT "" "" true || fail "Has beginning of content flag and no other content"
[[ "$RESULT" == "0 0" ]] || fail "Wrong returned value for no content '$RESULT'"
moIsStandalone RESULT "moo" "cow" false && fail "Needs newlines when there is content"
moIsStandalone RESULT "moo$CR$LF$TAB $TAB " " $TAB $TAB$CR${LF}pasture" false || fail "Has newlines and content but did not flag"
[[ "$RESULT" == "5 6" ]] || fail "Wrong returned value when there was whitespace '$RESULT'"
)
echo "ok"
echo -n "moSplit ... "
(
moSplit RESULT "abc-def-ghi" "-"
[[ "${#RESULT[@]}" == "2" ]] || fail "Returned wrong number of elements with one delimiter ${#RESULT[@]}"
[[ "${RESULT[0]}" == "abc" ]] || fail "Returned wrong left hand string with one delimiter '${RESULT[0]}'"
[[ "${RESULT[1]}" == "def-ghi" ]] || fail "Returned wrong right hand string with one delimiter '${RESULT[1]}'"
moSplit RESULT "abc-def-ghi" "-" "g"
[[ "${#RESULT[@]}" == "3" ]] || fail "Returned wrong number of elements with two delimiters ${#RESULT[@]}"
[[ "${RESULT[0]}" == "abc" ]] || fail "Returned wrong left hand string with two delimiters '${RESULT[0]}'"
[[ "${RESULT[1]}" == "def-" ]] || fail "Returned wrong middle string with two delimiters '${RESULT[1]}'"
[[ "${RESULT[2]}" == "hi" ]] || fail "Returned wrong right hand string with two delimiters '${RESULT[2]}'"
)
echo "ok"
echo -n "moTrimWhitespace ... "
(
CR=$'\r'
LF=$'\n'
TAB=$'\t'
moTrimWhitespace RESULT "ab cd"
printf -v QUOTED '%q' "$RESULT"
[[ "${RESULT}" == "ab cd" ]] || fail "Trimmed a string that did not need trimming $QUOTED"
moTrimWhitespace RESULT "$CR$LF$TAB ab $CR$LF$TAB cd $CR$LF$TAB $CR$LF$TAB"
printf -v QUOTED '%q' "$RESULT"
[[ "${RESULT}" == "ab $CR$LF$TAB cd" ]] || fail "Did not fully trim a string $QUOTED"
)
echo "ok"
echo -n "moStandaloneAllowed ... "
(
# Mock moIsStandalone to make things simpler
moIsStandalone() { return 1; }
moStandaloneAllowed RESULT before tag after > diagnostic.test
[[ "$(cat diagnostic.test)" == "before" ]] || fail "Did not output content before tag when not standalone '$(cat diagnostic.test)'"
[[ "$RESULT" == "after" ]] || fail "Did not set the remaining content when not standalone '$RESULT'"
moIsStandalone() { local "$1" && moIndirect "$1" "3 5"; return 0; }
moStandaloneAllowed RESULT before tag afterwards > diagnostic.test
[[ "$(cat diagnostic.test)" == "bef" ]] || fail "Did not output content before tag when standalone '$(cat diagnostic.test)'"
[[ "$RESULT" == "wards" ]] || fail "Did not set the remaining content when standalone '$RESULT'"
rm diagnostic.test
)
echo "ok"
echo -n "moFindEndTag ... "
(
LF=$'\n'
moFindEndTag RESULT "moo{{ / cow }}pasture" "cow"
[[ "${#RESULT[@]}" == "3" ]] || fail "(simple) Wrong number of elements in the array ${#RESULT[@]}"
[[ "${RESULT[0]}" == "moo" ]] || fail "(simple) Wrong left-hand '${RESULT[0]}'"
[[ "${RESULT[1]}" == "{{ / cow }}" ]] || fail "(simple) Wrong middle '${RESULT[1]}'"
[[ "${RESULT[2]}" == "pasture" ]] || fail "(simple) Wrong right-hand '${RESULT[2]}'"
moFindEndTag RESULT "moo$LF {{/cow}} $LF pasture" "cow"
[[ "${#RESULT[@]}" == "3" ]] || fail "(standalone) Wrong number of elements in the array ${#RESULT[@]}"
[[ "${RESULT[0]}" == "moo$LF" ]] || fail "(standalone) Wrong left-hand '${RESULT[0]}'"
[[ "${RESULT[1]}" == " {{/cow}} $LF" ]] || fail "(standalone) Wrong middle '${RESULT[1]}'"
[[ "${RESULT[2]}" == " pasture" ]] || fail "(standalone) Wrong right-hand '${RESULT[2]}'"
moFindEndTag RESULT "aa{{#bb}}cc{{/bb}}dd{{^bb}}ee{{/bb}}ff{{/bb}}gg" "bb"
[[ "${#RESULT[@]}" == "3" ]] || fail "(recursive) Wrong number of elements in the array ${#RESULT[@]}"
[[ "${RESULT[0]}" == "aa{{#bb}}cc{{/bb}}dd{{^bb}}ee{{/bb}}ff" ]] || fail "(recursive) Wrong left-hand '${RESULT[0]}'"
[[ "${RESULT[1]}" == "{{/bb}}" ]] || fail "(recursive) Wrong middle '${RESULT[1]}'"
[[ "${RESULT[2]}" == "gg" ]] || fail "(recursive) Wrong right-hand '${RESULT[2]}'"
moFindEndTag RESULT "aa{{#bb}}cc{{/dd}}ee" "dd"
[[ "${#RESULT[@]}" == "3" ]] || fail "(unbalanced) Wrong number of elements in the array ${#RESULT[@]}"
[[ "${RESULT[0]}" == "aa{{#bb}}cc{{/dd}}ee" ]] || fail "(unbalanced) Wrong left-hand '${RESULT[0]}'"
[[ "${RESULT[1]}" == "" ]] || fail "(unbalanced) Wrong middle '${RESULT[1]}'"
[[ "${RESULT[2]}" == "" ]] || fail "(unbalanced) Wrong right-hand '${RESULT[2]}'"
)
echo "ok"
echo -n "moLoop ... "
(
moParse() { echo "parse[$1] context[$2] flag[$3]"; }
moLoop "content" "prefix" a b c > diagnostic.test
(
echo "parse[content] context[prefix.a] flag[false]"
echo "parse[content] context[prefix.b] flag[false]"
echo "parse[content] context[prefix.c] flag[false]"
) | diff -q diagnostic.test - || fail "Result was not as expected '$(cat diagnostic.test)'"
rm diagnostic.test
)
echo "ok"
echo -n "moPartial ... "
(
NL=$'\n'
moParse() { echo "parse[$1] context[$2] flag[$3]"; }
echo "partial" > diagnostic.partial
moPartial RESULT "line$NL" ">diagnostic.partial" " ${NL}line2" false "moo" > diagnostic.test
[[ "$RESULT" == "line2" ]] || fail "Did not consume newline for standalone '$RESULT'"
printf -v QUOTED '%q' "$(cat diagnostic.test)"
[[ "$(cat diagnostic.test)" == "line${NL}parse[partial${NL}] context[moo] flag[true]" ]] || fail "Did not parse right standalone content $QUOTED"
moPartial RESULT "line" ">diagnostic.partial" "line2" false "moo" > diagnostic.test
[[ "$RESULT" == "line2" ]] || fail "Did not preserve content for non-standalone '$RESULT'"
printf -v QUOTED '%q' "$(cat diagnostic.test)"
[[ "$(cat diagnostic.test)" == "lineparse[partial${NL}] context[moo] flag[true]" ]] || fail "Did not parse right non-standalone content $QUOTED"
rm diagnostic.test diagnostic.partial
)
echo "ok"
echo -n "moShow ... "
# shellcheck disable=SC2034
(
aa() { echo "this is aa"; }
bb="BB"
cc=( zero one two )
[[ "$(moShow aa)" == "this is aa" ]] || fail "Did not load function"
[[ "$(moShow bb)" == "BB" ]] || fail "Did not show variable"
[[ "$(moShow cc.1)" == "one" ]] || fail "Did not show value from indexed array"
)
echo "ok"
echo -n "moParse ... skipped (tested by specs)"
echo ""
echo "All diagnostic tests pass"

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# This requires tomdoc.sh to be in your PATH.
# https://github.com/tests-always-included/tomdoc.sh

2477
mo

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,22 @@
#!/usr/bin/env bash
# Create a package.json so the dependency package is installed in the local
# directory
echo '{"private":true, "dependencies":{"async": "*"}}' > package.json
npm install
# Install or update the specs
if [[ ! -d spec ]]; then
git clone https://github.com/mustache/spec.git spec
else
(
cd spec;
cd spec
git pull
)
fi
if [[ "$BASH_VERSION" == 3.* ]]; then
echo "WARNING! Specs assume you are using a version of Bash with associative arrays!"
fi
# Actually run the specs
node run-spec.js spec/specs/*.json
if [[ "$BASH_VERSION" == 3.* ]]; then
echo "Some tests may have failed because they assume Bash supports associative arays"
fi

View File

@ -1,241 +1,512 @@
#!/usr/bin/env node
var async, exec, fs, summary;
const exec = require("child_process").exec;
const fsPromises = require("fs").promises;
function makeShellString(value) {
if (typeof value === 'string') {
// Newlines are tricky
return value.split(/\n/).map(function (chunk) {
return JSON.stringify(chunk);
}).join('"\n"');
// Skip or override portions of tests. The goal is to still have as much
// coverage as possible, but skip things that Bash does not support.
//
// To skip a test, define a "skip" property and explain why the test is
// skipped.
//
// To override any test property, just define that property. It replaces the
// original property, not augmenting it.
const testOverrides = {
"Comments -> Variable Name Collision": {
// Can't use variables with exclamation points easily
data: {
comment: 4
}
},
"Interpolation -> Dotted Names - Arbitrary Depth": {
skip: "Not able to use more than one level of depth"
},
"Interpolation -> Dotted Names - Broken Chain Resolution": {
data: {
a: {
b: "wrong"
},
name: "Jim"
},
template: '"{{a.name}}" == ""'
},
"Interpolation -> Dotted Names - Initial Resolution": {
data: {
a: {
name: "Phil"
},
name: "Wrong"
},
template: "\"{{#a}}{{name}}{{/a}}\" == \"Phil\""
},
"Interpolation -> Implicit Iterators - Ampersand": {
skip: "HTML escaping is not supported"
},
"Interpolation -> Implicit Iterators - Basic Interpolation": {
skip: "Can not use {{.}} outside of a loop. Need to use a variable name."
},
"Interpolation -> Implicit Iterators - Basic Integer Interpolation": {
skip: "Can not use {{.}} outside of a loop. Need to use a variable name."
},
"Interpolation -> Implicit Iterators - Triple Mustache": {
skip: "Can not use {{.}} outside of a loop. Need to use a variable name."
},
"Interpolation -> HTML Escaping": {
skip: "HTML escaping is not supported"
},
"Interpolation -> Implicit Iterators - HTML Escaping": {
skip: "HTML escaping is not supported"
},
"Inverted -> Dotted Names - Falsey": {
data: {
a: {
b: ""
}
},
template: '"{{^a.b}}Not Here{{/a.b}}" == "Not Here"'
},
"Inverted -> Dotted Names - Truthy": {
data: {
a: {
b: "1"
}
},
template: '"{{^a.b}}Not Here{{/a.b}}" == ""'
},
"Lambdas -> Escaping": {
skip: "HTML escaping is not supported"
},
"Lambdas -> Interpolation - Alternate Delimiters": {
skip: "There is no difference between a lamba used as a value and a lambda used as a block. Both will parse using the current delimiters."
},
"Lambdas -> Inverted Section": {
// This one passed mostly by accident. Correcting so the test still
// tests what is was designed to illustrate.
data: {
static: "static",
lambda: {
__tag__: 'code',
bash: 'false'
}
}
},
"Lambdas -> Interpolation": {
data: {
lambda: {
__tag__: 'code',
bash: 'echo -n "world"'
}
}
},
"Lambdas -> Interpolation - Expansion": {
data: {
lambda: {
__tag__: 'code',
bash: 'mo::parse result "{{planet}}"; echo -n "$result"'
},
planet: 'world'
}
},
"Lambdas -> Interpolation - Multiple Calls": {
skip: "Calls are not cached, but they run in isolated environments, so saving a global variable does not work."
},
"Lambdas -> Section": {
data: {
lambda: {
__tag__: 'code',
bash: 'if [[ "$(cat)" == "{{x}}" ]]; then echo -n yes; else echo -n no; fi'
},
x: "Error!"
}
},
"Lambdas -> Section - Alternate Delimiters": {
data: {
lambda: {
__tag__: 'code',
bash: 'local content=$(cat); mo::parse content "$content{{planet}} => |planet|$content"; echo -n "$content"'
},
planet: 'Earth'
}
},
"Lambdas -> Section - Expansion": {
data: {
lambda: {
__tag__: 'code',
bash: 'local content=$(cat); mo::parse content "$content{{planet}}$content"; echo -n "$content"'
},
planet: "Earth"
}
},
"Lambdas -> Section - Multiple Calls": {
data: {
lambda: {
__tag__: 'code',
bash: 'echo -n "__$(cat)__"'
}
}
},
"Partials -> Recursion": {
skip: "Complex objects are not supported and context is reset to the global level, so the recursion will loop forever"
},
"Sections -> Deeply Nested Contexts": {
skip: "Nested objects are not supported"
},
"Sections -> Dotted Names - Broken Chains": {
// Complex objects are not supported
template: `"{{#a.b}}Here{{/a.b}}" == ""`
},
"Sections -> Dotted Names - Falsey": {
// Complex objects are not supported
data: { a: { b: false } },
template: `"{{#a.b}}Here{{/a.b}}" == ""`
},
"Sections -> Dotted Names - Truthy": {
// Complex objects are not supported
data: { a: { b: true } },
template: `"{{#a.b}}Here{{/a.b}}" == "Here"`
},
"Sections -> Implicit Iterator - Array": {
skip: "Nested arrays are not supported"
},
"Sections -> List": {
// Arrays of objects are not supported
data: { list: [1, 2, 3] },
template: `"{{#list}}{{.}}{{/list}}"`
},
"Sections -> List Context": {
skip: "Deeply nested objects are not supported"
},
"Sections -> List Contexts": {
skip: "Deeply nested objects are not supported"
}
};
function specFileToName(file) {
return file
.replace(/.*\//, "")
.replace(".json", "")
.replace("~", "")
.replace(/(^|-)[a-z]/g, function (match) {
return match.toUpperCase();
});
}
function processArraySequentially(array, callback) {
function processCopy() {
if (arrayCopy.length) {
const item = arrayCopy.shift();
return Promise.resolve(item)
.then(callback)
.then((singleResult) => {
result.push(singleResult);
return processCopy();
});
} else {
return Promise.resolve(result);
}
}
if (typeof value === 'number') {
const result = [];
const arrayCopy = array.slice();
return processCopy();
}
function debug(...args) {
if (process.env.DEBUG) {
console.debug(...args);
}
}
function makeShellString(value) {
if (typeof value === "boolean") {
return value ? '"true"' : '""';
}
if (typeof value === "string") {
// Newlines are tricky
return value
.split(/\n/)
.map(function (chunk) {
return JSON.stringify(chunk);
})
.join('"\n"');
}
if (typeof value === "number") {
return value;
}
return 'ERR_CONVERTING';
return "ERR_CONVERTING";
}
function addToEnvironmentArray(name, value) {
const result = ["("];
value.forEach(function (subValue) {
result.push(makeShellString(subValue));
});
result.push(")");
return name + "=" + result.join(" ");
}
function addToEnvironmentObjectConvertedToAssociativeArray(name, value) {
const values = [];
for (const [k, v] of Object.entries(value)) {
if (typeof v === "object") {
if (v) {
// An object - abort
return `# ${name}.${k} is an object that can not be converted to an associative array`;
}
// null
values.push(`[${k}]=`);
} else {
values.push(`[${k}]=${makeShellString(v)}`);
}
}
return `declare -A ${name}\n${name}=(${values.join(" ")})`;
}
function addToEnvironmentObject(name, value) {
if (!value) {
// null
return `#${name} is null`;
}
if (value.__tag__ === "code") {
return `${name}() { ${value.bash || 'echo "NO BASH VERSION OF CODE"'}; }`;
}
return addToEnvironmentObjectConvertedToAssociativeArray(name, value);
}
function addToEnvironment(name, value) {
var result;
if (Array.isArray(value)) {
result = [
'('
];
value.forEach(function (subValue) {
result.push(makeShellString(subValue));
});
result.push(')');
return name + '=' + result.join(' ');
return addToEnvironmentArray(name, value);
}
// Sometimes the __tag__ property of the code in the lambdas may
// be missing. :-(
if (typeof value === 'object') {
if (value.__tag__ === 'code' || (value.ruby && value.php && value.perl)) {
if (value.bash) {
return name + '() { ' + value.bash + '; }';
}
return name + '() { perl -e \'print ((' + value.perl + ')->("\'"$1"\'"))\'; }';
}
if (typeof value === "object") {
return addToEnvironmentObject(name, value);
}
if (typeof value === 'object') {
return '# ' + name + ' is an object and will not work in bash';
}
if (typeof value === 'boolean') {
if (value) {
return name + '="true"';
}
return name + '=""';
}
return name + '=' + makeShellString(value);
return `${name}=${makeShellString(value)}`;
}
function runTest(test, done) {
var output, partials, script;
script = [
'#!/bin/bash'
];
partials = test.partials || {};
function buildScript(test) {
const script = ["#!/usr/bin/env bash"];
Object.keys(test.data).forEach(function (name) {
script.push(addToEnvironment(name, test.data[name]));
});
script.push('. mo');
script.push('mo spec-runner/spec-template');
test.script = script.join('\n');
async.series([
function (taskDone) {
fs.mkdir("spec-runner/", function (err) {
if (err && err.code !== 'EEXIST') {
return taskDone(err);
}
script.push(". ./mo");
script.push("mo spec-runner/spec-template");
script.push("");
taskDone();
});
},
function (taskDone) {
fs.writeFile('spec-runner/spec-script', test.script, taskDone);
},
function (taskDone) {
fs.writeFile('spec-runner/spec-template', test.template, taskDone);
},
function (taskDone) {
async.eachSeries(Object.keys(partials), function (partialName, partialDone) {
fs.writeFile('spec-runner/' + partialName, test.partials[partialName], partialDone);
}, taskDone);
},
function (taskDone) {
exec('bash spec-runner/spec-script', function (err, stdout) {
if (err) {
return taskDone(err);
}
output = stdout;
taskDone();
});
},
function (taskDone) {
async.eachSeries(Object.keys(partials), function (partialName, partialDone) {
fs.unlink('spec-runner/' + partialName, partialDone);
}, taskDone);
},
function (taskDone) {
fs.unlink('spec-runner/spec-script', taskDone);
},
function (taskDone) {
fs.unlink('spec-runner/spec-template', taskDone);
},
function (taskDone) {
fs.rmdir('spec-runner/', taskDone);
}
], function (err) {
if (err) {
return done(err);
}
done(null, output);
});
return '';
return script.join("\n");
}
function prepareAndRunTest(test, done) {
async.waterfall([
function (taskDone) {
console.log('### ' + test.name);
console.log('');
console.log(test.desc);
console.log('');
runTest(test, taskDone);
},
function (actual, taskDone) {
test.actual = actual;
test.pass = (test.actual === test.expected);
function writePartials(test) {
return processArraySequentially(
Object.keys(test.partials),
(partialName) => {
debug("Writing partial:", partialName);
if (test.pass) {
console.log('Passed.');
} else {
console.log('Failed.');
console.log('');
console.log(test);
}
console.log('');
taskDone();
return fsPromises.writeFile(
"spec-runner/" + partialName,
test.partials[partialName]
);
}
], done);
);
}
function specFileToName(file) {
return file.replace(/.*\//, '').replace('.json', '').replace('~', '').replace(/(^|-)[a-z]/g, function (match) {
return match.toUpperCase();
});
function setupEnvironment(test) {
return cleanup()
.then(() => fsPromises.mkdir("spec-runner/"))
.then(() =>
fsPromises.writeFile("spec-runner/spec-script", test.script)
)
.then(() =>
fsPromises.writeFile("spec-runner/spec-template", test.template)
)
.then(() => writePartials(test));
}
function processSpecFile(specFile, done) {
fs.readFile(specFile, 'utf8', function (err, data) {
var name;
if (err) {
return done(err);
}
name = specFileToName(specFile);
data = JSON.parse(data);
console.log(name);
console.log('====================');
console.log('');
console.log(data.overview);
console.log('');
console.log('Tests');
console.log('-----');
console.log('');
async.series([
function (taskDone) {
async.eachSeries(data.tests, prepareAndRunTest, taskDone);
function executeScript(test) {
return new Promise((resolve) => {
exec(
"bash spec-runner/spec-script 2>&1",
{
timeout: 2000
},
function (taskDone) {
summary[name] = {};
data.tests.forEach(function (test) {
summary[name][test.name] = test.pass;
});
taskDone();
(err, stdout) => {
if (err) {
test.scriptError = err.toString();
}
test.output = stdout;
resolve();
}
], done);
);
});
}
function cleanup() {
return fsPromises.rm("spec-runner/", { force: true, recursive: true });
}
function detectFailure(test) {
if (test.scriptError) {
return true;
}
if (test.output !== test.expected) {
return true;
}
return false;
}
function showFailureDetails(test) {
console.log(`FAILURE: ${test.fullName}`);
console.log("");
console.log(test.desc);
console.log("");
console.log(JSON.stringify(test, null, 4));
}
function applyTestOverrides(test) {
const overrides = testOverrides[test.fullName];
const originals = {};
if (!overrides) {
return;
}
for (const [key, value] of Object.entries(overrides)) {
originals[key] = test[key];
test[key] = value;
}
test.overridesApplied = true;
test.valuesBeforeOverride = originals;
}
function runTest(testSet, test) {
test.partials = test.partials || {};
test.fullName = `${testSet.name} -> ${test.name}`;
applyTestOverrides(test);
test.script = buildScript(test);
if (test.skip) {
debug("Skipping test:", test.fullName, `(${test.skip})`);
return Promise.resolve();
}
debug("Running test:", test.fullName);
return setupEnvironment(test)
.then(() => executeScript(test))
.then(cleanup)
.then(() => {
test.isFailure = detectFailure(test);
if (test.isFailure) {
showFailureDetails(test);
} else {
debug('Test pass:', test.fullName);
}
});
}
function processSpecFile(filename) {
debug("Read spec file:", filename);
return fsPromises.readFile(filename, "utf8").then((fileContents) => {
const testSet = JSON.parse(fileContents);
testSet.name = specFileToName(filename);
return processArraySequentially(testSet.tests, (test) =>
runTest(testSet, test)
).then(() => {
testSet.pass = 0;
testSet.fail = 0;
testSet.skip = 0;
testSet.passOverride = 0;
for (const test of testSet.tests) {
if (test.isFailure) {
testSet.fail += 1;
} else if (test.skip) {
testSet.skip += 1;
} else {
testSet.pass += 1;
if (test.overridesApplied) {
testSet.passOverride += 1;
}
}
}
console.log(
`### ${testSet.name} Results = ${testSet.pass} passed (with ${testSet.passOverride} overridden), ${testSet.fail} failed, ${testSet.skip} skipped`
);
return testSet;
});
});
}
// 0 = node, 1 = script, 2 = file
if (process.argv.length < 3) {
console.log('Specify one or more JSON spec files on the command line');
console.log("Specify one or more JSON spec files on the command line");
process.exit();
}
async = require('async');
fs = require('fs');
exec = require('child_process').exec;
summary = {};
async.eachSeries(process.argv.slice(2), processSpecFile, function () {
var fail, pass;
processArraySequentially(process.argv.slice(2), processSpecFile).then(
(result) => {
console.log("=========================================");
console.log("");
console.log("Failed Test Summary");
console.log("");
let pass = 0,
fail = 0,
skip = 0,
total = 0,
passOverride = 0;
console.log('');
console.log('Summary');
console.log('=======');
console.log('');
pass = 0;
fail = 0;
Object.keys(summary).forEach(function (name) {
var groupPass, groupFail, testResults;
testResults = [];
groupPass = 0;
groupFail = 0;
Object.keys(summary[name]).forEach(function (testName) {
if (summary[name][testName]) {
testResults.push(' * pass - ' + testName);
groupPass += 1;
pass += 1;
} else {
testResults.push(' * FAIL - ' + testName);
groupFail += 1;
fail += 1;
for (const testSet of result) {
pass += testSet.pass;
fail += testSet.fail;
skip += testSet.skip;
total += testSet.tests.length;
passOverride += testSet.passOverride;
console.log(
`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass (with ${passOverride} overridden), ${testSet.fail} fail, ${testSet.skip} skip`
);
for (const test of testSet.tests) {
if (test.isFailure) {
console.log(` * Failure: ${test.name}`);
}
}
});
testResults.unshift('* ' + name + ' (failed ' + groupFail + ' out of ' + (groupPass + groupFail) + ' tests)');
console.log(testResults.join('\n'));
});
}
console.log('');
console.log('Failed ' + fail + ' out of ' + (pass + fail) + ' tests');
});
console.log("");
console.log(
`Final result: ${total} total, ${pass} pass (with ${passOverride} overridden), ${fail} fail, ${skip} skip`
);
if (fail) {
process.exit(1);
}
},
(err) => {
console.error(err);
console.error("FAILURE RUNNING SCRIPT");
console.error("Testing artifacts are left in script-runner/ folder");
}
);

185
run-tests
View File

@ -1,43 +1,162 @@
#!/usr/bin/env bash
#
# Run one or more tests.
#
# Command-line usage to run all tests.
#
# ./run-tests
#
# To run only one test, run "tests/test-name".
#
# Usage within a test as a template. Source run-tests to get functions, export
# any necessary variables, then call runTest.
#
# #!/usr/bin/env bash
# cd "${0%/*}" || exit 1
# . ../run-tests
#
# export template="This is a template"
# export expected="This is a template"
# runTest
#
# When used within the test, you control various aspects with environment
# variables or functions.
#
# - The content passed into mo is either the variable "$template" or the output
# of the function called template.
# - The expected result is either "$expected" or the function called expected.
# - The expected return code is "$returnCode" and defaults to 0.
# - The arguments to pass to mo is the array "${arguments[@]}" and defaults to ().
#
# When $MO_DEBUG is set to a non-empty value, the test does not run, but mo is
# simply executed directly. This allows for calling mo in the same manner as
# the test but does not buffer output nor expect the output to match the
# expected.
#
# When $MO_DEBUG_TEST is set to a non-empty value, the expected and actual
# results are shown using "declare -p" to provide an easier time seeing the
# differences, especially with whitespace.
cd "${0%/*}" || exit 1
testCase() {
echo "Input: $1"
echo "Expected: $2"
}
# shellcheck disable=SC1091
. ./mo
PASS=0
FAIL=0
indirect() {
unset -v "$1"
printf -v "$1" '%s' "$2"
}
for TEST in tests/*.expected; do
export BASE="${TEST%.expected}"
export MO_FALSE_IS_EMPTY=
getValue() {
local name temp len hardSpace
echo -n "$BASE ... "
name=$2
hardSpace=" "
(
if [[ -f "${BASE}.sh" ]]; then
# Run a shell script if one exists
"${BASE}.sh"
else
# Fall back to using .env and .template
# shellcheck disable=SC1090
. "${BASE}.env"
echo "Do not read this input" | mo "${BASE}.template"
if declare -f "$name" &> /dev/null; then
temp=$("$name"; echo -n "$hardSpace")
len=$((${#temp} - 1))
if [[ "${temp:$len}" == "$hardSpace" ]]; then
temp=${temp:0:$len}
fi
) | diff -U5 - "${TEST}" > "${BASE}.diff"
statusCode=$?
if [[ $statusCode -ne 0 ]]; then
echo "FAIL (status code $statusCode)"
FAIL=$(( FAIL + 1 ))
else
echo "ok"
PASS=$(( PASS + 1 ))
rm "${BASE}.diff"
temp=${!name}
fi
done
echo ""
echo "Pass: $PASS"
echo "Fail: $FAIL"
[[ $FAIL -gt 0 ]] && exit 1
local "$1" && indirect "$1" "$temp"
}
runTest() (
local testTemplate testExpected testActual hardSpace len testReturnCode testFail
hardSpace=" "
. ../mo
getValue testTemplate template
getValue testExpected expected
if [[ -n "${MO_DEBUG:-}" ]]; then
echo -n "$testTemplate" | mo ${arguments[@]+"${arguments[@]}"} 2>&1
return $?
fi
testActual=$(echo -n "$testTemplate" | mo ${arguments[@]+"${arguments[@]}"} 2>&1; echo -n "$hardSpace$?")
testReturnCode=${testActual##*$hardSpace}
testActual=${testActual%$hardSpace*}
testFail=false
if [[ "$testActual" != "$testExpected" ]]; then
echo "Failure"
echo "Expected:"
echo "$testExpected"
echo "Actual:"
echo "$testActual"
if [[ -n "${MO_DEBUG_TEST-}" ]]; then
declare -p testExpected
# Align the two declare outputs
echo -n " "
declare -p testActual
fi
testFail=true
fi
if [[ "$testReturnCode" != "$returnCode" ]]; then
echo "Expected return code $returnCode, but got $testReturnCode"
testFail=true
fi
if [[ "$testFail" == "true" ]]; then
return 1
fi
return 0
)
runTestFile() (
local file=$1
echo "Test: $file"
"$file"
)
runTests() (
PASS=0
FAIL=0
if [[ $# -gt 0 ]]; then
for TEST in "$@"; do
runTestFile "$TEST" && PASS=$((PASS + 1)) || FAIL=$((FAIL + 1))
done
else
cd "${0%/*}"
for TEST in tests/*; do
if [[ -f "$TEST" ]]; then
runTestFile "$TEST" && PASS=$((PASS + 1)) || FAIL=$((FAIL + 1))
fi
done
fi
echo ""
echo "Pass: $PASS"
echo "Fail: $FAIL"
if [[ $FAIL -gt 0 ]]; then
exit 1
fi
)
# Clear test related variables
template="Template not defined"
expected="Expected not defined"
returnCode=0
arguments=()
# If sourced, load functions.
# If executed, perform the actions as expected.
if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
runTests ${@+"${@}"}
fi

9
tests/ampersand Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export thing="Works"
export template="{{&thing}}"
export expected="Works"
runTest

21
tests/array Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export repo=( "resque" "hub" "rip" )
template() {
cat <<EOF
{{#repo}}
<b>{{@key}} - {{.}}</b>
{{/repo}}
EOF
}
expected() {
cat <<EOF
<b>0 - resque</b>
<b>1 - hub</b>
<b>2 - rip</b>
EOF
}
runTest

View File

@ -1 +0,0 @@
repo=( "resque" "hub" "rip" )

View File

@ -1,3 +0,0 @@
<b>resque</b>
<b>hub</b>
<b>rip</b>

View File

@ -1,3 +0,0 @@
{{#repo}}
<b>{{.}}</b>
{{/repo}}

25
tests/assoc-array Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
declare -A repo
# The order of the array elements can be shuffled depending on the version of
# Bash. Keeping this to a minimal set and alphabetized seems to help.
repo[hub]="Hub"
repo[rip]="Rip"
export repo
template() {
cat <<EOF
{{#repo}}
<b>{{@key}} - {{.}}</b>
{{/repo}}
EOF
}
expected() {
cat <<EOF
<b>hub - Hub</b>
<b>rip - Rip</b>
EOF
}
runTest

8
tests/comment Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export template="Wor{{!comment}}ks"
export expected="Works"
runTest

15
tests/comment-newline Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
template() {
cat <<EOF
<h1>Today{{! ignore me
and this can
run through multiple
lines}}.</h1>
EOF
}
export expected=$'<h1>Today.</h1>\n'
runTest

View File

@ -1 +0,0 @@
<h1>Today.</h1>

View File

@ -1,4 +0,0 @@
<h1>Today{{! ignore me
and this can
run through multiple
lines}}.</h1>

8
tests/comment-with-spaces Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export template="Wor{{! comment }}ks"
export expected="Works"
runTest

View File

View File

@ -1 +0,0 @@
<h1>Today.</h1>

View File

@ -1 +0,0 @@
<h1>Today{{! ignore me }}.</h1>

10
tests/concatenated-variables Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export thing="Wor"
export thing2="ks"
export template="{{thing thing2}}"
export expected="Works"
runTest

9
tests/delimiters Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export thing="Works"
export template="{{=| |=}}|thing|"
export expected="Works"
runTest

10
tests/double-hyphen Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export arguments=(--fail-on-file -- --help)
export returnCode=1
export template=""
export expected=$'ERROR: No such file: --help\n'
runTest

View File

@ -1 +0,0 @@
cat: --help: No such file or directory

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
# This should display a message indicating that the file --help
# could not be found. It should not display a help messsage.
cd "${0%/*}" || exit 1
../mo -- --help 2>&1

8
tests/double-quote Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export template='{{"Works"}}'
export expected="Works"
runTest

20
tests/fail-not-set Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
unset __NO_SUCH_VAR
export POPULATED="words"
export EMPTY=""
export arguments=(--fail-not-set)
export returnCode=1
template() {
cat <<EOF
Populated: {{POPULATED}};
Empty: {{EMPTY}};
Unset: {{__NO_SUCH_VAR}};
EOF
}
export expected=$'ERROR: Environment variable not set: __NO_SUCH_VAR\n'
runTest

View File

@ -1 +0,0 @@
Env variable not set: __NO_SUCH_VAR

View File

@ -1,9 +0,0 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
unset __NO_SUCH_VAR
POPULATED="words" EMPTY="" ../mo --fail-not-set ./fail-not-set-file.template 2>&1
if [[ $? -ne 1 ]]; then
echo "Did not return 1"
fi

View File

@ -1,3 +0,0 @@
Populated: {{POPULATED}};
Empty: {{EMPTY}};
Unset: {{__NO_SUCH_VAR}};

View File

@ -1,3 +0,0 @@
Populated: words;
Empty: ;
Unset: Env variable not set: __NO_SUCH_VAR

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
unset __NO_SUCH_VAR
POPULATED="words" EMPTY="" ../mo --fail-not-set 2>&1 <<EOF
Populated: {{POPULATED}};
Empty: {{EMPTY}};
Unset: {{__NO_SUCH_VAR}};
EOF
if [[ $? -ne 1 ]]; then
echo "Did not return 1"
fi

13
tests/fail-on-function Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
failFunction() {
false
}
export arguments=(--fail-on-function)
export returnCode=1
export template="Fail on function? {{failFunction}}"
export expected=$'ERROR: Function failed with status code 1: "failFunction"\n'
runTest

View File

@ -1 +0,0 @@
Fail on function? Function 'failFunction' with args () failed with status code 1

View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
failFunction() {
false
}
# Must be sourced to use functions
# shellcheck disable=SC1091
. ../mo
mo --fail-on-function 2>&1 <<EOF
Fail on function? {{failFunction}}
EOF
if [[ $? -ne 1 ]]; then
echo "Did not return 1"
fi

18
tests/false-is-empty-arg Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export USER=j.doe
export ADMIN=false
export arguments=(--false)
template() {
cat <<EOF
The user {{USER}} exists.
{{#ADMIN}}
WRONG - should not be an admin.
{{/ADMIN}}
EOF
}
export expected=$'The user j.doe exists.\n'
runTest

View File

@ -1 +0,0 @@
The user j.doe exists.

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
USER=j.doe ADMIN=false ../mo --false false-is-empty-arg.template

View File

@ -1,4 +0,0 @@
The user {{USER}} exists.
{{#ADMIN}}
WRONG - should not be an admin.
{{/ADMIN}}

18
tests/false-is-empty-env Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export USER=j.doe
export ADMIN=false
MO_FALSE_IS_EMPTY=yeppers
template() {
cat <<EOF
The user {{USER}} exists.
{{#ADMIN}}
WRONG - should not be an admin.
{{/ADMIN}}
EOF
}
export expected=$'The user j.doe exists.\n'
runTest

View File

@ -1,2 +0,0 @@
MO_FALSE_IS_EMPTY=yeppers
someFalseValue=false

View File

@ -1 +0,0 @@
Works

View File

@ -1,4 +0,0 @@
Works
{{#someFalseValue}}
Never shown!
{{/someFalseValue}}

16
tests/false-list Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export person=""
template() {
cat <<EOF
Shown.
{{#person}}
Never shown!
{{/person}}
EOF
}
export expected=$'Shown.\n'
runTest

View File

@ -1 +0,0 @@
person=""

View File

@ -1 +0,0 @@
Shown.

View File

@ -1,4 +0,0 @@
Shown.
{{#person}}
Never shown!
{{/person}}

2
tests/fixtures/inline-indentation vendored Normal file
View File

@ -0,0 +1,2 @@
>
>

View File

@ -0,0 +1,3 @@
|
{{content}}
|

25
tests/function Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export name=Willy
wrapped() {
# Wrapping 'cat' in a subshell eats the trailing whitespace
# The echo adds a newline, which is preserved.
echo "<b>$(cat)</b>"
}
template() {
cat <<EOF
{{#wrapped}}
{{name}} is awesome.
{{/wrapped}}
... this is the last line.
EOF
}
# We don't expect {{name}} to be changed. The function returns whatever content
# that should be the result. There is a separate test where the function handles
# parsing mustache tags.
export expected=$'<b> {{name}} is awesome.</b>\n... this is the last line.\n'
runTest

View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export planet=Earth
lambda() {
local content
content=$(cat)
mo::parse content "$content{{planet}} => |planet|$content"
echo -n "$content"
}
export template="{{= | | =}}<|#lambda|-|/lambda|>"
export expected="<-{{planet}} => Earth->"
runTest

35
tests/function-args Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export name=Willy
MO_ALLOW_FUNCTION_ARGUMENTS=true
pipeTo() {
cat | "$1"
}
testArgs() {
printf "%d" "$#"
# Display all arguments
printf " %q" ${@+"$@"}
}
template() {
cat <<EOF
No args: {{testArgs}} - done
One arg: {{testArgs 'one'}} - done
Getting name in a string: {{testArgs {"The name is " name}}} - done
Reverse this: {{#pipeTo "rev"}}abcde{{/pipeTo "rev"}}
EOF
}
expected() {
cat <<EOF
No args: 0 '' - done
One arg: 1 one - done
Getting name in a string: 1 The\ name\ is\ Willy - done
Reverse this: edcba
EOF
}
runTest

42
tests/function-args-read Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
testArgs() {
local args
# shellcheck disable=SC2031
args=$(declare -p MO_FUNCTION_ARGS)
# The output from declare -p could look like these
# declare -a MO_FUNCTION_ARGS=([0]="one")
# declare -ax MO_FUNCTION_ARGS='([0]="one")'
# Trim leading declare statement and variable name
args="${args#*=}"
# If there are any quotes, remove them. The function arguments will always
# be an array.
if [[ "${args:0:1}" == "'" ]]; then
args=${args#\'}
args=${args%\'}
fi
echo -n "$args"
}
template() {
cat <<EOF
No args: {{testArgs}} - done
One arg: {{testArgs 'one'}} - done
Multiple arguments: {{testArgs 'aa' 'bb' 'cc' 'x' "" '!' '{[_.|' }} - done
Evil: {{testArgs bla; cat /etc/issue}} - done
EOF
}
expected() {
cat <<EOF
No args: () - done
One arg: ([0]="one") - done
Multiple arguments: ([0]="aa" [1]="bb" [2]="cc" [3]="x" [4]="" [5]="!" [6]="{[_.|") - done
Evil: ([0]="" [1]="" [2]="") - done
EOF
}
runTest

View File

@ -1,3 +0,0 @@
testArgs() {
echo "$MO_FUNCTION_ARGS"
}

View File

@ -1,4 +0,0 @@
No args: [] - done
One arg: [one] - done
Multiple arguments: [aa bb cc 'x' " ! {[_.|] - done
Evil: [bla; cat /etc/issue] - done

View File

@ -1,4 +0,0 @@
No args: [{{testArgs}}] - done
One arg: [{{testArgs one}}] - done
Multiple arguments: [{{testArgs aa bb cc 'x' " ! {[_.| }}] - done
Evil: [{{testArgs bla; cat /etc/issue}}] - done

View File

@ -1,13 +0,0 @@
name=Willy
MO_ALLOW_FUNCTION_ARGUMENTS=true
pipeTo() {
cat | "$1"
}
testArgs() {
printf "%d" "$#"
# Display all arguments
printf " %q" ${@+"$@"}
}

View File

@ -1,4 +0,0 @@
No args: 0 '' - done
One arg: 1 one - done
Getting name in a string: 1 The\ name\ is\ Willy - done
Reverse this: edcba

View File

@ -1,4 +0,0 @@
No args: {{testArgs}} - done
One arg: {{testArgs one}} - done
Getting name in a string: {{testArgs "The name is $name"}} - done
Reverse this: {{#pipeTo rev}}abcde{{/pipeTo}}

View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export name=Willy
wrapped() {
local content
# Wrapping 'cat' in a subshell eats the trailing whitespace
content="<b>$(cat)</b>"
# Parse the content using mustache
mo::parse content "$content"
# The echo adds a newline, which is preserved.
echo "$content"
}
template() {
cat <<EOF
{{#wrapped}}
{{name}} is awesome.
{{/wrapped}}
... this is the last line.
EOF
}
export expected=$'<b> Willy is awesome.</b>\n... this is the last line.\n'
runTest

View File

@ -1,5 +0,0 @@
name=Willy
wrapped() {
# This eats the newline in the content
echo "<b>$(cat)</b>"
}

View File

@ -1 +0,0 @@
<b> Willy is awesome.</b>... this is the last line.

View File

@ -1,4 +0,0 @@
{{#wrapped}}
{{name}} is awesome.
{{/wrapped}}
... this is the last line.

28
tests/globals-in-loop Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export STR=abc
export DATA=(111 222)
template() {
cat <<EOF
Issue #7
{{STR}}
{{#DATA}}
Item: {{.}}
String: {{STR}}
{{/DATA}}
EOF
}
expected() {
cat <<EOF
Issue #7
abc
Item: 111
String: abc
Item: 222
String: abc
EOF
}
runTest

View File

@ -1,2 +0,0 @@
STR=abc
DATA=(111 222)

View File

@ -1,6 +0,0 @@
Issue #7
abc
Item: 111
String: abc
Item: 222
String: abc

View File

@ -1,6 +0,0 @@
Issue #7
{{STR}}
{{#DATA}}
Item: {{.}}
String: {{STR}}
{{/DATA}}

101
tests/help Executable file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export arguments=(--help)
expected() {
cat <<'EOF'
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 [OPTIONS] filenames...
Options:
--allow-function-arguments
Permit functions to be called with additional arguments. Otherwise,
the only way to get access to the arguments is to use the
MO_FUNCTION_ARGS environment variable.
-d, --debug
Enable debug logging to stderr.
-u, --fail-not-set
Fail upon expansion of an unset variable. Will silently ignore by
default. Alternately, set MO_FAIL_ON_UNSET to a non-empty value.
-x, --fail-on-function
Fail when a function returns a non-zero status code instead of
silently ignoring it. Alternately, set MO_FAIL_ON_FUNCTION to a
non-empty value.
-f, --fail-on-file
Fail when a file (from command-line or partial) does not exist.
Alternately, set MO_FAIL_ON_FILE to a non-empty value.
-e, --false
Treat the string "false" as empty for conditionals. Alternately,
set MO_FALSE_IS_EMPTY to a non-empty value.
-h, --help
This message.
-s=FILE, --source=FILE
Load FILE into the environment before processing templates.
Can be used multiple times. The file must be a valid shell script
and should only contain variable assignments.
-o=DELIM, --open=DELIM
Set the opening delimiter. Default is "{{".
-c=DELIM, --close=DELIM
Set the closing delimiter. Default is "}}".
-- Indicate the end of options. All arguments after this will be
treated as filenames only. Use when filenames may start with
hyphens.
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.
MO_CLOSE_DELIMITER - The string used when closing a tag. Defaults to "}}".
Used internally.
MO_CLOSE_DELIMITER_DEFAULT - The default value of MO_CLOSE_DELIMITER. Used
when resetting the close delimiter, such as when parsing a partial.
MO_CURRENT - Variable name to use for ".".
MO_DEBUG - When set to a non-empty value, additional debug information is
written to stderr.
MO_FUNCTION_ARGS - Arguments passed to the function.
MO_FAIL_ON_FILE - If a filename from the command-line is missing or a
partial does not exist, abort with an error.
MO_FAIL_ON_FUNCTION - If a function returns a non-zero status code, abort
with an error.
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_OPEN_DELIMITER - The string used when opening a tag. Defaults to "{{".
Used internally.
MO_OPEN_DELIMITER_DEFAULT - The default value of MO_OPEN_DELIMITER. Used
when resetting the open delimiter, such as when parsing a partial.
MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate a
help message.
MO_PARSED - Content that has made it through the template engine.
MO_STANDALONE_CONTENT - The unparsed content that preceeded the current tag.
When a standalone tag is encountered, this is checked to see if it only
contains whitespace. If this and the whitespace condition after a tag is
met, then this will be reset to $'\n'.
MO_UNPARSED - Template content yet to make it through the parser.
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
MO_VERSION=3.0.7
EOF
}
runTest

View File

@ -1,28 +0,0 @@
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 [OPTIONS] filenames...
Options:
-u, --fail-not-set
Fail upon expansion of an unset variable.
-x, --fail-on-function
Fail when a function returns a non-zero status code.
-e, --false
Treat the string "false" as empty for conditionals.
-h, --help
This message.
-s=FILE, --source=FILE
Load FILE into the environment before processing templates.
Can be used multiple times.
MO_VERSION=2.2.0

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
../mo --help

56
tests/indented-partials Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export thisIsTrue=true
template() {
cat <<EOF
With spacing
{{> fixtures/indented-partials.partial}}
{{> fixtures/indented-partials.partial}}
Without spacing
{{> fixtures/indented-partials.partial}}
{{> fixtures/indented-partials.partial}}
With text
{{> fixtures/indented-partials.partial}}
text
{{> fixtures/indented-partials.partial}}
In a conditional
{{#thisIsTrue}}
{{> fixtures/indented-partials.partial}}
{{/thisIsTrue}}
EOF
}
expected() {
cat <<EOF
With spacing
first line
second line
first line
second line
Without spacing
first line
second line
first line
second line
With text
first line
second line
text
first line
second line
In a conditional
first line
second line
EOF
}
runTest

View File

@ -1 +0,0 @@
thisIsTrue=true

View File

@ -1,23 +0,0 @@
With spacing
first line
second line
first line
second line
Without spacing
first line
second line
first line
second line
With text
first line
second line
text
first line
second line
In a conditional
first line
second line

View File

@ -1,18 +0,0 @@
With spacing
{{> indented-partials.partial}}
{{> indented-partials.partial}}
Without spacing
{{> indented-partials.partial}}
{{> indented-partials.partial}}
With text
{{> indented-partials.partial}}
text
{{> indented-partials.partial}}
In a conditional
{{#thisIsTrue}}
{{> indented-partials.partial}}
{{/thisIsTrue}}

19
tests/inline-indentation Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export data="|"
template() {
cat <<EOF
{{data}} {{> fixtures/inline-indentation}}
EOF
}
expected() {
cat <<EOF
| >
>
EOF
}
runTest

9
tests/internal-whitespace Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export boolean=true
export template=$' | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n'
export expected=$' | \n | \n'
runTest

11
tests/invalid-option Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
cd "${0%/*}" || exit 1
. ../run-tests
export person=""
export template=""
export returnCode=1
export arguments=(--something)
export expected=$'ERROR: Unknown option: --something (See --help for options)\n'
runTest

View File

@ -1 +0,0 @@
cat: --something: No such file or directory

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
# This should display a message indicating that the file --something
# could not be found.
cd "${0%/*}" || exit 1
../mo --something 2>&1

Some files were not shown because too many files have changed in this diff Show More