Markdown Notebook
2025-11-28
Here’s a simple bash script that turns markdown files into executable notebooks.
#!/usr/bin/env bash
set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
# shellcheck disable=SC2034
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
# --- Defaults ---------------------------------------------------
auto_run=false
use_color=true
# --- Parse flags ------------------------------------------------
for arg in "$@"; do
case "$arg" in
--no-prompt) auto_run=true ;;
--no-color) use_color=false ;;
-h|--help) usage ;;
esac
done
# remove handled flags
set -- "${@/--no-prompt/}"
set -- "${@/--no-color/}"
# --- Logging ----------------------------------------------------
timestamp=$(date +"%Y%m%d-%H%M%S")
log_file="${TMPDIR:-/tmp}/runmd-${timestamp}.log"
# --- Colours ----------------------------------------------------
if [[ "$use_color" == true && -t 1 && -z "${NO_COLOR:-}" ]]; then
C_RESET=$'\033[0m'
C_MD=$'\033[2m'
C_CMD=$'\033[1;36m'
C_PROMPT=$'\033[1;33m'
C_RUN=$'\033[1;32m'
C_SKIP=$'\033[1;90m'
C_ERR=$'\033[1;31m'
else
C_RESET='' C_MD='' C_CMD='' C_PROMPT='' C_RUN='' C_SKIP='' C_ERR=''
fi
# --- Usage / Cleanup --------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [--no-prompt] [--no-color] FILE.md
Displays a Markdown file, asks before executing fenced code blocks,
and logs only the commands and their output to a timestamped log file.
Options:
--no-prompt Run all blocks automatically without confirmation
--no-color Disable colored output
-h, --help Show this help
Logs are written to:
${TMPDIR:-/tmp}/runmd-<timestamp>.log
EOF
exit 0
}
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
printf "\nLog stored at: %s\n" "$log_file" > /dev/tty || true
}
# --- Core functions ---------------------------------------------
run_block() {
local code="$1" lang="$2"
local runner="${lang:-bash}"
local header
header="[$(date '+%F %T')] LANG=${runner}"
{
echo "──────────────────────"
echo "$header"
echo "$code"
echo "──────────────────────"
} >>"$log_file"
if ! command -v "$runner" &>/dev/null; then
printf "%s\n" "${C_ERR}✗ Interpreter '${runner}' not found. Skipping.${C_RESET}"
{
echo "Interpreter '${runner}' not found."
echo
} >>"$log_file"
return
fi
printf "%s\n" "${C_RUN}▶ Executing with interpreter: ${runner}${C_RESET}"
if "$runner" - <<<"$code" 2>&1 | tee -a "$log_file"; then
printf "%s\n" "${C_RUN}✓ Done.${C_RESET}"
else
local status=$?
printf "%s\n" "${C_ERR}✗ Block failed (exit $status).${C_RESET}"
echo "Exit code: $status" >>"$log_file"
fi
echo >>"$log_file"
}
# --- Main -------------------------------------------------------
main() {
if [[ $# -lt 1 || "${1:-}" =~ ^(-h|--help|help)$ ]]; then
usage
fi
local file="$1"
if [[ ! -f "$file" ]]; then
printf "%s\n" "${C_ERR}Error:${C_RESET} '${file}' not found." >&2
exit 1
fi
local inblock=false lang="" block=""
# Normalize CRLF endings
while IFS='' read -r rawline || [[ -n "$rawline" ]]; do
local line=${rawline//$'\r'/}
if [[ "$line" =~ ^\`\`\` ]]; then
if ! $inblock; then
lang="$(sed -E 's/^```[[:space:]]*//; s/[[:space:]]*$//' <<<"$line")"
inblock=true
block=""
continue
else
if [[ "$line" =~ ^\`\`\`[[:space:]]*$ ]]; then
inblock=false
printf "%s\n" "${C_CMD}──────────────── ${lang:-plain} block ────────────────${C_RESET}"
printf "%s\n" "${C_CMD}${block}${C_RESET}"
printf "%s\n" "${C_CMD}────────────────────────────────────────────${C_RESET}"
if [[ "$auto_run" == true ]]; then
run_block "$block" "$lang"
else
printf "%s" "${C_PROMPT}Run this ${lang:-plain} block? [y/N] ${C_RESET}" > /dev/tty
local ans=""
read -r ans < /dev/tty || ans=""
if [[ "$ans" =~ ^[Yy]$ ]]; then
run_block "$block" "$lang"
else
printf "%s\n" "${C_SKIP}⏩ Skipped.${C_RESET}"
fi
fi
echo
lang=""
block=""
continue
fi
fi
fi
if $inblock; then
block+="${block:+$'\n'}$line"
else
printf "%s\n" "${C_MD}${line}${C_RESET}"
fi
done <"$file"
}
main "$@"