shocco.sh |
||
---|---|---|
shocco is a quick-and-dirty, literate-programming-style documentation generator written for and in POSIX shell. It borrows liberally from Docco, the original Q&D literate-programming-style doc generator.
shocco is built with
Once installed, the
The generated HTML is written to |
#!/bin/sh
| |
Usage and Prerequisites |
| |
The most important line in any shell program. |
set -e
| |
There’s a lot of different ways to do usage messages in shell scripts.
This is my favorite: you write the usage message in a comment —
typically right after the shebang line — BUT, use a special comment prefix
like This also illustrates one of shocco’s corner features. Only comment lines
padded with a space are considered documentation. A |
#/ Usage: shocco [-t <title>] [<source>]
#/ Create literate-programming-style documentation for shell scripts.
#/
#/ The shocco program reads a shell script from <source> and writes
#/ generated documentation in HTML format to stdout. When <source> is
#/ '-' or not specified, shocco reads from stdin.
| |
This is the second part of the usage message technique: |
expr -- "$*" : ".*--help" >/dev/null && {
grep '^#/' <"$0" | cut -c4-
exit 0
}
| |
A custom title may be specified with the |
test "$1" = '-t' && {
title="$2"
shift;shift
}
| |
Next argument should be the |
file="$1"
: ${title:=$(basename "$file")}
| |
These are replaced with the full paths to real utilities by the configure/make system. |
MARKDOWN='@@MARKDOWN@@'
PYGMENTIZE='@@PYGMENTIZE@@'
| |
We’re going to need a |
command -v "$MARKDOWN" >/dev/null || {
if command -v Markdown.pl >/dev/null
then alias markdown='Markdown.pl'
elif test -f "$(dirname $0)/Markdown.pl"
then alias markdown="perl $(dirname $0)/Markdown.pl"
else echo "$(basename $0): markdown command not found." 1>&2
exit 1
fi
}
| |
Check that Pygments is installed for syntax highlighting. This is a fairly hefty prerequisite. Eventually, I’d like to fallback
on a simple non-highlighting preformatter when Pygments isn’t available. For
now, just bail out if we can’t find the |
command -v "$PYGMENTIZE" >/dev/null || {
echo "$(basename $0): pygmentize command not found." 1>&2
exit 1
}
| |
Work and Cleanup |
| |
Make sure we have a |
: ${TMPDIR:=/tmp}
| |
Create a temporary directory for doing work. Use |
: ${WORK:=$(
if command -v mktemp 1>/dev/null 2>&1
then
mktemp -d "$TMPDIR/$(basename $0).XXXXXXXXXX"
else
dir="$TMPDIR/$(basename $0).$$"
mkdir "$dir"
echo "$dir"
fi
)}
| |
We want to be absolutely sure we’re not going to do something stupid like
use |
test -z "$WORK" -o "$WORK" = '/' && {
echo "$(basename $0): could not create a temp work dir."
exit 1
}
| |
We’re about to create a ton of shit under our |
trap "rm -rf $WORK" 0
| |
PreformattingStart out by applying some light preformatting to the |
| |
Get a pipeline going with the |
(cat "$file" && printf "\n\n# \n\n") |
| |
We want the shebang line and any code preceding the first comment to appear as the first code block. This inverts the normal flow of things. Usually, we have comment text followed by code; in this case, we have code followed by comment text. Read the first code and docs headers and flip them so the first docs block comes before the first code block. |
(
lineno=0
codebuf=;codehead=
docsbuf=;docshead=
while read -r line
do
| |
Issue a warning if the first line of the script is not a shebang line. This can screw things up and wreck our attempt at flip-flopping the two headings. |
lineno=$(( $lineno + 1 ))
test $lineno = 1 && ! expr "$line" : "#!.*" >/dev/null &&
echo "$(basename $0): $(file):1 [warn] shebang! line missing." 1>&2
| |
Accumulate comment lines into |
if expr "$line" : '# ' >/dev/null || test "$line" = "#"
then docsbuf="$docsbuf$line
"
else codebuf="$codebuf$line
"
fi
| |
If we have stuff in both |
if test -n "$docsbuf" -a -n "$codebuf"
then
if test -n "$codehead"
then docshead="$docsbuf"
docsbuf=""
printf "%s" "$docshead"
printf "%s" "$codehead"
echo "$line"
exec cat
else codehead="$codebuf"
codebuf=
fi
fi
done
| |
We made it to the end of the file without a single comment line, or there was only a single comment block ending the file. Output our docsbuf or a fake comment and then the codebuf or codehead. |
echo "${docsbuf:-#}"
echo "${codebuf:-"$codehead"}"
) |
| |
Remove comment leader text from all comment lines. Then prefix all
comment lines with “DOCS” and interpreted / code lines with “CODE”.
The stream text might look like this after moving through the
Once we pass through |
sed -n '
s/^/:/
s/^: \{0,\}# /DOCS /p
s/^: \{0,\}#$/DOCS /p
s/^:/CODE /p
' > "$WORK/raw"
| |
Now that we’ve read and formatted our input file for further parsing, change into the work directory. The program will finish up in there. |
cd "$WORK"
| |
First Pass: Comment Formatting |
| |
Start a pipeline going on our preformatted input. Replace all CODE lines with entirely blank lines. We’re not interested in code right now, other than knowing where comments end and code begins and code begins and comments end. |
sed 's/^CODE.*//' < raw |
| |
Now squeeze multiple blank lines into a single blank line. TODO: |
cat -s |
| |
At this point in the pipeline, our stream text looks something like this:
Blank lines represent code segments. We want to replace all blank lines with a dividing marker and remove the “DOCS” prefix from docs lines. |
sed '
s/^$/##### DIVIDER/
s/^DOCS //' |
| |
The current stream text is suitable for input to |
$MARKDOWN |
| |
Now this where shit starts to get a little crazy. We use |
(
csplit -sk \
-f docs \
-n 4 \
- '/<h5>DIVIDER<\/h5>/' '{9999}' \
2>/dev/null ||
true
)
| |
Second Pass: Code FormattingThis is exactly like the first pass but we’re focusing on code instead of comments. We use the same basic technique to separate the two and isolate the code blocks. |
| |
Get another pipeline going on our performatted input file. Replace DOCS lines with blank lines. |
sed 's/^DOCS.*//' < raw |
| |
Squeeze multiple blank lines into a single blank line. |
cat -s |
| |
Replace blank lines with a |
sed '
s/^$/# DIVIDER/
s/^CODE //' |
| |
Now pass the code through |
$PYGMENTIZE -l sh -f html |
| |
Post filter the pygments output to remove partial |
sed '
s/<div class="highlight"><pre>//
s/^<\/pre><\/div>//' |
| |
Again with the |
(
DIVIDER='/<span class="c"># DIVIDER</span>/'
csplit -sk \
-f code \
-n 4 - \
"$DIVIDER" '{9999}' \
2>/dev/null ||
true
)
| |
At this point, we have separate files for each docs section and separate files for each code section. |
| |
HTML Template |
| |
Create a function for apply the standard Docco HTML layout, using jashkenas’s gorgeous CSS for styles. Wrapping the layout in a function lets us apply it elsewhere simply by piping in a body. |
layout () {
cat <<HTML
<!DOCTYPE html>
<html>
<head>
<meta http-eqiv='content-type' content='text/html;charset=utf-8'>
<title>$1</title>
<link rel=stylesheet href="http://jashkenas.github.com/docco/resources/docco.css">
</head>
<body>
<div id=container>
<div id=background></div>
<table cellspacing=0 cellpadding=0>
<thead>
<tr>
<th class=docs><h1>$1</h1></th>
<th class=code></th>
</tr>
</thead>
<tbody>
<tr><td class='docs'>$(cat)</td><td class='code'></td></tr>
</tbody>
</table>
</div>
</body>
</html>
HTML
}
| |
Recombining |
| |
Alright, we have separate files for each docs section and separate files for each code section. We’ve defined a function to wrap the results in the standard layout. All that’s left to do now is put everything back together. |
| |
Start the pipeline with a simple list of split out temp filename. One file per line. |
ls -1 docs[0-9]* code[0-9]* 2>/dev/null |
| |
Now sort the list of files by the number first and then by the type. The
list will look something like this when
|
sort -n -k1.5 -k1.1r |
| |
And if we pass those files to We could also have written this as:
I like to keep things to a simple flat pipeline when possible, hence the
|
xargs cat |
| |
Run a quick substitution on the embedded dividers to turn them into table
rows and cells. This also wraps each code block in a |
{
DOCSDIVIDER='<h5>DIVIDER</h5>'
DOCSREPLACE='</pre></div></td></tr><tr><td class=docs>'
CODEDIVIDER='<span class="c"># DIVIDER</span>'
CODEREPLACE='</td><td class=code><div class=highlight><pre>'
sed "
s@${DOCSDIVIDER}@${DOCSREPLACE}@
s@${CODEDIVIDER}@${CODEREPLACE}@
"
} |
| |
Pipe our recombined HTML into the layout and let it write the result to
|
layout "$title"
| |
Moreshocco is the third tool in a growing family of quick-and-dirty, literate-programming-style documentation generators:
If you like this sort of thing, you may also find interesting Knuth’s massive body of work on literate programming: |
| |
Copyright © Ryan Tomayko <tomayko.com/about> |
:
|
|
| ||