diff --git a/Makefile b/Makefile
index d5edecf..4883294 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,23 @@
-.PHONY: test
+# * Verbosity
-test:
- cask exec buttercup -L .
+# Since the "-v" in "make -v" gets intercepted by Make itself, we have
+# to use a variable.
+
+verbose = $(v)
+
+ifneq (,$(findstring vv,$(verbose)))
+ VERBOSE = "-vv"
+else ifneq (,$(findstring v,$(verbose)))
+ VERBOSE = "-v"
+endif
+
+# * Rules
+
+# Unless/until I add Cask support to makem.sh, I'm keeping this,
+# because Cask lets me run the tests with a different version of Org
+# than I have installed personally.
+cask:
+ @cask exec buttercup -L .
+
+%:
+ @./makem.sh $(VERBOSE) $(@)
diff --git a/makem.sh b/makem.sh
new file mode 100755
index 0000000..909d9eb
--- /dev/null
+++ b/makem.sh
@@ -0,0 +1,539 @@
+#!/bin/bash
+
+# * makem.sh --- Script to aid building and testing Emacs Lisp packages
+
+# * Commentary:
+
+
+# * License:
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# * Safety
+
+# NOTE: These are disabled by default in this template but should be
+# enabled when feasible. Documentation is from the Bash man page.
+
+# ** errexit
+
+# Exit immediately if a pipeline (which may consist of a single simple
+# command), a list, or a compound command (see SHELL GRAMMAR above),
+# exits with a non-zero status. The shell does not exit if the
+# command that fails is part of the command list immediately following
+# a while or until keyword, part of the test following the if or elif
+# reserved words, part of any command executed in a && or || list
+# except the command follow‐ ing the final && or ||, any command in a
+# pipeline but the last, or if the command's return value is being
+# inverted with !. If a compound command other than a subshell
+# returns a non-zero status because a command failed while -e was
+# being ignored, the shell does not exit. A trap on ERR, if set, is
+# executed before the shell exits. This option applies to the shell
+# environment and each subshell environment separately (see COMMAND
+# EXECUTION ENVIRONMENT above), and may cause subshells to exit before
+# executing all the commands in the subshell.
+
+# If a compound command or shell function executes in a context where
+# -e is being ignored, none of the commands executed within the
+# compound command or function body will be affected by the -e
+# setting, even if -e is set and a command returns a failure status.
+# If a compound command or shell function sets -e while executing in a
+# context where -e is ignored, that setting will not have any effect
+# until the compound command or the command containing the function
+# call completes.
+
+# set -o errexit
+
+# ** nounset
+
+# Treat unset variables and parameters other than the special
+# parameters "@" and "*" as an error when performing parameter
+# expansion. If expansion is attempted on an unset variable or
+# parameter, the shell prints an error message, and, if not
+# interactive, exits with a non-zero status.
+
+# NOTE: When this is not enabled, individual variables can be required
+# to be set by using "${var:?}" parameter expansion syntax.
+
+# set -o nounset
+
+# ** pipefail
+
+# If set, the return value of a pipeline is the value of the last
+# (rightmost) command to exit with a non-zero status, or zero if all
+# commands in the pipeline exit successfully. This option is disabled
+# by default.
+
+# set -o pipefail
+
+# * Elisp
+
+# These functions return a path to an elisp file which can be loaded
+# by Emacs on the command line with -l or --load.
+
+function elisp-buttercup-file {
+ # The function buttercup-run, which is called by buttercup-run-discover,
+ # signals an error if it can't find any Buttercup test suites. We don't
+ # want that to be an error, so we define advice which ignores that error.
+ local file=$(mktemp)
+ cat >$file <$file <$file <$output_file
+
+ exit=$?
+ [[ $exit != 0 ]] && debug "Emacs exited non-zero: $exit"
+ if [[ $verbose -gt 1 || $exit != 0 ]]
+ then
+ cat $output_file
+ fi
+ rm -f $output_file
+
+ return $exit
+}
+
+# ** Compilation
+
+function batch-byte-compile {
+ debug "batch-byte-compile: ERROR-ON-WARN:$compile_error_on_warn FILES:$@"
+
+ [[ $compile_error_on_warn ]] && local error_on_warn=(--eval "(setq byte-compile-error-on-warn t)")
+
+ run_emacs \
+ "${error_on_warn[@]}" \
+ --funcall batch-byte-compile \
+ "$@"
+}
+
+# ** Files
+
+function project-elisp-files {
+ # Echo list of Elisp files in project.
+ git ls-files 2>/dev/null | egrep "\.el$" | exclude-files
+}
+
+function project-source-files {
+ # Echo list of Elisp files that are not tests.
+ project-elisp-files | egrep -v '^tests?/test-?'
+}
+
+function project-test-files {
+ # Echo list of Elisp test files.
+ project-elisp-files | egrep '^tests?/test-?'
+}
+
+function exclude-files {
+ # Filter out paths (STDIN) which should be excluded by default.
+ egrep -v "(/\.cask/|-autoloads.el)"
+}
+
+function load-files-args {
+ # For file in $@, echo "--load $file".
+ for file in "$@"
+ do
+ printf -- '--load %q ' "$file"
+ done
+}
+
+function files_args {
+ # For file in STDIN, echo "$file".
+ while read file
+ do
+ printf -- '%q ' "$file"
+ done
+}
+
+# ** Utility
+
+function cleanup {
+ # Remove temporary paths (${temp_paths[@]}).
+
+ for path in "${temp_paths[@]}"
+ do
+ if [[ $debug ]]
+ then
+ debug "Debugging enabled: not deleting temporary path: $path"
+ elif [[ -r $path ]]
+ then
+ rm -rf "$path"
+ else
+ debug "Temporary path doesn't exist, not deleting: $path"
+ fi
+ done
+}
+
+function echo_color {
+ # This allows bold, italic, etc. without needing a function for
+ # each variation.
+ if [[ $color ]]
+ then
+ local color_code="COLOR_$1"
+ shift
+
+ echo -e "${!color_code}${@}${COLOR_off}"
+ else
+ echo "$@"
+ fi
+}
+function debug {
+ if [[ $debug ]]
+ then
+ function debug {
+ echo_color yellow "DEBUG ($(ts)): $@" >&2
+ }
+ debug "$@"
+ else
+ function debug {
+ true
+ }
+ fi
+}
+function error {
+ echo_color red "ERROR ($(ts)): $@" >&2
+ ((errors++))
+}
+function die {
+ error "$@"
+ exit $errors
+}
+function log {
+ echo "LOG ($(ts)): $@" >&2
+}
+function log_color {
+ local color=$1
+ shift
+ echo_color $color "LOG ($(ts)): $@" >&2
+}
+function success {
+ if [[ $verbose -ge 2 ]]
+ then
+ log_color green "$@" >&2
+ fi
+}
+function verbose {
+ # $1 is the verbosity level, rest are echoed when appropriate.
+ if [[ $verbose -ge $1 ]]
+ then
+ [[ $1 -eq 1 ]] && local color=blue
+ [[ $1 -ge 2 ]] && local color=cyan
+
+ shift
+ log_color $color "$@" >&2
+ fi
+}
+
+function ts {
+ date "+%Y-%m-%d %H:%M:%S"
+}
+
+function usage {
+ cat </dev/null | grep "$rule is a function" &>/dev/null
+ then
+ $rule
+ elif [[ $rule = test ]]
+ then
+ # Allow the "tests" rule to be called as "test". Since "test"
+ # is a shell builtin, this workaround is required.
+ tests
+ else
+ error "Invalid rule: $rule"
+ fi
+done
+
+if [[ $errors -gt 0 ]]
+then
+ log_color red "Finished with $errors errors."
+else
+ success "Finished without errors."
+fi
+
+exit $errors