diff --git a/mo b/mo index c1affcb..99696be 100755 --- a/mo +++ b/mo @@ -178,31 +178,39 @@ mo() ( # # $1 - Variable for output # $2 - Function to call -# $3 - Content to pass -# $4 - Additional arguments as a single string +# $3 - Current Context +# $4 - Content to pass +# $5 - Additional arguments as a single string # # This can be dangerous, especially if you are using tags like # {{someFunction ; rm -rf / }} # # Returns nothing. moCallFunction() { - local moArgs moContent moFunctionArgs moFunctionResult + local moArgs moQuoted moContent MO_FUNCTION_ARGS moFunctionResult + + moTrimWhitespace MO_FUNCTION_ARGS "$5" + moSplitArgs MO_FUNCTION_ARGS "$MO_FUNCTION_ARGS" moArgs=() - moTrimWhitespace moFunctionArgs "$4" + for (( m=0; m<${#MO_FUNCTION_ARGS[@]}; m++ )); do + MO_FUNCTION_ARGS[$m]="$(moParse "(${MO_FUNCTION_ARGS[$m]})" "$3" "$3" true)" + moQuote moQuoted "${MO_FUNCTION_ARGS[$m]}" + moArgs+=($moQuoted) + done # shellcheck disable=SC2031 - if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then + if [[ ! -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then # Intentionally bad behavior # shellcheck disable=SC2206 - moArgs=($4) + moArgs=() fi - moContent=$(echo -n "$3" | MO_FUNCTION_ARGS="$moFunctionArgs" eval "$2" "${moArgs[@]}") || { + moContent=$(echo -n "$4" | eval "$2" "${moArgs[@]}") || { moFunctionResult=$? # shellcheck disable=SC2031 if [[ -n "${MO_FAIL_ON_FUNCTION-}" && "$moFunctionResult" != 0 ]]; then - echo "Function '$2' with args (${moArgs[*]+"${moArgs[@]}"}) failed with status code $moFunctionResult" + echo "Function '$2' with args (${MO_FUNCTION_ARGS[*]+"${MO_FUNCTION_ARGS[@]}"}) failed with status code $moFunctionResult" exit "$moFunctionResult" fi } @@ -316,6 +324,72 @@ moFindString() { } +# Internal: Find the index of the first unescaped backslash +# +# $1 - Destination variable for the index +# $2 - Haystack +# +# Returns nothing. +moFindUnescapedSlash() { + local moSlashPos moSlashHaystack moSlashOffset moEscSlashOffset moEscaped + + moSlashHaystack=$2 + moSlashPos=-2 + moEscaped=true + + while [[ $moEscaped == true ]] && [[ $moSlashPos -ne ${#2} ]]; do + moSlashOffset=$(($moSlashPos + 2)) + moSlashHaystack="${2:$moSlashOffset}" + + moFindString moSlashPos "$moSlashHaystack" "\\" + moSlashPos=$(($moSlashOffset + $moSlashPos)) + moEscSlashOffset=$(($moSlashPos+1)) + + moEscaped=false + [[ "${2:$moSlashPos+1:1}" == "\\" ]] && moEscaped=true + done + + [[ $moSlashPos -eq ${#2} ]] && moSlashPos=-1 + local "$1" && moIndirect "$1" "$moSlashPos" +} + + +# Internal: Find the index of an unescaped substring. If not found, sets the +# index to -1. +# +# $1 - Destination variable for the index +# $2 - Haystack +# $3 - Needle +# +# Returns nothing. +moFindUnescaped() { + local moHaystack moPos moOffset moEscPos + + if [[ "$3" == "\\" ]]; then + moFindUnescapedSlash "$1" "$2" + return 0 + fi + + moHaystack=$2 + moPos=-1 + moEscPos=-2 + + while [[ $moPos -ne ${#2} ]] && [[ $moEscPos -ne -1 ]] && [[ $(($moEscPos+1)) -eq $moPos ]]; do + moOffset=$(($moPos + 1)) + moHaystack="${2:$moOffset}" + + moFindString moPos "$moHaystack" "$3" + moPos=$(($moOffset + $moPos)) + + moFindUnescapedSlash moEscPos "$moHaystack" + moEscPos=$(($moOffset + $moEscPos)) + done + + [[ $moPos -eq ${#2} ]] && moPos=-1 + local "$1" && moIndirect "$1" "$moPos" +} + + # Internal: Generate a dotted name based on current context and target name. # # $1 - Target variable to store results @@ -432,6 +506,65 @@ moIndentLines() { } +# Internal: Removes escape characters for displaying. +# +# $1 - Destination variable for the finalized content +# $2 - Content to be cleaned +# +# Returns nothing. +moUnescape() { + local moUnResult moSlashPos moUnclean + + if [[ "$2" =~ "\\" ]]; then + moUnResult="" + moUnclean="$2" + moSlashPos=-1 + + moFindString moSlashPos "$2" "\\" + + while [[ $moSlashPos -gt -1 ]]; do + moUnResult+="${moUnclean:0:$moSlashPos}" + moUnclean="${moUnclean:$moSlashPos+1}" + moFindString moSlashPos "$moUnclean" "\\" + done + moUnResult+=$moUnclean + else + moUnResult="$2" + fi + + local "$1" && moIndirect "$1" "$moUnResult" +} + + +# Internal: Outputs a string of text with quotes around it, and escapes existing quotes +# +# $1 - Destination variable +# $2 - Content to quote +# +# Returns nothing. +moQuote() { + #echo "FOR: $1" >&2 + #echo "UNQUOTED: >$2<" >&2 + #echo "QUOTED: >\"${2//\"/\\\"}\"<" >&2 + local "$1" && moIndirect "$1" "\"${2//\"/\\\"}\"" +} + + +# Internal: Removes quotes from a string an unescapes the quotes inside of it +# +# $1 - Destination variable +# $2 - Content to unquote +# +# Returns nothing. +moUnquote() { + local moLen moUnquoted + moUnquoted="$2" + moLen=${#moUnquoted} + moUnescape moUnquoted "${moUnquoted:1:$moLen - 2}" + local "$1" && moIndirect "$1" "$moUnquoted" +} + + # Internal: Send a variable up to the parent of the caller of this function. # # $1 - Variable name @@ -675,7 +808,7 @@ moLoop() { moParse() { # Keep naming variables mo* here to not overwrite needed variables # used in the string replacements - local moArgs moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag moKey moIsSubExp moOpen moClose + local moArgs moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag moKey moIsSubExp moOpen moClose moParsed moCurrent=$2 moIsBeginning=$3 @@ -692,14 +825,12 @@ moParse() { moSplit moContent "$1" "$moOpen" "$moClose" fi - - while [[ "${#moContent[@]}" -gt 1 ]]; do moTrimWhitespace moTag "${moContent[1]}" moNextIsBeginning=false - if [[ ! "$moTag" == ">"* ]] && [[ "${moContent[1]}" =~ "(" ]]; then - moParsed=$(moParse "${moContent[1]}" "$moCurrent" "$moCurrent" true) + if [[ ! "$moTag" == ">"* ]] && [[ "$moTag" =~ "(" ]]; then + moParsed=$(moParse "$moTag" "$moCurrent" "$moCurrent" true) moContent[1]="$moParsed" moTrimWhitespace moTag "${moContent[1]}" fi @@ -712,11 +843,9 @@ moParse() { if [[ $moIsSubExp == true ]]; then moStandaloneDenied moContent "${moContent[@]}" - if moTest "$moTag"; then - echo -n "true" - else - echo -n "false" - fi + echo -n "\"" + moTest "$moTag" && echo -n "true" || echo -n "false" + echo -n "\"" else moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" @@ -732,7 +861,7 @@ moParse() { if moTest "$moTag"; then # Show / loop / pass through function if moIsFunction "$moTag"; then - moCallFunction moContent "$moTag" "${moBlock[0]}" "$moArgs" + moCallFunction moContent "$moTag" "$moCurrent" "${moBlock[0]}" "$moArgs" moParse "$moContent" "$moCurrent" false moContent="${moBlock[2]}" elif moIsArray "$moTag"; then @@ -759,17 +888,22 @@ moParse() { moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" ;; + '"'*|"'"*) + # Quoted string + moUnquote moContent "${moContent[1]}" + echo -n "$moContent" + moContent="" + ;; + '^'*) # Display section if named thing does not exist moTrimWhitespace moTag "${moTag:1}" if [[ $moIsSubExp == true ]]; then moStandaloneDenied moContent "${moContent[@]}" - if ! moTest "$moTag"; then - echo -n "true" - else - echo -n "false" - fi + echo -n "\"" + ! moTest "$moTag" && echo -n "true" || echo -n "false" + echo -n "\"" else moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" moFindEndTag moBlock "$moContent" "$moTag" "" "$moCurrent" @@ -849,7 +983,7 @@ moParse() { moFullTagName moTag "$moCurrent" "$moTag" # Quote moArgs here, do not quote it later. - moShow "$moTag" "$moCurrent" "$moArgs" + moShow "$moTag" "$moCurrent" "$moArgs" $moIsSubExp ;; esac @@ -944,7 +1078,7 @@ moShow() { local moJoined moNameParts moContent if moIsFunction "$1"; then - moCallFunction moContent "$1" "" "$3" + moCallFunction moContent "$1" "$2" "" "$3" moParse "$moContent" "$2" false return 0 fi @@ -977,6 +1111,7 @@ moShow() { # $2 - String to split # $3 - Starting delimiter # $4 - Ending delimiter (optional) +# $5 - Restore delimiter (optional) # # Returns nothing. moSplit() { @@ -985,7 +1120,7 @@ moSplit() { moResult=( "$2" ) moClosePos=-1 - moFindString moOpenPos "${moResult[0]}" "$3" + moFindUnescaped moOpenPos "${moResult[0]}" "$3" if [[ $moOpenPos -gt -1 ]]; then moResult[1]=${moResult[0]:$moOpenPos + ${#3}} moResult[0]=${moResult[0]:0:$moOpenPos} @@ -993,13 +1128,13 @@ moSplit() { moNext=$moOpenPos if [[ -n "${4-}" ]]; then # Is there another open delimiter inside this one? - moFindString moNext "${moResult[1]}" "$3" - moFindString moClosePos "${moResult[1]}" "$4" + moFindUnescaped moNext "${moResult[1]}" "$3" + moFindUnescaped moClosePos "${moResult[1]}" "$4" while [[ $moNext -gt ${#3} ]] && [[ $moClosePos -gt $moNext ]]; do moFurthest=$(($moClosePos + ${#4})) - moFindString moNext "${moResult[1]:$moFurthest}" "$3" - moFindString moClosePos "${moResult[1]:$moFurthest}" "$4" + moFindUnescaped moNext "${moResult[1]:$moFurthest}" "$3" + moFindUnescaped moClosePos "${moResult[1]:$moFurthest}" "$4" moNext=$(($moNext + $moFurthest)) moClosePos=$(($moFurthest + $moClosePos)) done @@ -1012,10 +1147,49 @@ moSplit() { fi fi + [[ "$5" == "true" ]] && moResult[1]="$3${moResult[1]}$4" + local "$1" && moIndirectArray "$1" "${moResult[@]}" } +# Internal: Parse function arguments +# +# $1 - Destination +# $2 - Arguments as string +# +# Returns nothing. +moSplitArgs() { + local moSCurArg moSArgs moSingle moDouble moParen + + moSArgs=() + + moSCurArg=() + moSCurArg[2]=$2 + while : ; do + [[ ! -z "${moSCurArg[2]}" ]] || break + + moFindUnescaped moSingle "${moSCurArg[2]}" "'" + moFindUnescaped moDouble "${moSCurArg[2]}" "\"" + [[ $moSingle -gt -1 ]] || [[ $moDouble -gt -1 ]] || { + moSArgs+=(${moSCurArg[2]}) + break + } + + if [[ $moSingle < $moDouble ]] && [[ $moSingle -gt -1 ]]; then + moSplit moSCurArg "${moSCurArg[2]}" "'" "'" true + else + moSplit moSCurArg "${moSCurArg[2]}" '"' '"' true + fi + + moSArgs+=(${moSCurArg[0]}) + moSArgs+=("${moSCurArg[1]}") + done + + local "$1" && moIndirectArray "$1" "${moSArgs[@]}" +} + + # 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. diff --git a/tests/function-args-read.env b/tests/function-args-read.env index 0257bfd..98b68dc 100644 --- a/tests/function-args-read.env +++ b/tests/function-args-read.env @@ -1,3 +1,5 @@ +MO_ALLOW_FUNCTION_ARGUMENTS=true + testArgs() { - echo "$MO_FUNCTION_ARGS" + echo "$@" } diff --git a/tests/function-args-read.template b/tests/function-args-read.template index de02070..12a1494 100644 --- a/tests/function-args-read.template +++ b/tests/function-args-read.template @@ -1,4 +1,4 @@ No args: [{{testArgs}}] - done -One arg: [{{testArgs one}}] - done -Multiple arguments: [{{testArgs aa bb cc 'x' " ! {[_.| }}] - done -Evil: [{{testArgs bla; cat /etc/issue}}] - done +One arg: [{{testArgs "one"}}] - done +Multiple arguments: [{{testArgs "aa" "bb" "cc" "'x'" "\" ! {[_.|" }}] - done +Evil: [{{testArgs "bla; cat /etc/issue"}}] - done diff --git a/tests/function-args.template b/tests/function-args.template index 889dfb8..289035d 100644 --- a/tests/function-args.template +++ b/tests/function-args.template @@ -1,4 +1,4 @@ No args: {{testArgs}} - done -One arg: {{testArgs one}} - done +One arg: {{testArgs "one"}} - done Getting name in a string: {{testArgs "The name is $name"}} - done -Reverse this: {{#pipeTo rev}}abcde{{/pipeTo}} +Reverse this: {{#pipeTo "rev"}}abcde{{/pipeTo}} diff --git a/tests/subexpressions.env b/tests/subexpressions.env index 09543ec..fb65400 100644 --- a/tests/subexpressions.env +++ b/tests/subexpressions.env @@ -1,3 +1,5 @@ +MO_ALLOW_FUNCTION_ARGUMENTS=true + x="repo" i=2 repo=( "resque" "hub" "rip" ) @@ -8,14 +10,16 @@ double_quote() { echo "\"${MO_FUNCTION_ARGS[@]}\"" } inter_examp() { - args=($MO_FUNCTION_ARGS) - first_t=${args[0]} - second_t=${args[1]} - first_v=${args[2]} - second_v=${args[3]} + #for v in "$@"; do + # echo "> $v" + #done + first_t="$1" + second_t="$2" + first_v="$3" + second_v="$4" - [[ "$first_t" == "true" ]] && echo -n "$first_v " - [[ "$second_t" == "true" ]] && echo -n "$second_v " + [[ $first_t == true ]] && echo -n "$first_v " + [[ $second_t == true ]] && echo -n "$second_v " } exists="hello" #doesnt="foo" diff --git a/tests/subexpressions.expected b/tests/subexpressions.expected index f2f80ee..b44b87b 100644 --- a/tests/subexpressions.expected +++ b/tests/subexpressions.expected @@ -5,5 +5,5 @@ Loop: "'hub'" hub "'rip'" rip -hello world +hello world help me "escape ! X: repo diff --git a/tests/subexpressions.template b/tests/subexpressions.template index ff5339c..43eeccc 100644 --- a/tests/subexpressions.template +++ b/tests/subexpressions.template @@ -1,9 +1,9 @@ -Function: {{quote (x)}} +Function: {{quote x}} Specific Element: {{repo.(i)}} Loop: {{#repo}} - {{double_quote (quote (.))}} {{.}} + {{double_quote "(quote .)"}} {{.}} {{/repo}} -{{inter_examp (#exists) (^doesnt) (exists) world}} +{{inter_examp (#exists) (^doesnt) exists "world help me \"escape !" exists "testing"}} X: {{x}}