#!/bin/sh -eu # Generate Markdown documentation from a 'shelldoc' documented shell script # Version: 2.0.0 # Copyright: 2017, Koalephant Co., Ltd # Author: Stephen Reay , Koalephant Packaging Team # Generate Markdown documentation from a 'shelldoc' documented shell script # # Input: # $1 - file to process # # Output: # Markdown formatted documentation # # Example: # k_shell_doc file1.sh > file1.md proc k_shell_doc { var KOALEPHANT_TOOL_DESCRIPTION = '"Generate Markdown documentation from a 'shelldoc' documented shell script'" var KOALEPHANT_TOOL_VERSION = '"1.0.0'" var KOALEPHANT_TOOL_OPTIONS = $[cat << """ Options: -h, --help show this help -v, --version show the version -V, --verbose show verbose output -D, --debug show debug output -q, --quiet suppress all output except errors """ ] var KOALEPHANT_TOOL_ARGUMENTS = '"file'" source ./base.lib.sh source ./string.lib.sh source ./bool.lib.sh source ./fs.lib.sh k_log_level $(KOALEPHANT_LOG_LEVEL_NOTICE) > /dev/null var TMP_FUNC_DESCRIPTION = '"func-description'" const TMP_FUNC_DESCRIPTION = '' var TMP_FUNC_INPUT = '"func-input'" const TMP_FUNC_INPUT = '' var TMP_FUNC_OUTPUT = '"func-output'" const TMP_FUNC_OUTPUT = '' var TMP_FUNC_RETURN = '"func-return'" const TMP_FUNC_RETURN = '' var TMP_FUNC_EXAMPLE = '"func-example'" const TMP_FUNC_EXAMPLE = '' var tmpDir = $[k_fs_temp_dir] const tmpDir = '' var newLineWSpace = $[printf '\n ] const newLineWSpace = '' var newLine = $(newLineWSpace% ) const newLine = '' var file = ''"" var headerBlock = ''"" var headerDescription = ''"" var headerMode = 'true' var funcMode = 'false' var functionSegment = '"description'" proc tmp_string_file { var name = $1 printf "%s/%s" $(tmpDir) $(name) } proc tmp_string_get { var name = $1 cat $[tmp_string_file $(name)] } proc tmp_string_append { var name = $1 var string = $2 printf "%s\n" $(string) >> $[tmp_string_file $(name)] } proc tmp_string_set { var name = $1 var string = $2 printf "%s\n" $(string) > $[tmp_string_file $(name)] } proc tmp_string_clear { var name = $1 printf "" > $[tmp_string_file $(name)] } proc tmp_string_exists { var name = $1 test -s $[tmp_string_file $(name)] } proc add_header_property { var name = $1 var value = $2 var line = $[printf "%s: %s" $(name) $(value)] k_log_info "Adding header property: $(name)" if test -n $(headerBlock) { set line = ""$(newLine)$(line)"" } setglobal headerBlock = ""$(headerBlock)$(line)"" } proc add_header_description { var line = $1 k_log_info "Adding header description: $(line)" if test -n $(headerDescription) { set line = ""$(newLineWSpace)$(line)"" } setglobal headerDescription = ""$(headerDescription)$(line)"" } proc print_headers { add_header_property "description" $(headerDescription) var block = '"---'" printf "%s\n%s\n%s\n\n" $(block) $(headerBlock) $(block) } proc parse_header_line { var line = $1, property = '', value = '' if ! k_string_starts_with "#" $(line) { k_log_info "End of file header detected" print_headers setglobal headerMode = 'false' return } set line = $[k_string_remove_start "#" $(line)] match $line { with @Ignore*|!* with *:* set property = $[k_string_remove_end ":*" $(line)] set value = $[k_string_trim $[k_string_remove_start "$(property):" $(line)]] add_header_property $[k_string_lower $[k_string_trim $(property)]] $(value) with * add_header_description $[k_string_trim $line] } } proc parse_function_input { tmp_string_append $(TMP_FUNC_INPUT) $[format_function_input $1] } proc parse_function_output { tmp_string_append $(TMP_FUNC_OUTPUT) $[k_string_trim $1] } proc parse_function_return { tmp_string_append $(TMP_FUNC_RETURN) $[k_string_trim $1] } proc parse_function_description { tmp_string_set $(TMP_FUNC_DESCRIPTION) $[format_function_links $[k_string_trim $(1)]] } proc parse_function_example { tmp_string_append $(TMP_FUNC_EXAMPLE) $1 } proc format_function_input { var line = $1 var name = $[k_string_remove_end " - *" $(line)] var description = $[k_string_trim $[k_string_remove_start "$(name) - " $(line)]] printf " * \`%s\` - %s\n" $[k_string_trim $name] $[format_function_links $description] } proc format_function_links { printf "%s" $1 | sed -E -e 's/\(([^#]*)#([A-Za-z_]{1,})\)/[`\1\2`](#\2)/g' } proc print_function_title { printf '### `%s` {#%s}\n%s\n\n' $1 $(2:-${1}) $[tmp_string_get $(TMP_FUNC_DESCRIPTION)] } proc print_function_input { if tmp_string_exists $(TMP_FUNC_INPUT) { printf '#### Input:\n%s\n\n' $[tmp_string_get $(TMP_FUNC_INPUT)] } } proc print_function_output { if tmp_string_exists $(TMP_FUNC_OUTPUT) { printf '#### Output:\n%s\n\n' $[tmp_string_get $(TMP_FUNC_OUTPUT)] } } proc print_function_return { if tmp_string_exists $(TMP_FUNC_RETURN) { printf '#### Return:\n%s\n\n' $[tmp_string_get $(TMP_FUNC_RETURN)] } } proc print_function_example { if tmp_string_exists $(TMP_FUNC_EXAMPLE) { printf '#### Example:\n~~~sh\n%s\n~~~\n\n' $[tmp_string_get $(TMP_FUNC_EXAMPLE)] } } proc parse_body_line { var line = $1 var paddedLine = ''"" if test $(#line) -eq 0 { return } if ! k_string_starts_with "#" $(line) { if test $(funcMode) != true { return } var name = '' var doFuncBits = 'false' var identifier = ''"" if k_string_contains "()" $(line) { set name = $[k_string_remove_end "()*" $(line)] } else { set identifier = $[k_string_remove_end "=*" $(line)] set name = ""\$$(identifier)"" } print_function_title $(name) $(identifier) print_function_input print_function_output print_function_return print_function_example setglobal funcMode = 'false' setglobal functionSegment = 'description' tmp_string_clear $(TMP_FUNC_DESCRIPTION) tmp_string_clear $(TMP_FUNC_INPUT) tmp_string_clear $(TMP_FUNC_OUTPUT) tmp_string_clear $(TMP_FUNC_RETURN) tmp_string_clear $(TMP_FUNC_EXAMPLE) } else { setglobal funcMode = 'true' set paddedLine = $[k_string_remove_start "\#" $(line)] set line = $[k_string_trim $(paddedLine)] if test $(#line) -eq 0 { return } match $(line) { with @Ignore*|@TODO*|@Todo* return with Input:* tmp_string_clear $(TMP_FUNC_INPUT) setglobal functionSegment = 'input' return with Output:* tmp_string_clear $(TMP_FUNC_OUTPUT) setglobal functionSegment = 'output' return with Return:* tmp_string_clear $(TMP_FUNC_RETURN) setglobal functionSegment = 'return' return with Example:* tmp_string_clear $(TMP_FUNC_EXAMPLE) setglobal functionSegment = 'example' return } k_log_info "Found function segment type: $(functionSegment) for '$(line)'" match $(functionSegment) { with description parse_function_description $(line) with input parse_function_input $(line) with output parse_function_output $(line) with return parse_function_return $(line) with example parse_function_example $[k_string_remove_start " " $(paddedLine)] } } } proc parse_file { var line = '' add_header_property title $(file##*/) while read -r line { if test $(headerMode) = true { parse_header_line $(line) } else { parse_body_line $(line) } } < "${file}" } while test $Argc -gt 0 { match $1 { with -h|--help k_usage exit with -V|--verbose k_log_level $(KOALEPHANT_LOG_LEVEL_INFO) > /dev/null shift with -D|--debug k_log_level $(KOALEPHANT_LOG_LEVEL_DEBUG) > /dev/null shift with -q|--quiet k_log_level $(KOALEPHANT_LOG_LEVEL_ERR) > /dev/null shift with -v|--version k_version return with -* k_log_err "Unknown option: $(1)" k_usage return 1 with -- shift break with * break } } if test -z $(1:-) { k_log_err "No file specified" k_usage return 1 } set file = $1 parse_file k_fs_temp_cleanup } k_shell_doc @Argv