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 "$@"
RSS
https://razavi.io/posts/rss.xml