⚠️ 不要在家尝试这个:仅用 Bash 编写的 CMS??

2025-06-10

⚠️ 不要在家尝试这个:仅用 Bash 编写的 CMS??

又来了。

在仅使用 CSS 构建图像模式(完全忽略了可访问性(抱歉,@grahamthedev))并尝试将 CSS 确立为后端语言(尽管它有效,但出于某种原因,你们并不太喜欢它。我不知道为什么。)之后,我们终于回来做一些你和我可能永远不应该做的事情。

今天:让我们使用 Bash(是的,Bash)来创建一个 CMS!

帕斯卡号

帕斯卡,是的!帕斯卡总是好的!

功劳归功劳:这个想法源于我和一位同事关于如何把事情复杂化的一次有趣讨论。他提出了一个“bash 服务器”,我们开始把它越做越大,直到我说“接受挑战”,于是我们就这么定了下来。

免责声明:除了一些密码加密和大多数最佳实践外,我们将忽略安全性。我永远不会在生产环境中使用这个工具,亲爱的读者,您也不应该这样做。拜托。好了。我已经警告过你了。

定义它的作用

我们将创建一个非常基本的 CMS:

  • 用户可以登录和注销(因此我们需要用户和会话处理)
  • 登录用户可以创建、更新和删除页面
  • 匿名用户可以阅读所有页面
  • 页面由导航标题、路由路径、一些标记以及是否应显示在主导航中的标志组成

然而,要使用 Bash 作为后端语言,我们首先需要让它处理 HTTP 请求。幸运的是,有一些 Bash 实用程序可以用来监听 TCP 请求并发送响应,其中最值得注意的是netcat。我们“仅仅”需要解析请求并生成响应。

一旦完成,我们将使用 SQLite 加载请求的页面并呈现其标记。

让我们继续讨论第一部分代码。

数据库模式

先说无聊的部分。我们将使用以下数据库模式作为我们的 SQLite 数据库,并添加一些默认记录:

CREATE TABLE IF NOT EXISTS pages (
  routePath TEXT NOT NULL,
  navTitle TEXT,
  isInMainNavigation BOOLEAN NOT NULL,
  markup TEXT NOT NULL
);

INSERT INTO pages VALUES
('/', 'Home', 1, '<h2>Hello, Bash CMS!</h2>'),
('/about', 'About', 1, '<h2>About</h2><p>This page was created entirely with BashCMS!</p> <p><a href="/about/bash-cms">Learn more</a></p>'),
('/about/bash-cms', NULL, 0, '<h2>About BashCMS</h2><p>BashCMS is a CMS entirely written in Bash!</p>');

CREATE TABLE IF NOT EXISTS users (
  username TEXT NOT NULL,
  password TEXT NOT NULL /* Hash generated with sha256sum */
);

INSERT INTO users VALUES
('admin', 'fc8252c8dc55839967c58b9ad755a59b61b67c13227ddae4bd3f78a38bf394f7'); /* pw: admin */

CREATE TABLE IF NOT EXISTS sessions (
  sessId TEXT NOT NULL,
  userRowId INTEGER
);
Enter fullscreen mode Exit fullscreen mode

我们可以通过 CLI 工具访问数据库sqlite3,并从 Bash 脚本中向数据库提供数据。

实际服务器

现在让我们从 Bash 部分开始。为了监听 HTTP 请求,我们使用 Netcat。我们将使用 OpenBSD 版本的 Netcat。

在监听模式下,Netcat 是交互式的。它会在 STDOUT 上打印出请求的详细信息(例如,请求头、请求体、HTTP 方法等等),并要求用户在 STDIN 上写入响应。

对于不熟悉 Linux/Bash 的人来说,STDIN 和 STDOUT 是与程序通信的默认方式。我们在终端中看到的内容通常是 STDOUT,键盘输入是 STDIN。程序可以从 STDIN 读取并写入 STDOUT。

一旦我们向 Netcat 发送了数据,它就会通过网络发送并终止。这意味着我们一次只能处理一个请求。为了让服务器持续运行,我们需要在它终止后重新启动 Netcat 并让它监听。

要从 Netcat 进行读写,我们还需要一种以编程方式读取 STDOUT 并写入 STDIN 的方法。我们可以使用一个名为 的实用程序来实现这一点coproc,该实用程序在子 shell 中异步执行给定命令。只要 Netcat 正在等待传入请求,我们什么也不做。只有当 Netcat 开始写入 STDOUT 时,我们才会开始读取并将其保存到变量中。

然而,有一个小问题:Netcat 不会告诉我们它是否以及何时完成了对 STDOUT 的写入。我们需要自己判断。最直接的方法是等待一个空的新行,然后就此停止。

我们最终会得到如下结构:

while true # You know your script will be fun when it starts with an endless loop.
do
  coproc nc -l -p 1440 -q1 -v # Spawns netcat such that we can read from and write to it

  REQ_RAW="" # This will contain the entire request
  IFS="" # Delimiter for `read`

  while read -r TMP; do # Read every line from STDOUT
    REQ_RAW+=$TMP$'\n' # Append the line to the REQ variable

    # If the length of TMP is equal to one byte
    if [[ ${#TMP} -eq 1 ]] ; then
      break
    fi
  done <&"${COPROC[0]}" # Reads from the coproc STDOUT, line by line

  echo $REQ_RAW # Output the request for testing purposes

  kill "$COPROC_PID" # Kill the process for the subsequent request
  wait "$COPROC_PID" # Wait until it's actually gone
done
Enter fullscreen mode Exit fullscreen mode

如果你真的不熟悉 Bash,这看起来会让人望而生畏。即使对 Bash有所了解的人,可能也会如此。我在这次实验中学到了很多,并且深信 Bash 的工作原理非常类似于量子物理学:人们可能不理解 Bash,但习惯了它。

言归正传……当我们想要读取 POST 请求的 HTTP 正文时,“空行”方法就失效了。幸好,HTTP 有一个叫做 的头Content-Length,可以告诉我们确切的字节数。

这极大地破坏了我们的服务器代码:

while true
do
  coproc nc -l -p 1337 -q1 -v # Spawns netcat such that we can read from and write to it. Listens on port 1337.

  REQ_RAW="" # This will contain the entire request
  IFS="" # Delimiter for `read`

  while read -r TMP; do
    REQ_RAW+=$TMP$'\n' # Append the line to the REQ variable

    TMPLEN=$(echo $TMP|wc -c) # Figure out the length of $TMP in bytes

    # Deduct the length of the read bytes from the rest of the body length
    if [[ $BODYLENGTH -ge 0 ]]; then # Still some body left to read
      BODYLENGTH=$((BODYLENGTH - TMPLEN))
    fi

    # If the request has a body (determined by the header, which is usually the last one)
    # We continue reading the exact number of bytes
    if [[ "$TMP" =~ ^"Content-Length: " ]]; then
      BODYLENGTH=$(echo "$TMP"|grep -o '[[:digit:]]\+')
      HAS_BODY=1
    fi

    # Read the entire body; abort reading
    if [[ $HAS_BODY -eq 1 ]] && [[ $BODYLENGTH -le 0 ]]; then
      break
    fi

    # No body but empty line encountered, abort reading
    if [[ $HAS_BODY -eq 0 ]] && [[ $TMPLEN -le 2 ]]; then
      break
    fi
  done <&"${COPROC[0]}" # Reads from the coproc STDOUT, line by line

  # Display the entire request for debugging
  echo $REQ_RAW

  kill "$COPROC_PID" # Kill the process for the subsequent request
  wait "$COPROC_PID" # Wait until it's actually buried
done
Enter fullscreen mode Exit fullscreen mode

这已经运行良好了。我们现在基本上有一个请求记录器了。进展顺利!

HTTP 请求的剖析

首先,我们需要解析请求,以确定服务器应该执行什么。让我们看看我们正在处理什么。

典型的 HTTP 请求结构如下:

[Method] [Path + Query String] HTTP/[HTTP Version]
[Headers]

[Body]
Enter fullscreen mode Exit fullscreen mode

当我在服务器上执行 GET 请求时,它会输出如下内容:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Cache-Control: no-cache
Host: localhost:1440
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Enter fullscreen mode Exit fullscreen mode

另一方面,POST 请求可能如下所示:

POST / HTTP/1.1
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Cache-Control: no-cache
Host: localhost:1440
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------328683080620751780512479
Content-Length: 169

----------------------------328683080620751780512479
Content-Disposition: form-data; name="hello"

world
----------------------------328683080620751780512479--
Enter fullscreen mode Exit fullscreen mode

我们可以利用这一点。

添加一些逻辑

该请求作为单个字符串存储在名为的变量中REQ_RAW,因此我们可以使用其他几个 Bash 实用程序对其进行解析。

我们创建一个名为的函数parse_request,并将其放入单独的文件中,以便保持条理清晰。然后在读取循环之后调用此函数:

#!/usr/bin/bash

source ./server/parse_request.sh

while true # Continue doing this, kind of like an event loop.
do
  coproc nc -l -p 1440 -q1 -v # Spawns netcat such that we can read from and write to it

  ## ...

  # Declare an associative array called `REQUEST`
  declare -A REQUEST=()

  parse_request $REQ_RAW REQUEST

  # Print the contents of the associative array
  declare -p REQUEST

  # Add more magic here

  kill "$COPROC_PID" # Kill the process for the subsequent request
  wait "$COPROC_PID" # Wait until it's actually gone
done
Enter fullscreen mode Exit fullscreen mode

此函数需要同时执行几件事:

  • 确定 HTTP 方法
  • 确定用户请求的路线
  • 解析任何 GET 变量(即?foo=bar等)
  • 解析主体
  • 解析出 cookies

我们可以解析请求的第一行来获取 HTTP 方法和路由路径。之后,我们解析 Cookie 并检查是否需要解析请求体(这只发生在 POST 和 PUT 请求中)。

#
# Parses the entire request
#
function parse_request() {
  RAW_REQ=$1

  # This makes the REQUEST associative array available to write to
  # We need to make sure to not call it REQUEST, though, because
  # that name is already reserved in the outer scope
  declare -n INNER_REQ="$2"

  # Extract the request line: method, path (+ query string) and version
  REQUESTLINE=`echo "${RAW_REQ}" | sed -n 1p`
  IFS=' ' read -ra PARTS <<< "$REQUESTLINE"
  METHOD=${PARTS[0]}
  REQUEST_PATH=${PARTS[1]}

  # Split query string from the actual route
  IFS='?' read -ra REQUEST_PATH_PARTS <<< "$REQUEST_PATH"
  REQUEST_ROUTE=${REQUEST_PATH_PARTS[0]}
  QUERY_STRING=${REQUEST_PATH_PARTS[1]}

  if [[ "$QUERY_STRING" != "" ]]; then
    parse_query_string $QUERY_STRING INNER_REQ
  fi

  parse_cookies $RAW_REQ INNER_REQ

  # If we're dealing with either a POST or a PUT request, chances are there's a form body.
  # We extract that with the previously found $FORMDATA_BOUNDARY.
  if [[ "$METHOD" == "POST" ]] || [[ "$METHOD" == "PUT" ]]; then
    parse_body $RAW_REQ INNER_REQ
  fi

  INNER_REQ["METHOD"]="$METHOD"
  INNER_REQ["ROUTE"]="$REQUEST_ROUTE"
}
Enter fullscreen mode Exit fullscreen mode

查询字符串解析非常简单:

#
# Parses the query string and assigns it to the request object
#
function parse_query_string() {
  RAW_QUERY_STRING=$1
  declare -n REQ_ARR="$2"

  # Split the query parameters into a hashmap
  IFS='&' read -ra QUERYPARTS <<< "$QUERY_STRING"
  for PART in "${QUERYPARTS[@]}"; do
    IFS='=' read -ra KEYVALUE <<< "$PART"
    KEY=${KEYVALUE[0]}
    VALUE=${KEYVALUE[1]}
    REQ_ARR["QUERY","$KEY"]="$VALUE"
  done
}
Enter fullscreen mode Exit fullscreen mode

Cookie 解析也是如此:

#
# Parses cookies out of the request headers
#
function parse_cookies() {
  RAW_REQ_BODY=$1
  declare -n REQ_ARR="$2"

  COOKIE_LINE=`echo $RAW_REQ_BODY|grep 'Cookie:'`
  COOKIE=${COOKIE_LINE#"Cookie:"}

  if [[ "$COOKIE" != "" ]]; then
    IFS=';' read -r -d '' -a COOKIEPARTS <<< "$COOKIE"

    for PART in "${COOKIEPARTS[@]}"; do
      if [[ "$PART" != "" ]]; then
        IFS='=' read -ra KEYVALUE <<< "$PART"
        KEY=${KEYVALUE[0]//" "/""} # Remove all spaces, so we don't have leading spaces
        VALUE=${KEYVALUE[1]}
        REQ_ARR["COOKIE","$KEY"]=${VALUE::-1}
      fi
    done
  fi
}
Enter fullscreen mode Exit fullscreen mode

在这两个函数中,我们仔细地从整个请求中抽取必要的部分,并用一些字符进行拆分,例如,查询字符串?用和,cookie 用和。然后,我们删除一些不必要的空格,并将其写入关联数组。=;=REQUEST

解析正文更加复杂。我们需要处理multipart/form-data格式以支持多行字符串,甚至可能支持文件上传。我发现它实际上比任何 URL 编码都更简单易用。

#
# Parses the POST body and assigns it to the request object
#
function parse_body() {
  RAW_REQ_BODY=$1
  declare -n REQ_ARR="$2"

  FORM_BOUNDARY_LINE=`echo $RAW_REQ_BODY|grep 'Content-Type: multipart/form-data; boundary='`
  FORM_BOUNDARY=${FORM_BOUNDARY_LINE#"Content-Type: multipart/form-data; boundary="}

  # Replace the $FORMDATA_BOUNDARY with a single character so we can split with that.
  TMP_BODY_PARTS=`echo "${RAW_REQ_BODY//"$FORM_BOUNDARY"/$'§'}" | head -n -2` # We need to use _some_ character to use `read` here.

  IFS='§' read -r -d '' -a BODYPARTS <<< "$TMP_BODY_PARTS"

  for PART in "${BODYPARTS[@]}"; do
    KEY=`echo "${PART}" | grep -o -P '(?<=name=").*?(?=")'`
    if [[ "$KEY" != "" ]]; then
      VALUE=`echo "${PART}" | head -n -1 | tail -n +4`
      REQ_ARR["BODY","$KEY"]=${VALUE::-1}
    fi
  done
}
Enter fullscreen mode Exit fullscreen mode

当我们使用之前的 GET 请求运行代码时,我们会从 Bash 服务器获得以下输出:

declare -A REQUEST=([ROUTE]="/" [METHOD]="GET" )
Enter fullscreen mode Exit fullscreen mode

(是的,declare -p创建一个declare -A语句,因此可以再次执行该语句以获得相同的关联数组。)

上述 POST 请求将输出以下内容:

declare -A REQUEST=([BODY,hello]="world" [ROUTE]="/" [METHOD]="POST" )
Enter fullscreen mode Exit fullscreen mode

整洁的!

对请求作出反应

与数组类似REQUEST,我们声明一个RESPONSE数组。该数组将包含我们传递的 DOM、状态码以及一些标头,例如用于重定向的Set-Cookie或。Location

由于我们需要区分用户(有些用户已登录,有些用户未登录),我们实现了一个名为 的函数set_session。该函数会生成一个会话 ID,将其写入 SQLite 数据库,并设置一个会话 Cookie。来自同一客户端的任何后续请求都将发送相同的会话 ID Cookie。

function set_session() {
  declare -n REQ="$1"
  declare -n RES="$2"

  if [[ "${REQ[COOKIE,SESSID]}" != "" ]]; then
    # SESSID cookie was already set once; reset it
    RES["COOKIES,SESSID"]="${REQ[COOKIE,SESSID]}"
  else
    # No SESSID cookie, so let's generate one
    SESSID=`echo $RANDOM | md5sum | head -c 20; echo;` # Taken from SO.

    # Save cookie into database
    sqlite3 db.sqlite "insert into sessions values ('${SESSID}', NULL);" ".exit"
    RES["COOKIES,SESSID"]="$SESSID"
  fi
}
Enter fullscreen mode Exit fullscreen mode

请注意,我们需要REQ和数组:我们已经通过设置一个带有名为的子键的键RES来写入数组RESPONSECOOKIESSESSID

我们在调用之后调用此函数parse_request

while true; do

  # Request reading shenanigans

  declare -A REQUEST=()
  declare -A RESPONSE=()
  parse_request $REQ_RAW REQUEST

  set_session REQUEST RESPONSE

  # More stuff later, don't worry

  kill "$COPROC_PID" # Kill the process for the subsequent request
  wait "$COPROC_PID" # Wait until it's actually gone
done
Enter fullscreen mode Exit fullscreen mode

接下来,我们可以实现一个函数来响应实际的请求。我们称之为render_cms_page。在这个函数中,我们在数据库中查找任何与请求中的路由匹配的条目:

function render_cms_page() {
  REQUEST=$1
  declare -n RES="$2"

  DOM=`sqlite3 db.sqlite "select markup from pages where routePath='${REQUEST[ROUTE]}';" ".exit"`

  if [[ "$DOM" == "" ]]; then
    RES["BODY"]=`render_response "Not found."`
    RES["STATUS"]="404 Not found"
  else
    RES["BODY"]=`render_response $DOM`
    RES["STATUS"]="200 OK"
  fi
}
Enter fullscreen mode Exit fullscreen mode

你可能render_response也注意到了里面的函数。我们用它生成所有相关的 HTML,比如页面标题、导航和一些 CSS:

function render_response() {
  DOC_START=`doc_start $1`
  PAGE_HEADER=`page_header`
  DOC_END=`doc_end`

  cat <<EOF
    $DOC_START
    $PAGE_HEADER

    <main>
    $2
    </main>

    $DOC_END
EOF
}
Enter fullscreen mode Exit fullscreen mode

然而,那里doc_start我们有函数,page_headerdoc_end

function doc_start() {
  cat <<EOF
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>$1</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jgthms/minireset.css@master/minireset.min.css">
    <style>
      * { font-family: monospace; font-size: 16px; }
      h1 { font-size: 1.5rem; font-weight: bold; }
      h2 { font-size: 2rem; font-weight: bold; margin-bottom: 0.5rem; }
      p { margin-bottom: 0.5rem; }
      header { padding: 1rem; border-bottom: 1px solid #000; }
      main { padding: 1rem; }
      nav ul { margin-top: 1rem; display: flex; gap: 1rem; }
      label { display: block; }
    </style>
  </head>
  <body>
EOF
}

function doc_end() {
  cat <<EOF
  </body>
</html>
EOF
}

function page_header() {
  # Fetch header-relevant pages
  cat <<EOF
  <header>
    <h1>Bash CMS</h1>
    <nav>
      <ul>
EOF
  while read -r PAGE_ROW; do
    IFS='|' read -r -d '' -a PAGE_PARTS <<< "$PAGE_ROW"
    NAV_TITLE=${PAGE_PARTS[0]}
    ROUTE_PATH=${PAGE_PARTS[1]}
    cat <<EOF
        <li><a href="$ROUTE_PATH">$NAV_TITLE</a></li>
EOF
  done <<< `sqlite3 db.sqlite "select navTitle, routePath from pages where isInMainNavigation=1;" ".exit"`

  LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`
  if [[ "$LOGGEDIN_USERID" == "" ]]; then
    cat <<EOF
      <li><a href="/login" class="login">Login</a></li>
EOF
  else
    cat <<EOF
        <li><a href="/add-new-page" class="login">Add new page</a></li>
        <li><a href="/logout" class="login">Logout</a></li>
EOF
  fi
  cat <<EOF
    </ul>
  </header>
EOF
}
Enter fullscreen mode Exit fullscreen mode

这样我们就快完成了。

实际响应的最后一步是渲染响应字符串。与 HTTP 请求类似,响应是一个由不同部分组成的多行字符串。我们只需要正确地组装它:

function generate_response_string() {
  declare -n RES="$1"

  # Transform cookie entries into Set-Cookie headers
  COOKIES=""
  for RESKEY in "${!RES[@]}"; do
    if [[ "$RESKEY" =~ ^"COOKIES," ]]; then
      COOKIE_NAME=${RESKEY#"COOKIES,"}
      COOKIES+="Set-Cookie: $COOKIE_NAME=${RES[$RESKEY]}
" # Adds a newline after this Set-Cookie header.
    fi
  done

  RES["CONTENT_TYPE"]="text/html"
  RES["HEADERS","Content-Type"]="${RES[CONTENT_TYPE]}; charset=UTF-8"
  RES["HEADERS","Server"]="Bash. Please don't send cat /etc/passwd as a cookie because hacking is bad :("

  HEADERS=""
  for RESKEY in "${!RES[@]}"; do
    if [[ "$RESKEY" =~ ^"HEADERS," ]]; then
      HEADER_NAME=${RESKEY#"HEADERS,"}
      HEADERS+="${HEADER_NAME}: ${RES[$RESKEY]}
" # Adds a newline after this Set-Cookie header.
    fi
  done

  # declare -p RES

  cat <<EOF
HTTP/1.1 ${RES[STATUS]}
${COOKIES::-1}
${HEADERS}

${RES[BODY]}
EOF
}
Enter fullscreen mode Exit fullscreen mode

一切准备就绪,让我们看看它做了什么:

来自原始“/” endraw 页面的标题、导航和内容!

(附注:现在在任何地方使用反引号都让我感到紧张。谁知道它会执行什么操作......)

添加路线

现在我们已经实现了动态路由,让我们来处理静态路由,例如/login/edit。为此,我们再添加两个函数:一个用于登录表单,一个用于编辑表单:/add-new-page/logout/delete

function render_login_page() {
  cat <<EOF
  <form method="POST" action="/login" enctype="multipart/form-data">
    <label for="username">User name</label>
    <input type="text" name="username" id="username" value="$1">

    <label for="password">Password</label>
    <input type="password" name="password" id="password">

    <button type="submit">Login</button>
  </form>
EOF
}

function render_edit_form() {
  NAVTITLE=$1
  IS_IN_MAIN_NAVIGATION=""
  ROUTEPATH=$3
  DOM=$4

  if [[ "$2" == "1" ]]; then
    IS_IN_MAIN_NAVIGATION=" checked"
  fi

  cat <<EOF
  <form method="POST" enctype="multipart/form-data">
    <label for="navtitle">Navigation title</label>
    <input type="text" name="navtitle" id="navtitle" value="$NAVTITLE">

    <label for="routepath">Route path</label>
    <input type="text" name="routepath" id="routepath" value="$ROUTEPATH">

    <label for="is_in_main_navigation">
      Is it in navigation?
      <input type="checkbox" value="1" name="is_in_navigation"$IS_IN_MAIN_NAVIGATION>
    </label>

    <div>
      <label for="dom">Content</label>
      <textarea name="dom" id="dom" style="width: 100%; height: 300px;">$DOM</textarea>
    </div>

    <button type="submit">Save</button>
  </form>
EOF
}
Enter fullscreen mode Exit fullscreen mode

最后,我们扩展该render_cms_page函数:

function render_cms_page() {
  REQUEST=$1
  declare -n RES="$2"

  if [[ "${REQUEST[ROUTE]}" == "/login" ]]; then
    if [[ "${REQUEST[METHOD]}" == "POST" ]]; then
      USERNAME=${REQUEST[BODY,username]}
      PASSWORD=`echo ${REQUEST[BODY,password]} | sha256sum`
      USERID=`sqlite3 db.sqlite "select rowid from users where username='$USERNAME' and password='${PASSWORD::-3}'"`

      if [[ "$USERID" == "" ]]; then
        DOM=`render_login_page $USERNAME`
        DOM+="<p>Username or password incorrect</p>"
        RES["BODY"]=`render_response "Login" $DOM`
        RES["STATUS"]="200 OK"
      else
        sqlite3 db.sqlite "update sessions set userRowId = $USERID where sessId = '${REQUEST[COOKIE,SESSID]}'"
        RES["STATUS"]="307 Temporary Redirect"
        RES["HEADERS","Location"]="/"
      fi
    else
      DOM=`render_login_page`
      RES["BODY"]=`render_response "Login" $DOM`
      RES["STATUS"]="200 OK"
    fi

    DOM=`render_login_page ${REQUEST[BODY,username]}`
  elif [[ "${REQUEST[ROUTE]}" == "/logout" ]]; then
    sqlite3 db.sqlite "update sessions set userRowId = NULL where sessId = '${REQUEST[COOKIE,SESSID]}'"
    RES["STATUS"]="307 Temporary Redirect"
    RES["HEADERS","Location"]="/"
  elif [[ "${REQUEST[ROUTE]}" == "/add-new-page" ]]; then
    if [[ "${REQUEST[METHOD]}" == "POST" ]]; then
      IS_IN_MAIN_NAVIGATION="0"
      if [[ "${REQUEST[BODY,is_in_navigation]}" == "1" ]]; then
        IS_IN_MAIN_NAVIGATION="1"
      fi
      sqlite3 db.sqlite "insert into pages values ('${REQUEST[BODY,routepath]}', '${REQUEST[BODY,navtitle]}', ${IS_IN_MAIN_NAVIGATION}, '${REQUEST[BODY,dom]}');" ".exit"
      RES["STATUS"]="307 Temporary Redirect"
      RES["HEADERS","Location"]="${REQUEST[BODY,routepath]}"
    else
      DOM=`render_edit_form`
      RES["BODY"]=`render_response "New page" $DOM`
      RES["STATUS"]="200 OK"
    fi
  elif [[ "${REQUEST[ROUTE]}" == "/edit" ]]; then
    LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`

    if [[ "$LOGGEDIN_USERID" == "" ]]; then
      RES["STATUS"]="403 Forbidden"
      RES["BODY"]=`render_response "Nope" "Not allowed to do that"`
    else
      if [[ "${REQUEST[METHOD]}" == "POST" ]]; then
        IS_IN_MAIN_NAVIGATION="0"
        if [[ "${REQUEST[BODY,is_in_navigation]}" == "1" ]]; then
          IS_IN_MAIN_NAVIGATION="1"
        fi
        sqlite3 db.sqlite "update pages set routePath='${REQUEST[BODY,routepath]}', navTitle='${REQUEST[BODY,navtitle]}', isInMainNavigation=${IS_IN_MAIN_NAVIGATION}, markup='${REQUEST[BODY,dom]}' where routePath='${REQUEST[QUERY,route]}';" ".exit"
        RES["STATUS"]="307 Temporary Redirect"
        RES["HEADERS","Location"]="${REQUEST[BODY,routepath]}"
      else
        PAGE=`sqlite3 db.sqlite "select navTitle, isInMainNavigation, routePath, markup from pages where routePath='${REQUEST[QUERY,route]}'"`
        IFS='|' read -r -d '' -a PAGEPARTS <<< "$PAGE"
        DOM=`render_edit_form ${PAGEPARTS[0]} ${PAGEPARTS[1]} ${PAGEPARTS[2]} ${PAGEPARTS[3]}`
        RES["BODY"]=`render_response "Edit" $DOM`
        RES["STATUS"]="200 OK"
      fi
    fi
  elif [[ "${REQUEST[ROUTE]}" == "/delete" ]]; then
    LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`

    if [[ "$LOGGEDIN_USERID" == "" ]]; then
      RES["STATUS"]="403 Forbidden"
      RES["BODY"]=`render_response "Nope" "Not allowed to do that"`
    else
      sqlite3 db.sqlite "delete from pages where routePath='${REQUEST[QUERY,route]}';" ".exit"
      echo "delete from pages where routePath='${REQUEST[QUERY,route]}';"
      RES["STATUS"]="307 Temporary Redirect"
      RES["HEADERS","Location"]="/"
    fi
  else
    DOM=`sqlite3 db.sqlite "select markup from pages where routePath='${REQUEST[ROUTE]}';" ".exit"`
    LOGGEDIN_USERID=`sqlite3 db.sqlite "select userRowId from sessions where sessId = '${REQUEST[COOKIE,SESSID]}'" ".exit"`

    if [[ "$LOGGEDIN_USERID" != "" ]]; then
      DOM+="<div style='margin-top: 20px;'><a href='/edit?route=${REQUEST[ROUTE]}'>Edit</a> | <a href='/delete?route=${REQUEST[ROUTE]}'>Delete</a></div>"
    fi

    if [[ "$DOM" == "" ]]; then
      RES["BODY"]=`render_response "Not found" "Not found."`
      RES["STATUS"]="404 Not found"
    else
      RES["BODY"]=`render_response "Bash CMS!" $DOM`
      RES["STATUS"]="200 OK"
    fi
  fi
}
Enter fullscreen mode Exit fullscreen mode

一切顺利!我们用 443 行代码,仅用 Bash 从头开始​​编写了一个基本的 CMS!

演示时间!

(该 gif 可能需要几秒钟才能加载...)

BashCMS 正在运行!

问答时间!

问:性能好吗?

答:不,完全不会。这个脚本一次只能处理一个请求。Apache 也可以同时处理数百个连接。

问:我应该使用这个...

答:不。看在上帝的份上,请不要这么做。

问:字体一定要等宽吗?这太90年代了。

答:是的。我们用的是 Bash,那为什么不用等宽字体呢?

问:还有什么吗?

答:顺便说一下,我使用 Arch。


希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下你的❤️ !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。

如果你想支持我的努力, 可以请我喝杯咖啡 ,或者 在推特上关注我🐦 你也可以直接通过PayPal支持我!

给我买个咖啡按钮

鏂囩珷鏉ユ簮锛�https://dev.to/thormeier/dont-try-this-at-home-a-cms-writing-in-bash-only-4j6i
PREV
让我们组装一套真正能用的数字鼓🥁,你可以用你的键盘⌨️来演奏🤘 组装鼓组 怎么演奏?是时候测试音质了 总结一下
NEXT
您最喜欢的网络开发博客有哪些?