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 '\n';
+ html='Cats
';
for key in "${!cats[@]}"; do
- printf '- %s
\n' "$key" "${cats[$key]}";
+ html+="- ${cats[$key]}
";
done
- printf '
\n';
+ html+='
';
+
+ 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 '\n';
+ html='Cats
';
for key in "${!cats[@]}"; do
- printf '- %s
\n' "$key" "${cats[$key]}";
+ html+="- ${cats[$key]}
";
done
- printf '
\n';
+ html+='
';
+
+ 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