Build a Simple Bash HTML Editor — Edit HTML Files from the TerminalEditing HTML files directly from the terminal can be fast, portable, and empowering—especially when you want a lightweight workflow or need to make quick edits on a remote server. In this article you’ll learn how to build a simple, usable Bash-based HTML editor that opens, edits, previews, and saves HTML files without leaving the shell. The editor will provide basic features: open/create files, search and replace, insert templates, basic navigation, simple line editing, and an in-terminal preview using a terminal browser. It’s not meant to replace full-featured editors (vim, emacs, code editors), but it’s a useful tool for quick fixes, teaching, automation, or constrained environments.
Why build a Bash HTML editor?
- Lightweight: No GUI, minimal dependencies, runs on most Unix-like systems.
- Scriptable: Easily integrated into automation, CI, SSH sessions.
- Educational: Teaches shell scripting, file handling, and simple text manipulation.
- Portable: Works over SSH and on systems without graphical environments.
Requirements and dependencies
- POSIX-compatible shell (bash preferred)
- Standard Unix utilities: sed, awk, grep, cat, printf, read, mv, cp, mkdir, rm
- Optional for preview: w3m, lynx, or a local headless browser (e.g., w3m is tiny and convenient)
- Optional for improved editing: nano, sed-based multi-line editing tricks
Install w3m (Debian/Ubuntu):
sudo apt update && sudo apt install -y w3m
Design overview
The editor will be a single Bash script that:
- Loads or creates an HTML file.
- Displays a numbered list of lines (or a portion) for context.
- Provides commands to insert, delete, replace, view, preview, save, and exit.
- Supports search and simple regex replace using sed.
- Offers templates/snippets for common HTML boilerplate.
We will implement a command loop that reads user input and dispatches commands. File data will be managed in a temporary working copy to avoid accidental corruption.
The script: full implementation
Save the following as edit-html.sh and make it executable (chmod +x edit-html.sh). The script is commented to explain each part.
#!/usr/bin/env bash # edit-html.sh - Simple Bash HTML editor for terminal use # Usage: ./edit-html.sh [filename] set -euo pipefail IFS=$' ' # Config TMPDIR="${TMPDIR:-/tmp}" WORKDIR="$(mktemp -d "${TMPDIR}/bash-html-edit.XXXXXX")" CLEANUP_ON_EXIT=true PREVIEWER="${PREVIEWER:-w3m}" # set to 'lynx' or 'w3m' if available # Cleanup function cleanup() { if [[ "${CLEANUP_ON_EXIT}" == "true" ]]; then rm -rf -- "${WORKDIR}" fi } trap cleanup EXIT # Helper: print usage usage() { cat <<EOF Usage: $0 [file.html] Simple terminal HTML editor. Commands (type command then Enter): open <file> - open or create file show [start [end]] - show lines start..end (default: 1..40) insert <line> - insert text at line (interactive; end with a lone '.' on a line) append <line> - append after line (interactive; end with a lone '.' on a line) replace <line> - replace a single line interactively delete <start> [end] - delete line(s) search <pattern> - grep pattern (basic) sed <expr> - run sed expression on file (e.g., 's/foo/bar/g') template - insert basic HTML boilerplate preview - open in terminal browser (${PREVIEWER}) save [filename] - save to original or new filename mv <newname> - rename current file export <out> - write current to out (without changing current file) help - show this help quit|exit - exit (prompts to save if changed) EOF } # Load initial file CURRENT_FILE="${1:-}" if [[ -n "${CURRENT_FILE}" ]]; then if [[ -e "${CURRENT_FILE}" ]]; then cp -- "${CURRENT_FILE}" "${WORKDIR}/buffer.html" else touch "${WORKDIR}/buffer.html" fi else touch "${WORKDIR}/buffer.html" fi ORIGINAL_NAME="${CURRENT_FILE:-untitled.html}" BUFFER="${WORKDIR}/buffer.html" MODIFIED=false # Functions for editing show_lines() { local start=${1:-1} local end=${2:-40} nl -ba -w4 -s' ' "${BUFFER}" | sed -n "${start},${end}p" } read_block() { echo "Enter text; finish with a single dot '.' on its own line." local tmp="${WORKDIR}/$$.in" : > "${tmp}" while IFS= read -r line; do [[ "${line}" == "." ]] && break printf '%s ' "${line}" >> "${tmp}" done cat "${tmp}" } insert_at() { local lineno=$1 local tmp="${WORKDIR}/$$.tmp" local block block="$(read_block)" awk -v L="${lineno}" -v blk="${block}" 'BEGIN{split(blk,lines," ");} {print; if(NR==L){for(i in lines)print lines[i]}}' "${BUFFER}" > "${tmp}" && mv "${tmp}" "${BUFFER}" MODIFIED=true } append_after() { local lineno=$1 local tmp="${WORKDIR}/$$.tmp" local block block="$(read_block)" awk -v L="${lineno}" -v blk="${block}" 'BEGIN{split(blk,lines," ");} {print; if(NR==L){for(i=1;i<=length(lines);i++)print lines[i]}}' "${BUFFER}" > "${tmp}" && mv "${tmp}" "${BUFFER}" MODIFIED=true } replace_line() { local lineno=$1 local tmp="${WORKDIR}/$$.tmp" echo "Current line:" sed -n "${lineno}p" "${BUFFER}" echo "Enter replacement (single line):" IFS= read -r newline awk -v L="${lineno}" -v nl="${newline}" 'NR==L{print nl; next} {print}' "${BUFFER}" > "${tmp}" && mv "${tmp}" "${BUFFER}" MODIFIED=true } delete_range() { local start=$1 local end=${2:-$1} local tmp="${WORKDIR}/$$.tmp" sed "${start},${end}d" "${BUFFER}" > "${tmp}" && mv "${tmp}" "${BUFFER}" MODIFIED=true } search_pattern() { grep -n --color=auto -E -- "$1" "${BUFFER}" || true } run_sed() { local expr="$1" local tmp="${WORKDIR}/$$.tmp" sed -E "${expr}" "${BUFFER}" > "${tmp}" && mv "${tmp}" "${BUFFER}" MODIFIED=true } insert_template() { cat >"${WORKDIR}/tmpl.html" <<'EOF' <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>New Document</title> <link rel="stylesheet" href="styles.css"> </head> <body> <header><h1>Heading</h1></header> <main> <p>Your content here.</p> </main> <footer><small>© $(date +%Y)</small></footer> </body> </html> EOF # Insert template at end cat "${WORKDIR}/tmpl.html" >> "${BUFFER}" MODIFIED=true } preview_buffer() { if command -v "${PREVIEWER}" >/dev/null 2>&1; then "${PREVIEWER}" "${BUFFER}" else echo "Previewer '${PREVIEWER}' not found. Install w3m or set PREVIEWER env var." fi } save_buffer() { local out="${1:-${ORIGINAL_NAME}}" cp -- "${BUFFER}" "${out}" ORIGINAL_NAME="${out}" MODIFIED=false echo "Saved to ${out}" } rename_current() { local newname="$1" ORIGINAL_NAME="${newname}" echo "Current name set to ${ORIGINAL_NAME}" } export_to() { local out="$1" cp -- "${BUFFER}" "${out}" echo "Exported to ${out}" } confirm_save_on_exit() { if [[ "${MODIFIED}" == "true" ]]; then echo -n "You have unsaved changes. Save before exit? (y/n) " IFS= read -r ans if [[ "${ans}" =~ ^[Yy]$ ]]; then echo -n "Save as (default: ${ORIGINAL_NAME}): " IFS= read -r fname if [[ -z "${fname}" ]]; then save_buffer "${ORIGINAL_NAME}" else save_buffer "${fname}" fi fi fi } # Main REPL echo "Simple Bash HTML Editor — editing: ${ORIGINAL_NAME}" usage while true; do printf ' editor:%s> ' "${ORIGINAL_NAME}" if ! IFS= read -r cmdline; then echo confirm_save_on_exit exit 0 fi cmd=( $cmdline ) case "${cmd[0]}" in open) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: open <file>" else cp -- "${cmd[1]}" "${BUFFER}" 2>/dev/null || touch "${BUFFER}" ORIGINAL_NAME="${cmd[1]}" MODIFIED=false echo "Opened ${ORIGINAL_NAME}" fi ;; show) show_lines "${cmd[1]:-1}" "${cmd[2]:-40}" ;; insert) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: insert <line>" else insert_at "${cmd[1]}" fi ;; append) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: append <line>" else append_after "${cmd[1]}" fi ;; replace) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: replace <line>" else replace_line "${cmd[1]}" fi ;; delete) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: delete <start> [end]" else delete_range "${cmd[1]}" "${cmd[2]:-}" fi ;; search) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: search <pattern>" else search_pattern "${cmdline#* }" fi ;; sed) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: sed <expr>" else run_sed "${cmdline#* }" fi ;; template) insert_template echo "Inserted HTML template." ;; preview) preview_buffer ;; save) save_buffer "${cmd[1]:-}" ;; mv) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: mv <newname>" else rename_current "${cmd[1]}" fi ;; export) if [[ -z "${cmd[1]:-}" ]]; then echo "Usage: export <out>" else export_to "${cmd[1]}" fi ;; help) usage ;; quit|exit) confirm_save_on_exit exit 0 ;; *) if [[ -n "${cmd[0]}" ]]; then echo "Unknown command: ${cmd[0]}. Type 'help' for commands." fi ;; esac done
Usage examples
-
Start editing a file: ./edit-html.sh index.html
-
Show lines 1–60: Type: show 1 60
-
Insert a block after line 10: Type: insert 10 Then paste lines and finish with a line containing just a single dot (.)
-
Replace line 5: Type: replace 5 Then enter the replacement line.
-
Quick search: Type: search “
Preview in terminal: Type: preview (requires w3m/lynx)
Save: Type: save or save newname.html
Tips and extensions
- Add syntax highlighting by piping to source-highlight or bat (if available).
- Integrate with git: auto-commit after save.
- Add multi-line replace using awk/perl for more robust patterns.
- Create custom snippets for common components (navbars, cards).
- Use an external editor: add a command to open \(BUFFER in \)EDITOR (e.g., nano, vim) then reload.
Example to open in $EDITOR and reload:
editor() { ${EDITOR:-nano} "${BUFFER}" MODIFIED=true }
Limitations
- Not a full-featured editor — lacks undo stack, smart indentation, and advanced search/replace.
- Designed for small-to-medium files; very large files may be slow.
- Editing UX is basic; for heavy editing use vim/nano/emacs/VS Code.
Conclusion
This simple Bash HTML editor gives you the ability to create and edit HTML files directly from the terminal with minimal dependencies. It’s a practical utility for quick fixes, remote work over SSH, and learning shell scripting. Customize it with snippets, external editors, or more advanced parsing as your needs grow.
Comments
Leave a Reply