From a18f52b6a1a83c04555696f78e61d64d05ee990c Mon Sep 17 00:00:00 2001 From: Ben Ashton Date: Wed, 28 Sep 2022 23:05:02 -0600 Subject: [PATCH] Initial commit --- README.md | 50 +++++++++++++++++++++ examples/cats.sh | 33 ++++++++++++++ src/exprash.sh | 114 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 README.md create mode 100644 examples/cats.sh create mode 100644 src/exprash.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..c15617b --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Exprash + +An Express-like HTTP router for Bash compatible with CGI 1.1 + +I'm aware that the name of this project evokes a feeling of physical disgust. That is by design. + +## Usage/Examples + +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 ../src/exprash.sh; + +declare -A cats; +cats[calico]="Calico"; +cats[sphynx]="Sphynx"; +cats[ragdoll]="Ragdoll"; +cats[scottish_fold]="Scottish Fold"; + +get '/' && { + printf '

Cats

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

Your Cat: %s

\n' "${cats[$key]}"; + else + next; + fi; +} + +all '/cats/:cat' && { + printf '

Error: Cannot find that cat

\n'; +} +``` + + +## License + +[WTFPL](http://www.wtfpl.net/txt/copying/) diff --git a/examples/cats.sh b/examples/cats.sh new file mode 100644 index 0000000..eadc019 --- /dev/null +++ b/examples/cats.sh @@ -0,0 +1,33 @@ +#!/bin/bash +printf '%s\n\n' 'Content-Type: text/html'; + +source ../src/exprash.sh; + +declare -A cats; +cats[calico]="Calico"; +cats[sphynx]="Sphynx"; +cats[ragdoll]="Ragdoll"; +cats[scottish_fold]="Scottish Fold"; + +get '/' && { + printf '

Cats

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

Your Cat: %s

\n' "${cats[$key]}"; + else + next; + fi; +} + +all '/cats/:cat' && { + printf '

Error: Cannot find that cat

\n'; +} \ No newline at end of file diff --git a/src/exprash.sh b/src/exprash.sh new file mode 100644 index 0000000..5ba4214 --- /dev/null +++ b/src/exprash.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# shellcheck disable=SC2034 # false flags nameref params + +# ============= +# Route Methods +# ============= + +# $1: path +function get() { + [ "$REQUEST_METHOD" != "GET" ] && return 1; + all "$1"; +} + +# $1: path +function post() { + [ "$REQUEST_METHOD" != "POST" ] && return 1; + all "$1"; +} + +# $1: path +function put() { + [ "$REQUEST_METHOD" != "PUT" ] && return 1; + all "$1"; +} + +# $1: path +function delete() { + [ "$REQUEST_METHOD" != "DELETE" ] && return 1; + all "$1"; +} + +# $1: path +function all() { + [ "$routeHandled" -eq 1 ] && return 1; + + # Reset params + params=(); + pathMatch "$1" params || return 1; + + # # if path ooesn't match PATH_INFO: return 1 + routeHandled=1; + return 0; +} + +function next() { + routeHandled=0; +} + +# ================= +# Request Functions +# ================= + +# $1: param name +function param() { + printf '%s\n' "${params[$1]}"; +} + +# $1: param name +function hasParam() { + [[ -v "params[$1]" ]]; +} + +# ========= +# Internals +# ========= + +# $1: path +# $2: (nameref) array +function pathToArray() { + readarray -t "$2" < <(printf '%s\n' "$1" | tr '/' '\n' | grep .); +} + +# $1: route +# $2: (nameref) associative array for params +function pathMatch() { + [ -z ${PATH_INFO+x} ] && return 1; + [ -z ${1+x} ] && return 1; + + local path="$PATH_INFO"; + local route="$1"; + + 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; + + for ((i=0; i<${#routeArr[@]}; i++)); do + local routeComponent="${routeArr[$i]}"; + local pathComponent="${pathArr[$i]}"; + + # If route component starts with ":" + if [[ "$routeComponent" == :* ]]; then + routeParams["${routeComponent:1}"]="$pathComponent"; + else + # Confirm paths match + [ "$routeComponent" != "$pathComponent" ] && return 1; + fi; + done; + + return 0; +} + +# ======= +# Globals +# ======= +routeHandled=0; +declare -A params;