diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96a8c20 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +watch.sh \ No newline at end of file diff --git a/README.md b/README.md index df86b03..3ba4854 100644 --- a/README.md +++ b/README.md @@ -10,37 +10,52 @@ Here is some leaked source code for my brand new Cat website. Do not steal! ```shell #!/bin/bash -printf '%s\n\n' 'Content-Type: text/html'; - source exprash.sh; +redirectStdout 'log'; + declare -A cats; cats[calico]="Calico"; cats[sphynx]="Sphynx"; cats[ragdoll]="Ragdoll"; +cats[holland_lop]="Holland Lop"; cats[scottish_fold]="Scottish Fold"; +cats[orange]="Orange"; get '/' && { - printf '

Cats

\n'; - printf ''; + + printf '%s' "$html" | send; } get '/cats/:cat' && { key=$(param 'cat'); if [[ -v "cats[$key]" ]]; then - printf '

Your Cat: %s

\n' "${cats[$key]}"; + if [ "$key" == 'holland_lop' ]; then + next "That's not a cat!"; + else + printf '

Your Cat: %s

\n' "${cats[$key]}" | send; + fi; else next; fi; } -use && { - printf '

Error: Cannot find that cat

\n'; +get '/cats/*' && { + printf '

Error: Cannot find that cat

\n' | send; +} + +getError '/cats/*' && { + printf '

%s

' "$(errorMessage)" | send; +} + +(use || useError) && { + printf '

404

' | send; } ``` diff --git a/examples/cats.sh b/examples/cats.sh index 262dedb..ad8f36d 100644 --- a/examples/cats.sh +++ b/examples/cats.sh @@ -1,33 +1,48 @@ #!/bin/bash -printf '%s\n\n' 'Content-Type: text/html'; - source exprash.sh; +redirectStdout 'log'; + declare -A cats; cats[calico]="Calico"; cats[sphynx]="Sphynx"; cats[ragdoll]="Ragdoll"; +cats[holland_lop]="Holland Lop"; cats[scottish_fold]="Scottish Fold"; +cats[orange]="Orange"; get '/' && { - printf '

Cats

\n'; - printf ''; + + printf '%s' "$html" | send; } get '/cats/:cat' && { key=$(param 'cat'); if [[ -v "cats[$key]" ]]; then - printf '

Your Cat: %s

\n' "${cats[$key]}"; + if [ "$key" == 'holland_lop' ]; then + next "That's not a cat!"; + else + printf '

Your Cat: %s

\n' "${cats[$key]}" | send; + fi; else next; fi; } -use && { - printf '

Error: Cannot find that cat

\n'; +get '/cats/*' && { + printf '

Error: Cannot find that cat

\n' | send; +} + +getError '/cats/*' && { + printf '

%s

' "$(errorMessage)" | send; +} + +(use || useError) && { + printf '

404

' | send; } diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..d6fdc99 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash +source "$(dirname "$0")/src/exprash.sh"; + +source "$(dirname "$0")/tests/utils.sh"; + +printf '%s\n' "Routes:"; +source "$(dirname "$0")/tests/routes.sh"; + +printf '\n'; +testSummary; diff --git a/src/exprash.sh b/src/exprash.sh index c48ab00..268287d 100644 --- a/src/exprash.sh +++ b/src/exprash.sh @@ -1,8 +1,30 @@ #!/bin/bash # shellcheck disable=SC2034 # false flags nameref params +# ===================== +# Require shell options +# ===================== + +# Necessary for piping to "send" funciton +shopt -s lastpipe + +# =============== +# Setup Functions +# =============== + +# $1: Log file to redirect output +function redirectStdout() { + local output="$1"; + [ -z "$output" ] && output="/dev/null"; + + exec 5<&1; + _exprashRedirectStdout="$1"; + exec 1>>"$_exprashRedirectStdout"; + printf '%s: %s\n' "${REQUEST_METHOD:-UNKNOWN}" "${PATH_INFO:-/}" +} + # ============= -# Route Methods +# Route Function # ============= # $1: path @@ -31,24 +53,96 @@ function delete() { # $1: path function all() { - [ "$routeHandled" -eq 1 ] && return 1; + [ "$_exprashRouteHandled" -eq 1 ] && return 1; + [ -n "$_exprashErrorMessage" ] && return 1; # Reset params - params=(); - pathMatch "$1" params || return 1; + _exprashParams=(); + + pathMatch "$1" _exprashParams || return 1; + _exprashRouteHandled=1; - routeHandled=1; return 0; } function use() { - [ "$routeHandled" -eq 1 ] && return 1; - routeHandled=1; + [ "$_exprashRouteHandled" -eq 1 ] && return 1; + [ -n "$_exprashErrorMessage" ] && return 1; + + _exprashRouteHandled=1; + return 0; +} + +# ===================== +# Error Route Functions +# ===================== + +# $1: path +function getError() { + [ "$REQUEST_METHOD" != "GET" ] && return 1; + allError "$1"; +} + +# $1: path +function postError() { + [ "$REQUEST_METHOD" != "POST" ] && return 1; + allError "$1"; +} + +# $1: path +function putError() { + [ "$REQUEST_METHOD" != "PUT" ] && return 1; + allError "$1"; +} + +# $1: path +function deleteError() { + [ "$REQUEST_METHOD" != "DELETE" ] && return 1; + allError "$1"; +} + +# $1: path +function allError() { + [ "$_exprashRouteHandled" -eq 1 ] && return 1; + [ -z "$_exprashErrorMessage" ] && return 1; + + # Reset params + _exprashParams=(); + + pathMatch "$1" _exprashParams || return 1; + _exprashRouteHandled=1; + return 0; } +function useError() { + [ "$_exprashRouteHandled" -eq 1 ] && return 1; + [ -z "$_exprashErrorMessage" ] && return 1; + + _exprashRouteHandled=1; + return 0; +} + +# ==== +# Next +# ==== + +# $1 (optional): error message function next() { - routeHandled=0; + _exprashRouteHandled=0; + [ -n "$1" ] && _exprashErrorMessage="$1"; +} + +# ============= +# App Functions +# ============= + +function errorMessage() { + printf '%s' "$_exprashErrorMessage"; +} + +function hasErrorMessage() { + [ -n "$_exprashErrorMessage" ]; } # ================= @@ -57,18 +151,51 @@ function next() { # $1: param name function param() { - printf '%s\n' "${params[$1]}"; + printf '%s\n' "${_exprashParams[$1]}"; } -# $1: param name function hasParam() { - [[ -v "params[$1]" ]]; + [[ -v "_exprashParams[$1]" ]]; +} + +# ================== +# Response Functions +# ================== + +function send() { + if [ "$_exprashHeadersSent" -eq 0 ]; then + sendHeaders + _exprashHeadersSent=1 + fi + + sendRaw +} + +# $1: Header Name +# $2: Header Value +function setHeader() { + _exprashHeaders["$1"]="$2"; +} + +function sendHeaders() { + printf '%s\n' "Content-Type: ${_exprashHeaders['Content-Type']}" | sendRaw + for key in "${!_exprashHeaders[@]}"; do + [ "$key" == 'Content-Type' ] && continue + printf '%s\n' "${key}: ${_exprashHeaders[$key]}" | sendRaw + done; + printf '\n' | sendRaw } # ========= # Internals # ========= +function sendRaw() { + [ -n "$_exprashRedirectStdout" ] && exec >&5; + cat; + [ -n "$_exprashRedirectStdout" ] && exec 1>>"$_exprashRedirectStdout"; +} + # $1: path # $2: (nameref) array function pathToArray() { @@ -84,25 +211,31 @@ function pathMatch() { local path="$PATH_INFO"; local route="$1"; + # Params associative array + local -n routeParams="$2"; + local pathArr; pathToArray "$path" pathArr; local routeArr; pathToArray "$route" routeArr; - # Params associative array - local -n routeParams="$2"; - - # Not a match if pathArr and routeArr have different length - [ "${#pathArr[@]}" -ne "${#routeArr[@]}" ] && return 1; + # Get max path length + local routeLen=${#routeArr[@]}; + local pathLen=${#pathArr[@]}; + local maxLen=$(( routeLen >= pathLen ? routeLen : pathLen )); - for ((i=0; i<${#routeArr[@]}; i++)); do + for ((i=0; i