├── .github └── FUNDING.yml ├── .gitignore ├── .tools ├── compile ├── debugger │ └── preamble.bash ├── hooks │ └── pre-push ├── install_hooks ├── local-ci ├── pow.png ├── runtests └── version-update ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── doc ├── FAQ.md ├── contributing.md ├── dependencies-and-packages.md ├── json.md ├── modules-example.md ├── reference.md ├── similar-projects.md └── why.md ├── lib ├── json.pow ├── require.bash ├── std.pow └── unicode.pow ├── package.json ├── powscript ├── src ├── ast │ ├── assign.bash │ ├── ast.bash │ ├── blocks.bash │ ├── commands.bash │ ├── conditionals.bash │ ├── declare.bash │ ├── expand.bash │ ├── expressions.bash │ ├── flag.bash │ ├── functions.bash │ ├── heap.bash │ ├── helper.bash │ ├── indent.bash │ ├── lowerer.bash │ ├── math.bash │ ├── parallel.bash │ ├── parse.bash │ ├── patterns.bash │ ├── pop.bash │ ├── print.bash │ ├── require.bash │ ├── sequence.bash │ ├── states.bash │ └── substitutions.bash ├── compiler │ ├── cache.bash │ ├── compiler.bash │ ├── files.bash │ ├── helptext.bash │ ├── interactive.bash │ ├── options.bash │ ├── temp.bash │ └── version.bash ├── extra │ ├── end.bash │ └── start.bash ├── helper.bash ├── lang │ ├── backends.bash │ ├── bash │ │ ├── compile.bash │ │ ├── interactive.bash │ │ └── start.bash │ ├── common.bash │ └── sh │ │ ├── .settings │ │ ├── compile.bash │ │ ├── interactive.bash │ │ └── start.bash ├── lexer │ ├── lexer.bash │ ├── states.bash │ ├── stream.bash │ ├── tokens.bash │ └── unicode.bash └── powscript.bash └── test ├── .code-1.pow.swp ├── collections └── operations.pow ├── compilation └── basics.bash ├── control-flow ├── async.pow ├── conditions.pow ├── for.pow ├── if.pow └── while.pow ├── etc ├── helper.bash └── shebang.bash ├── functions ├── definition.pow ├── keywords.pow ├── pop.pow ├── references.pow └── rest.pow ├── interactive ├── error-handling.pow └── multiline.pow ├── lib ├── json.pow └── unicode.pow ├── math ├── assignments.pow ├── basics.pow └── variables.pow ├── parser └── lexer.bash ├── repo ├── .ignore_trailing_whitespace ├── forgot_compile │ ├── .tools │ │ └── compile │ ├── powscript │ └── powscript.1 ├── test_pre_push.sh ├── tests_fail │ └── .tools │ │ └── runtests ├── whitespace_trail_pow │ └── file1.pow └── whitespace_trail_sh │ ├── file1.sh │ └── file2.pow ├── strings ├── case.pow ├── expansion.pow ├── indexing.pow ├── linebreak.pow ├── nested-quotes.pow ├── quoting.pow ├── substitution.pow └── unicode.pow └── test-files ├── code-test.pow ├── mymod.pow └── parse-me.pow /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://gumroad.com/l/hGYGh 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .powscript.backup 2 | -------------------------------------------------------------------------------- /.tools/compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -E 4 | 5 | Dir="$(readlink -m "$(dirname "${BASH_SOURCE[0]}")/..")" 6 | PowscriptSourceDirectory="$Dir/src" 7 | PowscriptLibDirectory="$Dir/lib" 8 | 9 | if [ -f ./powscript ]; then 10 | mv "$Dir/powscript" "$Dir/.powscript.backup" 11 | trap '{ mv "$Dir/.powscript.backup" "$Dir/powscript"; exit; }' ERR 12 | fi 13 | 14 | source "$PowscriptSourceDirectory/compiler/version.bash" 15 | OldPatchVersion="$(version:patch-of "$(version:number)")" 16 | NewPatchVersion="$(( OldPatchVersion + 1))" 17 | 18 | 19 | update_patch_version() { 20 | sed -i -e 's/"version":[ ]*\"\(.*\)\.\(.*\)\..*",/"version": "\1.\2.'"$1"'",/g' "$Dir/package.json" || true 21 | sed -i -e 's/POWSCRIPT_VERSION=*\(.*\)\.\(.*\)\..*/POWSCRIPT_VERSION=\1.\2.'"$1"'/g' "$Dir/powscript" || true 22 | } 23 | 24 | 25 | printf '' >"$Dir/powscript" 26 | 27 | add() { 28 | echo "$1" >>"$Dir/powscript" 29 | } 30 | 31 | 32 | if ${POWSCRIPT_CDEBUG-false}; then 33 | LineNum=0 34 | InsideSingleQuotes=false 35 | InsideDoubleQuotes=false 36 | MultiLine=false 37 | NextMultiLine=false 38 | Escape=false 39 | 40 | echo >"$Dir/.cdebug" 41 | 42 | add "$(cat "$Dir/.tools/debugger/preamble.bash")" 43 | 44 | debuggable_line() { 45 | ! $InsideSingleQuotes && ! $InsideDoubleQuotes && ! $MultiLine 46 | } 47 | non_empty_line() { 48 | [ -n "$line" ] && [[ ! "$line" =~ ^[\ ]*'#'.*$ ]] 49 | } 50 | not_before_case() { 51 | [[ ! "$line" =~ ^("'""$"?"("|[^'('])*')' ]] && [[ ! "$line" =~ esac$ ]] 52 | } 53 | at_function_start() { 54 | [[ "$line" =~ ^.*'() {'$ ]] 55 | } 56 | at_function_exit() { 57 | [ "$line" = '}' ] || [[ "$line" =~ ^[\ ]*exit([\ ]+[0-9]+)?$ ]] 58 | } 59 | 60 | flip() { 61 | if ${!1}; then 62 | printf -v "$1" false 63 | else 64 | printf -v "$1" true 65 | fi 66 | } 67 | 68 | update_quote_count() { 69 | local i 70 | MultiLine=false 71 | for i in $( seq 0 $((${#line}-1)) ); do 72 | case ${line:$i:1} in 73 | '\') 74 | if ! $InsideSingleQuotes; then flip Escape; fi; ;; 75 | '"') 76 | if ! $Escape && ! $InsideSingleQuotes; then flip InsideDoubleQuotes; fi 77 | Escape=false;; 78 | "'") 79 | if ! $Escape && ! $InsideDoubleQuotes; then flip InsideSingleQuotes; fi 80 | Escape=false;; 81 | *) 82 | Escape=false;; 83 | esac 84 | done 85 | if $Escape && ! $InsideSingleQuotes && ! $InsideDoubleQuotes; then MultiLine=true; Escape=false; fi 86 | } 87 | 88 | check_before_line() { 89 | LineNum=$((LineNum+1)) 90 | if debuggable_line && at_function_exit; then 91 | local ec="cdebug_err_code_$LineNum" 92 | if [ "$line" = '}' ]; then add "$ec=\$?"; fi 93 | 94 | add "powscript-cdebug:line '${line//\'/,}'" 95 | add 'powscript-cdebug:function-end' 96 | 97 | if [ "$line" = '}' ]; then add "return \$$ec"; fi 98 | 99 | elif debuggable_line && non_empty_line && not_before_case; then 100 | add "powscript-cdebug:line '${line//\'/\`}'" 101 | fi 102 | if at_function_start; then 103 | add "POWSCRIPT_CDEBUG_FUNCTIONS[${line:0:-4}]=true" 104 | fi 105 | if non_empty_line; then update_quote_count; fi 106 | } 107 | 108 | check_after_line() { 109 | if debuggable_line && non_empty_line && at_function_start; then 110 | add "powscript-cdebug:function-start '${line:0:-4}' \"\$@\"" 111 | fi 112 | } 113 | else 114 | if [ -f "$Dir/.cdebug" ]; then rm "$Dir/.cdebug"; fi 115 | fi 116 | 117 | read_file() { 118 | local line file lib var val noshadow_mode=false noshadow_func="" 119 | export RequireOp=add 120 | export ShadowingOp=trimming_add 121 | export ShadowingGetFunc=get_noshadow_func 122 | trimming_add() { 123 | local line 124 | while IFS='' read -r line || [ -n "$line" ]; do 125 | line="${line% }" 126 | if ${POWSCRIPT_CDEBUG-false}; then check_before_line; fi 127 | add "$line" 128 | if ${POWSCRIPT_CDEBUG-false}; then check_after_line; fi 129 | done <<< "$1" 130 | } 131 | get_noshadow_func() { 132 | echo "$noshadow_func" 133 | } 134 | 135 | while IFS='' read -r line || [ -n "$line" ]; do 136 | if ${POWSCRIPT_CDEBUG-false} && ! $noshadow_mode; then check_before_line; fi 137 | 138 | if [[ "$line" =~ .*'#<>' ]]; then 139 | file=${line//*source/} 140 | file=${file//#*/} 141 | file=${file// /} 142 | read_file "$Dir/src/$file" 143 | add "# FILE: $file" 144 | 145 | elif [[ "$line" =~ .*'#<>' ]]; then 146 | file=${line//*source/} 147 | file=${file//#*/} 148 | file=${file// /} 149 | read_file "$(eval echo "$file")" 150 | source "$(eval echo "$file")" 151 | add "# FILE: $file" 152 | 153 | elif [[ "$line" =~ .*'#<>' ]]; then 154 | lib=${line//*powscript_require/} 155 | lib=${lib//#*/} 156 | lib=${lib// /} 157 | powscript_require "$lib" 158 | 159 | elif [[ "$line" =~ .*'#<>' ]]; then 160 | : 161 | 162 | elif [[ "$line" =~ .*'#<>' ]]; then 163 | var="${line%%=*}" 164 | eval "val=${line#$var=}" 165 | add "$var=$val" 166 | 167 | elif [[ "$line" =~ .*'#<>' ]]; then 168 | noshadow_func="dummy()"$'\n'"{" 169 | noshadow_mode=true 170 | 171 | elif $noshadow_mode; then 172 | if [[ "$line" =~ noshadow.* ]]; then 173 | eval "$line" 174 | noshadow_mode=false 175 | else 176 | noshadow_func="$noshadow_func"$'\n'"$line" 177 | fi 178 | else 179 | add "$line" 180 | fi 181 | 182 | if ${POWSCRIPT_CDEBUG-false}; then check_after_line; fi 183 | done <"$1" 184 | } 185 | 186 | 187 | 188 | read_file "$Dir/src/powscript.bash" 189 | 190 | chmod +x "$Dir/powscript" 191 | 192 | 193 | if [ -f "$Dir/.powscript.backup" ]; then 194 | if ! cmp -s "$Dir/.powscript.backup" "$Dir/powscript"; then 195 | if [[ "$*" =~ (--verbose|-v) ]]; then 196 | diff "$Dir/.powscript.backup" "$Dir/powscript" || true 197 | fi 198 | update_patch_version "$NewPatchVersion" 199 | fi 200 | rm "$Dir/.powscript.backup" 201 | fi 202 | -------------------------------------------------------------------------------- /.tools/debugger/preamble.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | shopt -s extglob 3 | 4 | POWSCRIPT_CDEBUG_SKIP=0 5 | POWSCRIPT_CDEBUG_FUNCTION_NESTING=0 6 | POWSCRIPT_CDEBUG_BREAK_FUNCTIONS="interactive:start powscript_compile_file" 7 | POWSCRIPT_CDEBUG_STOP=false 8 | 9 | declare -gA POWSCRIPT_CDEBUG_FUNCTIONS 10 | 11 | powscript-cdebug:echo() { 12 | if ${POWSCRIPT_CDEBUG_STOP}; then 13 | echo "$1" >>/dev/tty 14 | fi 15 | } 16 | 17 | powscript-cdebug:eval() { 18 | local __cdebug_cmd="$1" 19 | shift 20 | POWSCRIPT_CDEBUG_STOP=false 21 | eval "$__cdebug_cmd" >>/dev/tty 22 | POWSCRIPT_CDEBUG_STOP=true 23 | } 24 | 25 | powscript-cdebug:line() { 26 | local __cdebug_cmd __cdebug_unfinished=true __cdebug_args __cdebug_space=true __cdebug_empty_count=0 27 | 28 | if [ ${POWSCRIPT_CDEBUG_SKIP} -gt 0 ]; then 29 | POWSCRIPT_CDEBUG_SKIP=$((POWSCRIPT_CDEBUG_SKIP-1)) 30 | 31 | elif [ -n "$POWSCRIPT_CDEBUG_NEXT" ] && [ $POWSCRIPT_CDEBUG_FUNCTION_NESTING -gt $POWSCRIPT_CDEBUG_NEXT ]; then 32 | true 33 | 34 | elif ${POWSCRIPT_CDEBUG_STOP}; then 35 | POWSCRIPT_CDEBUG_NEXT="" 36 | powscript-cdebug:echo "pow-cdebug line : ${1//\`/\'}" 37 | eval "__cdebug_args=( \"\${POWSCRIPT_CDEBUG_ARGS_$POWSCRIPT_CDEBUG_FUNCTION_NESTING[@]}\" )" 38 | while $__cdebug_unfinished; do 39 | read -r -e -p "pow-cdebug cmd ]] " __cdebug_cmd >/dev/tty 116 | fi 117 | ;; 118 | 'test '*) 119 | powscript-cdebug:eval "if ${__cdebug_cmd:5}; then echo true; else echo false; fi" "${__cdebug_args[@]}" 120 | ;; 121 | '') 122 | ;; 123 | *) 124 | powscript-cdebug:echo "invalid command" 125 | ;; 126 | esac 127 | if $__cdebug_space; then 128 | powscript-cdebug:echo 129 | else 130 | __cdebug_space=true 131 | fi 132 | done 133 | fi 134 | } 135 | 136 | powscript-cdebug:function-start() { 137 | local func args arg i 138 | 139 | POWSCRIPT_CDEBUG_FUNCTION_NESTING=$((POWSCRIPT_CDEBUG_FUNCTION_NESTING+1)) 140 | if ! $POWSCRIPT_CDEBUG_STOP && [ -z "$POWSCRIPT_CDEBUG_NEXT" ]; then 141 | POWSCRIPT_CDEBUG_NEXT= 142 | for func in $POWSCRIPT_CDEBUG_BREAK_FUNCTIONS; do 143 | if [ "$func" = "$1" ]; then 144 | POWSCRIPT_CDEBUG_STOP=true 145 | POWSCRIPT_CDEBUG_SKIP=0 146 | echo "breaking on function '$1'" >>/dev/tty 147 | args="POWSCRIPT_CDEBUG_ARGS_$POWSCRIPT_CDEBUG_FUNCTION_NESTING" 148 | unset "$args" 149 | declare -gA "$args" 150 | i=0 151 | for arg in "${@:2}"; do 152 | eval "$args[$i]=\"$arg\"" 153 | i=$((i+1)) 154 | done 155 | return 156 | fi 157 | done 158 | fi 159 | } 160 | 161 | powscript-cdebug:function-end() { 162 | POWSCRIPT_CDEBUG_FUNCTION_NESTING=$((POWSCRIPT_CDEBUG_FUNCTION_NESTING-1)) 163 | if [ -n "$POWSCRIPT_CDEBUG_NEXT" ] && [ $POWSCRIPT_CDEBUG_FUNCTION_NESTING -le $POWSCRIPT_CDEBUG_NEXT ]; then 164 | POWSCRIPT_CDEBUG_NEXT= 165 | POWSCRIPT_CDEBUG_STOP=true 166 | fi 167 | } 168 | 169 | 170 | -------------------------------------------------------------------------------- /.tools/hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## Ignore script if POWSCRIPT_NOPREPUSH is set 4 | 5 | case $POWSCRIPT_NOPREPUSH in 6 | yes|on|ok) exit 0;; 7 | esac 8 | 9 | ## Check if githook is up to date 10 | 11 | if [ -f '.git/hooks/pre-push' ] && ! diff "$0" '.git/hooks/pre-push' > /dev/null; then 12 | echo "You're using an outdated git pre-push hook! Please update it before pushing your changes." 13 | echo 14 | exit 1 15 | fi 16 | 17 | 18 | ## Utilities 19 | 20 | deref() { 21 | eval "echo \"\$$1\"" 22 | } 23 | 24 | append() { 25 | local varname="$1" 26 | local value="$2" 27 | if [ -n "$(deref $varname)" ]; then 28 | local arg1="$(deref $varname)" 29 | local arg2="$value" 30 | eval "$varname=\"\$(printf '%s\n%s' '$arg1' '$arg2')\"" 31 | else 32 | eval "$varname='$value'" 33 | fi 34 | } 35 | 36 | 37 | ## Helper functions for finding and highlighting trailing whitespace 38 | 39 | find_trailing_whitespace() { 40 | local found_trailing="" 41 | for file in "$@"; do 42 | if [ -d "$file" ] && [ ! -f "$file"/.ignore_trailing_whitespace ]; then 43 | append found_trailing "$(find_trailing_whitespace "$file/"*)" 44 | elif [ -f "$file" ]; then 45 | if echo "$file" | grep -q -E '\.pow$'; then 46 | local pattern='\S\s+$' 47 | else 48 | local pattern='\s+$' 49 | fi 50 | local found_trailing_file="$(grep -n -E "$pattern" "$file")" 51 | if [ -n "$found_trailing_file" ]; then 52 | local filename_color='\033[1;34m' 53 | local reset='\033[0m' 54 | append found_trailing "${filename_color}${file}:${reset}" 55 | append found_trailing "$found_trailing_file" 56 | fi 57 | fi 58 | done 59 | echo "$found_trailing" 60 | } 61 | 62 | highlight_trailing_whitespace() { 63 | local text="$1" 64 | 65 | local arrow_color='\\033[33m' 66 | local whitespace_bg='\\033[41m' 67 | local reset='\\033[0m' 68 | 69 | local find_trailing='\([ \t]\+\)$' 70 | local found_trailing='\1' 71 | 72 | 73 | local highlighted="$(echo "$text" | sed -e "s/${find_trailing}/${arrow_color}>>${whitespace_bg}${found_trailing}${reset}${arrow_color}<<${reset}/")" 74 | local indented="$(echo "$highlighted" | sed -e 's/^\(.*\)>\(.*\)$/ \1>\2/')" 75 | echo "$indented" 76 | } 77 | 78 | 79 | ## Error if trailing whitespace is found. 80 | 81 | case "$POWSCRIPT_PREPUSH_NOWHITESPACE" in 82 | yes|on|ok) ;; 83 | *) 84 | FoundTrailing="$(find_trailing_whitespace *)" 85 | if [ -n "$FoundTrailing" ]; then 86 | echo "Found trailing whitespace:" 87 | printf '%b\n' "$(highlight_trailing_whitespace "$FoundTrailing")" 88 | echo "Please fix it before pushing your changes." 89 | exit 1 90 | fi 91 | ;; 92 | esac 93 | 94 | ## Error if in CDEBUG mode 95 | 96 | if [ -f .cdebug ]; then 97 | echo "The compiler is in debug mode! Switch it off before committing your changes." 98 | exit 1 99 | fi 100 | 101 | ## Error if user forgot to compile 102 | 103 | case "$POWSCRIPT_PREPUSH_NOCOMPILE" in 104 | yes|on|ok) ;; 105 | *) 106 | .tools/compile 107 | if [ -n "$(git --no-pager diff powscript)" ]; then 108 | case "$POWSCRIPT_PREPUSH_NOINTERACTIVE" in 109 | yes|on|ok) 110 | echo "Compilation created unstaged changes! Please commit them before pushing your changes." 111 | exit 1 112 | ;; 113 | *) 114 | echo "Compilation created unstaged changes! Commit message: (leave blank to cancel)" 115 | exec < /dev/tty 116 | read -r commit_message 117 | exec <&- 118 | if [ -n "$commit_message" ]; then 119 | [ "$PAGER" = "less" ] && { PAGER="less -K" git diff powscript || exit 1; } 120 | git add powscript 121 | git commit -m "$commit_message" 122 | git rebase -i HEAD~"$(($(git rev-list --count "$1"/master..)+1))" 123 | else 124 | exit 1 125 | fi 126 | ;; 127 | esac 128 | fi 129 | ;; 130 | esac 131 | 132 | ## Error if tests fail 133 | 134 | case "$POWSCRIPT_PREPUSH_NOTESTS" in 135 | yes|on|ok) ;; 136 | *) .tools/runtests || exit 1 ;; 137 | esac 138 | 139 | exit 0 140 | -------------------------------------------------------------------------------- /.tools/install_hooks: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then 4 | echo 'install_hooks 5 | 6 | Installs git hooks in the powscript local repository. 7 | 8 | Currently only a pre-push hook exists, it will check before each push to 9 | remote if there is no trailling whitespace, .tools/compile was executed 10 | (easy to forget!) and that all tests pass. There are a few enviroment 11 | variables that affect the pre-push script: 12 | 13 | POWSCRIPT_PREPUSH_NOWHITESPACE: 14 | If set to 'yes' the script will not check for trailling whitespace 15 | 16 | POWSCRIPT_PREPUSH_NOCOMPILE: 17 | If set to 'yes' the script will not check for compilation 18 | 19 | POWSCRIPT_PREPUSH_NOTESTS: 20 | If set to 'yes' the script will not run the tests 21 | 22 | POWSCRIPT_NOPREPUSH: 23 | If set to 'yes' the script will not do anything 24 | 25 | To uninstall the hook simply run `rm .git/hooks/pre-push` on the main powscript directory. 26 | ' 27 | exit 0 28 | elif [ "$#" -gt 0 ]; then 29 | echo 'Invalid arguments to install_hooks script. 30 | To execute the script run `install_hooks` with no arguments or run `install_hooks --help` to get more information. 31 | ' 32 | fi 33 | ( 34 | GitHooks="pre-push" 35 | 36 | cd "$(git rev-parse --show-toplevel)" 37 | for hook in $GitHooks; do 38 | cp .tools/hooks/$hook .git/hooks/$hook 39 | done 40 | ) 41 | -------------------------------------------------------------------------------- /.tools/local-ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -x $(command -v docker) ]]; then 4 | docker build -t travis-powscript-test . 5 | if [[ $1 =~ ("-i"|"--interactive") ]]; then 6 | docker run -it travis-powscript-test /bin/bash 7 | else 8 | docker run travis-powscript-test 9 | fi 10 | else 11 | echo -e "please install docker.\n" 12 | fi 13 | 14 | 15 | -------------------------------------------------------------------------------- /.tools/pow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderofsalvation/powscript/862fe979037b0fc57d165fb180f6f5fdc593dca5/.tools/pow.png -------------------------------------------------------------------------------- /.tools/runtests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -E 4 | 5 | export ErrorTracker=$(mktemp) 6 | echo "0" > $ErrorTracker 7 | 8 | NoIndent="_noIdent--##$RANDOM" 9 | 10 | format() { 11 | while IFS='' read -e -r line; do 12 | if [[ ! "$line" =~ "$NoIndent".* ]]; then 13 | printf " $line\n" 14 | else 15 | printf "${line/$NoIndent/}\n" 16 | fi 17 | done 18 | } 19 | 20 | trap 'echo $? > "$ErrorTracker"' ERR 21 | 22 | { 23 | 24 | Info="\033[1;35m" 25 | File="\033[1;34m" 26 | Pass="\033[1;32m" 27 | Error="\033[1;31m" 28 | Nc="\033[0;00m" 29 | 30 | decl_file() { 31 | printf "${NoIndent}${File}$1\n${Nc}" 32 | } 33 | 34 | checktest() { 35 | if [ -n "$2" ]; then 36 | wait "$2" 37 | local exitcode=$? 38 | else 39 | local exitcode=$1 40 | fi 41 | if [[ $exitcode == "0" ]]; then 42 | printf "${NoIndent}${Pass}Tests passed!\n${Nc}\n" 43 | else 44 | printf "${NoIndent}${Error}Tests failed!\n${Nc}\n" 45 | exit 1 46 | fi 47 | } 48 | 49 | header() { 50 | printf "${NoIndent}${Info}$1${Nc}\n" 51 | } 52 | 53 | finish() { 54 | printf "${NoIndent}${Info}OK!${Nc}\n" 55 | } 56 | 57 | testdirectory() { 58 | local directory="$1" 59 | local command="$2" 60 | local varname="$3" 61 | local pattern="$4" 62 | [[ ! -n $pattern ]] && pattern=".*" 63 | 64 | for file in "$directory"/*; do 65 | if [[ $file =~ $pattern ]]; then 66 | decl_file "$file" 67 | local rq_file="${file//\//\\/}" 68 | eval "$(sed -E "s/\\\$[{]?${varname}[}]?/\"${rq_file}\"/" < <(echo "${command}"))" 69 | checktest "$?" "$!" || exit 1 70 | fi 71 | done 72 | } 73 | 74 | header "Parser tests:" 75 | testdirectory "test/parser" './${file}' 'file' 76 | 77 | header "Compilation tests:" 78 | testdirectory "test/test-files" './powscript --no-std -c ${file} >/dev/null' '.*\.pow' 79 | 80 | header "String tests:" 81 | testdirectory "test/strings" './powscript --no-std ${file}' 'file' '.*\.pow' 82 | 83 | header "Function tests:" 84 | testdirectory "test/functions" './powscript --no-std ${file}' 'file' '.*\.pow' 85 | 86 | header "Collections tests:" 87 | testdirectory "test/collections" './powscript --no-std ${file}' 'file' '.*\.pow' 88 | 89 | header "Control flow tests:" 90 | testdirectory "test/control-flow" './powscript --no-std ${file}' 'file' '.*\.pow' 91 | 92 | header "Math tests:" 93 | testdirectory "test/math" './powscript --no-std ${file}' 'file' '.*\.pow' 94 | 95 | header "Evaluation tests:" 96 | testdirectory "test/test-files" './powscript ${file}' 'file' '.*\.pow' 97 | 98 | header "Library tests:" 99 | testdirectory "test/lib" './powscript ${file}' 'file' '.*\.pow' 100 | 101 | header "Interactive mode tests:" 102 | testdirectory "test/interactive" './powscript --no-std --interactive < ${file}' 'file' 103 | 104 | header "Repository maintainance tests:" 105 | testdirectory "test/repo" './${file}' 'file' '.*\.sh' 106 | 107 | header "Other:" 108 | testdirectory "test/etc" './${file}' 'file' 109 | 110 | finish 111 | 112 | } | format 113 | 114 | Err="$(cat "$ErrorTracker")" 115 | rm $ErrorTracker 116 | 117 | if [[ $Err != 0 ]]; then 118 | echo "Error code: ${Err}" 119 | fi 120 | 121 | exit $Err 122 | -------------------------------------------------------------------------------- /.tools/version-update: -------------------------------------------------------------------------------- 1 | case "$1" in 2 | --major) 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | install: true 2 | script: ./.tools/runtests 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/travisci/travis-ruby:latest 2 | 3 | WORKDIR /home/travis/ 4 | RUN mkdir powscript 5 | COPY . ./powscript 6 | USER travis 7 | WORKDIR /home/travis/powscript 8 | RUN {\ 9 | {\ 10 | linenum=0;\ 11 | while IFS="" read line; do\ 12 | linenum=$(($linenum+1));\ 13 | echo "$linenum| $line";\ 14 | done;\ 15 | } < "./powscript";\ 16 | } 17 | CMD {\ 18 | ./.tools/runtests;\ 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Leon van Kammen. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY Leon van Kammen AS IS'' AND ANY EXPRESS OR IMPLIED 14 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Leon van Kammen OR 16 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 21 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | The views and conclusions contained in the software and documentation are those of the 24 | authors and should not be interpreted as representing official policies, either expressed 25 | or implied, of Leon van Kammen 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Travis build status](https://travis-ci.org/coderofsalvation/powscript.svg?branch=master)](https://travis-ci.org/coderofsalvation/powscript.svg?branch=master) 4 | 5 | write shellscript in a powful way! 6 | 7 | ## Usage 8 | 9 | $ wget "https://raw.githubusercontent.com/coderofsalvation/powscript/master/powscript" -O /usr/local/bin/powscript && chmod 755 /usr/local/bin/powscript 10 | $ powscript myscript.pow # run directly 11 | $ powscript -c myscript.pow > myscript # output bashscript 12 | $ powscript -c --to sh myscript.pow > myscript.sh # output sh-script (experimental) 13 | 14 | * [unnoficial AUR package](https://aur.archlinux.org/packages/powscript/) 15 | 16 | ## Wiki 17 | 18 | * [Syntax reference](https://github.com/coderofsalvation/powscript/blob/master/doc/reference.md) 19 | * [ Modules / Developer Info / Contribution / Similar Projects / Why ](https://github.com/coderofsalvation/powscript/wiki) 20 | 21 | ## Example 22 | 23 | #!/usr/bin/env powscript 24 | require_cmd 'echo' 25 | require_env 'TERM' 26 | 27 | error(msg exitcode) 28 | echo "error: $msg" 29 | if set? $exitcode 30 | exit $exitcode 31 | 32 | run(@args -- foo) 33 | if empty? foo 34 | error "please pass --foo " 1 35 | echo $args[@] "$foo universe!!" 36 | echo "HOME=$HOME" 37 | 38 | run $@ 39 | 40 | Output: 41 | 42 | $ powscript -c foo.pow -o foo.bash 43 | $ ./foo.bash hello --foo powful 44 | hello powful universe! 45 | HOME=/home/yourusername 46 | 47 | Check a [json example here](https://github.com/coderofsalvation/powscript/blob/master/doc/json.md) and here for more examples 48 | 49 | ## Features 50 | 51 | * indentbased, memorizable, coffeescript-inspired syntax 52 | * removes semantic noise like { ! [[ @ ]] || ~= 53 | * safetynets: automatic quoting, halt on error or missing dependencies (`require_cmd`,`require_env`) 54 | * comfort: [json, easy arrays, easy async, functional programming](https://github.com/coderofsalvation/powscript/blob/master/doc/reference.md), named variables instead of positionals 55 | * [Modules / bundling](https://github.com/coderofsalvation/powscript/blob/master/doc/modules-example.md) 56 | * [remote/local packages & dependencies](http://github.com/coderofsalvation/powscript/blob/master/doc/dependencies-and-packages.md) 57 | * written/generated for bash >= 4.x, 'zero'-dependency solution for embedded devices e.g. 58 | * hasslefree: easy installation without gcc compilation/3rd party software 59 | 60 | ## Examples 61 | 62 | * [m3uchecker (19 lines powscript vs 57 lines bash)](https://gist.github.com/coderofsalvation/b1313d287c1f0a7e6cdf) 63 | * [pm.sh](https://github.com/coderofsalvation/pm.sh) 64 | * [Collection of codesnippets](https://github.com/coderofsalvation/powscript/blob/master/doc/reference.md) 65 | 66 | ## Interactive mode (experimental) 67 | 68 | Put this line in your `.inputrc`: 69 | 70 | "\C-p" "powscript --interactive\n" 71 | 72 | Then hitting ctrl-p in your console will enter powscript mode: 73 | 74 | hit ctrl-c to exit powscript, type 'edit' to launch editor, and 'help' for help 75 | > each(line) 76 | > echo line=$line 77 | > run() 78 | > tail -2 ~/.kanban.csv | mappipe each 79 | > run 80 | line=1,foo,bar,flop 81 | line=2,foo2,bar2,flop2 82 | > 83 | 84 | ## POSIX /bin/sh compatibility 85 | 86 | Powscript can produce 'kindof' POSIX `/bin/sh`-compatible output by removing bashisms, by introducing the `--sh` flag: 87 | 88 | $ powscript --c foo.pow -o foo.bash 89 | $ powscript --to sh --c foo.pow -o foo.sh 90 | 91 | This however, is experimental, as well as the standalone bash2sh converter: 92 | 93 | $ cat foo.bash | powscript --to sh > foo.sh 94 | 95 | > NOTE: remove bashisms manually using docs/tools like [bashism guide](http://mywiki.wooledge.org/Bashism) or [checkbashisms](https://linux.die.net/man/1/checkbashisms) 96 | > The general rule for POSIX sh-output is: `don't write bashfeatures in powscript` 97 | 98 | ## Debug your powscript syntax 99 | 100 | See [FAQ](doc/FAQ.md) 101 | 102 | ## OSX users 103 | 104 | OSX might protest since it isn't quite GNU focused. Please run these commands after installing: 105 | 106 | $ brew install bash 107 | $ brew install coreutils gnu-sed grep gawk --default-names 108 | $ echo 'export PATH=/usr/local/opt/coreutils/libexec/gnubin:$PATH' >> ~/.bashrc 109 | $ sed -i 's|#!/bin/bash|#!/usr/local/bin/bash|g' powscript 110 | 111 | ## Live expansion inside editor 112 | 113 | > HINT: use live expansion inside vim. 114 | > Put the lines below in .vimrc and hit 'p>' in normal/visual mode to expand powscript to bash 115 | 116 | vmap p> :!PIPE=2 powscript -c 117 | nmap p> ggVG:!PIPE=2 powscript -c 118 | 119 | -------------------------------------------------------------------------------- /doc/FAQ.md: -------------------------------------------------------------------------------- 1 | > **Q: where is ALL the shellscript documentation?** 2 | 3 | > A: type `info bash` for more on the language itself 4 | 5 | > **Q: Is powscript a programming language ** 6 | 7 | > A: not really. 8 | 9 | > **Q: What do i need to run powscript?** 10 | 11 | > A: nothing, just a linux bash-shell (v4 and upwards). 12 | 13 | > **Q: can i just use bash syntax inside powscript?** 14 | 15 | > A: yes (in theory), however always check the output (or `powscript -c yourfile`) in case you get weird errors. 16 | 17 | > **Q: How fast is shellscript** 18 | 19 | > A: Fast enough. But as with any programming languages: it depends on what you're trying to achieve. A good rule of thumb is: the more file-based operations (`sed` or `> foo.txt` e.g.), the slower the code. Use `time yourfunction` to benchmark your function. Use async operations or `GNU parallel` to use multiple cores. 20 | 21 | > **Q: Why is `./powscript` saying 'compiling ...' during first run?** 22 | 23 | > A: this happens only once. It's just building a cache of pre-compiled libraries. 24 | 25 | > **Q: Did Jeremy Ashkenas pay you to do this?** 26 | 27 | > A: No, but he is a big inspiration. Powscript, just like coffeescript focuses on getting things done quicker. 28 | -------------------------------------------------------------------------------- /doc/contributing.md: -------------------------------------------------------------------------------- 1 | ## Building 2 | 3 | running `./.tools/compile` will produce `powscript`, which is a bundle of: 4 | 5 | src/powscript.bash <-- the main source 6 | lang/bash/.transpile <-- the transpile regexes 7 | lang/bash/* <-- extra internal powscript syntax 8 | 9 | ## Testing 10 | 11 | ./.tools/runtests 12 | 13 | 14 | ## Git Hooks 15 | 16 | ./.tools/install_hooks 17 | 18 | This will install a pre-push git hook that will check for trailing whitespace, automatically performs the tests and checks if you've compiled the source code. See `./.tools/install_hooks --help` for more information. 19 | 20 | ## Philosophy / Scope 21 | 22 | * powscript limits itself to functional & procedural programming (no OOP) 23 | * making bashscripting easier and less errorprone (like coffeescript for javascript) 24 | -------------------------------------------------------------------------------- /doc/dependencies-and-packages.md: -------------------------------------------------------------------------------- 1 | 2 | ## Packages & dependency handling 3 | 4 | Installed commands can be checked at runtime using `require_cmd`: 5 | 6 | #!/usr/bin/env powscript 7 | require_cmd 'echo' 8 | 9 | echo 'hello world' 10 | 11 | Remote packages can be included using [aap](https://github.com/coderofsalvation/aap), which is npm for bash+git. Assume this powscript: 12 | 13 | 14 | #!/usr/bin/env powscript 15 | require 'username/powscriptrepo/foo.pow' 16 | 17 | echo 'hello world' 18 | 19 | This can be installed by simply running `aap install` in the projectdirectory (which contains an `aap.json`). 20 | Create `aap.json` like this: 21 | 22 | $ wget "https://github.com/coderofsalvation/aap/raw/master/aap" -O ~/bin/aap && chmod 755 ~/bin/aap 23 | $ git init 24 | $ aap init 25 | $ aap install ssh+git://user@github.com/username/powscriptrepo.git --save 26 | $ git add aap.json && git commit -m "added remote dependencies" && git push origin master 27 | 28 | once you pushed your repo to github, other users can simply run: 29 | 30 | $ aap install 31 | installing 'user@github.com/username/powscriptrepo.git' 32 | ├─ $ git clone ssh+git://user@github.com/username/powscriptrepo.git 33 | ├─ Cloning into 'powscriptrepo'... 34 | ├─ 35 | ├─ ʕ•x•ʔ 36 | ├─ +-+-+-+ Your personal nested build & dependency monkey 37 | ├─ |a|a|p| [https://github.com/coderofsalvation/aap] 38 | ├─ +-+-+-+ 39 | 40 | 41 | -------------------------------------------------------------------------------- /doc/json.md: -------------------------------------------------------------------------------- 1 | # JSON 2 | 3 | JSON is a very popular format. 4 | While there are great utilities like `jq`, powscript also supports basic json: 5 | 6 | require 'json' 7 | require_cmd 'curl' 8 | 9 | usage(app) 10 | local example_url="https://raw.githubusercontent.com/coderofsalvation/powscript/master/package.json" 11 | echo " 12 | $app --url 13 | 14 | Description: 15 | obtains and parses the json file given by the url, 16 | then prints the data corresponding to the received keys. 17 | 18 | Examples: 19 | $app version --url '$example_url' 20 | $app repository type --url '$example_url' 21 | $app repository url --url '$example_url' 22 | " 23 | 24 | run(@keys -- url) 25 | if empty? url 26 | echo "Usage: $(usage myapp)" && exit 27 | data={} 28 | json=$(curl -s $url) 29 | json_parse data "$json" 30 | json_print data $keys[@] 31 | 32 | run $@ 33 | -------------------------------------------------------------------------------- /doc/modules-example.md: -------------------------------------------------------------------------------- 1 | ## Modules example 2 | 3 | to include external scripts / cmd 4 | 5 | * use `source` to include file during **runtime** 6 | * use `require` to include file during **compiletime** 7 | * use `require_cmd` to check whether external cmds are installed during **runtime** 8 | * use `require_env` to check whether certain environment variables are set during **runtime** 9 | 10 | #### /mylibs.pow 11 | 12 | require 'mod/mymod.pow' 13 | require 'mod/foo.bash' 14 | require_cmd 'awk' 15 | require_env 'EDITOR' 16 | 17 | mymodfunc 18 | bar 19 | 20 | #### /mod/mymod.pow 21 | 22 | mymodfunc() 23 | echo "hi im a powscript module!" 24 | 25 | #### /mod/foo.bash 26 | 27 | function bar(){ 28 | echo "hi im a bash module" 29 | 30 | } 31 | 32 | # Bundle all modules 33 | 34 | just run `powscript -c mylibs.pow -o mylibs.bash` 35 | 36 | -------------------------------------------------------------------------------- /doc/reference.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 38 | 39 | 40 | 41 | 55 | 77 | 78 | 79 | 80 | 81 | 82 | 93 | 107 | 108 | 109 | 110 | 111 | 132 | 157 | 158 | 159 | 160 | 161 | 177 | 195 | 196 | 197 | 198 | 199 | 215 | 232 | 233 | 234 | 235 | 236 | 244 | 253 | 254 | 255 | 256 | 257 | 265 | 278 | 279 | 280 | 281 | 282 | 295 | 301 | 302 | 303 | 304 | 305 | 319 | 336 | 337 | 338 | 339 | 340 | 351 | 359 | 360 | 361 | 362 | 363 | 371 | 380 | 381 | 382 | 383 | 384 | 397 | 405 | 406 | 407 | 408 | 409 | 424 | 432 | 433 | 434 | 435 | 436 | 452 | 461 | 462 | 463 | 464 | 465 | 477 | 484 | 485 | 486 | 487 | 488 | 500 | 508 | 509 | 510 | 511 | 512 | 523 | 533 | 534 | 535 | 536 | 537 | 551 | 557 | 558 | 559 | 560 | 561 | 573 | 580 | 581 | 582 | 583 | 584 | 600 | 607 | 608 | 609 | 610 |
WhatPowscriptBash output
Functions 15 |
 16 |         
 17 | foo(a b)
 18 |   echo a=$a b=$b
 19 |         
 20 |         
 21 | foo one two
 22 |         
 23 |       
24 |
26 |
 27 |         
 28 | f() {
 29 |   local a="${1}" b="${2}"
 30 |   echo a="${a}" b="${b}"
 31 | }
 32 |         
 33 |         
 34 | foo one two
 35 |         
 36 |       
37 |
Variable/Array-ish arguments 42 |
 43 |         
 44 | foo(a b @extra)
 45 |   echo a=$a b=$b
 46 |   if set? extra
 47 |     echo first: $extra[0]
 48 |     echo all: $extra[@]
 49 |         
 50 |         
 51 | foo one two a b c
 52 |         
 53 |       
54 |
56 |
 57 |         
 58 | foo() {
 59 |   {
 60 |     local a b;
 61 |     declare -a extra
 62 |     [ $# -ge 1 ] && a="${1}"
 63 |     [ $# -ge 2 ] && b="${2}"
 64 |     [ $# -ge 3 ] && extra=( "${@:3}" )
 65 |   }
 66 |   echo a="${a}" b="${b}"
 67 |   if set? extra; then
 68 |   echo first: "${extra[0]}"
 69 |   echo all: "${extra[@]}"
 70 |   fi
 71 | }
 72 | 
 73 | foo 1 2 foo bar flop        
 74 |         
 75 |       
76 |
Switch statement 83 |
 84 |         
 85 | switch $foo
 86 |   case [0-9]*
 87 |     echo "bar"
 88 |   case *
 89 |     echo "foo"
 90 |         
 91 |       
92 |
94 |
 95 |         
 96 | case "${foo}" in
 97 |   [0-9]*)
 98 |     echo "bar"
 99 |     ;;
100 |   *)
101 |     echo "foo"
102 |     ;;
103 | esac
104 |         
105 |       
106 |
Easy if statements 112 |
113 |         
114 | if $i is "foo"
115 |   echo "foo"
116 | else
117 |   echo "bar"
118 | 
119 | 
120 | if not false and true
121 |   if true or false
122 |     echo "foo"
123 |         
124 |         
125 | if $x > $y
126 |   echo "bar"
127 | elif $a < $b
128 |   echo "baz"
129 | 
130 |       
131 |
133 |
134 |         
135 | if [[ "$i" == "foo" ]]; then
136 |   echo "foo"
137 | else
138 |   echo "bar"
139 | fi
140 |         
141 |         
142 | if ! false && true; then
143 |   if true || false; then
144 |     echo 10
145 |   fi
146 | fi
147 |         
148 |         
149 | if [[ "$x" -gt "$y" ]]; then
150 |   echo "bar"
151 | elif [[ "$a" -lt "$b" ]]; then
152 |   echo "baz"
153 | fi
154 |         
155 |       
156 |
Associative arrays 162 |
163 |         
164 | foo={}
165 | foo["bar"]="a value"
166 |         
167 |         
168 | for k,v of foo
169 |   echo k=$k
170 |   echo v=$v
171 |         
172 |         
173 | echo $foo["bar"]
174 |         
175 |       
176 |
178 |
179 |         
180 | declare -A foo
181 | foo["bar"]="a value"
182 |         
183 |         
184 | for k in "${!foo[@]}"; do
185 |   v="${foo[$k]}"
186 |   echo k="$k"
187 |   echo v="$v"
188 | done
189 |         
190 |         
191 | echo "${foo["bar"]}"
192 |         
193 |       
194 |
Indexed array 200 |
201 |         
202 | bla=[]
203 | bla[0]="foo"
204 | bla@="push value"
205 |         
206 |         
207 | for i of bla
208 |   echo bla=$i
209 |         
210 |         
211 | echo $bla[0]
212 |         
213 |       
214 |
216 |
217 |         
218 | declare -a bla
219 | bla[0]="foo"
220 | bla[${#bla}]="push value"
221 |         
222 |         
223 | for i in "${bla[@]}"; do
224 |   echo bla="$i"
225 | done
226 |         
227 |         
228 | echo "${bla[0]}"
229 |         
230 |       
231 |
Read file line by line (shared scope) 237 |
238 |         
239 | for line from $selfpath/foo.txt
240 |   echo "->"$line
241 |         
242 |       
243 |
245 |
246 |         
247 |   while IFS="" read -r line; do
248 |     echo "->"$line
249 |   done < $1
250 |         
251 |       
252 |
Regex 258 |
259 |         
260 | if $f match ^([f]oo)
261 |   echo "foo found!"
262 |         
263 |       
264 |
266 |
267 |         
268 | # extended pattern matching
269 | # (google 'extglob' for more)
270 |         
271 |         
272 | if [[ "$f" =~ ^([f]oo) ]]; then
273 |   echo "foo found!"
274 | fi
275 |         
276 |       
277 |
Requiring modules 283 |
284 |         
285 | # compile and include
286 | # powscript file
287 | require 'mymodule.pow'
288 |         
289 |         
290 | # source works at runtime
291 | source foo.bash
292 |         
293 |       
294 |
296 |
297 |         
298 |         
299 |       
300 |
empty / isset checks 306 |
307 |         
308 | bar()
309 |   if empty $1
310 |     echo "no argument given"
311 |   if isset $1
312 |     echo "string given"
313 |         
314 |         
315 | foo "$@"
316 |         
317 |       
318 |
320 |
321 |         
322 | foo(){
323 |   if [ -z "${1}" ]; then
324 |     echo "no argument given"
325 |   fi
326 |   if [ -n "${#1}" ]; then
327 |     echo "string given"
328 |   fi
329 | }
330 |         
331 |         
332 | foo "$@"
333 |         
334 |       
335 |
mappipe unwraps a pipe 341 |
342 |         
343 | fn()
344 |   echo "value=$1"
345 |         
346 |         
347 | echo -e "a\nb\n" | mappipe fn
348 |         
349 |       
350 |
352 |
353 |         
354 | # outputs: value=a
355 | #          value=b
356 |         
357 |       
358 |
Easy math 364 |
365 |         
366 | math (9 / 2)
367 | math (9 / 2) 4
368 |         
369 |       
370 |
372 |
373 |         
374 | # outputs: '4' and '4.5000'
375 | # NOTE: floating point math
376 | #       requires bc
377 |         
378 |       
379 |
Easy async 385 |
386 |         
387 | fn()
388 |   sleep 1s
389 |   echo "one"
390 |         
391 |         
392 | await fn 123 then
393 |   echo "async done"
394 |         
395 |       
396 |
398 |
399 |         
400 | # outputs: one
401 | #          async done
402 |         
403 |       
404 |
Easy async pipe 410 |
411 |         
412 | fn()
413 |   sleep 1s
414 |   echo "one"
415 |         
416 |         
417 | await fn 123 then |
418 |   cat -
419 | when done
420 |   echo "async done"
421 |         
422 |       
423 |
425 |
426 |         
427 | # outputs: one
428 | #          async done
429 |         
430 |       
431 |
Easy async pipe (per line) 437 |
438 |         
439 | fn()
440 |   sleep 1s
441 |   echo "one"
442 |   echo "two"
443 |         
444 |         
445 | await fn 123 then for line
446 |   echo "line: $*"
447 | when done
448 |   echo "async done"
449 |         
450 |       
451 |
453 |
454 |         
455 | # outputs: line: one
456 | #          line: two
457 | #          async done
458 |         
459 |       
460 |
JSON parsing 466 |
467 |         
468 | obj={}
469 | json='{"a": {"b": "c"}}'
470 |         
471 |         
472 | json_parse obj "$json"
473 | json_print obj a b
474 |         
475 |       
476 |
478 |
479 |         
480 | # outputs: c
481 |         
482 |       
483 |
FP: curry 489 |
490 |         
491 | fn(a b)
492 |   echo "1=$a 2=$b"
493 |         
494 |         
495 | curry fnc a
496 | echo -e "b\nc\n" | mappipe fnc
497 |         
498 |       
499 |
501 |
502 |         
503 | # outputs: 1=a 2=b
504 | #          1=a 2=c
505 |         
506 |       
507 |
FP: array values, keys 513 |
514 |         
515 | foo={}
516 | foo["one"]="foo"
517 | foo["two"]="bar"
518 | map foo keys
519 | map foo values
520 |         
521 |       
522 |
524 |
525 |         
526 | # outputs: one
527 | #          two
528 | #          foo
529 | #          bar
530 |         
531 |       
532 |
FP: map 538 |
539 |         
540 | printitem(k v)
541 |   echo "key=$k value=$v"
542 |         
543 |         
544 | foo={}
545 | foo["one"]="foo"
546 | foo["two"]="bar"
547 | map foo printitem
548 |         
549 |       
550 |
552 |
553 |         
554 |         
555 |       
556 |
FP: pick 562 |
563 |         
564 | foo={}
565 | bar={}
566 | foo["one"]="foo"
567 | bar["foo"]="123"
568 | map foo values |\
569 |   mappipe pick bar
570 |         
571 |       
572 |
574 |
575 |         
576 | # outputs: 123
577 |         
578 |       
579 |
FP: compose 585 |
586 |         
587 | fnA(x)
588 |   echo "($x)"
589 |         
590 |         
591 | fnB(x)
592 |   echo "|$x|"
593 |         
594 |         
595 | compose decorate fnA fnB
596 | decorate "foo"
597 |         
598 |       
599 |
601 |
602 |         
603 | # outputs: (|foo|)
604 |         
605 |       
606 |
611 | 612 | # Predefined vars 613 | 614 | | **description** | **variable name** | **example** | 615 | |---------------------------|-------------------|------------------------------------| 616 | | script path | $selfpath | for line in $selfpath/foo.txt | 617 | | current working directory | $self | ls $self/* | 618 | | temporary file | $tmpfile | echo '{foo:"bar"}' > $tmpfile.json | 619 | 620 | -------------------------------------------------------------------------------- /doc/similar-projects.md: -------------------------------------------------------------------------------- 1 | Here are similar projects which i found, which all require a certain development environment to be installed. 2 | Powscript, only requires bash only instead, which can be handy for IoT/embedded systems. 3 | 4 | 5 | [cash](https://github.com/aseemk/cash) 6 | > language: javascript 7 | > needs nodejs install 8 | 9 | [bish](https://github.com/tdenniston/bish) 10 | > language: c++ 11 | > needs user compiler / compilation 12 | 13 | [bash](https://github.com/shelljs/shelljs) 14 | > language: javascript 15 | > needs nodejs install 16 | 17 | [plumbum](http://plumbum.readthedocs.org/en/latest/index.html) 18 | > language: python 19 | > needs python install 20 | 21 | [batsh](https://github.com/BYVoid/Batsh) 22 | > language: ocaml 23 | > needs ocaml install 24 | 25 | [zsh](http://www.zsh.org/) 26 | > language: zsh 27 | > needs zsh install 28 | 29 | [nscript](https://github.com/mweststrate/nscript) 30 | > language: javascript 31 | > needs nodejs install 32 | -------------------------------------------------------------------------------- /doc/why.md: -------------------------------------------------------------------------------- 1 | Powscript is just a bash dialect which transpiles to bash. 2 | Just a tool, not a replacement. 3 | 4 | --- 5 | 6 | It can turn huge codebases into a smaller readable one. 7 | 8 | * Less noise, Less picky, more readable 9 | * Make it easy to write bash applications without too much hassle 10 | * Some people prefer memorizing a minilanguage over bash code snippets/manuals/wikis 11 | * the shell language evolves slowly, which makes it suitable for transpiling. 12 | -------------------------------------------------------------------------------- /lib/json.pow: -------------------------------------------------------------------------------- 1 | # usage: echo '{"foo":"bar"}' | json_tokenize | json_parse 2 | 3 | json_print() 4 | local value 5 | json_value ${value:ref} $@ || return 1 6 | echo $value 7 | 8 | json_print_type() 9 | local type 10 | json_type ${type:ref} $@ || return 1 11 | echo $type 12 | 13 | json_value() 14 | json_property value $@ 15 | 16 | json_type() 17 | json_property type $@ 18 | 19 | 20 | json_property(kind out start_id values) 21 | shift 3 22 | local id type value 23 | if $start_id match ^[0-9]+$ 24 | shift 25 | else 26 | values=$start_id 27 | start_id=1 28 | json_get_id ${id:ref} $start_id $values $@ || return 1 29 | expand 30 | type=${~values[$id:type]} 31 | value=${~values[$id:value]} 32 | switch $kind 33 | case type 34 | out:ref=$type 35 | case value 36 | switch $type 37 | case object|array 38 | out:ref=$id 39 | case * 40 | out:ref=$value 41 | 42 | json_get_id(out id values) 43 | shift 3 44 | if not ismap? $values 45 | echo "ERROR: in json_get: $values is not a json object" 46 | return 1 47 | expand 48 | for key in $@ 49 | id=${~values[$id-$key]} 50 | out:ref=$id 51 | 52 | json_parse(__out __string) 53 | declare map __objects 54 | test _json_parse $__out $__string or return 1 55 | copy_map __objects $__out 56 | 57 | _json_parse(out string) 58 | # 59 | local index=0 60 | local c 61 | local state="top" 62 | # 63 | local unicode1 unicode2 64 | # 65 | local key='' 66 | local value='' 67 | declare array value_id_stack 68 | declare integer value_stack_pos 69 | declare integer value_id 70 | declare integer previous_id 71 | declare integer next_id 72 | # 73 | value_id_stack[0]=0 74 | value_stack_pos=0 75 | value_id=0 76 | next_id=1 77 | # 78 | declare integer state_depth 79 | declare array state_list 80 | state_depth=0 81 | # 82 | declare integer line 83 | declare integer collumn 84 | line=0 85 | collumn=0 86 | # 87 | state_pop() 88 | if $state_depth > 0 89 | state_depth-=1 90 | state=$state_list[$state_depth] 91 | # 92 | state_push(v) 93 | state_list[$state_depth]=$state 94 | state_depth+=1 95 | state=$v 96 | # 97 | state_replace(v) 98 | state_pop 99 | state_push $v 100 | # 101 | set_type(t) 102 | __objects["$value_id:type"]=$t 103 | # 104 | add_value_to_object() 105 | __objects["$previous_id-$key"]=$value_id 106 | # 107 | add_value_to_array() 108 | local index=$__objects[$previous_id:length] 109 | __objects["$previous_id-$index"]=$value_id 110 | __objects["$previous_id:length"]=$(math index+1) 111 | # 112 | start_value() 113 | previous_id=$value_id 114 | value_id=$next_id 115 | next_id+=1 116 | value_stack_pos+=1 117 | value_id_stack[$value_stack_pos]=$value_id 118 | state_push 'value' 119 | # 120 | end_value() 121 | __objects["$value_id:value"]="$value" 122 | value= 123 | value_stack_pos-=1 124 | value_id=$previous_id 125 | previous_id=$value_id_stack[$(math value_stack_pos-1)] 126 | state_pop 127 | # 128 | start_array() 129 | __objects["$value_id:length"]=0 130 | # 131 | error(msg) 132 | echo "ERROR:$line:$collumn: while parsing json: $msg" >&2 133 | # 134 | if not ismap? $out 135 | error "expected an associative array as an argument, but $out isn't one"; return 1 136 | # 137 | start_value 138 | while $index < ${string:length} 139 | c=${string:index index} 140 | index+=1 141 | # 142 | if $c is $NL 143 | newline+=1 144 | collumn=0 145 | else 146 | collumn+=1 147 | # 148 | switch $state 149 | case 'value' 150 | switch $c 151 | case [[:space:]] 152 | pass 153 | case '{' 154 | state_push 'object' 155 | state_push 'object-key' 156 | case '[' 157 | start_array 158 | state_push 'array-start' 159 | case '"' 160 | state_push 'string' 161 | case '-' 162 | state_push 'negative' 163 | value='-' 164 | case '0' 165 | state_push 'after-integer' 166 | value='0' 167 | case [1-9] 168 | value&=$c 169 | set_type 'number' 170 | state_push 'integer' 171 | case 't' 172 | value=${string:slice index-1 length 5} 173 | switch $value 174 | case 'true'|'true'[[:space:]] 175 | set_type 'bool' 176 | value='true' 177 | end_value 178 | index+=4 179 | case 'true,'|'true]'|'true}' 180 | set_type 'bool' 181 | value='true' 182 | end_value 183 | index+=3 184 | case * 185 | error "unexpected character $c: didn't lead to 'true' ($value...)"; return 1 186 | case 'f' 187 | value=${string:slice index-1 length 6} 188 | switch $value 189 | case 'false'|'false'[[:space:]] 190 | set_type 'bool' 191 | value='false' 192 | end_value 193 | index+=5 194 | case 'false,'|'false]'|'false}' 195 | set_type 'bool' 196 | value='false' 197 | end_value 198 | index+=4 199 | case * 200 | error "unexpected character $c: didn't lead to 'false' ($value...)"; return 1 201 | case * 202 | error "unexpected character $c"; return 1 203 | case 'object-key' 204 | switch $c 205 | case [[:space:]] 206 | pass 207 | case '"' 208 | state_pop 209 | state_push 'object-colon' 210 | state_push 'string' 211 | case '}' 212 | state_pop 213 | end_value 214 | case * 215 | error "unexpected character $c: expected a string as a object key"; return 1 216 | case 'object-colon' 217 | switch $c 218 | case [[:space:]] 219 | pass 220 | case ':' 221 | state_pop 222 | start_value 223 | add_value_to_object 224 | case * 225 | error "unexpected character $c: expected a colon (:) after the object key"; return 1 226 | case 'object' 227 | switch $c 228 | case [[:space:]] 229 | pass 230 | case ',' 231 | state_push 'object-key' 232 | case '}' 233 | set_type 'object' 234 | state_pop 235 | end_value 236 | case * 237 | error "unexpected character $c: expected ',' or '}' in object"; return 1 238 | case 'array-start' 239 | switch $c 240 | case ']' 241 | set_type 'array' 242 | state_pop 243 | end_value 244 | case * 245 | index-=1 246 | state_replace 'array' 247 | start_value 248 | add_value_to_array 249 | case 'array' 250 | switch $c 251 | case [[:space:]] 252 | pass 253 | case ',' 254 | start_value 255 | add_value_to_array 256 | case ']' 257 | set_type 'array' 258 | state_pop 259 | end_value 260 | case * 261 | error "expected ',' or '}' in array"; return 1 262 | case 'string' 263 | switch $c 264 | case '"' 265 | set_type 'string' 266 | state_pop 267 | if $state is 'value' 268 | end_value 269 | elif $state is 'object-colon' 270 | key="$value" 271 | value='' 272 | case '\' 273 | state_push 'escape' 274 | case * 275 | value&=$c 276 | case 'escape' 277 | state_pop 278 | switch $c 279 | case '"'|'\'|'/' 280 | value&=$c 281 | case [bfnrt] 282 | local v 283 | printf -v v "\\$c" 284 | value&=$v 285 | case u 286 | local unicode offset 287 | parse_utf16 -r ${unicode:ref} -o ${offset:ref} ${string:slice index+1 length 8} || return 1 288 | value&=$unicode 289 | index+=$offset 290 | case * 291 | error "invalid escape character: $c"; return 1 292 | case 'negative' 293 | switch $c 294 | case 0 295 | state_replace 'after-integer' 296 | value&=0 297 | case [1-9] 298 | state_replace 'integer' 299 | value&=$c 300 | case * 301 | error "unexpected character $c: expected a digit from 0 to 9"; return 1 302 | case 'integer' 303 | switch $c 304 | case [0-9] 305 | value&=$c 306 | case '.' 307 | value&='.' 308 | state_replace 'floating-point' 309 | case [[:space:]] 310 | state_pop 311 | set_type 'number' 312 | end_value 313 | case ','|']'|'}' 314 | state_pop 315 | set_type 'number' 316 | end_value 317 | index-=1 318 | case * 319 | error "unexpected character $c: expected a digit from 0 to 9, . or whitespace"; return 1 320 | case 'after-integer' 321 | switch $c 322 | case '.' 323 | value&='.' 324 | state_replace 'floating-point-start' 325 | case [[:space:]] 326 | state_pop 327 | set_type 'number' 328 | end_value 329 | case ','|']'|'}' 330 | state_pop 331 | set_type 'number' 332 | end_value 333 | index-=1 334 | case * 335 | error "unexpected character $c: expected . or whitespace"; return 1 336 | case 'floating-point-start' 337 | switch $c 338 | case [0-9] 339 | value&=$c 340 | state_replace 'floating-point' 341 | case * 342 | error "unexpected character $c: expected a digit from 0 to 9 after ."; return 1 343 | case 'floating-point' 344 | switch $c 345 | case [0-9] 346 | value&=$c 347 | case [eE] 348 | value&=e 349 | state_replace 'float-e' 350 | case [[:space:]] 351 | state_pop 352 | set_type 'number' 353 | end_value 354 | case ','|']'|'}' 355 | state_pop 356 | set_type 'number' 357 | end_value 358 | index-=1 359 | case * 360 | error "unexpected character $c: expected a digit from 0 to 9, e/E or whitespace."; return 1 361 | case 'float-e' 362 | switch $c 363 | case [0-9]|+|- 364 | value&=$c 365 | state_replace 'float-e-value' 366 | case * 367 | error "unexpected character $c: expected a digit from 0 to 9, + or -."; return 1 368 | case 'float-e-value' 369 | switch $c 370 | case [0-9] 371 | value&=$c 372 | case [[:space:]] 373 | state_pop 374 | set_type 'number' 375 | end_value 376 | while $state isnt 'top' 377 | switch $state 378 | case 'integer'|'floating-point'|'float-e-value' 379 | state_pop 380 | end_value 381 | case * 382 | error "premature end of input. ($state)" 383 | return 1 384 | 385 | 386 | -------------------------------------------------------------------------------- /lib/require.bash: -------------------------------------------------------------------------------- 1 | declare -A PowscriptLib 2 | 3 | powscript_require() { 4 | local lib="$1" 5 | local req="PowscriptLib[$lib]=$(printf '%q\n' "$(cat "$PowscriptLibDirectory/$lib.pow")")" 6 | 7 | ${RequireOp-eval} "$req" 8 | } 9 | -------------------------------------------------------------------------------- /lib/std.pow: -------------------------------------------------------------------------------- 1 | map(A f) 2 | shift 2 3 | expand 4 | for k,v of ~A 5 | ~f $@ $k $v 6 | 7 | mappipe(f) 8 | shift 9 | while read -r line 10 | $f $@ $line 11 | 12 | curry(out f x) 13 | expand 14 | ~out() 15 | ~f '~x' $@ 16 | 17 | keys(x y) 18 | echo $x 19 | 20 | values(x y) 21 | echo $y 22 | 23 | set?(s) 24 | expand 25 | return ${~s:set?} 26 | 27 | unset?(s) 28 | expand 29 | return ${~s:unset?} 30 | 31 | empty?(s) 32 | expand 33 | return ${~s:empty?} 34 | 35 | nonempty?(s) 36 | expand 37 | return ${~s:nonempty?} 38 | 39 | bool(int) 40 | return $int 41 | 42 | filter(A p) 43 | shift 2 44 | expand 45 | for k,v of ~A 46 | if ~p $@ $k $v 47 | echo $v 48 | 49 | compose(fg f g) 50 | expand 51 | ~fg() 52 | ~f $(~g $@) 53 | 54 | pick(A k) 55 | if -z $k 56 | return 1 57 | expand 58 | local output=${~A[~k]} 59 | if -n $output 60 | echo $output 61 | 62 | first(A) 63 | if unset? A 64 | return 1 65 | expand 66 | echo ${~A[0]} 67 | 68 | last(A) 69 | if unset? A 70 | return 1 71 | expand 72 | echo ${~A[-1]} 73 | 74 | require_env(var) 75 | expand 76 | pass ${~var:set! "Required ENV-variable ~var not set (hint: '~var=yourvalue ./yourscript')."} 77 | 78 | require_cmd(var) 79 | if not command -v $var 1>/dev/null 2>/dev/null 80 | echo "Please install the required command '$var' (it was not found)." >/dev/stderr 81 | exit 1 82 | 83 | ismap?(x) 84 | local result k1=__ismap_dummy1 k2=__ismap_dummy2 85 | if empty? x 86 | return 1 87 | expand 88 | local ~k1=0 ~k2=0 89 | local v 90 | test ${~x[~k1]:set?} and v=${~x[~k1]} 91 | ~x[~k1]=a 92 | ~x[~k2]=b 93 | if ${~x[~k1]} is a 94 | unset ~x[~k1] 95 | unset ~x[~k2] 96 | return 0 97 | else 98 | test ${v:set?} and ~x=$v or unset ~x 99 | return 1 100 | 101 | copy_map(__A __B) 102 | if not ismap? $__A 103 | echo "ERROR: in copy_map: $__A is not a map" 104 | return 1 105 | if not ismap? $__B 106 | echo "ERROR: in copy_map: $__B is not a map" 107 | return 1 108 | expand 109 | for __k,__v of ~__A 110 | ~__B[$__k]=$__v 111 | 112 | copy_array(__A __B) 113 | expand 114 | for __k,__v of ~__A 115 | if not $__k match ^[1-9]+$ 116 | echo "ERROR: not a valid array key: $__k" 117 | return 1 118 | ~__B[$__k]=$__v 119 | 120 | clear_array(__A) 121 | expand 122 | for __k in ${~__A:keys} 123 | unset ~__A[$__k] 124 | 125 | NL=" 126 | " 127 | 128 | pass() 129 | true 130 | 131 | 132 | -------------------------------------------------------------------------------- /lib/unicode.pow: -------------------------------------------------------------------------------- 1 | unicode_parsing_error(utf_type msg) 2 | echo "ERROR: While parsing a $utf_type unicode character: $msg" >&2 3 | 4 | unicode_test_eof(utf_type string i) 5 | if i > ${string:length} 6 | unicode_parsing_error $utf_type 'Premature end of input' 7 | return 1 8 | 9 | unicode_eat_delimiter(type string delim i iref) 10 | if ${string:slice i length ${delim:length}} isnt $delim 11 | unicode_parsing_error $type "required delimiter '$delim' not found after byte $gotten" 12 | return 1 13 | else 14 | iref:ref= $(math i+${delim:length}) 15 | 16 | 17 | parse_utf8(string -- result offset start delim prefix) 18 | local c byte='' unicode='' final_value 19 | local state='get-byte' 20 | declare integer i gotten needed 21 | i=${state:set 0} 22 | gotten=0 23 | needed=1 24 | if ${delim:unset?} 25 | prefix:unset='\x' 26 | delim:unset='\x' 27 | # 28 | unicode_eat_delimiter utf-8 $string $prefix $i ${i:ref} || return 1 29 | while gotten < needed 30 | unicode_test_eof utf-8 $string $i || return 1 31 | # 32 | c=${string:index i} 33 | switch $c 34 | case [0-9abcdefABCDEF] 35 | i+=1 36 | byte&=$c 37 | case * 38 | if "${byte:length}:$gotten" is '1:0' 39 | unicode="\\x$byte" 40 | gotten=1 41 | else 42 | unicode_parsing_error utf-8 "Invalid unicode $unicode\\x$byte" 43 | return 1 44 | if ${byte:length} is 2 45 | switch $gotten 46 | case 0 47 | local byte_value=0x$byte 48 | if byte_value < 0x80 49 | needed=1 50 | elif byte_value < 0xe0 51 | needed=2 52 | elif byte_value < 0xf0 53 | needed=3 54 | elif byte_value < 0xf8 55 | needed=4 56 | else 57 | unicode_parsing_error utf-8 "Invalid byte 1: $byte" 58 | case * 59 | local byte_value=0x$byte 60 | if byte_value < 0x80 or byte_value > 0xbf 61 | unicode_parsing_error utf-8 "Invalid byte $(math gotten+1): $byte" 62 | return 1 63 | state='get-byte' 64 | gotten+=1 65 | unicode&="\\x$byte" 66 | byte='' 67 | if gotten < needed 68 | unicode_eat_delimiter utf-8 $string $delim $i ${i:ref} || return 1 69 | printf -v unicode "$unicode" 70 | if ${result:set?} 71 | result:ref= $unicode 72 | else 73 | echo $unicode 74 | # 75 | if ${offset:set?} 76 | offset:ref= $i 77 | 78 | 79 | parse_utf16(string -- result start offset delim prefix) 80 | local c unicode1='' unicode2='' final_value 81 | local state='unicode1' result 82 | declare integer i 83 | i=${start:unset 0} 84 | # 85 | if ${delim:unset?} 86 | prefix=${prefix:unset '\u'} 87 | delim=${delim:unset '\u'} 88 | # 89 | unicode_eat_delimiter utf-16 $string $prefix $i ${i:ref} || return 1 90 | while ${final_value:unset?} 91 | unicode_test_eof utf-16 $string $i || return 1 92 | # 93 | c=${string:index i} 94 | switch $state 95 | case unicode1 96 | switch $c 97 | case [0-9abcdefABCDEF] 98 | unicode1&=$c 99 | i+=1 100 | if ${unicode1:length} is 4 101 | local value=0x$unicode1 102 | if value > 0xd7ff and value < 0xdbff 103 | state='unicode2' 104 | unicode_eat_delimiter utf-16 $string $delim $i ${i:ref} || return 1 105 | elif value > 0xdbff and value < 0xdc00 106 | unicode_parsing_error utf-16 "Invalid character: \\u$unicode1" 107 | else 108 | printf -v final_value "\\u$unicode1" 109 | case * 110 | if ${unicode1:length} > 0 111 | printf -v final_value "\\u$unicode1" 112 | else 113 | unicode_parsing_error utf-16 "Invalid character: \\u$unicode1" 114 | case unicode2 115 | switch $c 116 | case [0-9abcdefABCDEF] 117 | unicode2&=$c 118 | i+=1 119 | if ${unicode2:length} is 4 120 | local value=0x$unicode2 121 | if value > 0xdc00 and value < 0xdfff 122 | local high=0x$unicode1 low=$value 123 | final_value=$(math 124 | ((high - 0xd800) * 0x0400) + 125 | ((low - 0xdc00) + 0x10000)) 126 | printf -v final_value '%x' $final_value 127 | printf -v final_value "\\U$final_value" 128 | else 129 | unicode_parsing_error utf-16 "Invalid low surrogate: \\u$unicode2" 130 | case * 131 | unicode_parsing_error utf-16 "Invalid low surrogate: \\u$unicode2" 132 | if ${result:set?} 133 | result:ref= "$final_value" 134 | else 135 | echo "$final_value" 136 | if ${offset:set?} 137 | offset:ref= $i 138 | 139 | 140 | parse_utf32(string -- result start offset prefix) 141 | local unicode="" final_value c 142 | declare integer i 143 | i=${start:unset 0} 144 | prefix:unset='\U' 145 | unicode_eat_delimiter utf-32 $string $prefix $i ${i:ref} || return 1 146 | # 147 | while ${final_value:unset?} 148 | unicode_test_eof utf-32 $string $i || return 1 149 | # 150 | c=${string:index i} 151 | switch $c 152 | case [0-9abcdefABCDEF] 153 | unicode&=$c 154 | i+=1 155 | if ${unicode:length} is 8 156 | printf -v final_value "\\U$unicode" 157 | case * 158 | if ${unicode:nonempty?} 159 | printf -v final_value "\\U$unicode" 160 | else 161 | unicode_parsing_error utf-32 "Invalid character: \\U" 162 | return 1 163 | if ${result:set?} 164 | result:ref= "$final_value" 165 | else 166 | echo "$final_value" 167 | if ${offset:set?} 168 | offset:ref= $i 169 | 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powscript", 3 | "version": "1.1.29", 4 | "description": "bash dialect transpiler in bash: painless shellscript / indentbased / coffeescript for shellscript / bash for hipsters", 5 | "main": "powscript", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "bin":{ 10 | "powscript":"powscript" 11 | }, 12 | "scripts": { 13 | "test": "./.tools/runtests" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://coderofsalvation@github.com/coderofsalvation/powscript.git" 18 | }, 19 | "keywords": [ 20 | "bash", 21 | "shell", 22 | "coffeescript", 23 | "functional", 24 | "transpiler", 25 | "sh", 26 | "unix" 27 | ], 28 | "author": "coder of salvation (Leon van Kammen)", 29 | "license": "BSD-2-CLAUSE", 30 | "bugs": { 31 | "url": "https://github.com/coderofsalvation/powscript/issues" 32 | }, 33 | "homepage": "https://github.com/coderofsalvation/powscript#readme" 34 | } 35 | -------------------------------------------------------------------------------- /src/ast/assign.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:assign $expr 2 | # 3 | # Parse an AST representing the attribution 4 | # of a value to a name. 5 | # Can be of the type: 6 | # - var=single-value 7 | # - var=(...) 8 | # - var=[...] 9 | # - var={} 10 | # The given $expr is an undefined AST 11 | # with the var name as a child, which 12 | # will be transformed into the assign. 13 | # 14 | 15 | ast:parse:assign() { 16 | local expr="$1" 17 | local assigned_value head 18 | local value class 19 | 20 | token:peek -v value -c class 21 | if [ "$class" = special ]; then 22 | case "$value" in 23 | '('|'[') head=list-assign ;; 24 | '{') head=associative-assign ;; 25 | *) head=assign ;; 26 | esac 27 | else 28 | head=assign 29 | fi 30 | ast:set $expr head $head 31 | ast:parse:expr assigned_value 32 | ast:push-child $expr $assigned_value 33 | } 34 | 35 | 36 | # ast:parse:array-dereference-assign $name $out 37 | # 38 | # Parse an assignment of the form: 39 | # 40 | # :ref[ 41 | # 42 | 43 | ast:parse:array-dereference-assign() { #<> 44 | local var="$1" out="$2" 45 | local name index value indexing 46 | 47 | token:require name ref 48 | token:require special '[' 49 | 50 | ast:push-state '[' 51 | ast:parse:expr index 52 | ast:pop-state 53 | 54 | token:require special ']' 55 | token:require special '=' 56 | 57 | ast:parse:expr value 58 | 59 | ast:from $var value name 60 | ast:make indexing indexing "$name" $index 61 | ast:make "$out" assign-ref '' $indexing $value 62 | } 63 | noshadow ast:parse:array-dereference-assign 1 64 | 65 | 66 | # ast:parse:conditional-assign $name $out 67 | # 68 | # Parse an special assingment of the form: 69 | # 70 | # * unset= 71 | # * set= 72 | # * empty= 73 | # * nonempty= 74 | # * ref= 75 | # 76 | 77 | ast:parse:conditional-assign() { #<> 78 | local name="$1" out="$2" 79 | local assign_type assign_expr tclass 80 | 81 | token:get -v assign_type -c tclass 82 | case "$assign_type:$tclass" in 83 | 'ref:name') ;; 84 | 'set:name') ;; 85 | 'unset:name') ;; 86 | 'empty:name') ;; 87 | 'nonempty:name') ;; 88 | *) 89 | ast:error "Invalid conditional assign: $assign_type:$tclass" 90 | ;; 91 | esac 92 | 93 | token:require special '=' 94 | ast:parse:expr assign_expr 95 | 96 | if [ "$assign_type" = ref ]; then 97 | ast:make "$out" assign-ref '' $name $assign_expr 98 | else 99 | ast:make "$out" assign-conditional "$assign_type" $name $assign_expr 100 | fi 101 | } 102 | noshadow ast:parse:conditional-assign 1 103 | 104 | # ast:parse:math-assign $expr $name $op 105 | # 106 | # Parse an math AST to be attributed to a 107 | # variable. 108 | # 109 | 110 | ast:parse:math-assign() { 111 | local expr="$1" name="$2" op="$3" 112 | local right_side 113 | 114 | ast:push-state math 115 | ast:parse:expr right_side 116 | ast:pop-state 117 | 118 | ast:set $expr head math-assign 119 | ast:set $expr value "$op" 120 | ast:set $expr children "$name $right_side" 121 | } 122 | 123 | 124 | # ast:parse:push-assign $expr $name 125 | # 126 | # Parse an AST to be pushed at the 127 | # end of an array. 128 | # 129 | 130 | ast:parse:push-assign() { 131 | local expr="$1" name="$2" 132 | local name_value index value 133 | 134 | ast:from $name value name_value 135 | 136 | ast:make index array-length "$name_value" 137 | 138 | ast:parse:expr value 139 | ast:set $expr children "$name $index $value" 140 | ast:set $expr head indexing-assign 141 | } 142 | 143 | # ast:parse:concat-assign $expr $name 144 | # 145 | # Parse an AST to be concatenated to a variable 146 | # 147 | 148 | ast:parse:concat-assign() { 149 | local expr="$1" name="$2" 150 | local name_value value subst concat 151 | 152 | ast:from $name value name_value 153 | 154 | ast:parse:expr value 155 | 156 | ast:set $expr children "$name $value" 157 | ast:set $expr head concat-assign 158 | } 159 | 160 | 161 | # ast:parse:assign-sequence $first $out 162 | # 163 | # parse a sequence of assignments, followed by either a newline or command call 164 | # 165 | 166 | ast:parse:assign-sequence() { #<> 167 | local first=$1 out="$2" 168 | local predicate seq extra cmd 169 | 170 | ast:make seq assign-sequence '' $first 171 | 172 | if ast:state-is ==; then 173 | predicate='ast:is-assign % && ast:parse:not-binary-conditional %' 174 | else 175 | predicate='ast:is-assign %' 176 | fi 177 | ast:parse:sequence $seq "$predicate" extra 178 | 179 | if ast:state-is == && ! ast:parse:not-binary-conditional $extra; then 180 | setvar "$out" $seq 181 | 182 | elif [ "$extra" = '-1' ]; then 183 | setvar "$out" $seq 184 | 185 | else 186 | ast:parse:command-call-with-cmd $seq $extra cmd 187 | setvar "$out" $cmd 188 | fi 189 | } 190 | noshadow ast:parse:assign-sequence 1 191 | 192 | ast:is-assign() { 193 | local expr="$1" 194 | local expr_head 195 | 196 | ast:from $expr head expr_head 197 | 198 | case $expr_head in 199 | *assign) return 0 ;; 200 | *) return 1 ;; 201 | esac 202 | } 203 | 204 | -------------------------------------------------------------------------------- /src/ast/ast.bash: -------------------------------------------------------------------------------- 1 | powscript_source ast/heap.bash #<> 2 | powscript_source ast/parse.bash #<> 3 | 4 | -------------------------------------------------------------------------------- /src/ast/blocks.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:block 2 | # 3 | # Parses an indent-based sequence of expressions divided by newlines. 4 | # e.g. 5 | # block-starting-expr 6 | # x=1 7 | # echo $(math x + 1) 8 | # 9 | 10 | ast:parse:block() { #<> 11 | local state=$1 out="$2" 12 | local expr child indent_state indent_layers 13 | local value class token_line ln="0" 14 | 15 | ast:make expr block 16 | 17 | ast:push-state $state 18 | ast:new-indentation 19 | 20 | ast:indentation-layers indent_layers 21 | ast:set $expr value $indent_layers 22 | 23 | indent_state=ok 24 | 25 | while [ ! $indent_state = end ]; do 26 | token:peek -v value -c class -ls token_line 27 | 28 | ast:test-indentation "$value" $class indent_state 29 | 30 | case $indent_state in 31 | ok) 32 | ast:parse:top child 33 | if [ ! "$child" = -1 ]; then 34 | ast:push-child $expr $child 35 | ln="$token_line" 36 | fi 37 | ;; 38 | error*) 39 | if [ $class = eof ] && ${POWSCRIPT_ALLOW_INCOMPLETE-false}; then 40 | POWSCRIPT_INCOMPLETE_STATE="$(ast:last-state)" 41 | exit 42 | fi 43 | 44 | local req found or_more="" 45 | [ $indent_state = error-start ] && or_more=" or more" 46 | 47 | ast:indentation-required req 48 | ast:count-indentation "$value" $class found 49 | ast:error "$indent_state : indentation error at line $token_line, expected $req spaces$or_more, found $found." 50 | ;; 51 | esac 52 | done 53 | 54 | ast:pop-state 55 | ast:pop-indentation 56 | setvar "$out" $expr 57 | } 58 | noshadow ast:parse:block 1 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/ast/commands.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:command-call $command_ast $out 2 | # 3 | # parse an AST of the form: command expr1 expr2... 4 | # 5 | 6 | ast:parse:command-call() { #<> 7 | local assigns="$1" out="$2" 8 | local cmd 9 | 10 | if [ -z "$assigns" ]; then 11 | ast:make assigns 'assign-sequence' '' 12 | fi 13 | 14 | ast:parse:expr cmd 15 | ast:parse:command-call-with-cmd "$assigns" "$cmd" "$out" 16 | } 17 | noshadow ast:parse:command-call 1 18 | 19 | ast:parse:command-call-with-cmd() { #<> 20 | local assigns=$1 command_ast=$2 out="$3" 21 | 22 | if ast:is $command_ast name math; then 23 | local state 24 | ast:parse:math "$out" 25 | ast:last-state state 26 | case "$state" in 27 | '(') token:require special ')' ;; 28 | '[') token:require special ']' ;; 29 | '{') token:require special '}' ;; 30 | esac 31 | 32 | else 33 | local expression child predicate 34 | ast:make expression call '' $assigns $command_ast 35 | 36 | if ast:state-is ==; then 37 | predicate='ast:parse:not-binary-conditional %' 38 | else 39 | predicate='true' 40 | fi 41 | ast:parse:sequence $expression "$predicate" 42 | setvar "$out" $expression 43 | fi 44 | } 45 | noshadow ast:parse:command-call-with-cmd 2 46 | 47 | 48 | ast:parse:not-binary-conditional() { 49 | local expr="$1" 50 | local name 51 | 52 | if ast:is $expr name; then 53 | ast:from $expr value name 54 | case "$name" in 55 | and|or|'&&'|'||') return 1 ;; 56 | *) return 0 ;; 57 | esac 58 | fi 59 | return 0 60 | } 61 | -------------------------------------------------------------------------------- /src/ast/declare.bash: -------------------------------------------------------------------------------- 1 | ast:parse:declare() { #<> 2 | local out="$1" 3 | local glob='' type 4 | 5 | if token:next-is name global; then 6 | glob="global " 7 | token:skip 8 | fi 9 | 10 | token:get -v type 11 | 12 | case "$type" in 13 | integer|string|array|map) ;; 14 | *) ast:error "invalid type $type" ;; 15 | esac 16 | 17 | type="$glob$type" 18 | 19 | ast:make "$out" declare 20 | ast:set "${!out}" value "$type" 21 | ast:parse:sequence "${!out}" 'ast:is % name' 22 | } 23 | noshadow ast:parse:declare 24 | -------------------------------------------------------------------------------- /src/ast/expand.bash: -------------------------------------------------------------------------------- 1 | ast:parse:expand() { #<> 2 | local out="$1" 3 | local block 4 | 5 | ast:parse:require-newline "expand" 6 | ast:parse:block "ex" block 7 | ast:make "$out" expand "" $block 8 | } 9 | noshadow ast:parse:expand 10 | -------------------------------------------------------------------------------- /src/ast/expressions.bash: -------------------------------------------------------------------------------- 1 | powscript_source ast/assign.bash #<> 2 | powscript_source ast/substitutions.bash #<> 3 | 4 | # ast:parse:expr $out 5 | # 6 | # Basic expression, which can be a name, string, 7 | # list, assign, substitution or concatenation. 8 | # 9 | 10 | ast:parse:expr() { #<> 11 | local out="$1" allowcat=${2-true} 12 | local value class glued 13 | local root root_head=undefined 14 | local expression exprnum=0 last_expression 15 | 16 | case "$allowcat" in 17 | --nocat) allowcat=false ;; 18 | --cat) allowcat=true ;; 19 | esac 20 | 21 | ast:new root 22 | 23 | while [ $root_head = undefined ]; do 24 | token:ignore-whitespace 25 | token:get -v value -c class -g glued 26 | 27 | if ${AST_MATH_MODE-false} && [ $class = special ]; then 28 | case "$value" in 29 | '+'|'-'|'/'|'*'|'^') glued=true ;; 30 | esac 31 | fi 32 | 33 | while ast:state-is '(' && [[ $class =~ (newline|whitespace) ]]; do 34 | token:get -v value -c class -g glued 35 | done 36 | 37 | if { $glued && $allowcat; } || [ $exprnum = 0 ]; then 38 | case $class in 39 | name|string) 40 | ast:make expression $class "$value" 41 | ;; 42 | special) 43 | case "$value" in 44 | '$') 45 | ast:parse:substitution false expression 46 | ;; 47 | 48 | '${') 49 | ast:parse:curly-substitution expression 50 | ;; 51 | 52 | '$(') 53 | ast:parse:command-substitution expression 54 | ;; 55 | '$#') 56 | local next_value next_class 57 | token:peek -v next_value -c next_class 58 | 59 | if [ $next_class = "name" ] && [[ "$next_value" =~ [a-zA-Z_][a-zA-Z_0-9]* ]]; then 60 | ast:set $root value $next_value 61 | root_head=array-length 62 | token:skip 63 | else 64 | ast:make root name '$#' 65 | root_head=name 66 | fi 67 | ;; 68 | 69 | '('|'{') 70 | if [ $exprnum -gt 0 ]; then 71 | root_head=determinable 72 | else 73 | ast:push-state $value 74 | if ${AST_MATH_MODE-false}; then 75 | ast:parse:list expression 76 | else 77 | ast:parse:list root 78 | root_head=list 79 | fi 80 | ast:pop-state 81 | fi 82 | ;; 83 | 84 | ')'|']'|'}') 85 | local opener 86 | case $value in 87 | ')') opener='('; ;; 88 | ']') opener='['; ;; 89 | '}') opener='{'; ;; 90 | esac 91 | if [ $exprnum -gt 0 ] && { ast:state-is $opener || ${AST_MATH_MODE-false}; }; then 92 | root_head=determinable 93 | else 94 | root_head=name 95 | ast:clear $root 96 | ast:make root name "$value" 97 | fi 98 | ;; 99 | 100 | ':') 101 | if ! ast:state-is '{' && [ $exprnum = 1 ] && ast:is $last_expression name; then 102 | local stream_position pcolon_value pcolon_class pcolon_glued 103 | token:mark-position stream_position 104 | token:skip 105 | token:get -v pcolon_value -c pcolon_class -g pcolon_glued 106 | token:return-to-mark $stream_position 107 | 108 | case "$pcolon_value:$pcolon_class:$pcolon_glued" in 109 | "=:special:true") 110 | ast:clear $root 111 | ast:parse:conditional-assign $last_expression root 112 | ast:from $root head root_head 113 | ;; 114 | "[:special:true") 115 | if token:next-is name ref; then 116 | ast:clear $root 117 | ast:parse:array-dereference-assign $last_expression root 118 | ast:from $root head root_head 119 | else 120 | ast:make expression name ":" 121 | fi 122 | ;; 123 | *) 124 | ast:make expression name ":" 125 | ;; 126 | esac 127 | else 128 | ast:make expression name ":" 129 | fi 130 | ;; 131 | 132 | '=') 133 | if [ $exprnum = 1 ] && ast:is $last_expression name; then 134 | exprnum=2 135 | ast:parse:assign $root 136 | ast:from $root head root_head 137 | 138 | elif [ $exprnum = 2 ] && ast:is $last_expression name; then 139 | local name op value 140 | ast:children $root name op 141 | ast:from $op value op 142 | 143 | case "$op" in 144 | '+'|'-'|'*'|'/'|'^'|'%') 145 | ast:parse:math-assign $root $name "$op" 146 | root_head=math-assign 147 | ;; 148 | '@') 149 | ast:parse:push-assign $root $name 150 | root_head=indexing-assign 151 | ;; 152 | '&') 153 | ast:parse:concat-assign $root $name 154 | root_head=concat-assign 155 | ;; 156 | *) 157 | ast:make expression string "=" 158 | ;; 159 | esac 160 | 161 | else 162 | ast:make expression string "=" 163 | fi 164 | ;; 165 | '[') 166 | if [ $exprnum = 1 ] && ast:is $last_expression name; then 167 | local index 168 | ast:push-state '[' 169 | ast:parse:expr index 170 | token:require special ']' 171 | ast:pop-state 172 | 173 | if token:next-is special '='; then 174 | token:skip 175 | ast:parse:expr expression 176 | root_head=indexing-assign 177 | ast:push-child $root $index 178 | ast:push-child $root $expression 179 | else 180 | local left_bracket right_bracket 181 | ast:make left_bracket name '[' 182 | ast:make right_bracket name ']' 183 | 184 | ast:push-child $root $left_bracket 185 | ast:push-child $root $index 186 | 187 | expression=$right_bracket 188 | exprnum=3 189 | fi 190 | elif [ $exprnum -gt 0 ]; then 191 | root_head=determinable 192 | else 193 | ast:push-state '[' 194 | if ${AST_MATH_MODE-false}; then 195 | ast:parse:list expression 196 | else 197 | ast:parse:list expression 198 | root_head=list 199 | fi 200 | ast:pop-state 201 | fi 202 | ;; 203 | 204 | '+'|'-'|'*'|'/'|'^'|'%') 205 | if ${AST_MATH_MODE-false}; then 206 | 207 | if [ $exprnum = 0 ]; then 208 | if [ $value = '+' ] || [ $value = '-' ]; then 209 | ast:parse:math-unary $root $value 210 | else 211 | ast:error "$value is not an unary operator" 212 | fi 213 | 214 | elif [ $exprnum = 1 ]; then 215 | ast:parse:math-binary $root $last_expression "$value" 216 | 217 | else 218 | ast:error "trailling $value in math expression" 219 | fi 220 | ast:from $root head root_head 221 | 222 | else 223 | if [ "$value" = '-' ] && [ "$exprnum" = 0 ]; then 224 | ast:parse:flag "$value" expression 225 | else 226 | ast:make expression name "$value" 227 | fi 228 | fi 229 | ;; 230 | 231 | '--') 232 | ast:parse:flag "$value" expression 233 | ;; 234 | 235 | *) 236 | if [ $value = '-' ]; then 237 | ast:parse:flag expression 238 | else 239 | ast:make expression name "$value" 240 | fi 241 | ;; 242 | esac 243 | ;; 244 | newline|eof) 245 | root_head=$class 246 | ;; 247 | *) 248 | ast:error "token of class $class found when parsing an expression ast" 249 | ;; 250 | esac 251 | 252 | if [ $root_head = undefined ]; then 253 | ast:push-child $root $expression 254 | exprnum=$((exprnum+1)) 255 | last_expression=$expression 256 | fi 257 | else 258 | root_head=determinable 259 | fi 260 | 261 | if [ $root_head = determinable ]; then 262 | if [ $exprnum = 1 ]; then 263 | ast:clear $root 264 | root=$last_expression 265 | ast:from $root head root_head 266 | else 267 | root_head=cat 268 | fi 269 | token:backtrack 270 | fi 271 | done 272 | ast:set $root head $root_head 273 | 274 | setvar "$out" $root 275 | } 276 | noshadow ast:parse:expr 277 | 278 | 279 | # ast:parse:specific-expr $required $out 280 | # 281 | # Parse an expression, errors out if it's 282 | # not the required one, otherwise put it 283 | # in $out. 284 | # 285 | 286 | ast:parse:specific-expr() { #<> 287 | local expr expr_head required="$1" out="$2" 288 | ast:parse:expr expr 289 | ast:from $expr head expr_head 290 | 291 | if [ $expr_head = $required ]; then 292 | setvar "$out" $expr 293 | else 294 | ast:error "Wrong expression: Found a $expr_head when a $required was required" 295 | fi 296 | } 297 | noshadow ast:parse:specific-expr 1 298 | -------------------------------------------------------------------------------- /src/ast/flag.bash: -------------------------------------------------------------------------------- 1 | ast:parse:flag() { #<> 2 | local dash="$1" out="$2" 3 | local expr_value='' class glued 4 | local flag_type only 5 | 6 | 7 | case "$dash" in 8 | '-') flag_type=single-dash ;; 9 | '--') flag_type=double-dash ;; 10 | esac 11 | 12 | token:peek -v value -c class -g glued 13 | if $glued; then 14 | case "$class" in 15 | 'name') 16 | token:skip 17 | expr_value="$value" 18 | only="" 19 | ;; 20 | *) 21 | only=yes 22 | ;; 23 | esac 24 | else 25 | only=yes 26 | fi 27 | ast:make "$out" "flag-$flag_type${only:+-only}" "$value" 28 | } 29 | noshadow ast:parse:flag 1 30 | -------------------------------------------------------------------------------- /src/ast/functions.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:function-definition $name $out 2 | # 3 | # Parses an expression of the form: 4 | # name(...vars...) 5 | # 6 | # 7 | 8 | ast:parse:function-definition() { #<> 9 | local name=$1 out="$2" 10 | local expr args block 11 | 12 | ast:make expr function-def 13 | 14 | ast:parse:specific-expr list args 15 | ast:parse:require-newline "function definition" 16 | ast:parse:block fn block 17 | 18 | ast:push-child $expr $name 19 | ast:push-child $expr $args 20 | ast:push-child $expr $block 21 | 22 | setvar "$out" $expr 23 | } 24 | noshadow ast:parse:function-definition 1 25 | 26 | -------------------------------------------------------------------------------- /src/ast/heap.bash: -------------------------------------------------------------------------------- 1 | declare -gA Asts 2 | 3 | Asts[index]=0 4 | Asts[length]=0 5 | Asts[required-indent]=0 6 | 7 | powscript_source ast/indent.bash #<> 8 | powscript_source ast/states.bash #<> 9 | 10 | ast:new() { #<> 11 | local index="${Asts[index]}" 12 | local length="${Asts[length]}" 13 | 14 | setvar "$1" "$index" 15 | 16 | Asts[head-$index]= 17 | Asts[value-$index]= 18 | Asts[children-$index]= 19 | 20 | if [ ! $index = $length ]; then 21 | Asts[index]=$(($index+1)) 22 | else 23 | Asts[index]=$(($length+1)) 24 | fi 25 | Asts[length]=$(($length+1)) 26 | } 27 | noshadow ast:new 28 | 29 | ast:make() { 30 | local __newast __newchild 31 | ast:new __newast 32 | ast:set "$__newast" head "$2" 33 | ast:set "$__newast" value "$3" 34 | for __newchild in ${@:4}; do 35 | ast:push-child "$__newast" $__newchild 36 | done 37 | setvar "$1" "$__newast" 38 | } 39 | 40 | ast:make-from-string() { 41 | local __mkstr_out 42 | ast:_make-from-string __mkstr_out "$2" 43 | setvar "$1" "$__mkstr_out" 44 | } 45 | 46 | ast:_make-from-string() { 47 | local out="$1" string="$2" 48 | local ast child 49 | local trimmed incomplete_marker marker_head marker info head value 50 | declare -a children 51 | 52 | while IFS="" read -r line; do 53 | trimmed="${line#${line%%[^" "]*}}" 54 | incomplete_marker="${trimmed%%[^-]*}" 55 | marker_head="${trimmed:${#incomplete_marker}:1}" 56 | if [[ ! "$marker_head" = '+' ]]; then 57 | marker_head="" 58 | fi 59 | marker="$incomplete_marker$marker_head" 60 | info="${trimmed#$marker}" 61 | info="${info#${info%%[^" "]*}}" 62 | 63 | case "$marker" in 64 | *+) 65 | ast="$info" 66 | ;; 67 | *) 68 | head="${info%% *}" 69 | if [ -n "$head" ]; then 70 | value="${info#*$head}" 71 | value="${value# }" 72 | ast:make ast $head "$value" 73 | children[${#marker}]=$ast 74 | fi 75 | ;; 76 | esac 77 | if [ ${#marker} -gt 0 ]; then 78 | ast:push-child ${children[${#marker}-1]} "$ast" 79 | fi 80 | done <<<"$string" 81 | setvar "$out" "${children[0]}" 82 | } 83 | 84 | ast:from() { 85 | setvar "$3" "${Asts["$2-$1"]}" 86 | } 87 | 88 | ast:all-from() { 89 | local __af_expr="$1" 90 | shift 91 | 92 | while [ $# -gt 0 ]; do 93 | case "$1" in 94 | -e|--expr) 95 | setvar "$2" $__af_expr 96 | shift 2 97 | ;; 98 | -v|--value) 99 | ast:from $__af_expr value "$2" 100 | shift 2 101 | ;; 102 | -h|--head) 103 | ast:from $__af_expr head "$2" 104 | shift 2 105 | ;; 106 | -c|--children) 107 | ast:from $__af_expr children "$2" 108 | shift 2 109 | ;; 110 | -@|--@children) 111 | ast:children $__af_expr "${@:2}" 112 | shift $# 113 | ;; 114 | *) 115 | ast:error "Invalid flag $1, expected -[evhc@]" 116 | ;; 117 | esac 118 | done 119 | } 120 | 121 | ast:set() { 122 | Asts["$2-$1"]="$3" 123 | } 124 | 125 | ast:is() { 126 | local ast_head ast_value res=false 127 | ast:from $1 head ast_head 128 | ast:from $1 value ast_value 129 | 130 | case $# in 131 | 2) case $ast_head in $2) res=true ;; esac 132 | ;; 133 | 3) case $ast_head in $2) 134 | case $ast_value in $3) res=true ;; esac ;; esac 135 | ;; 136 | esac 137 | $res 138 | } 139 | 140 | 141 | ast:push-child() { 142 | Asts["children-$1"]="${Asts["children-$1"]} $2" 143 | } 144 | 145 | ast:unshift-child() { 146 | Asts["children-$1"]="$2 ${Asts["children-$1"]}" 147 | } 148 | 149 | ast:shift-child() { 150 | setvar "$2" "${Asts["children-$1"]%% *}" 151 | Asts["children-$1"]="${Asts["children-$1"]#* }" 152 | } 153 | 154 | ast:pop-child() { 155 | setvar "$2" "${Asts["children-$1"]##* }" 156 | Asts["children-$1"]="${Asts["children-$1"]% *}" 157 | } 158 | 159 | ast:children() { #<> 160 | local ast="$1" 161 | local ast_children children_array child i 162 | 163 | ast:from $ast children ast_children 164 | children_array=( $ast_children ) 165 | 166 | i=0 167 | for child_name in ${@:2}; do 168 | setvar "$child_name" ${children_array[$i]} 169 | i=$((i+1)) 170 | done 171 | } 172 | noshadow ast:children 1 @ 173 | 174 | 175 | ast:clear() { 176 | unset Asts["value-$1"] 177 | unset Asts["head-$1"] 178 | unset Asts["children-$1"] 179 | } 180 | 181 | ast:clear-all() { 182 | unset Asts 183 | declare -gA Asts 184 | 185 | Asts[index]=0 186 | Asts[length]=0 187 | Asts[required-indent]=0 188 | } 189 | 190 | -------------------------------------------------------------------------------- /src/ast/helper.bash: -------------------------------------------------------------------------------- 1 | # ast:error $message 2 | # 3 | # Informs an AST parsing error and exits. 4 | # 5 | 6 | ast:error() { 7 | local message="$1" 8 | 9 | if ${POWSCRIPT_ALLOW_INCOMPLETE-false}; then 10 | POWSCRIPT_INCOMPLETE_STATE="error: while parsing the AST: $message" 11 | if ${POWSCRIPT_SHOW_INCOMPLETE_MESSAGE-false}; then 12 | >&2 echo "$message" 13 | fi 14 | else 15 | >&2 echo "$message" 16 | fi 17 | exit 18 | } 19 | 20 | 21 | # ast:parse:require-newline 22 | # 23 | # Parses next ast expression, errors if it's not 24 | # a newline, otherwise consume it. If it's the 25 | # end-of-file and we are in interactive mode, 26 | # ignore the error. 27 | # 28 | 29 | ast:parse:require-newline() { 30 | local nl nl_head 31 | ast:parse:expr nl 32 | ast:from $nl head nl_head 33 | case $nl_head in 34 | newline) 35 | ;; 36 | eof) 37 | if ! ${POWSCRIPT_ALLOW_INCOMPLETE-false}; then 38 | ast:error "unexpected end of file after $1" 39 | fi 40 | ;; 41 | *) 42 | ast:error "trailing expression after ${1}: $(ast:print $nl) :: $(ast:from $nl head)" 43 | ;; 44 | esac 45 | } 46 | 47 | 48 | # ast:is-flag $expr 49 | # 50 | # Check if the given AST $expr is of the form -name 51 | # 52 | 53 | ast:is-flag() { 54 | local expr="$1" 55 | local minus name extra 56 | 57 | if ast:is $expr cat; then 58 | ast:children $expr minus name extra 59 | if ast:is $minus name '-' && ast:is $name name && [ -z "$extra" ]; then 60 | return 0 61 | else 62 | return 1 63 | fi 64 | else 65 | return 1 66 | fi 67 | } 68 | 69 | # ast:to-double-string 70 | # 71 | # Change a normal string to a double quoted string. 72 | # 73 | 74 | ast:to-double-string() { 75 | local str="$1" 76 | local str_value cat_child kittens 77 | 78 | if ast:is $str string; then 79 | ast:from $str value str_value 80 | str_value="${str_value//\\/\\\\}" 81 | str_value="${str_value//\"/\\\"}" 82 | str_value="${str_value//\$/\\\$}" 83 | ast:set $str head double-string 84 | ast:set $str value "$str_value" 85 | 86 | elif ast:is $str cat; then 87 | ast:from $str children kittens 88 | 89 | for cat_child in $kittens; do 90 | ast:to-double-string $cat_child 91 | done 92 | fi 93 | } 94 | 95 | -------------------------------------------------------------------------------- /src/ast/indent.bash: -------------------------------------------------------------------------------- 1 | declare -gA IndentStack 2 | 3 | IndentStack[index]=0 4 | IndentStack[starting-block]=false 5 | IndentStack[0]=0 6 | 7 | 8 | ast:new-indentation() { 9 | local indent 10 | 11 | ast:indentation-required indent 12 | 13 | ast:push-indentation $((indent+1)) 14 | IndentStack[starting-block]=true 15 | } 16 | 17 | 18 | ast:test-indentation() { #<> 19 | local value="$1" class="$2" out="$3" 20 | local req found result temp_result 21 | ast:indentation-required req 22 | ast:count-indentation "$value" $class found 23 | 24 | if ${IndentStack[starting-block]}; then 25 | if [ $found -ge $req ]; then 26 | IndentStack[starting-block]=false 27 | IndentStack[${IndentStack[index]}]=$found 28 | result=ok 29 | else 30 | result=error-start 31 | fi 32 | else 33 | if [ $found -eq -1 ]; then 34 | result=error-eof 35 | 36 | elif [ $found -eq $req ]; then 37 | result=ok 38 | 39 | elif [ $found -lt $req ]; then 40 | result=end 41 | else 42 | result=error-exact 43 | fi 44 | fi 45 | setvar "$out" $result 46 | } 47 | noshadow ast:test-indentation 2 48 | 49 | 50 | ast:update-indentation() { 51 | IndentStack[${IndentStack[index]}]=$1 52 | } 53 | 54 | ast:pop-indentation() { 55 | IndentStack[index]=$((${IndentStack[index]}-1)) 56 | } 57 | 58 | ast:push-indentation() { 59 | IndentStack[index]=$((${IndentStack[index]}+1)) 60 | ast:update-indentation $1 61 | } 62 | 63 | ast:indentation-required() { 64 | setvar "$1" ${IndentStack[${IndentStack[index]}]} 65 | } 66 | 67 | ast:count-indentation() { 68 | case "$2" in 69 | whitespace) setvar "$3" "$1" ;; 70 | eof) setvar "$3" -1 ;; 71 | *) setvar "$3" 0 ;; 72 | esac 73 | } 74 | 75 | ast:indentation-layers() { 76 | setvar "$1" ${IndentStack[index]} 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/ast/math.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:math $out 2 | # 3 | # Entry point for parsing a math expression. 4 | # 5 | 6 | ast:parse:math() { #<> 7 | local out="$1" 8 | local expr math_expr 9 | local float_precision math_type 10 | 11 | if ast:state-is topmath; then 12 | math_type="top" 13 | else 14 | math_type="expr" 15 | fi 16 | 17 | 18 | AST_MATH_MODE=true ast:parse:expr expr 19 | 20 | ast:parse:validate-math-operand $expr 21 | 22 | if token:next-is name && ! ${NOBC-false}; then 23 | token:get -v float_precision 24 | if [[ "$float_precision" =~ [0-9]+ ]]; then 25 | ast:make math_expr math-float "$float_precision" $expr 26 | else 27 | ast:error "expected number for floating point precision in math expression, got $float_precision" 28 | fi 29 | else 30 | ast:make math_expr "math-$math_type" '' $expr 31 | fi 32 | 33 | setvar "$out" $math_expr 34 | } 35 | noshadow ast:parse:math 36 | 37 | 38 | # ast:parse:math-unary $expr $op 39 | # 40 | # Parse math expressions of the forms -expr or +expr. 41 | # 42 | 43 | ast:parse:math-unary() { 44 | local expr="$1" op="$2" 45 | local operand value head 46 | 47 | ast:parse:expr operand 48 | ast:parse:validate-math-operand $operand 49 | ast:push-child $expr $operand 50 | 51 | ast:set $expr value "$op" 52 | ast:set $expr head math 53 | } 54 | 55 | # ast:parse:math-binary $expr $left $op 56 | # 57 | # Parse math expressions of the form expr op expr. 58 | # 59 | 60 | ast:parse:math-binary() { 61 | local expr="$1" left="$2" op="$3" 62 | local right class 63 | 64 | token:peek -c class 65 | while [[ $class =~ (newline|whitespace) ]]; do 66 | token:skip 67 | token:peek -c class 68 | done 69 | if [ $class = eof ] && ${POWSCRIPT_ALLOW_INCOMPLETE-false}; then 70 | POWSCRIPT_INCOMPLETE_STATE="m$op" 71 | exit 72 | fi 73 | ast:parse:expr right 74 | ast:parse:validate-math-operand $left 75 | ast:parse:validate-math-operand $right 76 | ast:push-child $expr $right 77 | 78 | ast:set $expr value "$op" 79 | ast:set $expr head math 80 | } 81 | 82 | 83 | # ast:parse:validate-math-operand $operand 84 | # 85 | # Checks that the operand is a valid math expression, which can be 86 | # - var, becoming $var 87 | # - $var, $(command ...), $(math expr), expr 88 | # - (expr), [expr], {expr} 89 | # Anything else is an error. 90 | # 91 | 92 | ast:parse:validate-math-operand() { 93 | local operand="$1" value head 94 | 95 | ast:all-from "$operand" -v value -h head 96 | 97 | case "$head" in 98 | name) 99 | if [[ "$value" =~ ^[~]?([a-zA-Z_][a-zA-Z_0-9]*|@)$ ]]; then 100 | local default_op default_value 101 | ast:make default_op name ":-" 102 | ast:make default_value name "0" 103 | ast:set $operand head string-default 104 | ast:set $operand children "$default_value $default_op" 105 | elif [[ "$value" =~ ^(0x[0-9a-fA-F]+|[0-9]+)$ ]]; then 106 | true 107 | else 108 | ast:error "invalid variable name '$value' in math expression" 109 | fi 110 | ;; 111 | *substitution|string-*|math) 112 | ;; 113 | list) 114 | local element elements count=0 115 | ast:from $operand children elements 116 | for element in $elements; do 117 | ast:parse:validate-math-operand $element 118 | count=$((count+1)) 119 | done 120 | if [ ! $count = 1 ]; then 121 | ast:error "invalid math expression $(ast:print operand): trailling expressions in parentheses" 122 | fi 123 | ;; 124 | *) 125 | ast:error "not a valid math expression: $(ast:print operand) :: $head" 126 | ;; 127 | esac 128 | } 129 | 130 | -------------------------------------------------------------------------------- /src/ast/parallel.bash: -------------------------------------------------------------------------------- 1 | ast:parse:await() { #<> 2 | local out="$1" 3 | local expr expr_head cmd last_arg then_block done_block var 4 | 5 | ast:parse:command-call '' cmd 6 | ast:pop-child $cmd last_arg 7 | 8 | if ast:is $last_arg name '|'; then 9 | ast:pop-child $cmd last_arg 10 | expr_head='await-pipe' 11 | elif ast:is $last_arg name 'then'; then 12 | expr_head='await-then' 13 | else 14 | expr_head='await-for' 15 | var=$last_arg 16 | 17 | ast:pop-child $cmd last_arg 18 | if ast:is $last_arg name 'for'; then 19 | ast:pop-child $cmd last_arg 20 | else 21 | ast:error "expected 'for' or 'then' after await command" 22 | fi 23 | fi 24 | 25 | if ! ast:is $last_arg name 'then'; then 26 | ast:error "expected 'then' after await command" 27 | else 28 | ast:parse:block awt then_block 29 | if [ ! "$expr_head" = 'await-then' ] && token:next-is 'name' 'when'; then 30 | ast:parse:when-done done_block 31 | fi 32 | 33 | ast:make expr $expr_head "" $cmd $var $then_block $done_block 34 | setvar "$out" $expr 35 | fi 36 | } 37 | noshadow ast:parse:await 38 | 39 | ast:parse:when-done() { 40 | token:require name 'when' 41 | token:require name 'done' 42 | ast:parse:require-newline "when done" 43 | 44 | ast:parse:block wdn "$1" 45 | } 46 | -------------------------------------------------------------------------------- /src/ast/parse.bash: -------------------------------------------------------------------------------- 1 | powscript_source ast/helper.bash #<> 2 | powscript_source ast/expand.bash #<> 3 | powscript_source ast/require.bash #<> 4 | powscript_source ast/sequence.bash #<> 5 | powscript_source ast/expressions.bash #<> 6 | powscript_source ast/math.bash #<> 7 | powscript_source ast/flag.bash #<> 8 | powscript_source ast/commands.bash #<> 9 | powscript_source ast/declare.bash #<> 10 | powscript_source ast/blocks.bash #<> 11 | powscript_source ast/patterns.bash #<> 12 | powscript_source ast/conditionals.bash #<> 13 | powscript_source ast/functions.bash #<> 14 | powscript_source ast/parallel.bash #<> 15 | powscript_source ast/pop.bash #<> 16 | powscript_source ast/lowerer.bash #<> 17 | powscript_source ast/print.bash #<> 18 | 19 | # ast:parse:try 20 | # 21 | # Try parsing an ast expression from the input, 22 | # printing 'top' on success or the last 23 | # parser state on failure. 24 | 25 | ast:parse:try() { 26 | ( 27 | local ast 28 | POWSCRIPT_INCOMPLETE_STATE= 29 | 30 | trap ' 31 | if [ -n "$POWSCRIPT_INCOMPLETE_STATE" ]; then 32 | echo "$POWSCRIPT_INCOMPLETE_STATE" 33 | else 34 | ast:last-state 35 | fi 36 | exit' EXIT 37 | 38 | POWSCRIPT_ALLOW_INCOMPLETE=true ast:parse ast 39 | exit 40 | ) 41 | } 42 | 43 | 44 | # ast:parse $out 45 | # 46 | # Parse an ast expression from the input, 47 | # storing it in $out. 48 | 49 | ast:parse() { 50 | ast:parse:linestart "$1" 51 | } 52 | 53 | 54 | # ast:parse:linestart $out 55 | # 56 | # Test that there is no indentation before proceeding 57 | # to parse the expression. 58 | 59 | ast:parse:linestart() { #<> 60 | local value class line 61 | 62 | token:get -v value -c class -ls line 63 | 64 | if [ "$class" = whitespace ]; then 65 | ast:error "indentation error at line $line, unexpected indentation of $value." 66 | else 67 | token:backtrack 68 | ast:parse:top "$1" 69 | fi 70 | } 71 | noshadow ast:parse:linestart 72 | 73 | 74 | # ast:parse:top $out 75 | # 76 | # Analyze first expression and dispatch to the 77 | # appropriate function appropriate for it. 78 | 79 | ast:parse:top() { #<> 80 | local out="$1" 81 | local expr expr_head assigns 82 | 83 | ast:parse:expr expr 84 | ast:from $expr head expr_head 85 | 86 | case $expr_head in 87 | name) 88 | local expr_value 89 | ast:from $expr value expr_value 90 | if token:next-is special '(' true; then 91 | case "$expr_value" in 92 | 'if'|'for'|'math'|'case'|'while'|'switch'|\ 93 | 'switch'|'await'|'assert'|'require'|'expand'|\ 94 | 'declare'|'test'|'pop') 95 | ast:error "'(' not allowed after reserved name '$expr_value'" 96 | ;; 97 | esac 98 | fi 99 | case "$expr_value" in 100 | 'if') ast:parse:if 'if' "$out" ;; 101 | 'for') ast:parse:for "$out" ;; 102 | 'case') ast:parse:case "$out" ;; 103 | 'while') ast:parse:while "$out" ;; 104 | 'switch') ast:parse:switch "$out" ;; 105 | 'await') ast:parse:await "$out" ;; 106 | 'assert') ast:parse:assert "$out" ;; 107 | 'require') ast:parse:require "$out" ;; 108 | 'expand') ast:parse:expand "$out" ;; 109 | 'declare') ast:parse:declare "$out" ;; 110 | 'test') ast:parse:test "$out" ;; 111 | 'pop') ast:parse:pop "$out" ;; 112 | 'math') 113 | ast:push-state 'topmath' 114 | ast:parse:math "$out" 115 | ast:pop-state 116 | ;; 117 | *) 118 | if token:next-is special '(' true; then 119 | ast:parse:function-definition $expr "$out" 120 | else 121 | ast:make assigns assign-sequence 122 | ast:parse:command-call-with-cmd $assigns $expr "$out" 123 | fi 124 | ;; 125 | esac 126 | ;; 127 | *assign) 128 | ast:parse:assign-sequence $expr "$out" 129 | ;; 130 | newline) 131 | setvar "$out" -1 132 | ;; 133 | cat) 134 | if token:next-is special '(' true; then 135 | ast:parse:function-definition $expr "$out" 136 | else 137 | ast:make assigns assign-sequence 138 | ast:parse:command-call-with-cmd $assigns $expr "$out" 139 | fi 140 | ;; 141 | *) 142 | ast:make assigns assign-sequence 143 | ast:parse:command-call-with-cmd $assigns $expr "$out" 144 | ;; 145 | esac 146 | } 147 | noshadow ast:parse:top 148 | 149 | -------------------------------------------------------------------------------- /src/ast/patterns.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:pattern 2 | # 3 | # Parse a pattern to be used by match or case. 4 | # 5 | 6 | ast:parse:pattern() { #<> 7 | local pattern_type="$1" out="$2" 8 | local nclass class value glued 9 | local pattern unfinished=true nesting=0 escaped=false 10 | local pattern_expr midpat_expansion pattern_cat catted=false 11 | 12 | finish() { 13 | unfinished=false 14 | token:backtrack 15 | } 16 | 17 | ast:make pattern_cat cat '' 18 | pattern="" 19 | 20 | while $unfinished; do 21 | token:get -v value -c class -g glued 22 | 23 | if { [ -n "$pattern" ] || $catted; } && ! $glued; then 24 | pattern+=" " 25 | fi 26 | 27 | if $escaped; then 28 | escaped=false 29 | case "$class" in 30 | name|special) pattern+="\\$value" ;; 31 | string) pattern+="\\'$value'" ;; 32 | esac 33 | continue 34 | fi 35 | 36 | case "$class" in 37 | newline|eof) 38 | finish 39 | ;; 40 | name) 41 | case "$pattern_type:$value" in 42 | ==:or) finish ;; 43 | ==:and) finish ;; 44 | replace:by) finish ;; 45 | *:\\) pattern+='\\' ;; 46 | *) pattern+="$value" ;; 47 | esac 48 | ;; 49 | special) 50 | case "$pattern_type:$value:$nesting" in 51 | string-op:[{]:*) 52 | nesting=$((nesting+1)) 53 | pattern+="$value" 54 | ;; 55 | 56 | string-op:[}]:0) 57 | finish 58 | ;; 59 | 60 | string-op:[}]:*) 61 | pattern+="$value" 62 | nesting=$((nesting-1)) 63 | ;; 64 | 65 | *:\\:*) 66 | escape=true 67 | ;; 68 | 69 | *:[$]*:*) 70 | token:backtrack 71 | ast:make pattern_expr pattern "$pattern" 72 | ast:parse:expr midpat_expansion --nocat 73 | ast:push-child $pattern_cat $pattern_expr 74 | ast:push-child $pattern_cat $midpat_expansion 75 | pattern='' 76 | catted=true 77 | ;; 78 | 79 | *) 80 | pattern+="$value" 81 | ;; 82 | esac 83 | ;; 84 | string) 85 | pattern+="'$value'" 86 | ;; 87 | esac 88 | done 89 | 90 | if $catted; then 91 | if [ -n "$pattern" ]; then 92 | ast:make pattern_expr pattern "$pattern" 93 | ast:push-child $pattern_cat $pattern_expr 94 | fi 95 | setvar "$out" $pattern_cat 96 | else 97 | ast:make "$out" pattern "$pattern" 98 | fi 99 | } 100 | noshadow ast:parse:pattern 1 101 | 102 | -------------------------------------------------------------------------------- /src/ast/pop.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:pop $out 2 | # 3 | # Parses an expression of the forms: 4 | # pop 5 | # pop 6 | # 7 | ast:parse:pop() { #<> 8 | local out="$1" 9 | local argument argument_head 10 | 11 | ast:parse:expr argument 12 | ast:from $argument head argument_head 13 | 14 | case $argument_head in 15 | newline) 16 | ast:make "$out" pop '' 17 | ;; 18 | 19 | *) 20 | ast:make "$out" pop '' $argument 21 | ast:parse:require-newline 'pop' 22 | ;; 23 | esac 24 | } 25 | noshadow ast:parse:pop 26 | -------------------------------------------------------------------------------- /src/ast/print.bash: -------------------------------------------------------------------------------- 1 | ast:print() { 2 | printf '`' 3 | ast:print-child "$1" "$2" 4 | echo '`' 5 | } 6 | 7 | ast:print-child() { 8 | local ast=$1 indent= 9 | local ast_head ast_value ast_children 10 | ast:from $ast head ast_head 11 | ast:from $ast value ast_value 12 | ast:from $ast children ast_children 13 | 14 | local child_array=( $ast_children ) 15 | 16 | case $ast_head in 17 | name) 18 | printf "%s" "$ast_value" 19 | ;; 20 | cat) 21 | local child 22 | for child in ${child_array[@]:0:$((${#child_array[@]}-1))}; do 23 | ast:print-child $child 24 | done 25 | ast:print-child ${child_array[${#child_array[@]}-1]} 26 | ;; 27 | string) 28 | printf "'%s'" "$ast_value" 29 | ;; 30 | call) 31 | local command=${child_array[1]} 32 | local argument 33 | 34 | ast:print-child $command 35 | for argument in ${child_array[@]:2}; do 36 | printf ' ' 37 | ast:print-child $argument 38 | done 39 | ;; 40 | assign) 41 | local name=${child_array[0]} value=${child_array[1]} 42 | ast:print-child $name 43 | printf '=' 44 | ast:print-child $value 45 | ;; 46 | and) 47 | ast:print-child ${child_array[0]} 48 | printf " and " 49 | ast:print-child ${child_array[1]} 50 | ;; 51 | indexing-assign) 52 | local name=${child_array[0]} index=${child_array[1]} value=${child_array[2]} 53 | ast:print-child $name 54 | printf '[' 55 | ast:print-child $index 56 | printf ']=' 57 | ast:print-child $value 58 | ;; 59 | math-assign) 60 | ast:print-child ${child_array[0]} 61 | printf '%s=' $ast_value 62 | ast:print-child ${child_array[1]} 63 | ;; 64 | concat-assign) 65 | ast:print-child ${child_array[0]} 66 | printf '&=' 67 | ast:print-child ${child_array[1]} 68 | ;; 69 | simple-substitution) 70 | printf '$%s' "$ast_value" 71 | ;; 72 | indexing-substitution) 73 | printf '${%s[' "$ast_value" 74 | ast:print-child ${child_array[0]} 75 | printf ']}' 76 | ;; 77 | indirect-indexing-substitution) 78 | printf '${!%s[' "$ast_value" 79 | ast:print-child ${child_array[0]} 80 | printf ']}' 81 | ;; 82 | command-substitution) 83 | printf '$(' 84 | ast:print-child ${child_array[0]} 85 | printf ')' 86 | ;; 87 | math-top|math-expr) 88 | printf 'math ' 89 | ast:print-child ${child_array[0]} 90 | ;; 91 | math) 92 | if [ -n "${child_array[1]}" ]; then 93 | ast:print-child ${child_array[0]} 94 | printf '%s' "$ast_value" 95 | ast:print-child ${child_array[1]} 96 | else 97 | printf '%s' "$ast_value" 98 | ast:print-child ${child_array[0]} 99 | fi 100 | ;; 101 | math-assigned) 102 | ast:print-child ${child_array[0]} 103 | ;; 104 | array-length) 105 | printf '$#%s' "$ast_value" 106 | ;; 107 | function-def) 108 | local name=${child_array[0]} args=${child_array[1]} block=${child_array[2]} 109 | 110 | ast:print-child $name 111 | ast:print-child $args 112 | echo 113 | ast:print-child $block 114 | ;; 115 | if|elif) 116 | printf '%s ' $ast_head 117 | ast:print-child ${child_array[0]} 118 | echo 119 | ast:print-child ${child_array[1]} 120 | ast:print-child ${child_array[2]} 121 | ;; 122 | else) 123 | printf 'else\n' 124 | ast:print-child ${child_array[0]} 125 | ast:print-child ${child_array[1]} 126 | ;; 127 | end_if) 128 | ;; 129 | condition) 130 | case $ast_value in 131 | command) 132 | ast:print-child ${child_array[0]} 133 | ;; 134 | not|-*) 135 | printf '%s ' "$ast_value" 136 | ast:print-child ${child_array[0]} 137 | ;; 138 | *) 139 | ast:print-child ${child_array[0]} 140 | printf ' %s ' "$ast_value" 141 | ast:print-child ${child_array[1]} 142 | ;; 143 | esac 144 | ;; 145 | for) 146 | printf 'for ' 147 | ast:print-child ${child_array[0]} 148 | printf ' in ' 149 | ast:print-child ${child_array[1]} 150 | echo 151 | ast:print-child ${child_array[2]} 152 | ;; 153 | for-of) 154 | printf 'for ' 155 | ast:print-child ${child_array[0]} 156 | printf ' of ' 157 | ast:print-child ${child_array[1]} 158 | echo 159 | ast:print-child ${child_array[2]} 160 | ;; 161 | for-of-map) 162 | printf 'for ' 163 | ast:print-child ${child_array[0]} 164 | printf ',' 165 | ast:print-child ${child_array[1]} 166 | printf ' of ' 167 | ast:print-child ${child_array[2]} 168 | echo 169 | ast:print-child ${child_array[3]} 170 | ;; 171 | switch|case|while) 172 | printf '%s' "$ast_head " 173 | ast:print-child ${child_array[0]} 174 | echo 175 | ast:print-child ${child_array[1]} 176 | ;; 177 | expand) 178 | printf 'expand' 179 | echo 180 | ast:print-child ${child_array[0]} 181 | ;; 182 | local) 183 | local child 184 | printf 'local' 185 | for child in ${child_array[@]}; do 186 | printf ' ' 187 | ast:print-child $child 188 | done 189 | ;; 190 | string-length) 191 | printf '%s' "\${$ast_value:length}" 192 | ;; 193 | string-indirect) 194 | printf '%s' "\${$ast_value:indirect}" 195 | ;; 196 | string-removal|string-case|string-default) 197 | local op 198 | printf '%s' "\${$ast_value:" 199 | ast:from ${child_array[1]} value op 200 | case "$op" in 201 | '#') printf "prefix " ;; 202 | '%') printf "suffix " ;; 203 | '##') printf "prefix* " ;; 204 | '%%') printf "suffix* " ;; 205 | '^') printf "uppercase " ;; 206 | ',') printf "lowercase " ;; 207 | '^^') printf "uppercase* " ;; 208 | ',,') printf "lowercase* " ;; 209 | '-') printf "unset " ;; 210 | '=') printf "unset= " ;; 211 | ':-') printf "empty " ;; 212 | ':=') printf "empty= " ;; 213 | '+') printf "set " ;; 214 | ':+') printf "nonempty " ;; 215 | '?') printf "set! " ;; 216 | ':?') printf "nonempty! " ;; 217 | esac 218 | ast:print-child ${child_array[0]} 219 | printf "}" 220 | ;; 221 | string-replace) 222 | printf "\${$ast_value:" 223 | printf 'replace ' 224 | ast:print-child ${child_array[0]} 225 | printf ' by ' 226 | ast:print-child ${child_array[1]} 227 | printf '}' 228 | ;; 229 | string-from) 230 | printf "\${$ast_value:" 231 | printf 'from $(' 232 | ast:print-child ${child_array[0]} 233 | printf ') to $(' 234 | ast:print-child ${child_array[1]} 235 | printf ')}' 236 | ;; 237 | string-slice) 238 | printf "\${$ast_value:" 239 | printf 'slice $(' 240 | ast:print-child ${child_array[0]} 241 | printf ') length $(' 242 | ast:print-child ${child_array[1]} 243 | printf ')}' 244 | ;; 245 | string-index) 246 | printf "\${$ast_value:" 247 | printf 'index $(' 248 | ast:print-child ${child_array[0]} 249 | printf ')}' 250 | ;; 251 | string-test) 252 | local op 253 | printf "\${$ast_value:" 254 | ast:from ${child_array[1]} value op 255 | 256 | case "$op" in 257 | '+1') printf 'unset?}' ;; 258 | '+-1') printf 'set?}' ;; 259 | ':+1') printf 'empty?}' ;; 260 | ':+-1') printf 'nonempty?}' ;; 261 | esac 262 | ;; 263 | array-operation) 264 | local var index subst 265 | ast:from ${child_array[0]} value var 266 | ast:children ${child_array[0]} index 267 | index="$(ast:print-child $index)" 268 | subst="$(ast:print-child ${child_array[1]})" 269 | 270 | printf '%s' "${subst/@/$var[$index]}" 271 | ;; 272 | pattern) 273 | local op 274 | printf '%s' "$ast_value" 275 | ;; 276 | flag-double-dash*) 277 | printf '%s' "--$ast_value" 278 | ;; 279 | flag-single-dash*) 280 | printf '%s' "-$ast_value" 281 | ;; 282 | await*) 283 | local then 284 | 285 | printf 'await ' 286 | ast:print-child ${child_array[0]} 287 | printf ' then' 288 | 289 | case "$ast_head" in 290 | await-for) 291 | then=2 292 | printf ' for ' 293 | ast:print-child ${child_array[1]} 294 | echo 295 | ;; 296 | await-pipe) 297 | then=1 298 | printf ' |\n' 299 | ;; 300 | await-then) 301 | then=1 302 | echo 303 | ;; 304 | esac 305 | 306 | ast:print-child ${child_array[$then]} 307 | 308 | if [ -n "${child_array[$((then+1))]}" ]; then 309 | printf 'when done\n' 310 | ast:print-child ${child_array[$((then+1))]} 311 | fi 312 | ;; 313 | pipe) 314 | ast:print-child ${child_array[0]} 315 | printf " | " 316 | ast:print-child ${child_array[1]} 317 | ;; 318 | list) 319 | local element 320 | 321 | printf '( ' 322 | for element in "${child_array[@]}"; do 323 | ast:print-child $element 324 | printf ' ' 325 | done 326 | printf ')' 327 | ;; 328 | elements) 329 | local element 330 | 331 | for element in "${child_array[@]}"; do 332 | ast:print-child $element 333 | printf ' ' 334 | done 335 | ;; 336 | block) 337 | local statement 338 | 339 | for statement in "${child_array[@]}"; do 340 | printf "%$((ast_value*2)).s" '' 341 | ast:print-child $statement 342 | echo 343 | done 344 | ;; 345 | esac 346 | } 347 | 348 | 349 | -------------------------------------------------------------------------------- /src/ast/require.bash: -------------------------------------------------------------------------------- 1 | ast:parse:require() { #<> 2 | local out="$1" 3 | local file 4 | 5 | ast:parse:expr file 6 | ast:parse:require-newline 'require' 7 | 8 | ast:make "$out" require '' $file 9 | } 10 | noshadow ast:parse:require 11 | -------------------------------------------------------------------------------- /src/ast/sequence.bash: -------------------------------------------------------------------------------- 1 | ast:parse:list() { #<> 2 | local out="$1" 3 | local list_expr 4 | 5 | ast:make list_expr list 6 | ast:parse:sequence $list_expr 7 | 8 | setvar "$out" $list_expr 9 | } 10 | noshadow ast:parse:list 11 | 12 | ast:parse:sequence() { 13 | local __discard__ 14 | ast:parse:sequence_ "$1" "$2" "${3:-__discard__}" 15 | } 16 | 17 | ast:parse:sequence_() { #<> 18 | local seq="$1" predicate="${2:-true}" out="$3" 19 | local expr state 20 | 21 | ast:last-state state 22 | 23 | while true; do 24 | ast:parse:expr expr 25 | 26 | if ast:is $expr newline; then 27 | ast:parse:sequence:multiline $state || break 28 | 29 | elif ast:is $expr eof; then 30 | if ${POWSCRIPT_ALLOW_INCOMPLETE-false} || [ $state = top ]; then 31 | break 32 | else 33 | ast:error "unexpected end-of-file while parsing '$state' sequence" 34 | fi 35 | 36 | elif ast:parse:sequence:multiline-end $expr $state; then 37 | break 38 | 39 | elif ! ${predicate//%/$expr}; then 40 | setvar "$out" "$expr" 41 | break 42 | 43 | else 44 | ast:push-child $seq $expr 45 | 46 | fi 47 | done 48 | 49 | if [ -z "${!out}" ]; then 50 | setvar "$out" '-1' 51 | fi 52 | } 53 | noshadow ast:parse:sequence_ 2 54 | 55 | ast:parse:sequence:multiline() { 56 | local state="$1" 57 | 58 | case $state in 59 | '('|'['|'{') return 0 ;; 60 | *) return 1 ;; 61 | esac 62 | } 63 | 64 | ast:parse:sequence:multiline-end() { 65 | local expr="$1" state="$2" 66 | 67 | case $state in 68 | '(') ast:is $expr name ')' && return 0 ;; 69 | '[') ast:is $expr name ']' && return 0 ;; 70 | '{') ast:is $expr name '}' && return 0 ;; 71 | esac 72 | return 1 73 | } 74 | -------------------------------------------------------------------------------- /src/ast/states.bash: -------------------------------------------------------------------------------- 1 | declare -gA AstStates 2 | 3 | AstStates[index]=0 4 | AstStates[0]=top 5 | 6 | ast:push-state() { 7 | local index=$((${AstStates[index]}+1)) 8 | 9 | AstStates[index]=$index 10 | AstStates[$index]=$1 11 | } 12 | 13 | ast:pop-state() { 14 | AstStates[index]=$((${AstStates[index]}-1)) 15 | } 16 | 17 | ast:last-state() { 18 | setvar "$1" "${AstStates[${AstStates[index]}]}" 19 | } 20 | 21 | ast:state-is() { 22 | local state 23 | ast:last-state state 24 | [ "$state" = "$1" ] 25 | } 26 | 27 | ast:clear-states() { 28 | unset AstStates 29 | declare -gA AstStates 30 | AstStates[index]=0 31 | AstStates[0]=top 32 | } 33 | 34 | ast:ends-state() { 35 | local expr="$1" 36 | local state expr_value 37 | 38 | ast:last-state state 39 | 40 | if ast:is $expr name; then 41 | ast:from $expr value expr_value 42 | 43 | case "$state $expr_value" in 44 | "( )"|"[ ]"|"{ }") 45 | return 0 46 | ;; 47 | *) 48 | return 1 49 | ;; 50 | esac 51 | else 52 | return 1 53 | fi 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/ast/substitutions.bash: -------------------------------------------------------------------------------- 1 | # ast:parse:substitution $out 2 | # 3 | # Parse expressions of the form $variable or $array[index] 4 | # 5 | 6 | ast:parse:substitution() { #<> 7 | local subst curly="$1" out="$2" 8 | local expr value head varname aftervar 9 | local index lb rb aft 10 | local postcat 11 | local cat_children cat_array dollar 12 | 13 | ast:parse:expr expr 14 | ast:all-from "$expr" -v value -h head -@ varname aftervar 15 | 16 | case "$head" in 17 | name) 18 | if $curly && ! token:next-is special '}'; then 19 | if token:next-is special ':'; then 20 | token:require special ':' 21 | ast:make postcat cat '' 22 | ast:parse:parameter-substitution $postcat "$value" subst 23 | elif [ "$value" = "@:" ]; then 24 | ast:make postcat cat '' 25 | ast:parse:parameter-substitution $postcat "@" subst 26 | else 27 | local sym class 28 | token:peek -v sym -c class 29 | ast:error "unimplemented variable substitution (found $sym::$class instead of [ or :)" 30 | fi 31 | else 32 | ast:make subst simple-substitution "$value" 33 | fi 34 | ;; 35 | cat) 36 | if ast:is $varname name; then 37 | if ast:is $aftervar name '['; then 38 | ast:children "$expr" varname lb index rb aft 39 | ast:from $varname value value 40 | ast:make subst indexing-substitution "$value" $index 41 | 42 | if [ -n "$aft" ]; then 43 | if ${AST_MATH_MODE-false}; then 44 | ast:error "invalid math expression: $(ast:print $expr)" 45 | 46 | elif $curly; then 47 | if ast:is $aft name ':'; then 48 | local param 49 | ast:from $expr children cat_children 50 | cat_array=( $cat_children ) 51 | ast:make postcat cat '' "${cat_array[@]:5}" 52 | ast:parse:parameter-substitution $postcat "@" param 53 | 54 | if ast:is $param indirect-indexing-substitution; then 55 | ast:error "unimplemented variable substitution (keys used after indexing)" 56 | else 57 | if ast:is $param string-test; then 58 | ast:make subst array-test '' $subst $param 59 | else 60 | ast:make subst array-operation '' $subst $param 61 | fi 62 | fi 63 | 64 | else 65 | ast:error "unimplemented variable substitution (found $(ast:print $aft) while looking for } or :)" 66 | fi 67 | 68 | else 69 | ast:from $expr children cat_children 70 | cat_array=( $cat_children ) 71 | ast:make subst cat '' $subst "${cat_array[@]:4}" 72 | fi 73 | elif token:next-is special ':'; then 74 | local param 75 | token:skip 76 | ast:make postcat cat '' 77 | ast:parse:parameter-substitution $postcat "@" param 78 | 79 | if ast:is $param indirect-indexing-substitution; then 80 | ast:error "unimplemented variable substitution (keys used after indexing)" 81 | else 82 | if ast:is $param string-test; then 83 | ast:make subst array-test '' $subst $param 84 | else 85 | ast:make subst array-operation '' $subst $param 86 | fi 87 | fi 88 | 89 | fi 90 | elif $curly; then 91 | ast:from $expr children cat_children 92 | cat_array=( $cat_children ) 93 | 94 | if ast:is $aftervar name ':'; then 95 | ast:make postcat cat '' "${cat_array[@]:2}" 96 | ast:from $varname value value 97 | ast:parse:parameter-substitution $postcat "$value" subst 98 | 99 | elif ast:is $varname name "@:"; then 100 | ast:make postcat cat '' "${cat_array[@]:1}" 101 | ast:parse:parameter-substitution $postcat "@" subst 102 | 103 | else 104 | ast:error "unimplemented variable substitution ($(ast:print $varname) directly followed by $(ast:print $aftervar) instead of : or [)" 105 | fi 106 | else 107 | ast:set $varname head simple-substitution 108 | subst=$expr 109 | fi 110 | else 111 | ast:make dollar name '$' 112 | ast:unshift-child $expr $dollar 113 | subst=$expr 114 | fi 115 | ;; 116 | *) 117 | ast:error "unimplemented variable substitution (not a name nor a cat)" 118 | ;; 119 | esac 120 | setvar "$out" $subst 121 | } 122 | noshadow ast:parse:substitution 1 123 | 124 | 125 | # ast:parse:curly-substitution 126 | # 127 | # Parse expressions of the form ${}, ${variable} or ${array[index]} 128 | # 129 | 130 | ast:parse:curly-substitution() { #<> 131 | local out="$1" 132 | local subst 133 | 134 | if token:next-is special '}'; then 135 | ast:make subst empty-substitution '' 136 | else 137 | ast:push-state '{' 138 | ast:parse:substitution true subst 139 | ast:pop-state 140 | fi 141 | 142 | token:require special '}' 143 | setvar "$out" $subst 144 | } 145 | noshadow ast:parse:curly-substitution 146 | 147 | # ast:parse:parameter-substitution $postcat $varname $out 148 | # 149 | # Parse string substitution commands, of the form: 150 | # 151 | # unset 152 | # unset= 153 | # empty 154 | # empty= 155 | # set 156 | # set! 157 | # nonempty 158 | # nonempty! 159 | # length 160 | # index 161 | # from to 162 | # slice length 163 | # suffix 164 | # prefix 165 | # suffix* 166 | # prefix* 167 | # replace by 168 | # replace* by 169 | # uppercase 170 | # lowercase 171 | # uppercase* 172 | # lowercase* 173 | # keys 174 | # ref 175 | # ref[@] 176 | # deref 177 | # deref[] 178 | # 179 | 180 | ast:parse:parameter-substitution() { #<> 181 | local postcat="$1" varname="$2" out="$3" 182 | local child_a child_b child_c 183 | local opname opclass postname postclass 184 | local modifier mclass 185 | local glued 186 | 187 | ast:children $postcat child_a child_b child_c 188 | if [ -z "$child_a" ]; then 189 | token:get -v opname -c opclass 190 | [ $opclass = 'name' ] || ast:error "Invalid string operation: $opname::$opclass" 191 | elif ast:is $child_a name; then 192 | ast:from $child_a value opname 193 | else 194 | ast:error "Expected a name for a string operation, got: $(ast:print $child_b)" 195 | fi 196 | 197 | case "$opname" in 198 | unset|empty) modifier='='; mclass='string' ;; 199 | *fix|replace) modifier='*' mclass='name' ;; 200 | *case) modifier='*' mclass='name' ;; 201 | esac 202 | 203 | 204 | case "$opname" in 205 | unset|empty|*fix|*case|replace) 206 | if [ -n "$child_b" ]; then 207 | if ast:is $child_b "$mclass" "$modifier"; then 208 | opname="$opname$modifier" 209 | else 210 | ast:error "trailling expression after operation name: $(ast:print $child_b)" 211 | fi 212 | else 213 | if token:next-is special "$modifier"; then 214 | token:peek -g glued 215 | if $glued; then 216 | opname="$opname$modifier" 217 | token:skip 218 | fi 219 | else 220 | token:peek -v postname -g glued 221 | if $glued && ! token:next-is special '}'; then 222 | ast:error "Invalid string operation: $opname$postname" 223 | fi 224 | fi 225 | fi 226 | ;; 227 | 228 | ref|deref) 229 | ;; 230 | *) 231 | if [ -n "$child_b" ]; then 232 | ast:error "trailling expression after operation name: $(ast:print $child_b)" 233 | else 234 | token:peek -v postname -g glued 235 | if $glued && ! token:next-is special '}'; then 236 | ast:error "Invalid string operation: $opname$postname" 237 | fi 238 | fi 239 | ;; 240 | esac 241 | 242 | case "$opname" in 243 | length) 244 | ast:make "$out" "string-length" "$varname" 245 | ;; 246 | keys) 247 | local at 248 | ast:make at name '@' 249 | ast:make "$out" indirect-indexing-substitution "$varname" $at 250 | ;; 251 | slice) 252 | local start len 253 | NOBC=true ast:parse:math start 254 | if token:next-is name length; then 255 | token:require name length 256 | NOBC=true ast:parse:math len 257 | ast:make "$out" string-slice "$varname" $start $len 258 | else 259 | ast:make "$out" string-slice-from "$varname" $start 260 | fi 261 | ;; 262 | from) 263 | local from to 264 | NOBC=true ast:parse:math from 265 | token:require name to 266 | NOBC=true ast:parse:math to 267 | ast:make "$out" string-from "$varname" $from $to 268 | ;; 269 | index) 270 | local index 271 | NOBC=true ast:parse:math index 272 | ast:make "$out" string-index "$varname" $index 273 | ;; 274 | *fix*|*case*) 275 | local pattern op opval optype 276 | case "$opname" in 277 | suffix) opval='%' optype='removal';; 278 | prefix) opval='#' optype='removal';; 279 | suffix\*) opval='%%' optype='removal';; 280 | prefix\*) opval='##' optype='removal';; 281 | lowercase) opval=',' optype='case' ;; 282 | uppercase) opval='^' optype='case' ;; 283 | lowercase\*) opval=',,' optype='case' ;; 284 | uppercase\*) opval='^^' optype='case' ;; 285 | *) 286 | ast:error "Invalid string operation: $opname" 287 | ;; 288 | esac 289 | ast:make op name "$opval" 290 | ast:parse:pattern 'string-op' pattern 291 | ast:make "$out" "string-$optype" "$varname" $pattern $op 292 | ;; 293 | replace*) 294 | local pattern by op opval 295 | case "$opname" in 296 | replace) opval='/' ;; 297 | replace\*) opval='//' ;; 298 | *) 299 | ast:error "Invalid string operation: $opname" 300 | ;; 301 | esac 302 | ast:make op name "$opval" 303 | ast:parse:pattern 'replace' pattern 304 | token:require name by 305 | ast:parse:pattern 'string-op' by 306 | ast:make "$out" string-replace "$varname" $pattern $by $op 307 | ;; 308 | *set\?|*empty\?) 309 | local expr op opval def defval 310 | ast:conditional-exp-operators "$opname" opval defval 311 | ast:make op name "$opval" 312 | ast:make def name "$defval" 313 | ast:make "$out" string-test "$varname" $op $def 314 | ;; 315 | unset*|empty*|set*|nonempty*) 316 | local expr op opval 317 | case "$opname" in 318 | unset) opval='-' ;; 319 | unset=) opval='=' ;; 320 | empty) opval=':-' ;; 321 | empty=) opval=':=' ;; 322 | set) opval='+' ;; 323 | nonempty) opval=':+' ;; 324 | set!) opval='?' ;; 325 | nonempty!) opval=':?' ;; 326 | *) 327 | ast:error "Invalid string operation: $opname" 328 | ;; 329 | esac 330 | ast:make op name "$opval" 331 | if [ -n "$child_c" ]; then 332 | expr=$child_c 333 | else 334 | ast:parse:expr expr 335 | fi 336 | ast:to-double-string $expr 337 | ast:make "$out" string-default "$varname" $expr $op 338 | ;; 339 | 340 | ref) 341 | if token:next-is special '['; then 342 | token:require special '[' 343 | token:require special '@' 344 | token:require special ']' 345 | ast:make "$out" array-reference "$varname" 346 | else 347 | ast:make "$out" variable-reference "$varname" 348 | fi 349 | ;; 350 | deref) 351 | if token:next-is special '['; then 352 | local index 353 | token:require special '[' 354 | 355 | ast:push-state '[' 356 | ast:parse:expr index 357 | ast:pop-state 358 | 359 | token:require special ']' 360 | ast:make "$out" array-dereference "$varname" $index 361 | else 362 | ast:make "$out" variable-dereference "$varname" 363 | fi 364 | ;; 365 | *) 366 | ast:error "Invalid string operation: $opname" 367 | ;; 368 | esac 369 | 370 | } 371 | noshadow ast:parse:parameter-substitution 2 372 | 373 | ast:conditional-exp-operators() { #<> 374 | local opname="$1" op="$2" def="$3" 375 | local opval defval 376 | case "$opname" in 377 | unset\?) opval='+1' defval='0';; 378 | set\?) opval='+-1' defval='1';; 379 | empty\?) opval=':+1' defval='0';; 380 | nonempty\?) opval=':+-1' defval='1';; 381 | *) 382 | ast:error "Invalid string operation: $opname" 383 | ;; 384 | esac 385 | setvar "$op" "$opval" 386 | setvar "$def" "$defval" 387 | } 388 | noshadow ast:conditional-exp-operators 1 2 389 | 390 | # ast:parse:command-substitution 391 | # 392 | # Parse expressions of the form $(command ...) or $(math ) 393 | # 394 | 395 | ast:parse:command-substitution() { #<> 396 | local out="$1" 397 | local subst cmd call assigns 398 | 399 | ast:push-state '(' 400 | ast:parse:command-call '' call 401 | ast:pop-state 402 | 403 | ast:make subst command-substitution '' $call 404 | setvar "$out" $subst 405 | } 406 | noshadow ast:parse:command-substitution 407 | 408 | -------------------------------------------------------------------------------- /src/compiler/cache.bash: -------------------------------------------------------------------------------- 1 | PowscriptDirectory="$HOME/.powscript" 2 | PowscriptCacheDirectory="$PowscriptDirectory/cache" 3 | 4 | declare -gA PowscriptLibCache 5 | 6 | cache:init() { 7 | for lib in "${!PowscriptLib[@]}"; do 8 | if [ -f "$(cache:file "$lib")" ]; then 9 | PowscriptLibCache[$lib]="$(<"$(cache:file "$lib")")" 10 | fi 11 | done 12 | } 13 | 14 | cache:file() { 15 | echo "$PowscriptCacheDirectory/lib/$1.$PowscriptBackend" 16 | } 17 | 18 | cache:library() { 19 | if ${POWSCRIPT_NO_CACHE-false}; then 20 | files:compile-file <<<"${PowscriptLib[$1]}"$'\n\n' 21 | else 22 | cache:update 23 | echo "${PowscriptLibCache[$1]}" 24 | fi 25 | } 26 | 27 | cache:remove() { 28 | rm -r "$PowscriptCacheDirectory" 29 | } 30 | 31 | cache:update() { 32 | if ! ${POWSCRIPT_NO_CACHE:-false}; then 33 | if cache:init-directory || ! cache:up-to-date; then 34 | cache:update-libraries 35 | cache:update-version 36 | elif [ "${#PowscriptLibCache[@]}" = 0 ]; then 37 | cache:init 38 | fi 39 | fi 40 | } 41 | 42 | cache:up-to-date() { 43 | version:up-to-date "$(cache:version)" || return 1 44 | for lib in "${!PowscriptLib[@]}"; do 45 | [ -f "$(cache:file "$lib")" ] || return 1 46 | done 47 | } 48 | 49 | cache:update-version() { 50 | echo "$(version:number)" >"$PowscriptCacheDirectory/version" 51 | } 52 | 53 | cache:version() { 54 | if [ -f "$PowscriptCacheDirectory/version" ]; then 55 | cat "$PowscriptCacheDirectory/version" 56 | else 57 | echo "0.0.0" 58 | fi 59 | } 60 | 61 | cache:update-libraries() { 62 | local lib code 63 | backend:select "$PowscriptBackend" 64 | echo -e "\\033[1mCompiling libraries..." 65 | for lib in "${!PowscriptLib[@]}"; do 66 | echo "* compiling: $lib" 67 | code="${PowscriptLib[$lib]}"$'\n\n' 68 | PowscriptLibCache[$lib]="$(files:compile-file '/dev/stdout' <<<"$code")" 69 | echo "${PowscriptLibCache[$lib]}" >"$(cache:file "$lib")" 70 | done 71 | echo -e "\033[0m" 72 | } 73 | 74 | cache:init-directory() { 75 | if [ ! -d "$PowscriptDirectory" ]; then 76 | mkdir "$PowscriptDirectory" 77 | fi 78 | if [ ! -d "$PowscriptCacheDirectory" ]; then 79 | mkdir "$PowscriptCacheDirectory" 80 | mkdir "$PowscriptCacheDirectory/lib" 81 | cache:update-version 82 | return 0 83 | else 84 | return 1 85 | fi 86 | } 87 | -------------------------------------------------------------------------------- /src/compiler/compiler.bash: -------------------------------------------------------------------------------- 1 | powscript_source compiler/temp.bash #<> 2 | powscript_source compiler/helptext.bash #<> 3 | powscript_source compiler/version.bash #<> 4 | powscript_source compiler/cache.bash #<> 5 | powscript_source compiler/options.bash #<> 6 | powscript_source compiler/files.bash #<> 7 | powscript_source compiler/interactive.bash #<> 8 | -------------------------------------------------------------------------------- /src/compiler/files.bash: -------------------------------------------------------------------------------- 1 | powscript_source extra/start.bash #<> 2 | powscript_source extra/end.bash #<> 3 | 4 | files:compile() { 5 | local output="${1-/dev/stdout}" 6 | shift 7 | 8 | files:start-code "$output" 9 | files:compile-files "$output" "$@" 10 | files:end-code "$output" 11 | } 12 | 13 | files:compile-files() { 14 | local output="$1" 15 | shift 16 | 17 | for file in "$@"; do 18 | POWCOMP_DIR="$(dirname "$file")" 19 | files:compile-file "$output" <<<"$(<"$file")"$'\n\n' 20 | done 21 | } 22 | 23 | 24 | files:compile-file() { 25 | local output="${1-/dev/stdout}" 26 | local ast ast_lowered 27 | 28 | stream:init 29 | interactive:clear-compilation 30 | while ! stream:end; do 31 | ast:parse ast 32 | ast:lower $ast ast_lowered 33 | backend:compile $ast_lowered 1>>"$output" 34 | done 35 | } 36 | 37 | files:start-code() { 38 | backend:file-start >>"$1" 39 | files:compile-file "$1" <<<"$PowscriptFileStart"$'\n\n' 40 | 41 | if ${PowscriptIncludeStd-true}; then 42 | echo "$(cache:library std)"$'\n\n' >>"$1" 43 | fi 44 | } 45 | 46 | files:end-code() { 47 | files:compile-file "$1" <<<"$PowscriptEndFile"$'\n\n' 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/compiler/helptext.bash: -------------------------------------------------------------------------------- 1 | powscript:help() { 2 | echo ' 3 | usage: powscript [options...] input_files... 4 | 5 | options: 6 | -h|--help Display this message and exit. 7 | 8 | -v|--version Display version number and exit. 9 | 10 | -i|--interactive Launch interactive mode. 11 | Default if no input files are given. 12 | 13 | -c|--compile Force compilation mode. 14 | 15 | -o|--output $file Compile all input files into $file. 16 | If this option is not given, the compilation 17 | is sent to the standard output. 18 | 19 | -e|--evaluate $expr Evaluate a powscript expression. 20 | 21 | --no-std Don'"'"'t import the standard library, 22 | decreasing startup time. 23 | 24 | -d|--debug Debug mode for developers. 25 | In bash, using "source ./powscript --debug" 26 | will make all internal functions available 27 | for testing. 28 | 29 | --to sh|bash Select target language for compilation. 30 | 31 | --no-cache Don'"'"'t use cache'"'"'d compiled files. 32 | 33 | --update-cache Force cache files to be recompiled. 34 | ' 35 | } 36 | -------------------------------------------------------------------------------- /src/compiler/interactive.bash: -------------------------------------------------------------------------------- 1 | InteractiveFileLineNumber=0 2 | 3 | interactive:start() { 4 | local ast code compiled_code line="" state=none 5 | local proc rfifo wfifo end_token result 6 | local powhistory="${POWSCRIPT_HISTORY_FILE-$HOME/.powscript_history}" 7 | local extra_line='' 8 | local compile_flag=false ast_flag=false echo_flag=false incomplete_flag=false lower_flag=false 9 | 10 | [ ! -f "$powhistory" ] && echo >"$powhistory" 11 | history -c 12 | history -r "$powhistory" 13 | 14 | powscript:make-fifo ".interactive.wfifo" wfifo 15 | powscript:make-fifo ".interactive.rfifo" rfifo 16 | powscript:temp-name ".end" end_token 17 | 18 | backend:interactive "$wfifo" "$rfifo" "$end_token" & 19 | proc="$!" 20 | PowscriptGuestProcess="$proc" 21 | 22 | exec 3<>"$wfifo" 23 | exec 4<>"$rfifo" 24 | 25 | backend:file-start >>"$wfifo" 26 | 27 | if ${PowscriptIncludeStd-true}; then 28 | echo "$(cache:library std)" >>"$wfifo" 29 | fi 30 | 31 | if [ -f "$HOME/.powrc" ]; then 32 | files:compile-file "$wfifo" <"$HOME/.powrc" 33 | fi 34 | 35 | 36 | while pgrep -P $proc >/dev/null; do 37 | result= 38 | 39 | if [ -n "${extra_line// /}" ]; then 40 | line="$extra_line" 41 | extra_line="" 42 | else 43 | interactive:read-powscript top line 44 | fi 45 | code="$line" 46 | 47 | case "$code" in 48 | '.compile') 49 | interactive:toggle-flag compile_flag 50 | ;; 51 | '.ast') 52 | interactive:toggle-flag ast_flag 53 | ;; 54 | '.lower') 55 | interactive:toggle-flag lower_flag 56 | ;; 57 | '.echo') 58 | interactive:toggle-flag echo_flag 59 | ;; 60 | '.incomplete') 61 | interactive:toggle-flag incomplete_flag 62 | ;; 63 | '.show '*) 64 | interactive:show-ast "${code//.show /}" 65 | echo 66 | ;; 67 | '.tokens '*) 68 | interactive:show-tokens "${code//.tokens /}" 69 | ;; 70 | '.help'*) 71 | interactive:help 72 | ;; 73 | *) 74 | state=none 75 | while [ ! "$state" = top ]; do 76 | interactive:clear-compilation 77 | state="$( { stream:init; POWSCRIPT_SHOW_INCOMPLETE_MESSAGE=$incomplete_flag ast:parse:try; } <<< "$code" )" 78 | [ -z "$line" ] && state=top 79 | case "$state" in 80 | top) 81 | interactive:clear-compilation 82 | { stream:init; ast:parse ast; } <<< "$code"$'\n' 83 | ;; 84 | error*) 85 | >&2 echo "$state" 86 | state=none 87 | code= 88 | line= 89 | ;; 90 | *) 91 | interactive:read-powscript "$state" line 92 | code="$code"$'\n'"$line" 93 | ;; 94 | esac 95 | done 96 | 97 | while IFS= read -r codeline; do 98 | [ -n "$codeline" ] && history -s "$codeline" 99 | done <<<"$code" 100 | 101 | if $echo_flag; then 102 | echo "---- CODE ECHO -----" 103 | echo "$code" 104 | echo "---------------------" 105 | fi 106 | 107 | if ! stream:end; then 108 | interactive:get-remaining-input extra_line 109 | code="${code:0:$(($# - ${#extra_line}))}" 110 | fi 111 | 112 | if $ast_flag; then 113 | echo "---- SYNTAX TREE ----" 114 | interactive:show-ast $ast 115 | echo "---------------------" 116 | fi 117 | ast:lower $ast ast 118 | if $lower_flag; then 119 | echo "---- LOWERED TREE ---" 120 | interactive:show-ast $ast 121 | echo "---------------------" 122 | fi 123 | backend:compile $ast compiled_code 124 | if $compile_flag; then 125 | echo "--- COMPILED CODE ---" 126 | echo "$compiled_code" 127 | echo "---------------------" 128 | fi 129 | echo "$compiled_code" >>"$wfifo" 130 | echo "#<>" >>"$wfifo" 131 | while [ ! "$result" = "#<>" ]; do 132 | IFS= read -r result <"$rfifo" 133 | [ ! "$result" = "#<>" ] && echo "$result" 134 | done 135 | echo 136 | ;; 137 | esac 138 | done 139 | history -w "$powhistory" 140 | 141 | [ -p "$wfifo" ] && rm $wfifo 142 | [ -p "$rfifo" ] && rm $rfifo 143 | } 144 | 145 | interactive:help() { 146 | echo ' 147 | Special Commands: 148 | .help Display this message 149 | 150 | .ast Toggle the display of the abstract syntax tree 151 | 152 | .lower Toggle the display of the lowered ast 153 | 154 | .compile Toggle the display of compilated code 155 | 156 | .tokens t* Display information about the given tokens 157 | 158 | .echo Toggle echoing the code 159 | 160 | .incomplete Toggle allowing incomplete code 161 | 162 | .show ast Display information about the ast with the given ID 163 | ' 164 | } 165 | 166 | interactive:get-remaining-input() { #<> 167 | local collumn out="$1" 168 | token:peek -cs collumn <<< "" 169 | stream:jump-to-collumn $collumn 170 | stream:get-rest-of-line "$out" 171 | } 172 | noshadow interactive:get-remaining-input 173 | 174 | interactive:clear-compilation() { 175 | token:clear-all 176 | token:clear-states 177 | ast:clear-all 178 | ast:clear-states 179 | } 180 | 181 | interactive:show-ast() { 182 | echo "id: $1" 183 | echo "head: $(ast:from $1 head)" 184 | echo "value: $(ast:from $1 value)" 185 | echo "children: $(ast:from $1 children)" 186 | ast:print $1 187 | } 188 | 189 | interactive:show-tokens() { 190 | local value class 191 | { 192 | interactive:clear-compilation 193 | stream:init 194 | while ! stream:end; do 195 | token:get -v value -c class 196 | echo "-----------------" 197 | echo "$value :: $class" 198 | done 199 | } <<< "$1" 200 | echo 201 | } 202 | 203 | interactive:toggle-flag() { 204 | if ${!1}; then 205 | setvar "$1" false 206 | else 207 | setvar "$1" true 208 | fi 209 | } 210 | 211 | interactive:read-powscript() { 212 | IFS="" read -r -e -p "$(interactive:format-powscript-prompt "$1")" "$2" 213 | InteractiveFileLineNumber=$((InteractiveFileLineNumber+1)) 214 | } 215 | 216 | interactive:format-powscript-prompt() { 217 | local state_name=$1 state 218 | 219 | case $state_name in 220 | top) state="--" ;; 221 | double-quotes) state='""' ;; 222 | single-quotes) state="''" ;; 223 | *) state="$state_name" ;; 224 | 225 | esac 226 | 227 | local default_prompt='pow[%L]%S> ' 228 | local prompt="${POWSCRIPT_PS1-$default_prompt}" 229 | 230 | prompt="${prompt//%L/$(printf '%.3d' $InteractiveFileLineNumber)}" 231 | prompt="${prompt//%S/$(printf '%4s' $state)}" 232 | 233 | echo "$prompt" 234 | } 235 | 236 | 237 | -------------------------------------------------------------------------------- /src/compiler/options.bash: -------------------------------------------------------------------------------- 1 | PowscriptBackend=bash 2 | PowscriptInteractiveMode=nofile 3 | PowscriptCompileFile=false 4 | PowscriptOutput='/dev/stdout' 5 | PowscriptIncludeStd=true 6 | 7 | declare -gA PowscriptFiles 8 | PowscriptFileNumber=0 9 | 10 | powscript:parse-options() { 11 | while [ "$#" -gt 0 ]; do 12 | case "$1" in 13 | '-h'|'--help') 14 | powscript:help 15 | exit 16 | ;; 17 | '-o'|'--output') 18 | PowscriptOutput="$2" 19 | shift 2 20 | ;; 21 | '-i'|'--interactive') 22 | PowscriptInteractiveMode=yes 23 | shift 24 | ;; 25 | '-d'|'--debug') 26 | PowscriptInteractiveMode=false 27 | PowscriptCompileFile=false 28 | POWSCRIPT_DEBUG=true 29 | shift $# 30 | ;; 31 | '--to') 32 | shift 33 | case "$1" in 34 | bash|sh) 35 | PowscriptBackend="$1" 36 | shift 37 | ;; 38 | *) 39 | >&2 echo "Invalid powscript backend $1" 40 | exit 1 41 | ;; 42 | esac 43 | ;; 44 | '--no-std') 45 | shift 46 | PowscriptIncludeStd=false 47 | ;; 48 | '--update-cache') 49 | shift 50 | cache:remove 51 | ;; 52 | '--no-cache') 53 | shift 54 | POWSCRIPT_NO_CACHE=true 55 | ;; 56 | '-c'|'--compile') 57 | PowscriptCompileFile=true 58 | shift 59 | ;; 60 | 61 | '-e'|'--evaluate') 62 | shift 63 | backend:select bash 64 | backend:run "$(files:compile-file '/dev/stdout' <<<"$1"$'\n\n')" 65 | if [ ! $PowscriptInteractiveMode = yes ]; then 66 | PowscriptInteractiveMode=no 67 | fi 68 | shift 69 | ;; 70 | 71 | '-v'|'--version') 72 | shift 73 | version:number 74 | exit 0 75 | ;; 76 | 77 | '-'*) 78 | >&2 echo "Invalid powscript option $1" 79 | exit 1 80 | ;; 81 | 82 | *) 83 | PowscriptFiles[$PowscriptFileNumber]="$1" 84 | PowscriptFileNumber=$((PowscriptFileNumber+1)) 85 | shift 86 | ;; 87 | esac 88 | done 89 | 90 | if $PowscriptCompileFile && [ "$PowscriptFileNumber" -eq 0 ]; then 91 | >&2 echo "No input files given" 92 | fi 93 | 94 | case $PowscriptInteractiveMode in 95 | no) PowscriptInteractiveMode=false; ;; 96 | yes) PowscriptInteractiveMode=true; ;; 97 | nofile) 98 | if [ "$PowscriptFileNumber" -gt 0 ]; then 99 | PowscriptInteractiveMode=false 100 | else 101 | PowscriptInteractiveMode=true 102 | fi 103 | ;; 104 | esac 105 | } 106 | 107 | powscript:is-interactive() { 108 | $PowscriptInteractiveMode 109 | } 110 | 111 | powscript:in-compile-mode() { 112 | $PowscriptCompileFile 113 | } 114 | -------------------------------------------------------------------------------- /src/compiler/temp.bash: -------------------------------------------------------------------------------- 1 | 2 | PowscriptTempDirectory="$(mkdir "$(mktemp -u).powscript")" 3 | 4 | powscript:temp-name() { 5 | local suffix=".powscript$1" 6 | setvar "$2" "$(mktemp -u -p "$PowscriptTempDirectory")$suffix" 7 | } 8 | 9 | powscript:make-temp() { 10 | powscript:temp-name "$1" "$2" 11 | touch "${!2}" 12 | } 13 | 14 | powscript:make-fifo() { 15 | powscript:temp-name "$1" "$2" 16 | mkfifo "${!2}" 17 | } 18 | 19 | powscript:clean-up() { 20 | local exit_code=$? 21 | POWSCRIPT_CDEBUG_STOP=false 22 | [ -d "$PowscriptTempDirectory" ] && rm -r "$PowscriptTempDirectory" 23 | [ -n "$PowscriptGuestProcess" ] && pgrep -P "$PowscriptGuestProcess" >/dev/null && kill -QUIT "$PowscriptGuestProcess" 24 | exit $exit_code 25 | } 26 | 27 | trap 'powscript:clean-up' TERM INT QUIT ABRT EXIT 28 | 29 | -------------------------------------------------------------------------------- /src/compiler/version.bash: -------------------------------------------------------------------------------- 1 | POWSCRIPT_VERSION="$(grep -o -e '"version":.*' "$PowscriptSourceDirectory/../package.json" | sed -e 's/"version":[ ]*"\(.*\)",/\1/g')" #<> 2 | 3 | version:number() { 4 | echo "$POWSCRIPT_VERSION" 5 | } 6 | 7 | version:up-to-date() { 8 | [ "$(version:major-of "$1")" -ge "$(version:major-of "$POWSCRIPT_VERSION")" ] || 9 | [ "$(version:minor-of "$1")" -ge "$(version:minor-of "$POWSCRIPT_VERSION")" ] || 10 | [ "$(version:patch-of "$1")" -ge "$(version:patch-of "$POWSCRIPT_VERSION")" ] 11 | } 12 | 13 | version:major-of() { 14 | echo "${1%%.*}" 15 | } 16 | 17 | version:minor-of() { 18 | local minor_patch="${1#*.}" 19 | echo "${minor_patch%.*}" 20 | } 21 | 22 | version:patch-of() { 23 | echo "${1##*.}" 24 | } 25 | -------------------------------------------------------------------------------- /src/extra/end.bash: -------------------------------------------------------------------------------- 1 | PowscriptEndFile=' 2 | if $ASYNC is 1 3 | wait 4 | 5 | ' 6 | -------------------------------------------------------------------------------- /src/extra/start.bash: -------------------------------------------------------------------------------- 1 | PowscriptStartFile=' 2 | ' 3 | -------------------------------------------------------------------------------- /src/helper.bash: -------------------------------------------------------------------------------- 1 | # powscript_source 2 | # 3 | # include source relatively from the powscript source code directory 4 | 5 | powscript_source() { 6 | source "$PowscriptSourceDirectory/$1" 7 | } 8 | 9 | 10 | # printf_seq $start $end $format 11 | # 12 | # calls printf on each element of $(seq $start $end) 13 | # the specifier %N is used to reference the current number. 14 | 15 | printf_seq() { 16 | local start="$1" end="$2" format="$3" 17 | for n in $(seq $start $end); do 18 | printf "${format//%N/$n}" 19 | done 20 | } 21 | 22 | pop() { 23 | setvar "$2" "${*:$(($1 + 3))}" 24 | } 25 | 26 | 27 | # setvar $varname $value 28 | # 29 | # dynamic variable assignation 30 | 31 | setvar() { 32 | if [ -n "$1" ]; then 33 | printf -v "$1" '%s' "$2" 34 | else 35 | echo "$2" 36 | fi 37 | } 38 | 39 | # noshadow name ${argnumber:-0} ${varnumber:-1} 40 | # 41 | # wrap the function so that the name of the out variables 42 | # (assumed to be the last arguments) do not conflict 43 | # with the local variables declared within the function. 44 | # 45 | # Passing @ instead of a number means the number of arguments 46 | # is variable. You may have a argnumber or varnumber be @, but 47 | # not both. 48 | 49 | ClearShadowingCounter=0 50 | 51 | noshadow() { 52 | local name="$1" 53 | local argnumber="${2:-0}" 54 | local varnumber="${3:-1}" 55 | local arguments set_variables intermediary_variables intermediary_definition 56 | local prefix="__noshadow_${ClearShadowingCounter}_" 57 | 58 | case $argnumber in 59 | '@') 60 | arguments="\"\${@:1:\$((\$# - $varnumber))}\"" 61 | 62 | set_variables="shift \$((\$# - $varnumber)) 63 | $(printf_seq 1 $varnumber\ 64 | "setvar \"\$%N\" \"\$$prefix%N\"\n")" 65 | 66 | intermediary_variables="$(printf_seq 1 $varnumber "$prefix%N")" 67 | intermediary_definition="local $intermediary_variables" 68 | ;; 69 | *) 70 | arguments="$(printf_seq 1 $argnumber '"$%N" ')" 71 | 72 | case $varnumber in 73 | '@') 74 | local argshift="shift $((argnumber-1))" 75 | [ $argnumber = 0 ] && argshift= 76 | 77 | set_variables="# 78 | $argshift 79 | for ${prefix}n in \$(seq $((argnumber+1)) \$#); do 80 | setvar \"\$$((argnumber+1))\" \"\${!${prefix}all[\$${prefix}n]}\" 81 | shift 82 | done" 83 | 84 | intermediary_variables="\"\${${prefix}all[@]}\"" 85 | intermediary_definition="declare -A ${prefix}all 86 | for ${prefix}n in \$(seq $((argnumber+1)) \$#); do 87 | ${prefix}all[\$${prefix}n]=${prefix}\$${prefix}n 88 | done" 89 | ;; 90 | *) 91 | set_variables="$(printf_seq $((argnumber+1)) $((varnumber+argnumber)) "setvar \"\$%N\" \"\$$prefix%N\"\n")" 92 | intermediary_variables="$(printf_seq $((argnumber+1)) $((varnumber+argnumber)) "$prefix%N ")" 93 | intermediary_definition="local $intermediary_variables" 94 | ;; 95 | esac 96 | ;; 97 | esac 98 | 99 | ${ShadowingOp-eval} " 100 | __shadowing_$name() $(${ShadowingGetFunc-declare} -f $name | tail -n +2) 101 | 102 | $name() { 103 | if [ -z \${$prefix+x} ]; then 104 | local $prefix 105 | $intermediary_definition 106 | fi 107 | __shadowing_$name $arguments $intermediary_variables 108 | $set_variables 109 | } 110 | " 111 | ClearShadowingCounter=$(($ClearShadowingCounter+1)) 112 | } 113 | 114 | -------------------------------------------------------------------------------- /src/lang/backends.bash: -------------------------------------------------------------------------------- 1 | declare -gA PowscriptBackends 2 | 3 | powscript_source lang/common.bash #<> 4 | powscript_source lang/bash/compile.bash #<> 5 | powscript_source lang/sh/compile.bash #<> 6 | 7 | backend:select() { 8 | eval " 9 | backend:compile () { ${1}:compile \"\$@\"; } 10 | backend:interactive () { ${1}:interactive \"\$@\"; } 11 | backend:run () { ${1}:run \"\$@\"; } 12 | backend:file-start () { ${1}:file-start \"\$@\"; } 13 | " 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/lang/bash/compile.bash: -------------------------------------------------------------------------------- 1 | powscript_source lang/bash/start.bash #<> 2 | powscript_source lang/bash/interactive.bash #<> 3 | 4 | bash:run() { 5 | bash -c "$1" 6 | } 7 | 8 | bash:compile() { #<> 9 | local expr=$1 out="$2" 10 | local expr_head expr_value expr_children 11 | 12 | ast:from $expr head expr_head 13 | 14 | set_substitution() { 15 | if ${NO_QUOTING-false}; then 16 | setvar "$out" "$1" 17 | else 18 | setvar "$out" "\"$1\"" 19 | fi 20 | } 21 | 22 | case "$expr_head" in 23 | name|string|assign|cat|if|elif|else|end_if|call|for|\ 24 | while|expand|command-substitution|switch|case|require|\ 25 | pattern|and|pipe|elements|simple-substitution|*assert|\ 26 | function-def|local|block|math|math-top|math-float|\ 27 | math-assigned|math-expr|assign-sequence|readline|file-input|\ 28 | string-length|string-removal|string-default|string-test|\ 29 | variable-dereference|double-string|nothing|empty-substitution|flag*) 30 | 31 | sh:compile $expr "$out" 32 | ;; 33 | 34 | declare) 35 | local result type child_ast child expr_children 36 | 37 | ast:from $expr value type 38 | ast:from $expr children expr_children 39 | 40 | case "$type" in 41 | integer) result="declare -i" ;; 42 | array) result="declare -a" ;; 43 | map) result="declare -A" ;; 44 | string) result="declare" ;; 45 | 'global integer') result="declare -gi" ;; 46 | 'global array') result="declare -ga" ;; 47 | 'global map') result="declare -gA" ;; 48 | 'global string') result="declare -g" ;; 49 | esac 50 | 51 | for child_ast in $expr_children; do 52 | bash:compile $child_ast child 53 | result+=" $child" 54 | done 55 | 56 | setvar "$out" "$result" 57 | ;; 58 | 59 | indexing-substitution) 60 | local name index expr_children 61 | 62 | ast:from $expr children expr_children 63 | expr_children=( $expr_children ) 64 | 65 | ast:from $expr value name 66 | bash:compile ${expr_children[0]} index 67 | 68 | setvar "$out" "\"\${$name[$index]}\"" 69 | ;; 70 | 71 | indirect-indexing-substitution) 72 | local name expr_children 73 | 74 | ast:from $expr children expr_children 75 | expr_children=( $expr_children ) 76 | 77 | ast:from $expr value name 78 | bash:compile ${expr_children[0]} index 79 | 80 | setvar "$out" "\"\${!$name[$index]}\"" 81 | ;; 82 | 83 | indexing-assign) 84 | local name index value 85 | ast:from $expr children expr_children 86 | expr_children=( $expr_children ) 87 | 88 | bash:compile ${expr_children[0]} name 89 | bash:compile ${expr_children[1]} index 90 | bash:compile ${expr_children[2]} value 91 | 92 | setvar "$out" "$name[$index]=$value" 93 | ;; 94 | 95 | list-assign) 96 | local name_ast list_ast name list 97 | 98 | ast:children $expr name_ast list_ast 99 | 100 | sh:compile $name_ast name 101 | bash:compile $list_ast list 102 | 103 | setvar "$out" "$name=$list" 104 | ;; 105 | 106 | add-assign) 107 | local name_ast value_ast name value 108 | 109 | ast:children $expr name_ast value_ast 110 | 111 | bash:compile $name_ast name 112 | bash:compile $value_ast value 113 | 114 | setvar "$out" "$name+=$value" 115 | ;; 116 | 117 | associative-assign) 118 | local name_ast name value_ast value_children 119 | 120 | ast:children $expr name_ast value_ast 121 | ast:from $value_ast children value_children 122 | 123 | backend:compile $name_ast name 124 | 125 | if [ -n "$value_children" ]; then 126 | >&2 echo "warning: Associative arrays with elements aren't implemented yet. Ignoring elements." 127 | fi 128 | setvar "$out" "declare -A $name" 129 | ;; 130 | 131 | array-length) 132 | local name 133 | ast:from $expr value name 134 | 135 | setvar "$out" "\${#$name[@]}" 136 | ;; 137 | 138 | concat-assign) 139 | local name_ast value_ast 140 | local name value 141 | 142 | ast:children $expr name_ast value_ast 143 | 144 | backend:compile $name_ast name 145 | backend:compile $value_ast value 146 | 147 | setvar "$out" "$name=\"\${$name}\"$value" 148 | ;; 149 | 150 | list) 151 | local expr_children child_ast child result 152 | 153 | ast:from $expr children expr_children 154 | 155 | result="( " 156 | for child_ast in $expr_children; do 157 | bash:compile $child_ast child 158 | result="$result$child " 159 | done 160 | 161 | setvar "$out" "$result)" 162 | ;; 163 | 164 | string-index) 165 | local name index_ast 166 | local index 167 | 168 | ast:from $expr value name 169 | ast:children $expr index_ast 170 | 171 | backend:compile $index_ast index 172 | 173 | set_substitution "\${$name:$index:1}" 174 | ;; 175 | 176 | string-slice-from) 177 | local name start_ast 178 | local start 179 | 180 | ast:from $expr value name 181 | ast:children $expr start_ast 182 | 183 | backend:compile $start_ast start 184 | 185 | set_substitution "\${$name:$start}" 186 | ;; 187 | 188 | string-slice) 189 | local name start_ast len_ast 190 | local start len 191 | 192 | ast:from $expr value name 193 | ast:children $expr start_ast len_ast 194 | 195 | backend:compile $start_ast start 196 | backend:compile $len_ast len 197 | 198 | set_substitution "\${$name:$start:$len}" 199 | ;; 200 | 201 | string-from) 202 | local name from_ast to_ast 203 | local from to from_cond to_cond len 204 | 205 | ast:from $expr value name 206 | ast:children $expr from_ast to_ast 207 | 208 | backend:compile $from_ast from 209 | backend:compile $to_ast to 210 | 211 | from="${from#\$(( }" 212 | from="${from% ))}" 213 | 214 | to="${to#\$(( }" 215 | to="${to% ))}" 216 | 217 | from="\$(($from < 0 ? 0 : $from))" 218 | to="\$(($to < 0 ? -1 : $to))" 219 | len="\$(($to < $from ? 0 : $to-$from+1))" 220 | 221 | set_substitution "\${$name:$from:$len}" 222 | ;; 223 | 224 | string-case) 225 | local name pattern_ast op_ast 226 | local pattern op 227 | 228 | ast:from $expr value name 229 | ast:children $expr pattern_ast op_ast 230 | 231 | backend:compile $pattern_ast pattern 232 | backend:compile $op_ast op 233 | 234 | set_substitution "\${$name$op$pattern}" 235 | ;; 236 | 237 | string-replace) 238 | local name pattern_ast by_ast op_ast 239 | local pattern by op 240 | 241 | ast:from $expr value name 242 | ast:children $expr pattern_ast by_ast op_ast 243 | 244 | backend:compile $pattern_ast pattern 245 | backend:compile $by_ast by 246 | backend:compile $op_ast op 247 | 248 | pattern="${pattern//\//\\\/}" 249 | by="${by//\//\\\/}" 250 | 251 | pattern="${pattern% }" 252 | 253 | set_substitution "\${$name$op$pattern/$by}" 254 | ;; 255 | 256 | array-operation|array-test) 257 | local subst_ast param_ast 258 | local subst param 259 | 260 | ast:children $expr subst_ast param_ast 261 | 262 | backend:compile $subst_ast subst 263 | backend:compile $param_ast param 264 | 265 | subst="${subst#\"}" 266 | subst="${subst#\$\{}" 267 | subst="${subst%\"}" 268 | subst="${subst%\}}" 269 | 270 | setvar "$out" "${param/@/$subst}" 271 | ;; 272 | 273 | condition) 274 | local op left right quoted=no 275 | ast:from $expr value op 276 | ast:from $expr children expr_children 277 | expr_children=( $expr_children ) 278 | 279 | case "$op" in 280 | command) 281 | bash:compile ${expr_children[0]} left 282 | setvar "$out" "$left" 283 | ;; 284 | not) 285 | bash:compile ${expr_children[0]} right 286 | setvar "$out" "! $right" 287 | ;; 288 | -*) 289 | bash:compile ${expr_children[0]} right 290 | setvar "$out" "[ $op $right ]" 291 | ;; 292 | *) 293 | bash:compile ${expr_children[0]} left 294 | bash:compile ${expr_children[1]} right 295 | 296 | case "$op" in 297 | 'is'|'=') op='=' quoted=single ;; 298 | 'isnt'|'!=') op='!=' quoted=single ;; 299 | '==') op='-eq' quoted=single ;; 300 | '>') op='-gt' quoted=single ;; 301 | '>=') op='-ge' quoted=single ;; 302 | '<') op='-lt' quoted=single ;; 303 | '<=') op='-le' quoted=single ;; 304 | 'match') op='=~' quoted=double ;; 305 | 'and'|'&&') op='&&' ;; 306 | 'or'|'||') op='||' ;; 307 | esac 308 | 309 | case $quoted in 310 | double) setvar "$out" "[[ $left $op $right ]]" ;; 311 | single) setvar "$out" "[ $left $op $right ]" ;; 312 | no) setvar "$out" "$left $op $right" ;; 313 | esac 314 | ;; 315 | esac 316 | ;; 317 | 318 | pop) 319 | local pop_argument_ast pop_argument 320 | ast:children $expr pop_argument_ast 321 | if [ -n "$pop_argument_ast" ]; then 322 | bash:compile $pop_argument_ast pop_argument 323 | else 324 | pop_argument=1 325 | fi 326 | setvar "$out" 'set -- "${@:1:$(($# - '"$pop_argument"'))}"' 327 | ;; 328 | 329 | newline|eof|'') 330 | ;; 331 | *) 332 | backend:error "unimplemented: '$expr_head'" 333 | ;; 334 | esac 335 | } 336 | noshadow bash:compile 1 337 | -------------------------------------------------------------------------------- /src/lang/bash/interactive.bash: -------------------------------------------------------------------------------- 1 | bash:interactive() { 2 | local wfifo="$1" 3 | local rfifo="$2" 4 | local end="$3" 5 | local code="__PowscriptCompiledCode__" 6 | local line="__PowscriptCodeLine__" 7 | local result="__PowscriptResultLine__" 8 | bash -c " 9 | trap '{ echo \"#<>\" >>\"$rfifo\"; exit; }' EXIT ERR 10 | $code= 11 | $line= 12 | $result= 13 | while [ -p '$wfifo' ]; do 14 | IFS= read -r $line <'$wfifo' 15 | if [ \"\$$line\" = '#<>' ] ; then 16 | 2>&1 eval \"\$$code\" >>'$rfifo' || true 17 | echo '#<>' >>'$rfifo' 18 | $code= 19 | else 20 | $code=\"\$$code\"\$'\n'\"\$$line\" 21 | fi 22 | done 23 | " 2>/dev/null 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/lang/bash/start.bash: -------------------------------------------------------------------------------- 1 | FileStartBash="\ 2 | #!/usr/bin/env bash 3 | " 4 | 5 | bash:file-start() { 6 | echo "$FileStartBash" 7 | } 8 | -------------------------------------------------------------------------------- /src/lang/common.bash: -------------------------------------------------------------------------------- 1 | backend:error() { 2 | local message="$1" 3 | >&2 echo "$message" 4 | if ! powscript:is-interactive; then 5 | exit 1 6 | fi 7 | } 8 | 9 | backend:compile-children() { 10 | local expr="$1" 11 | local __ast __asts 12 | 13 | ast:from $expr children __asts 14 | 15 | for __ast in $__asts; do 16 | shift 17 | backend:compile $__ast "$1" 18 | done 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/lang/sh/.settings: -------------------------------------------------------------------------------- 1 | # Generated by POWSCRIPT (https://github.com/coderofsalvation/powscript) 2 | # 3 | # Unless you like pain: edit the .pow sourcefiles instead of this file 4 | 5 | # powscript general settings 6 | set -e # halt on error 7 | set +m # 8 | SHELL="$(echo $0)" # shellname 9 | path="$(pwd)" 10 | selfpath="$path" 11 | tmpfile="/tmp/.dot.tmp.$(whoami)" 12 | -------------------------------------------------------------------------------- /src/lang/sh/interactive.bash: -------------------------------------------------------------------------------- 1 | sh:interactive() { 2 | local wfifo="$1" 3 | local rfifo="$2" 4 | local end="$3" 5 | local code="__PowscriptCompiledCode__" 6 | local line="__PowscriptCodeLine__" 7 | local result="__PowscriptResultLine__" 8 | local newline=$'\n' 9 | sh -c " 10 | trap '{ [ -p \"$rfifo\" ] && echo \"#<>\" >>\"$rfifo\"; exit; }' INT TERM QUIT EXIT 11 | $code= 12 | $line= 13 | $result= 14 | while [ -p '$wfifo' ]; do 15 | IFS= read -r $line <'$wfifo' 16 | if [ \"\$$line\" = '#<>' ] ; then 17 | 2>&1 eval \"\$$code\" >>'$rfifo' || true 18 | echo '#<>' >>'$rfifo' 19 | $code= 20 | else 21 | $code=\"\$$code$newline\$$line\" 22 | fi 23 | done 24 | " 25 | } 26 | -------------------------------------------------------------------------------- /src/lang/sh/start.bash: -------------------------------------------------------------------------------- 1 | read -r -d '' FileStartSh <<'EOF' 2 | #!/usr/bin/env sh 3 | 4 | __powscript_pop() { 5 | local n=$(($1 - ${2:-1})) 6 | if [ $n -ge 500 ]; then 7 | __powscript_POP_EXPR="set -- $(seq -s " " 1 $n | sed 's/[0-9]\+/"${\0}"/g')" 8 | else 9 | local index=0 10 | local arguments="" 11 | while [ $index -lt $n ]; do 12 | index=$((index+1)) 13 | arguments="$arguments \"\${$index}\"" 14 | done 15 | __powscript_POP_EXPR="set -- $arguments" 16 | fi 17 | } 18 | EOF 19 | 20 | sh:file-start() { 21 | echo "$FileStartSh" 22 | } 23 | -------------------------------------------------------------------------------- /src/lexer/states.bash: -------------------------------------------------------------------------------- 1 | declare -gA States 2 | 3 | States[index]=0 4 | 5 | token:push-state() { 6 | States[${States[index]}]=$1 7 | States[index]=$((${States[index]}+1)) 8 | } 9 | 10 | token:pop-state() { 11 | local index=$((${States[index]}-1)) 12 | States[index]=$index 13 | setvar "$1" ${States[$index]} 14 | } 15 | 16 | token:in-topmost-state() { 17 | [ ${States[index]} = 1 ] 18 | } 19 | 20 | token:clear-states() { 21 | unset States 22 | declare -gA States 23 | States[index]=0 24 | token:push-state top 25 | } 26 | 27 | token:push-state top 28 | -------------------------------------------------------------------------------- /src/lexer/stream.bash: -------------------------------------------------------------------------------- 1 | declare -gA Stream 2 | 3 | stream:init() { 4 | Stream[line]="" 5 | Stream[index]=0 6 | Stream[linenumber]=0 7 | Stream[eof]=false 8 | 9 | stream:next-character 10 | } 11 | 12 | stream:get-character() { 13 | if stream:end; then 14 | (>&2 echo 'Tried to get an character after the end of file!') 15 | exit 1 16 | elif [ ${Stream[index]} = ${#Stream[line]} ]; then 17 | setvar "$1" $'\n' 18 | else 19 | setvar "$1" "${Stream[line]:${Stream[index]}:1}" 20 | fi 21 | } 22 | 23 | stream:next-character() { 24 | local line 25 | if [ ${Stream[index]} = ${#Stream[line]} ]; then 26 | if IFS='' read -r line || [ -n "$line" ]; then 27 | Stream[line]="$line" 28 | Stream[index]=0 29 | Stream[linenumber]=$((${Stream[linenumber]}+1)) 30 | else 31 | Stream[eof]=true 32 | fi 33 | else 34 | Stream[index]=$((${Stream[index]}+1)) 35 | fi 36 | } 37 | 38 | stream:require-string() { 39 | local req="$1" str="" c 40 | while [ ${#req} -gt ${#str} ]; do 41 | if stream:end; then 42 | >&2 echo "ERROR: end of input before the required string '$req' was found" 43 | fi 44 | stream:get-character c 45 | stream:next-character 46 | str+=$c 47 | done 48 | if [ "$str" = "$req" ]; then 49 | return 0 50 | else 51 | >&2 echo "ERROR:$( 52 | stream:get-line-number):$( 53 | stream:get-collumn):Found '$str' found instead of the required '$req'" 54 | return 1 55 | fi 56 | } 57 | 58 | 59 | stream:register-escaped-newline() { 60 | Stream[line]=" ${Stream[line]}" 61 | Stream[index]=$((${Stream[index]}+1)) 62 | } 63 | 64 | stream:get-rest-of-line() { #<> 65 | local line collumn out="$1" 66 | line="${Stream[line]}" 67 | stream:get-collumn collumn 68 | setvar "$out" "${line:$collumn}" 69 | } 70 | noshadow stream:get-rest-of-line 71 | 72 | 73 | stream:end() { 74 | ${Stream[eof]} 75 | } 76 | 77 | stream:line-start() { 78 | [ ${Stream[index]} = 0 ] 79 | } 80 | 81 | stream:jump-to-collumn() { 82 | Stream[index]=$1 83 | } 84 | 85 | stream:get-line-number() { 86 | setvar "$1" ${Stream[linenumber]} 87 | } 88 | 89 | stream:get-collumn() { 90 | setvar "$1" ${Stream[index]} 91 | } 92 | 93 | -------------------------------------------------------------------------------- /src/lexer/tokens.bash: -------------------------------------------------------------------------------- 1 | declare -gA Tokens 2 | 3 | Tokens[index]=0 4 | Tokens[length]=0 5 | 6 | TokenMark=0 7 | 8 | token:store() { #<> 9 | local idvar="$8" 10 | local index="${Tokens[length]}" 11 | 12 | Tokens[value-$index]="$1" 13 | Tokens[class-$index]="$2" 14 | Tokens[glued-$index]="$3" 15 | Tokens[linenumber_start-$index]="$4" 16 | Tokens[linenumber_end-$index]="$5" 17 | Tokens[collumn_start-$index]="$6" 18 | Tokens[collumn_end-$index]="$7" 19 | Tokens[index]=$(($index+1)) 20 | Tokens[length]=$(($index+1)) 21 | 22 | setvar "$idvar" $index 23 | } 24 | noshadow token:store 7 25 | 26 | token:from() { 27 | setvar "$3" "${Tokens[${2}-${1}]}" 28 | } 29 | 30 | token:all-from() { 31 | local __token="${@:$#}" 32 | 33 | while [ $# -gt 1 ]; do 34 | case "$1" in 35 | '-v'|'--value') 36 | token:from $__token value "$2" 37 | shift 2 38 | ;; 39 | '-c'|'--class') 40 | token:from $__token class "$2" 41 | shift 2 42 | ;; 43 | '-ls'|'--line-start') 44 | token:from $__token linenumber_start "$2" 45 | shift 2 46 | ;; 47 | '-le'|'--line-end') 48 | token:from $__token linenumber_end "$2" 49 | shift 2 50 | ;; 51 | '-g'|'--glued') 52 | token:from $__token glued "$2" 53 | shift 2 54 | ;; 55 | '-cs'|'--collumn-start') 56 | token:from $__token collumn_start "$2" 57 | shift 2 58 | ;; 59 | '-ce'|'--collumn-end') 60 | token:from $__token collumn_end "$2" 61 | shift 2 62 | ;; 63 | '-i'|'--id') 64 | setvar "$2" $__token 65 | shift 2 66 | ;; 67 | *) 68 | parse_error "unexpected argument $1, expecting -(v|c|ls|le|cs|ce|g|i), on line %line" 69 | ;; 70 | esac 71 | done 72 | } 73 | 74 | 75 | token:get-selected() { 76 | setvar "$1" ${Tokens[index]} 77 | } 78 | 79 | token:clear() { 80 | Tokens[length]=$((${Tokens[index]}-$1)) 81 | if [ ${Tokens[index]} -gt ${Tokens[length]} ]; then 82 | Tokens[index]=${Tokens[length]} 83 | fi 84 | } 85 | 86 | token:clear-all() { 87 | unset Tokens 88 | declare -gA Tokens 89 | Tokens[index]=0 90 | Tokens[length]=0 91 | } 92 | 93 | token:move-back-index() { 94 | Tokens[index]=$((${Tokens[index]}-1)) 95 | } 96 | 97 | token:forward() { 98 | Tokens[index]=$((${Tokens[index]}+1)) 99 | } 100 | 101 | token:in-topmost() { 102 | [ ${Tokens[index]} = ${Tokens[length]} ] 103 | } 104 | 105 | 106 | token:mark-position() { 107 | setvar "$1" ${Tokens[index]} 108 | } 109 | 110 | token:return-to-mark() { 111 | Tokens[index]="$1" 112 | } 113 | 114 | 115 | token:find-by() { #<> 116 | local field="$1" 117 | local value="$2" 118 | local token="${Tokens[index]}" 119 | local tokenvar="$3" 120 | local tvalue 121 | 122 | while [ $token -ge 0 ]; do 123 | token:from $token $field tvalue 124 | if [[ "$tvalue" =~ ^$value$ ]]; then 125 | setvar "$tokenvar" $token 126 | return 127 | else 128 | token=$(($token-1)) 129 | fi 130 | done 131 | setvar "$tokenvar" '-1' 132 | } 133 | noshadow token:find-by 2 134 | 135 | -------------------------------------------------------------------------------- /src/lexer/unicode.bash: -------------------------------------------------------------------------------- 1 | token:parse-unicode-utf-8() { #<> 2 | local out="$1" 3 | local c byte='' unicode='' final_value 4 | local state='get-byte' gotten=0 needed=1 5 | stream:next-character 6 | 7 | while [ $needed -gt $gotten ]; do 8 | if stream:end; then 9 | token:error "Premature end of input while parsing utf-8 unicode character" 10 | fi 11 | stream:get-character c 12 | 13 | case "$state" in 14 | 'get-byte') 15 | case "$c" in 16 | [0-9abcdefABCDEF]) 17 | stream:next-character 18 | byte+="$c" 19 | if [ ${#byte} = 2 ]; then 20 | state='test-byte' 21 | fi 22 | ;; 23 | *) 24 | if [ "${#byte}:$gotten" = "1:0" ]; then 25 | unicode="\\x$byte" 26 | gotten=1 27 | else 28 | token:error "Invalid utf-8 unicode character: $unicode\\x$byte (incomplete byte)" 29 | fi 30 | ;; 31 | esac 32 | ;; 33 | 'test-byte') 34 | case "$gotten" in 35 | 0) 36 | needed="$(( 37 | 0x$byte < 0x80 ? 1 : 38 | 0x$byte < 0xe0 ? 2 : 39 | 0x$byte < 0xf0 ? 3 : 40 | 0x$byte < 0xf8 ? 4 : -1))" 41 | if [ "$needed" = -1 ]; then 42 | token:error "Invalid utf-8 unicode character: \\x$byte" 43 | fi 44 | ;; 45 | *) 46 | if [ $((0x$byte)) -lt $((0x80)) ] || 47 | [ $((0x$byte)) -gt $((0xbf)) ]; then 48 | token:error "Invalid utf-8 unicode character: $unicode\\x$byte" 49 | fi 50 | ;; 51 | esac 52 | state='get-byte' 53 | gotten=$((gotten+1)) 54 | unicode+="\\x$byte" 55 | byte='' 56 | if [ $gotten -lt $needed ]; then 57 | if ! stream:require-string '\x'; then 58 | token:error "^^ while parsing a utf-8 unicode character" 59 | fi 60 | fi 61 | ;; 62 | esac 63 | done 64 | printf -v "$out" "$unicode" 65 | } 66 | noshadow token:parse-unicode-utf-8 67 | 68 | 69 | token:parse-unicode-utf-16() { #<> 70 | local out="$1" 71 | local c unicode1='' unicode2='' final_value 72 | local state='unicode1' 73 | stream:next-character 74 | 75 | while [ -z "${!out}" ]; do 76 | if stream:end; then 77 | token:error "Premature end of input while parsing utf-16 unicode character" 78 | fi 79 | stream:get-character c 80 | 81 | case "$state" in 82 | 'unicode1') 83 | case "$c" in 84 | [0-9abcdefABCDEF]) 85 | unicode1+="$c" 86 | stream:next-character 87 | if [ "${#unicode1}" = 4 ]; then 88 | if [ "$((0x${unicode1}))" -gt $((0xd7ff)) ] && 89 | [ "$((0x${unicode1}))" -lt $((0xdbff)) ]; then 90 | state='unicode2' 91 | if ! stream:require-string '\u'; then 92 | token:error "^^ while parsing a utf-16 unicode character" 93 | fi 94 | elif [ "$((0x${unicode1}))" -gt $((0xdbff)) ] && 95 | [ "$((0x${unicode1}))" -lt $((0xdc00)) ]; then 96 | token:error "Invalid utf-16 unicode character: \\u${unicode1}$c" 97 | else 98 | printf -v "$out" "\\u$unicode1" 99 | fi 100 | fi 101 | ;; 102 | *) 103 | if [ "${#unicode1}" -gt 0 ]; then 104 | printf -v "$out" "\\u$unicode1" 105 | else 106 | token:error "Invalid utf-16 unicode: \\u" 107 | fi 108 | ;; 109 | esac 110 | ;; 111 | 'unicode2') 112 | case "$c" in 113 | [0-9abcdefABCDEF]) 114 | unicode2+="$c" 115 | stream:next-character 116 | if [ "${#unicode2}" = 4 ]; then 117 | if [ "$((0x${unicode2}))" -gt $((0xdc00)) ] && 118 | [ "$((0x${unicode2}))" -lt $((0xdfff)) ]; then 119 | final_value="$(( 120 | ((0x${unicode1} - 0xd800) * 0x0400) + 121 | ((0x${unicode2} - 0xdc00) + 0x10000)))" 122 | 123 | printf -v final_value '%x' "$final_value" 124 | printf -v "$out" "\U$final_value" 125 | else 126 | token:error "Invalid utf-16 low surrogate: \\u$unicode2" 127 | fi 128 | fi 129 | ;; 130 | *) 131 | token:error "Invalid utf-16 low surrogate: \\u$unicode2" 132 | ;; 133 | esac 134 | ;; 135 | esac 136 | done 137 | } 138 | noshadow token:parse-unicode-utf-16 139 | 140 | token:parse-unicode-utf-32() { #<> 141 | local out="$1" 142 | local unicode="" c 143 | stream:next-character 144 | 145 | while [ -z "${!out}" ]; do 146 | if stream:end; then 147 | token:error "Premature end of input while parsing unicode" 148 | fi 149 | stream:get-character c 150 | 151 | case "$c" in 152 | [0-9abcdefABCDEF]) 153 | unicode+="$c" 154 | stream:next-character 155 | if [ ${#unicode} = 8 ]; then 156 | printf -v "$out" "\\U$unicode" 157 | fi 158 | ;; 159 | *) 160 | if [ -n "$unicode" ]; then 161 | printf -v "$out" "\\U$unicode" 162 | else 163 | token:error "Invalid utf-32 unicode character: \\U" 164 | fi 165 | ;; 166 | esac 167 | done 168 | } 169 | noshadow token:parse-unicode-utf-32 170 | -------------------------------------------------------------------------------- /src/powscript.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PowscriptSourceDirectory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" #<> 4 | PowscriptLibDirectory="$(cd "$PowscriptSourceDirectory/../lib" && pwd)" #<> 5 | 6 | source "$PowscriptLibDirectory/require.bash" #<> 7 | source "$PowscriptSourceDirectory/helper.bash" #<> 8 | 9 | powscript_source lexer/lexer.bash #<> 10 | powscript_source ast/ast.bash #<> 11 | powscript_source lang/backends.bash #<> 12 | powscript_source compiler/compiler.bash #<> 13 | 14 | powscript_require std #<> 15 | powscript_require unicode #<> 16 | powscript_require json #<> 17 | 18 | powscript:parse-options "$@" 19 | backend:select $PowscriptBackend 20 | 21 | cache:update 22 | 23 | powscript:compile() { 24 | printf '' >"$PowscriptOutput" 25 | files:compile "$PowscriptOutput" "${PowscriptFiles[@]}" 26 | } 27 | 28 | if powscript:is-interactive; then 29 | interactive:start 30 | elif powscript:in-compile-mode; then 31 | powscript:compile 32 | elif ! ${POWSCRIPT_DEBUG-false}; then 33 | backend:run "$(powscript:compile)" || exit 1 34 | fi 35 | 36 | if ! ${POWSCRIPT_DEBUG-false}; then 37 | powscript:clean-up 38 | fi 39 | -------------------------------------------------------------------------------- /test/.code-1.pow.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderofsalvation/powscript/862fe979037b0fc57d165fb180f6f5fdc593dca5/test/.code-1.pow.swp -------------------------------------------------------------------------------- /test/collections/operations.pow: -------------------------------------------------------------------------------- 1 | A={} 2 | V=(ab bc cd) 3 | 4 | A[a]=abc 5 | A[b]=def 6 | 7 | assert ${V[@]:length} is 3 8 | assert ${A[@]:length} is 2 9 | 10 | assert ${V[@] : length} is 3 11 | assert ${A[@] : length} is 2 12 | 13 | assert ${V[*]:index 0} is ab 14 | assert ${V[*]:index 1} is bc 15 | 16 | assert ${V[*] : index 1} is bc 17 | 18 | assert ${V[*]:from 0 to 1} is "ab bc" 19 | assert ${V[*] : from 1 to 2} is "bc cd" 20 | 21 | assert ${V[*]:slice 0 length 2} is "ab bc" 22 | assert ${V[*] : slice 1 length 1} is "bc" 23 | 24 | assert ${V[*]:uppercase [ac]} is "Ab bc Cd" 25 | assert ${V[*]:uppercase* [ab]} is "AB Bc cd" 26 | 27 | assert ${A[a]:length} is 3 28 | assert ${A[a]:index 1} is b 29 | assert ${A[a]:from 1 to 2} is bc 30 | assert ${A[a]:uppercase a} is Abc 31 | 32 | KA=${A:keys} 33 | KV=${V:keys} 34 | 35 | assert $KA is "a b" 36 | assert $KV is "0 1 2" 37 | 38 | KA=${A : keys} 39 | KV=${V : keys} 40 | 41 | assert $KA is "a b" 42 | assert $KV is "0 1 2" 43 | 44 | assert ${A[*]:deref} is "a b" 45 | assert ${V[*]:deref} is "0 1 2" 46 | 47 | assert ${A[*] : deref} is "a b" 48 | assert ${V[*] : deref} is "0 1 2" 49 | -------------------------------------------------------------------------------- /test/compilation/basics.bash: -------------------------------------------------------------------------------- 1 | ./powscript -c test/test-files/parse-me.pow >/dev/null || exit 1 2 | ./powscript -c test/test-files/code-test.pow >/dev/null || exit 1 3 | -------------------------------------------------------------------------------- /test/control-flow/async.pow: -------------------------------------------------------------------------------- 1 | myfunc(args) 2 | echo "args=$args" 3 | echo "one "$args 4 | sleep 1s 5 | echo "two "$args 6 | sleep 2s 7 | 8 | await su -c 'foo 123' then 9 | echo "'await ... then' done $1" 10 | 11 | sleep 1s 12 | 13 | await myfunc 123 then 14 | echo "removeme" 15 | 16 | await myfunc 456 then | 17 | echo "pipe received" 18 | cat - 19 | when done 20 | echo "'await .... then |' done" 21 | 22 | sleep 4s 23 | 24 | await myfunc 789 then for line 25 | if $line match (two|one) 26 | echo "line: $line" 27 | when done 28 | echo "'await ... then for line' done" 29 | -------------------------------------------------------------------------------- /test/control-flow/conditions.pow: -------------------------------------------------------------------------------- 1 | assert true 2 | assert not false 3 | assert true and true 4 | assert true or false 5 | assert x is x 6 | assert 1 < 2 7 | assert xy match .*y 8 | assert -f powscript 9 | -------------------------------------------------------------------------------- /test/control-flow/for.pow: -------------------------------------------------------------------------------- 1 | foo={} 2 | foo['one']="foo" 3 | foo['two']="bar" 4 | 5 | for k,v of foo 6 | echo $k 7 | echo $v 8 | 9 | for i in one two 10 | echo $i 11 | 12 | str="four five" 13 | for i in $str 14 | echo $i 15 | 16 | -------------------------------------------------------------------------------- /test/control-flow/if.pow: -------------------------------------------------------------------------------- 1 | if true 2 | if_true=true 3 | else 4 | if_true=false 5 | 6 | assert $if_true 7 | 8 | if false 9 | elif_true=false 10 | elif true 11 | elif_true=true 12 | 13 | assert $elif_true 14 | 15 | -------------------------------------------------------------------------------- /test/control-flow/while.pow: -------------------------------------------------------------------------------- 1 | i=0 2 | 3 | while $i < 10 4 | echo $i 5 | i+=1 6 | -------------------------------------------------------------------------------- /test/etc/helper.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$PWD/src/helper.bash" 4 | 5 | echo printf_seq 6 | 7 | [ "$(printf_seq 1 10 '%N,')" = "1,2,3,4,5,6,7,8,9,10," ] || exit 1 8 | 9 | echo setvar 10 | 11 | setvar x 10 12 | [ "$x" = 10 ] || exit 1 13 | 14 | test_noshadow() { 15 | eval " 16 | ( 17 | $1 $2 $(printf_seq 1 $3 'a%N ') 18 | $1 $2 $(printf_seq 1 $3 'x%N ') 19 | 20 | [ \"$(printf_seq 1 $3 '$a%N')\" = '$4' ] || return 1 21 | [ \"$(printf_seq 1 $3 '$x%N')\" = '' ] || return 1 22 | 23 | [ -z \"\$(declare -f __$1)\" ] && noshadow $1 $5 24 | 25 | $1 $2 $(printf_seq 1 $3 'x%N ') 26 | 27 | [ \"$(printf_seq 1 $3 '$x%N')\" = '$4' ] || return 1 28 | ) || exit 1 29 | " 30 | } 31 | 32 | echo noshadow with only a name 33 | 34 | cs_none() { 35 | local x1=10 36 | setvar $1 $x1 37 | } 38 | test_noshadow cs_none '' 1 '10' '' 39 | 40 | echo noshadow with only the argument number 41 | 42 | cs_an() { 43 | local x1=$1 44 | setvar "$2" $x1 45 | } 46 | test_noshadow cs_an '12' 1 '12' '1' 47 | 48 | echo noshadow with numeric arguments 49 | 50 | cs_an_vn() { 51 | local x1=$1 x2=$2 52 | 53 | setvar $3 $(($x1+$x2)) 54 | setvar $4 $(($x1-$x2)) 55 | } 56 | test_noshadow cs_an_vn '1 2' 2 '3-1' '2 2' 57 | 58 | echo noshadow with a variable number of arguments 59 | 60 | cs_avn() { 61 | local x1 62 | while [ $# -gt 1 ]; do 63 | x1="$x1,$1" 64 | shift 65 | done 66 | setvar "$1" "$x1" 67 | } 68 | 69 | test_noshadow cs_avn '1 2 3' 1 ',1,2,3' '@' 70 | test_noshadow cs_avn 'a b' 1 ',a,b' '@ 1' 71 | 72 | 73 | echo noshadow with variable number of out variables 74 | 75 | cs_vvn() { 76 | local x1=1 x2=2 x3=3 77 | for var in "$@"; do 78 | setvar $var "$(($x1+$x2+$x3))" 79 | done 80 | } 81 | 82 | test_noshadow cs_vvn '' 1 '6' '0 @' 83 | test_noshadow cs_vvn '' 2 '66' '0 @' 84 | test_noshadow cs_vvn '' 3 '666' '0 @' 85 | 86 | echo 'success' 87 | -------------------------------------------------------------------------------- /test/etc/shebang.bash: -------------------------------------------------------------------------------- 1 | file=$(mktemp -u).powscript 2 | ./powscript -c <(echo "echo 'hello world (bash)'") -o $file.bash 3 | ./powscript -c <(echo "echo 'hello world (sh)'") --to sh -o $file.sh 4 | 5 | chmod +x $file.bash 6 | chmod +x $file.sh 7 | 8 | $file.bash 9 | $file.sh 10 | 11 | rm $file.bash 12 | rm $file.sh 13 | -------------------------------------------------------------------------------- /test/functions/definition.pow: -------------------------------------------------------------------------------- 1 | bbar() 2 | echo flop 3 | 4 | foo(a b) 5 | echo a=$a b=$b 6 | 7 | foo one two 8 | -------------------------------------------------------------------------------- /test/functions/keywords.pow: -------------------------------------------------------------------------------- 1 | f(a b -- x y) 2 | echo "a=$a b=$b x=$x y=$y" 3 | 4 | assert $(f 1 2) is "a=1 b=2 x= y=" 5 | assert $(f 1 2 --x X) is "a=1 b=2 x=X y=" 6 | assert $(f 1 2 --y Y) is "a=1 b=2 x= y=Y" 7 | assert $(f 1 2 --x X --y Y) is "a=1 b=2 x=X y=Y" 8 | assert $(f --x X --y Y) is "a= b= x=X y=Y" 9 | assert $(f 2 --x X --y Y) is "a=2 b= x=X y=Y" 10 | 11 | g(-- arc) 12 | echo $arc 13 | 14 | assert $(g --arc 1) is 1 15 | assert $(g -a 1) is 1 16 | assert $(g) is ${} 17 | -------------------------------------------------------------------------------- /test/functions/pop.pow: -------------------------------------------------------------------------------- 1 | test_pop() 2 | pop 3 | echo $@ 4 | 5 | test_pop2() 6 | pop 2 7 | echo $@ 8 | 9 | assert $(test_pop 1 2) is "1" 10 | assert $(test_pop 1 2 3 4) is "1 2 3" 11 | assert $(test_pop a b c d e f g h j k) is "a b c d e f g h j" 12 | assert $(test_pop2 1 2 3 4) is "1 2" 13 | assert $(test_pop2 a b c d e f g h j k) is "a b c d e f g h" 14 | -------------------------------------------------------------------------------- /test/functions/references.pow: -------------------------------------------------------------------------------- 1 | f(r) 2 | r:ref=1 3 | 4 | g(r) 5 | echo ${r:deref} 6 | 7 | h(R) 8 | R:ref[0]=1 9 | R:ref[1]=2 10 | 11 | i(R) 12 | echo ${R:deref[1]} 13 | 14 | a=0 15 | A=(0 1) 16 | 17 | 18 | f ${a:ref} 19 | h ${A:ref[@]} 20 | 21 | assert $a is 1 22 | assert $A[0] is 1 23 | assert $A[1] is 2 24 | 25 | assert $(g ${a:ref}) is 1 26 | assert $(i ${A:ref[@]}) is 2 27 | 28 | assert $(i (2 3 4)) is 3 29 | -------------------------------------------------------------------------------- /test/functions/rest.pow: -------------------------------------------------------------------------------- 1 | f(@all) 2 | echo $all[*] 3 | 4 | assert $(f 1) is "1" 5 | assert $(f 1 2 3) is "1 2 3" 6 | 7 | g(a b @c) 8 | echo $c[*] $a $b 9 | 10 | assert $(g 1 2) is " 1 2" 11 | assert $(g 1 2 3 4) is "3 4 1 2" 12 | 13 | k(-- @ks) 14 | echo ${ks:keys} ${ks[@]} 15 | 16 | assert $(k 1 2) is "" 17 | assert $(k 1 2 --x a) is "x a" 18 | assert $(k 1 2 --y a) is "y a" 19 | assert $(k 1 2 -y a) is "y a" 20 | 21 | h(-- x y @ks) 22 | echo $x $y ${ks:keys} 23 | 24 | assert $(h 1 2) is " " 25 | assert $(h 1 2 --x a) is "a " 26 | assert $(h 1 2 --y a) is " a" 27 | assert $(h --x a --y b --z c) is "a b z" 28 | 29 | a(@ps -- @ks) 30 | echo $ps[@] $ks[@] 31 | 32 | assert $(a 1 --x 2) is "1 2" 33 | assert $(a 1 --x 3 2) is "1 2 3" 34 | 35 | -------------------------------------------------------------------------------- /test/interactive/error-handling.pow: -------------------------------------------------------------------------------- 1 | (] 2 | if true 3 | a 4 | echo 'I survived!' 5 | exit 6 | -------------------------------------------------------------------------------- /test/interactive/multiline.pow: -------------------------------------------------------------------------------- 1 | if true 2 | echo "1" 3 | 4 | f(x) 5 | while $x < 10 6 | echo "2" 7 | 8 | echo ' 9 | 3 10 | ' 11 | 12 | echo \ 13 | 4 14 | 15 | echo 5\ 16 | 6 17 | 18 | exit 19 | -------------------------------------------------------------------------------- /test/lib/json.pow: -------------------------------------------------------------------------------- 1 | require json 2 | 3 | val='' 4 | typ='' 5 | obj={} 6 | 7 | json_test(json expected_type expected_value) 8 | shift 3 9 | echo "testing json library: $json" 10 | json_parse obj "$json" 11 | json_value ${val:ref} obj $@ 12 | json_type ${typ:ref} obj $@ 13 | assert $typ is $expected_type "ERROR: \"$typ\" != \"$expected_type\"" 14 | if $expected_value isnt '*' 15 | assert $val is $expected_value "ERROR: \"$val\" != \"$expected_value\"" 16 | 17 | json_test '1' number '1' 18 | json_test '12' number '12' 19 | json_test '2.0' number '2.0' 20 | json_test '15.02' number '15.02' 21 | json_test 'true' bool 'true' 22 | json_test 'false' bool 'false' 23 | json_test '"ab"' string 'ab' 24 | json_test '"\n"' string "\n" 25 | 26 | json_test '[1,2]' array '*' 27 | json_test '[1,2]' number '1' 0 28 | json_test '[1,2]' number '2' 1 29 | 30 | json_test '{"a":1, "b":2}' object '*' 31 | json_test '{"a":1, "b":2}' number '1' 'a' 32 | json_test '{"a":1, "b":2}' number '2' 'b' 33 | 34 | 35 | # spaces in arrays 36 | json_test '[ 37 | 1, 38 | 2 39 | ]' array '*' 40 | 41 | # values after objects 42 | json_test '{"a":{"b":1}, "c":2}' number '2' 'c' 43 | 44 | -------------------------------------------------------------------------------- /test/lib/unicode.pow: -------------------------------------------------------------------------------- 1 | require unicode 2 | 3 | test_unicode(u s8 s16 s32) 4 | local r8 r16 r32 5 | parse_utf8 $s8 -r ${r8:ref} 6 | parse_utf16 $s16 -r ${r16:ref} 7 | parse_utf32 $s32 -r ${r32:ref} 8 | echo "Testing unicode library: '$u'" 9 | assert $r8 is $u 10 | assert $r16 is $u 11 | assert $r32 is $u 12 | 13 | test_unicode "\t" '\x9' '\u9' '\U9' 14 | test_unicode '@' '\x40' '\u40' '\U40' 15 | test_unicode 'ω' '\xcf\x89' '\u3c9' '\U3c9' 16 | test_unicode 'ὥ' '\xe1\xbd\xa5' '\u1f65' '\U1f65' 17 | test_unicode '🐱' '\xf0\x9f\x90\xb1' '\ud83d\udc31' '\U1f431' 18 | -------------------------------------------------------------------------------- /test/math/assignments.pow: -------------------------------------------------------------------------------- 1 | declare string i 2 | declare integer j 3 | 4 | i=0 5 | j=0 6 | 7 | assert $i is 0 8 | assert $j is 0 9 | 10 | i+=1 11 | j+=1 12 | 13 | assert $i is 1 14 | assert $j is 1 15 | 16 | i*=2 17 | j*=2 18 | 19 | assert $i is 2 20 | assert $j is 2 21 | 22 | i^=3 23 | j^=3 24 | 25 | assert $i is 8 26 | assert $j is 8 27 | 28 | i/=2 29 | j/=2 30 | 31 | assert $i is 4 32 | assert $j is 4 33 | 34 | i-=1 35 | j-=1 36 | 37 | assert $i is 3 38 | assert $j is 3 39 | -------------------------------------------------------------------------------- /test/math/basics.pow: -------------------------------------------------------------------------------- 1 | assert $(math 1) is 1 2 | assert $(math 1+2) is 3 3 | assert $(math 1-2) is -1 4 | assert $(math 2*3) is 6 5 | assert $(math 8/2) is 4 6 | assert $(math 3/2) is 1 7 | assert $(math 2^2) is 4 8 | assert $(math 2^3) is 8 9 | assert $(math -1 * -1) is 1 10 | assert $(math -1 / -1) is 1 11 | assert $(math 1 + 2 * 3) is 7 12 | -------------------------------------------------------------------------------- /test/math/variables.pow: -------------------------------------------------------------------------------- 1 | a=1 2 | b=2 3 | c=3 4 | 5 | assert $(math a) is 1 6 | assert $(math a+b) is 3 7 | assert $(math a*b) is 2 8 | assert $(math b*a) is 2 9 | assert $(math b/a) is 2 10 | assert $(math b*c) is 6 11 | assert $(math b^c) is 8 12 | 13 | -------------------------------------------------------------------------------- /test/parser/lexer.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$PWD/src/powscript.bash" -d 4 | 5 | test_tokens() { 6 | local value class glued 7 | local tvalue tclass tglued 8 | local ignore_til_match unmatched 9 | 10 | local tests=( 11 | '"echo" name true' 12 | '"hello world" string false' 13 | '"" newline false' 14 | '-"f" name true' 15 | '"(" special true' 16 | '")" special true' 17 | '"" newline false' 18 | '"2" whitespace true' 19 | '"printf" name true' 20 | '"-" special false' 21 | '"v" name true' 22 | '"x" name false' 23 | '"x: " string false' 24 | '"$" special true' 25 | '"1" name true' 26 | '"" string true' 27 | '-"if" name true' 28 | '"$" special false' 29 | '"x" name true' 30 | '-"4" whitespace true' 31 | '"echo" name true' 32 | '"10" name false' 33 | '"" newline false' 34 | '"" newline false' 35 | '"X" name true' 36 | '-"(" special true' 37 | '-"1" whitespace true' 38 | '"echo" name true' 39 | $'"a\n b\n c\n " string false' 40 | '-")" special true' 41 | '-"x" name true' 42 | '"[" special true' 43 | '"y" name true' 44 | '"]" special true' 45 | '"=" special true' 46 | '"10" name true' 47 | '-"eof" eof false' 48 | ) 49 | 50 | stream:init 51 | 52 | echo 'testing lexer...' 53 | for t in "${tests[@]}"; do 54 | if [ "${t:0:1}" = "-" ]; then 55 | ignore_til_match=true 56 | else 57 | ignore_til_match=false 58 | fi 59 | 60 | tvalue="${t#*\"}" 61 | tvalue="${tvalue%\"*}" 62 | 63 | tglued="${t##* }" 64 | 65 | tclass="${t##*\"}" 66 | tclass="${tclass%%$tglued}" 67 | tclass="${tclass// /}" 68 | 69 | unmatched=true 70 | while $unmatched && ! stream:end; do 71 | token:get -v value -c class -g glued 72 | 73 | if [ "$value" = "$tvalue" ] && 74 | [ "$class" = "$tclass" ] && 75 | [ "$glued" = "$tglued" ]; then 76 | unmatched=false 77 | elif ! $ignore_til_match || stream:end; then 78 | >&2 echo "failed test while testing lexer!" 79 | >&2 echo " tried to match {value: '$tvalue', class: '$tclass', glued: $tglued }" 80 | >&2 echo " instead found {value: '$value', class: '$class', glued: $glued }" 81 | exit 1 82 | fi 83 | done 84 | done 85 | echo success! 86 | } 87 | 88 | 89 | 90 | test_tokens < 'test/test-files/parse-me.pow' 91 | -------------------------------------------------------------------------------- /test/repo/.ignore_trailing_whitespace: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/repo/forgot_compile/.tools/compile: -------------------------------------------------------------------------------- 1 | DirName="$(readlink -m "$(dirname $0)/..")" 2 | 3 | mv "$DirName/powscript" "$DirName/powscript.2" 4 | mv "$DirName/powscript.1" "$DirName/powscript" 5 | -------------------------------------------------------------------------------- /test/repo/forgot_compile/powscript: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/repo/forgot_compile/powscript.1: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /test/repo/test_pre_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ScriptDir="$(dirname "$0")" 4 | TesterDir="$(pwd)" 5 | 6 | test_prepush() {( 7 | local gitroot="$ScriptDir/$1" 8 | shift 9 | 10 | local script="$TesterDir/.tools/hooks/pre-push" 11 | local succeed=false 12 | export POWSCRIPT_PREPUSH_NOWHITESPACE=yes 13 | export POWSCRIPT_PREPUSH_NOTESTS=yes 14 | export POWSCRIPT_PREPUSH_NOCOMPILE=yes 15 | export POWSCRIPT_PREPUSH_NOINTERACTIVE=yes 16 | for arg in "$@"; do 17 | case "$arg" in 18 | '-w') export POWSCRIPT_PREPUSH_NOWHITESPACE=no ;; 19 | '-t') export POWSCRIPT_PREPUSH_NOTESTS=no ;; 20 | '-c') export POWSCRIPT_PREPUSH_NOCOMPILE=no ;; 21 | '-i') export POWSCRIPT_PREPUSH_NOINTERACTIVE=no ;; 22 | '-s') local succeed=true ;; 23 | esac 24 | done 25 | 26 | cd "$gitroot" 27 | if $succeed; then 28 | "$script" 29 | else 30 | ( "$script" ) && exit 1 || exit 0 31 | fi 32 | 33 | ) || exit 1; } 34 | 35 | greenprint() { 36 | printf "%b%s%b\n" '\033[32m' "$1" '\033[0m' 37 | } 38 | 39 | echo "Testing pre-push githook:" 40 | 41 | greenprint "Testing if it detects invalid whitespace in non powscript files..." 42 | test_prepush whitespace_trail_sh -w 43 | 44 | greenprint "Testing if it detects invalid whitespace in powscript files..." 45 | test_prepush whitespace_trail_pow -w 46 | 47 | greenprint "Testing if it doesn't halt on valid whitespace..." 48 | test_prepush forgot_compile -w -s 49 | 50 | greenprint "Testing if it detects a forgotten compilation..." 51 | test_prepush forgot_compile -c 52 | mv "$ScriptDir/forgot_compile/powscript" "$ScriptDir/forgot_compile/powscript.1" 53 | mv "$ScriptDir/forgot_compile/powscript.2" "$ScriptDir/forgot_compile/powscript" 54 | 55 | greenprint "Testing if it halts on failed tests..." 56 | test_prepush tests_fail -t 57 | 58 | exit 0 59 | -------------------------------------------------------------------------------- /test/repo/tests_fail/.tools/runtests: -------------------------------------------------------------------------------- 1 | exit 1 2 | -------------------------------------------------------------------------------- /test/repo/whitespace_trail_pow/file1.pow: -------------------------------------------------------------------------------- 1 | mmm whitespace 2 | -------------------------------------------------------------------------------- /test/repo/whitespace_trail_sh/file1.sh: -------------------------------------------------------------------------------- 1 | do shell things 2 | trailing whitespace is good actually 3 | -------------------------------------------------------------------------------- /test/repo/whitespace_trail_sh/file2.pow: -------------------------------------------------------------------------------- 1 | do powscript things 2 | 3 | -------------------------------------------------------------------------------- /test/strings/case.pow: -------------------------------------------------------------------------------- 1 | x=aabb 2 | y=AABB 3 | 4 | assert ${x:uppercase a} is Aabb 5 | assert ${x:uppercase b} is aabb 6 | assert ${y:lowercase A} is aABB 7 | assert ${y:lowercase B} is AABB 8 | 9 | assert ${x:uppercase* a} is AAbb 10 | assert ${x:uppercase* b} is aaBB 11 | assert ${y:lowercase* A} is aaBB 12 | assert ${y:lowercase* B} is AAbb 13 | 14 | assert ${x:uppercase* *} is AABB 15 | assert ${y:lowercase* *} is aabb 16 | 17 | assert ${x:uppercase* *} is AABB 18 | assert ${y:lowercase* *} is aabb 19 | 20 | assert ${x:uppercase* [ab]} is AABB 21 | assert ${y:lowercase* [AB]} is aabb 22 | 23 | assert ${x:uppercase [ab]} is Aabb 24 | assert ${y:lowercase [AB]} is aABB 25 | -------------------------------------------------------------------------------- /test/strings/expansion.pow: -------------------------------------------------------------------------------- 1 | assert ${X:unset 1} is 1 2 | assert ${X:empty 1} is 1 3 | assert ${X:set 1} is '' 4 | assert ${X:nonempty 1} is '' 5 | assert ${X:unset= 1} is 1 6 | assert ${Y:empty= 1} is 1 7 | assert ${Z:empty= ''} is '' 8 | assert ${X:set 2} is 2 9 | assert ${X:nonempty 2} is 2 10 | assert ${Z:set 2} is 2 11 | assert ${Z:nonempty 2} is '' 12 | assert ${X:unset 2} is 1 13 | assert ${X:unset= 2} is 1 14 | assert ${X:empty 2} is 1 15 | assert ${X:empty= 2} is 1 16 | assert ${Z:unset 2} is '' 17 | assert ${Z:unset= 2} is '' 18 | assert ${Z:empty 2} is 2 19 | assert ${Z:empty= 2} is 2 20 | 21 | X=3 22 | Y=X 23 | 24 | assert ${Y:deref} is 3 25 | assert ${} is '' 26 | assert ${A:empty '\$\$""'} is '\$\$""' 27 | -------------------------------------------------------------------------------- /test/strings/indexing.pow: -------------------------------------------------------------------------------- 1 | X=abcdef 2 | 3 | assert ${X:length} is 6 4 | assert ${X:index 0} is a 5 | assert ${X:index 2} is c 6 | assert ${X:from 1 to 3} is bcd 7 | assert ${X:from 0 to 3} is abcd 8 | assert ${X:from -1 to 3} is abcd 9 | assert ${X:from 0 to -1} is '' 10 | assert ${X:from 2 to 1} is '' 11 | assert ${X:from 2 to 2} is c 12 | assert ${X:slice 1 length 2} is bc 13 | assert ${X:slice 2 length 3} is cde 14 | assert ${X:slice 2 length 4} is cdef 15 | assert ${X:slice 2 length 5} is cdef 16 | assert ${X:slice 6 length 1} is '' 17 | assert ${X:slice 0 length 1} is a 18 | assert ${X:slice 0 length 0} is '' 19 | 20 | -------------------------------------------------------------------------------- /test/strings/linebreak.pow: -------------------------------------------------------------------------------- 1 | x='foo 2 | bar' 3 | 4 | y=$(printf "foo\n bar\n") 5 | 6 | assert $x is $y 7 | -------------------------------------------------------------------------------- /test/strings/nested-quotes.pow: -------------------------------------------------------------------------------- 1 | a="'" 2 | b='"' 3 | c="''" 4 | d='""' 5 | -------------------------------------------------------------------------------- /test/strings/quoting.pow: -------------------------------------------------------------------------------- 1 | id(a) 2 | printf $a 3 | 4 | x=1 5 | y=2 6 | z="3 4" 7 | 8 | assert $z is "3 4" 9 | assert "$x $y" is "1 2" 10 | assert "$x ${x} $x" is "1 1 1" 11 | assert $(id $z) is $z 12 | -------------------------------------------------------------------------------- /test/strings/substitution.pow: -------------------------------------------------------------------------------- 1 | X=aabbcc 2 | 3 | assert ${X:prefix *a} is abbcc 4 | assert ${X:prefix* *a} is bbcc 5 | assert ${X:suffix c*} is aabbc 6 | assert ${X:suffix* c*} is aabb 7 | 8 | assert ${X:replace a by b} is babbcc 9 | assert ${X:replace* a by b} is bbbbcc 10 | assert ${X:replace* [ac] by b} is bbbbbb 11 | -------------------------------------------------------------------------------- /test/strings/unicode.pow: -------------------------------------------------------------------------------- 1 | assert "\x9" is "\t" 2 | assert "\x40" is '@' 3 | assert "\xcf\x89" is 'ω' 4 | assert "\xe1\xbd\xa5" is 'ὥ' 5 | assert "\xf0\x9f\x90\xb1" is '🐱' 6 | 7 | assert "\u9" is "\t" 8 | assert "\u40" is '@' 9 | assert "\u040" is '@' 10 | assert "\u0040" is '@' 11 | assert "\u3c9" is 'ω' 12 | assert "\u03c9" is 'ω' 13 | assert "\u1f65" is 'ὥ' 14 | assert "\ud83d\udc31" is '🐱' 15 | 16 | assert "\U9" is "\t" 17 | assert "\U009" is "\t" 18 | assert "\U00000009" is "\t" 19 | assert "\U00000040" is '@' 20 | assert "\U000003c9" is 'ω' 21 | assert "\U0001f431" is '🐱' 22 | 23 | assert "\x9x" is "\tx" 24 | assert "\x40x" is "@x" 25 | assert "\x40a" is "@a" 26 | assert "\u40x" is "@x" 27 | assert "\u0040a" is "@a" 28 | assert "\U40x" is '@x' 29 | assert "\U00000040a" is '@a' 30 | -------------------------------------------------------------------------------- /test/test-files/code-test.pow: -------------------------------------------------------------------------------- 1 | require 'mymod.pow' 2 | 3 | foo="1 2 3 4 5" 4 | 5 | # comparison 6 | i="foo" 7 | if "$i" is "foo" 8 | echo "foo" 9 | 10 | if $i is "foo" 11 | echo "foo" 12 | else 13 | echo "bar" 14 | 15 | foo() 16 | local appname="${1}" 17 | local pidfile=$(process:getpidfile "$appname") 18 | if -f $pidfile 19 | cat $pidfile 20 | else 21 | echo -1 22 | 23 | webserver:onrequest(request) 24 | if $request match (POST .*\/pull) 25 | appname=${request: replace POST by ''} 26 | appname=${appname: replace pull* by ''} 27 | deffile=$(process:gettmpfile $appname).def 28 | if -f $deffile 29 | workdir=$(apps:getworkdir $deffile) 30 | if -d $workdir/.git 31 | cd $workdir 32 | echo -e "received webhook: pull $appname, performing git pull" 33 | else 34 | echo -e "received webhook: pull $appname (error: $workdir is not a git repo)" 35 | else 36 | echo -e "received webhook: pull $appname (error: no such app)" 37 | 38 | 39 | if not $j is "foo" and $x is "bar" 40 | if $j is "foo" or $j is "xfoo" 41 | if $j > $y and $j != $y or $j >= $y 42 | echo "foo" 43 | 44 | if not $j is "foo" and $x is "bar" 45 | if $j is "foo" or $j is "xfoo" 46 | if $j > $y and $j != $y or $j >= $y 47 | echo "foo" 48 | else 49 | flop 50 | else 51 | echo flip 52 | else 53 | echo flap 54 | 55 | # extended pattern matching 56 | # (google 'extglob' for more 57 | 58 | if $f match ^([f]oo) 59 | echo "foo found!" 60 | 61 | for k,v of foo 62 | for j,x of bla 63 | for v,f of bla 64 | echo "foo" 65 | 66 | simple_arrays() 67 | bla=[] 68 | bla[0]="foo" 69 | bla@="push value" 70 | for i in bla 71 | echo bla=$i 72 | echo $bla[0] 73 | 74 | associative_arrays() 75 | foo={} 76 | foo["bar"]="a value" 77 | for k,v of foo 78 | echo k=$k 79 | echo v=$v 80 | echo $foo["bar"] 81 | echo $foo["bar"] > /tmp/foo 82 | rm /tmp/foo 83 | 84 | 85 | switch_test() 86 | switch $foo 87 | case 0-9 88 | echo "i want to switch an if for two years" 89 | echo "bar" 90 | case * 91 | echo "foo" 92 | echo "bar" 93 | 94 | 95 | bar() 96 | if set? $1 97 | echo "no argument given" 98 | if empty? $1 99 | echo "empty string given" 100 | 101 | bar one $@ 102 | 103 | printitem() 104 | echo "key=$1 value=$2" 105 | 106 | foo={} 107 | foo["one"]="foo" 108 | foo["two"]="bar" 109 | map foo printitem 110 | 111 | 112 | myfunc(a) 113 | if $a > 1 114 | echo "$1" 115 | 116 | simple_arrays 117 | associative_arrays 118 | 119 | afoo={} 120 | abar={} 121 | afoo["one"]="foo" 122 | abar["foo"]="123" 123 | map afoo values | mappipe pick abar 124 | 125 | vfoo=(1 2 3 4) 126 | echo last="$(last vfoo)" 127 | 128 | math 9 / 2 129 | math 9 / 2 4 130 | 131 | funcA() 132 | echo "($1)" 133 | 134 | funcB() 135 | echo "|$1|" 136 | 137 | compose decorate_string funcA funcB 138 | decorate_string "foo" 139 | 140 | usage() 141 | echo "foo " 142 | 143 | switch $1 144 | case [0-9]* 145 | echo "arg 1 is a number" 146 | case * 147 | help=$(usage) 148 | echo "Usage: $help" and exit 149 | 150 | exit 0 151 | 152 | # check literal multiline string 153 | 154 | foo=' 155 | literalfunc() 156 | this should not be parsed as powscript 157 | ' 158 | 159 | 160 | -------------------------------------------------------------------------------- /test/test-files/mymod.pow: -------------------------------------------------------------------------------- 1 | modulefunc() 2 | echo "hi im a module!" 3 | -------------------------------------------------------------------------------- /test/test-files/parse-me.pow: -------------------------------------------------------------------------------- 1 | echo 'hello world' 2 | 3 | f() 4 | printf -v x "x: $1" 5 | if $x 6 | echo 10 7 | 8 | X=( 9 | echo "a 10 | b 11 | c 12 | " 13 | ) 14 | 15 | x[y]=10 16 | 17 | --------------------------------------------------------------------------------