#!/usr/bin/env bash # -*- coding: utf-8 -*- #-------------------------- ## @Synopsis Reads specialy formated shell scripts and creates docs ## @Copyright Copyright 2003, Paul Mahon ## @Copyright Copyright 2007, Arvid Norlander ## @License GPL v2 ## Parses comments between lines of '#---' ## Lines to be parsed start with ##. All tags start with @. ## Lines without a tag are considered simple description of the section. ## If the line following the comment block doesn't start with 'function' ## the it's assumed that the comment is for the whole file. Only the first ## non-function comment block will be used, the other will be ignored. ##

## Multiple identical tags are allowed, the contents are appended and separated ## with a space. @param tags are treated specials and are assumed to be in order. ##

## There is an additional <@function FUNCTION_NAME> tag that can be embeded ## in any bashdoc comment. It will be transformed into a link to that function. ## Note, this will only work for functions that are defined in the same script. ##

##	Usage:	[OPTIONS] [--] script [ script ...]
##	-p, --project project   Name of the project
##	-o, --output directory  Specifies the directory you want the resulting html to go into
##	-c, --nocss             Do not write default CSS file.
##	-e, --exclusive tag     Only output if the block has this tag
##	-q, --quiet             Quiet the output
##	-h, --help              Display this help and exit
##	-V, --version           Output version information and exit
##	--                      No more arguments, only scripts
##	script                  The script you want documented
##
## #-------------------------- # Make env sane unset LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY unset LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS unset LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION export LC_ALL=C export LANG=C # Check bash version. We need at least 3.2.x # Lets not use anything like =~ here because # that may not work on old bash versions. if [[ "$(awk -F. '{print $1 $2}' <<< $BASH_VERSION)" -lt 32 ]]; then echo "Sorry your bash version is too old!" echo "You need at least version 3.2 of bash" echo "Please install a newer version:" echo " * Either use your distro's packages" echo " * Or see http://www.gnu.org/software/bash/" exit 2 fi # To make set -x more usable export PS4='(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]} : ' VERSION="0.1.8" HEADERS=" " GOOD=$'\e[32;01m' WARN=$'\e[33;01m' BAD=$'\e[31;01m' NORMAL=$'\e[0m' #-------------------------- ## Output error message ## @param Message ## @Stderr Formated message #-------------------------- print_error () { echo -e " ${BAD}*${NORMAL} $*" >&2 } #-------------------------- ## Output warning message ## @param Message ## @Stderr Formated message #-------------------------- print_warn () { echo -e " ${WARN}*${NORMAL} $*" >&2 } #-------------------------- ## Output info message ## @param Message ## @Stderr Formated message #-------------------------- print_info () { echo -e " ${GOOD}*${NORMAL} $*" >&2 } #-------------------------- ## Output debug message ## @param Message ## @Stderr Formated message #-------------------------- print_debug () { echo -e " $*" >&2 } #-------------------------- ## @Arguments -r: recursive, -o [directory]: output html ## Parses arguments for this script ## @Gobals RECURSIVE, OUT_DIR #-------------------------- function args() { local retVal=0 QUIET=0 while true ; do case $1 in -p|--project) PROJECT="$2" (( retVal+=2 )) shift 2 ;; -o|--output) OUT_DIR="$2" (( retVal+=2 )) shift 2 ;; -c|--nocss) NOCSS="1" (( retVal+=2 )) shift 1 ;; -h|--help) usage exit 0 ;; -V|--version) version exit 0 ;; -e|--exclusive) EXCLUSIVE="${2%%=*}" EXCLUSIVE_VAL="${2#*=}" (( retVal+=2 )) shift 2 ;; -q|--quiet) (( QUIET+=1 )) (( retVal+=1 )) shift 1 ;; --) (( retVal++ )) return $retVal ;; -*) usage exit 0 ;; *) [[ -e $1 ]] && return $retVal echo "$1 doesn't exist." usage exit 1 ;; esac done } #------------------------- ## Version for this script ## @Stdout Version information #------------------------- function version() { echo "bashdoc $VERSION - Generate HTML documentation from bash scripts" echo '' echo 'Copyright (C) 2003 Paul Mahon' echo 'Copyright (C) 2007 Arvid Norlander' echo 'This is free software; see the source for copying conditions. There is NO' echo 'warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.' echo '' echo 'Written by Paul Mahon and modified by Arvid Norlander' } #------------------------- ## Usage for this script ## @Stdout Usage information #------------------------- function usage() { cat <<- EOF bashdoc generates HTML documentation from bash scripts. Usage: $(basename $0) [OPTIONS] [--] script [script ...] Options: -p, --project project Name of the project -o, --output directory Specifies the directory you want the resulting html to go into -c, --nocss Do not write default CSS file. -e, --exclusive tag Only output if the block has this tag -q, --quiet Quiet the output -h, --help Display this help and exit -V, --version Output version information and exit -- No more arguments, only scripts script The script you want documented Examples: bashdoc.sh -p bashdoc -o docs/ bashdoc.sh Generate documentation for this program. bashdoc.sh -p appname -o docs/ -e Type=API someapp.sh Generate documentation for someapp.sh, exclude items that do not include the tag @Type API EOF } #-------------------------- ## Reads until it has read an entire comment block. A block starts with ##
#---

## Alone on a line, and continues until the next ##
#---

## All comment lines inside should have ## at the start or they ## will be ignored. ## ## @return 0 Possibly more blocks ## @return 1 Unexpected end of file ## @return 2 Expected end of file, no more blocks ## @Stdin Reads a chunk ## @Stdout Block with starting '##' removed ## @Globals paramDesc, retDesc, desc, block, split, out_comment_block #-------------------------- function get_comment_block() { local inComment commentBlock lastLine="" commentBlock="" while read LINE ; do (( srcLine++ )) if [[ ${LINE:0:4} == '#---' ]] ; then if [[ $inComment ]] ; then out_comment_block="$commentBlock" # I'm not sure why this is needed but it fixes incorrect line number (( srcLine++ )) return 0 else inComment=yes fi elif [[ ${LINE:0:2} != '##' ]] && [[ $inComment ]] ; then [[ $QUIET -lt 1 ]] && print_warn "Line $srcLine of $FILE isn't a doc comment! Ignoring." [[ $QUIET -lt 1 ]] && print_warn "Line in question is: $LINE" elif [[ $inComment ]] ; then commentBlock="$commentBlock"$'\n'${LINE####} fi done #If we make it out here, we hit the end of the file if [[ $commentBlock ]] ; then #If there is a comment block started, then it never ended [[ $QUIET -lt 2 ]] && print_error "Unfinished comment block:" [[ $QUIET -lt 2 ]] && print_error "$commentBlock" return 1 else return 2 fi } #----------------------- ## Parses the comments from stdin. Also reads the (non-commented) ## function name. Mostly uses <@function parse_block> and ## <@function output_parsed_block> to do the read work. ## @Stdin Reads line after comment block ## @Globals paramDesc, retDesc, desc, block, split, out_comment_block #----------------------- function parse_comments() { #We use a lot of $( echo ... ) in here to trim the blanks local funcLine funcName paramDesc=() retDesc=() local FIRST_BLOCK="yes" local skipRead local outBlock="" local lastOutBlock="" srcLine=0 # 1 = function # 2 = variable itemtype=0 while true ; do paramNames=() paramDesc=() split=() retDesc=() desc="" itemtype=0 unset out_comment_block get_comment_block [[ $? -gt 0 ]] && break block="$out_comment_block" if [[ $skipRead ]] ; then skipRead="" else funcLine="" funcName="" read funcLine fi # Is it a (global) variable? # Check before function to catch arrays. if [[ ${funcLine} =~ ^(declare -r +)?([a-zA-Z_][a-zA-Z0-9_]*)=.+$ ]]; then varName="${BASH_REMATCH[@]: -1}" itemtype=2 # Is it a function? elif [[ ${funcLine%%[[:blank:]]*} == function ]] || [[ ${funcLine} =~ ^[^\ ]+\ *\(\)\ *\{?$ ]]; then funcName=$( echo ${funcLine#function} ) funcName=$( echo ${funcName%%()*} ) itemtype=1 fi if [[ $funcName ]] || [[ $varName ]] || [[ $FIRST_BLOCK ]] ; then # Only bother with this block if it is a function block or # the first script block #This fills in paramDesc[*], tag_*, retDesc parse_block lastOutBlock="$outBlock" outBlock=$(output_parsed_block) if [[ $FIRST_BLOCK ]] && [[ ! $funcName ]] && [[ ! $varName ]]; then FIRST_BLOCK="" fi if [[ $EXCLUSIVE ]] ; then # If this is first block, include it anyway. if [[ $funcName ]] || [[ $varName ]]; then local i="tag_${EXCLUSIVE}" if [[ ${!i} != $EXCLUSIVE_VAL ]] ; then if [[ $itemtype = 2 ]]; then funcName="$varName" fi print_debug "$funcName block ignored, no $EXCLUSIVE=$EXCLUSIVE_VAL tag." # Code duplication but hard to avoid for i in ${!tag_*} ; do unset $i done continue fi fi fi for i in ${!tag_*} ; do unset $i done if [[ $funcName ]]; then FUNC_LIST="$FUNC_LIST $funcName" elif [[ $varName ]]; then VAR_LIST="$VAR_LIST $varName" fi unset funcName varName echo "$outBlock" else [[ $QUIET -lt 2 ]] && print_warn "Ignoring non-first non-function/variable comment block" [[ $QUIET -lt 1 ]] && print_warn "$block" fi done } #--------------------- ## Create HTML from the non-special tags ## @param var or func (is this for a variable or function) ## @Stdout HTMLized tags #--------------------- function output_parsed_tags() { local i for i in ${!tag_*} ; do # Convert _ in tags to space. Looks better. echo "

$(sed 's/_/ /g' <<< "${i#tag_}")

" # This may be fun, allow special formatting by tag. echo "

" echo " ${!i}" echo "

" unset $i done } #--------------------- ## Outputs the parsed information in a nice pretty format. ## @Stdout formated documentation ## @Globals paramDesc, retDesc, desc, block, split #--------------------- function output_parsed_block() { echo "
" if [[ $itemtype -eq 1 ]] && [[ $funcName ]]; then echo "" echo "

function $funcName()

" echo "

Parameters:

" echo " " if [[ ${#retDesc[*]} -gt 0 ]] ; then echo "

Returns:

" echo " " fi output_parsed_tags func [[ $desc ]] && echo "

Description

$desc

" elif [[ $itemtype -eq 2 ]]; then echo "" echo "

variable $varName

" output_parsed_tags var [[ $desc ]] && echo "

Description

$desc

" else echo '' echo "

$FILE

" echo "

$desc

" echo "$desc" >> $SCRIPT_DESC for i in ${!tag_*} ; do echo "

${i#tag_}

" echo "

${!i}

" unset $i done fi } #--------------- ## Does the real work of the parsing. Tags start with @. Special ## tags are @return and @param. Doc lines without a tag are ## considered description. ## @Globals paramDesc, retDesc, desc, block, split #--------------- function parse_block() { local tag local backIFS="$IFS" IFS=$'\n' for LINE in $block; do IFS="$backIFS" LINE=$( echo $LINE ) if [[ ${LINE:0:1} == '@' ]] ; then split_tag split $LINE case ${split} in @param) #paramNames[${#paramNames[*]}]=${split[1]} paramDesc=( "${paramDesc[@]}" "${split[1]}" ) ;; @return) retDesc=( "${retDesc[@]}" "${split[1]}" ) ;; @*) tag=${split[0]#@} local i="tag_${tag}" if [[ ${!i} ]] ; then local varname="tag_${tag}" eval "tag_${tag}=\"\${!varname}"$'\n'"\${split[1]}\"" else eval "tag_${tag}=\"\${split[1]}\"" fi ;; *) print_error "We shouldn't get here... it was a tag, but not a tag?" ;; esac else desc="$desc"$'\n'"$LINE" fi done IFS="$backIFS" } #---------------- ## Splits a line that starts with a tag into tag and data. ## @param Variable you want the result put into. Array is format is ( tag, data ). ## @param Tag ## @param Data ## @Globals The variable in $1 will get the results #---------------- function split_tag() { local out="${1}" ; shift local tag=$( echo ${1} ) ; shift # local key=$( echo ${1} ) ; shift local value=$( echo $* ) eval "$out=( \"\$tag\" \"\${value}\" )" } #-------------------- ## Outputs a header for script pages ## @Stdout html header ## @param Script name #-------------------- function script_header() { cat <<- EOF > $OUT_FILE $HEADERS $1 - $PROJECT

Script Index

EOF } # Initialise project variables OUT_DIR=$( dirname $0 ) NOCSS=0 args "$@" shift $? [[ $OUT_DIR ]] || OUT_DIR="." # Create output directory in case it doesn't exist mkdir -p "$OUT_DIR" || { print_error "Failed to create output directory." exit 1 } if [[ $NOCSS = 0 ]]; then print_info "Writing CSS" # Copy stylesheet to output directory. cat <<- EOF > "${OUT_DIR}/style.css" /* Based on Trac CSS */ body { background: #fff; color: #000; margin: 10px; padding: 0; } body, th, td { font: normal 13px verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif; } h1, h2, h3, h4 { font-family: arial,verdana,'Bitstream Vera Sans',helvetica,sans-serif; font-weight: bold; letter-spacing: -0.018em; } h1 { font-size: 19px; margin: .15em 1em 0 0 } h2 { font-size: 16px; font-weight: normal; } h3 { font-size: 14px } hr { border: none; border-top: 1px solid #ccb; margin: 2em 0 } address { font-style: normal } img { border: none } tt { white-space: pre } :link, :visited { text-decoration: none; color: #b00; border-bottom: 1px dotted #bbb; } :link:hover, :visited:hover { background-color: #eee; color: #555; } h1 :link, h1 :visited ,h2 :link, h2 :visited, h3 :link, h3 :visited, h4 :link, h4 :visited, h5 :link, h5 :visited, h6 :link, h6 :visited { color: inherit; } /* Partly own stuff: */ .nav body { margin: 0; padding: 0; background: inherit; color: inherit; } .nav ul { font-size: 11px; list-style: none; margin: 0; padding: 0; text-align: left } .nav li { display: block; padding: 0; margin: 0; white-space: nowrap; } /* Own stuff */ .nav-header { font-weight: bold; } .right { text-align: right } .tag-Deprecated { color: #e00; } EOF else print_warn "Not writing a stylesheet. You will need to add your own by hand afterwards." fi while [[ $# -gt 0 ]] ; do #Initialise vars for this src FILE=$1 [[ ! -f $FILE ]] || [[ ! -r $FILE ]] && { print_error "$FILE is not a file or is not readable, skipping." shift continue } print_info "Parsing $FILE" shift OUT_FILE=${FILE#/} #Remove leading / OUT_FILE="$OUT_DIR/${OUT_FILE//\//.}.html" FUNC_FILE="${OUT_FILE%.html}.funcs" VAR_FILE="${OUT_FILE%.html}.vars" SCRIPT_DESC="${OUT_FILE%.html}.desc" # Store real name (reuse in script list) REAL_NAME_FILE="${OUT_FILE%.html}.name" echo -n "${FILE#/}" > "$REAL_NAME_FILE" FUNC_LIST="" VAR_LIST="" #Start this src's html file script_header "$FILE" # Parse and write out function list { parse_comments < $FILE echo "$FUNC_LIST" > $FUNC_FILE echo "$VAR_LIST" > $VAR_FILE # Convert references like <@function file,functioname> into links } | sed -e 's!<@[[:blank:]]*function \([^,>]*\)[[:blank:]]*>!\1!g' \ -e 's!<@[[:blank:]]*function \([^,>]*\),[[:blank:]]*\([^>]*\)[[:blank:]]*>!\1!g' >> $OUT_FILE #Close off the html for this src cat <<- EOF >> $OUT_FILE EOF done #Go on to next src #Now for tying the scripts all together pushd $OUT_DIR >/dev/null print_info "Writing function list" # Start page that will have all the function calls cat <<- EOF > function_list.html $HEADERS Functions of $PROJECT EOF print_info "Writing script list" # Start the list of scripts TITLE="Scripts" [[ $PROJECT ]] && TITLE="$PROJECT Script Documentation" cat <<- EOF > script_list.html $HEADERS Scripts of $PROJECT

$TITLE


EOF # List all the sources + descriptions, sort by script dir/name for i in *.name ; do name=${i%.name} echo "${name} $(cat "$i")" done | sort | while read LINE realname; do echo "
$realname
" echo "
" cat ${LINE}.desc 2>/dev/null || { [[ $QUIET -lt 2 ]] && print_warn "$LINE has no description."; } echo "
" done >> script_list.html # Close off the html for the global script list cat <<- EOF >> script_list.html
EOF print_info "Writing index file" # Create the index file for the whole shbang cat <<- EOF > index.html $HEADERS BashDoc - $PROJECT EOF # Remove the temporary .desc and .name files, leave the .func and .vars files, someone may want them later. rm *.desc rm *.name popd >/dev/null