diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d97859b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 + +[shdoc] +trim_trailing_whitespace = false + +[*.md] +insert_final_newline = true +trim_trailing_whitespace = false diff --git a/README.md b/README.md index 5b9bc48..5052e42 100644 --- a/README.md +++ b/README.md @@ -3,31 +3,41 @@ shdoc is a documentation generator for bash/zsh/sh for generating API documentation in Markdown from shell scripts source. -shdoc parses [annotations](#features) in the beginning of a given file and alongside function -definitions, and creates a markdown file with ready to use documentation. +shdoc parses [annotations](#features) in the beginning of a given file and alongside function definitions, and creates a markdown file with ready to use documentation. +This fork adds a few features and bugfixes to the upstream at [reconquest/shdoc](https://github.com/reconquest/shdoc). Features specific to this fork will be marked accordingly. ## Index -* [Example](#example) -* [Annotations](#annotations) * [Usage](#usage) +* [Example](#example) +* [Annotations](#features) * [Installation](#installation) -* [More examples](#examples) * [License](#license) -## Example +## Usage - - -
+shdoc has no args and expects a shell script with comments on stdin and will produce markdown as stdout. + +```bash +$ shdoc < your-shell-script.sh > doc.md +``` + +## Example Generate documentation with the following command: + ~~~bash $ shdoc < lib.sh > doc.md ~~~ -_Source_ [examples/readme-example.sh](examples/readme-example.sh)
+The table below shows the result for the following files: +_Source_: [examples/readme-example.sh](examples/readme-example.sh)
_Output_: [examples/readme-example.md](examples/readme-example.md)

+ + + + @@ -150,6 +204,11 @@ echo "test: $(say-hello World)" A name of the project, used as a title of the doc. Can be specified once in the beginning of the file. +**Note:** +A comment block linked to `@name` or `@file` must be followed by at least one none-comment line +after its `@description` and other annotations, or it will not be recognized correctly. +correctly. + **Example** ```bash @@ -193,17 +252,63 @@ function super() { ### `@section` -The name of a section of the file. Can be used to group functions. +The name of a section of the file. Can be used to group functions. Creates a paragraph titled with the section name. +All functions following this annotation will be placed as sub-paragraphs of the given section. +Use again to start the next section or `@endsection` to break out of the current section. +`@section` blocks need to be followed by at least one none-comment none-EOF line to be printed. + +**fork-specific** +- The sections will also appear in the TOC. +- `@section` influences the header level used for function description titles. + 1. When a section is active, function headers will use `###`, the section header itself uses `##`. + 2. After `@endsection` has been encountered, or before and `@section` annotation, functions will use `##`. + 3. The nesting will also be reflected in the TOC. **Example** ```bash # @section My utilities functions # @description The following functions can be used to solve problems. + +# @description sub-function +func-in-section() { ... } +``` + +### `@endsection` + +**This annotation is fork-specific** +When `@section` is used, shdoc can not by itself detect the end of a section. It would assume the remaining script to be on a deeper nested level and only ever insert subsequent section titles in-between. +This annotation can be used to disable the grouping again for the following functions. It has no effect on its own. Captions for functions contained in sections are level 3 (`###`), +ungrouped functions are level 2 (`##`). + +**original:** +In the original, there is no termination marker for `@section` and no adjustment of header levels depending on section nesting. There, all functions are always level 3, sections are level 2 and section descriptions +are inserted in-between. So there is no visible distinction between function descriptions contained in a section and others not contained. In fact, once a `@section` was specified, everything following it +appears as if it is contained in that. + +**Example** +```bash +# @section First group +# @description Logical group + +# @description +some-group() { ... } + +# @endsection + +# @description something not grouped +important-top-level() { ... } + +# @section +# @description another sub-group ``` ### `@example` -A multiline example of the function usage. Can be specified only alongside the function definition. +A multiline example of the function usage. Can be specified only alongside the function and section definitions. +Must be followed by an empty line if other annotations shall be used after it in the same block. Without the empty line, +the annotation would instead be put into the example. + +**fork-specific**: Also works for `@section` and `@file`. **Example** ```bash @@ -225,8 +330,8 @@ If an option argument is expected, it must be specified between `<` and `>` ```bash # @description Says hi to a given person. # @option -h A short option. -# @arg --value= A long option with argument. -# @arg -v | --value A long option with short option alternative. +# @option --value= A long option with argument. +# @option -v | --value A long option with short option alternative. say-hello() { ... } @@ -265,7 +370,9 @@ say-hello-world() { ### `@set` A description of a global variable that is set while calling the function. -Can be specified multiple times to describe any number of variables +Can be specified multiple times to describe any number of variables. + +**fork-specific**: Also works for `@section` and `@file`. **Example** @@ -339,6 +446,8 @@ say-hello-world() { Create a link on the given function in the "See Also" section. +**fork-specific**: Also works for `@section` and `@file`. + **Example** ```bash @@ -351,10 +460,11 @@ say-hello-world() { ### `@internal` -When you want to skip documentation generation for a particular function, you can specify this -`@internal` tag. -It allows you to have the same style of doc comments across the script and keep internal -functions hidden from users. +When you want to skip documentation generation for a particular function, you can specify this `@internal` tag. +It allows you to have the same style of doc comments across the script and keep internal functions hidden from users. + +**fork-specific:** +When used in `@section` blocks, all functions until the next `@endsection` or `@section` will be considered internal. **Example** @@ -365,40 +475,140 @@ show-msg() { } ``` -## Usage +## Advanced features -shdoc has no args and expects a shell script with comments on stdin and will produce markdown as stdout. +**fork-specific** + +This section describes advanced features and capabilities which are not necessarily needed for simple function documentation. + +### Literal MD source lines + +By default, `shdoc` performs whitespace trimming at the start of a comment line while it builds description blocks. +This behavior disables the possibility to use formatting options like code blocks by indentation (4 spaces), or use +pre-formatted (and indented) text within fenced code blocks (`~~~` or ` ``` `). + +It is possible to disable the trimming behavior by using `#|` as comment prefix, instead of just `# ` (as for annotations). +`shdoc` will only consume the beginning of the line matching the regex `[ ]*#|` and leave the rest untouched. +**Note:** This behaviour has implications for lines where tables are declared. + +**Examples** +- Simple +
+ ~~~bash #!/bin/bash # @file libexample @@ -69,8 +79,24 @@ say-hello() { echo "Hello $1" } -~~~ +# @section Sub-section +# @description Some grouped functions. +# Sections allow a sub-set of other annotations and will ignore unsupported ones. +# @see README +# @example +# # @section example +# # @see [some-link](./README.md) +# # @example ... + +# @description This is nested +deeper-level() { echo; } + +# @endsection + +# @description Back up again +up-again() { echo; } +~~~ @@ -91,19 +117,22 @@ The project solves lots of problems: ## Index * [say-hello](#say-hello) +* [Sub-section](#sub-section) + * [deeper-level](#deeper-level) +* [up-again](#up-again) -### say-hello +## say-hello My super function. Not thread-safe. -#### Example +### Example ```bash echo "test: $(say-hello World)" ``` -#### Options +### Options * **-h** | **--help** @@ -113,30 +142,55 @@ echo "test: $(say-hello World)" Set a value. -#### Arguments +### Arguments * **$1** (string): A value to print -#### Exit codes +### Exit codes * **0**: If successful. * **1**: If an empty string passed. -#### Output on stdout +### Output on stdout * Output 'Hello $1'. It hopes you say Hello back. -#### Output on stderr +### Output on stderr * Output 'Oups !' on error. It did it again. -#### See also +### See also * [validate()](#validate) * [shdoc](https://github.com/reconquest/shdoc). +## Sub-section + +Some grouped functions. +Sections allow a sub-set of other annotations and will ignore unsupported ones. + +### Example + +```bash +# @section example +# @see [some-link](./README.md) +# @example ... +``` + +### See also + +* [README](#readme) + +### deeper-level + +This is nested + +## up-again + +Back up again + ~~~
+ + + + + + + + +
InputResult
+ ```bash -$ shdoc < your-shell-script.sh > doc.md +# @description Simple +#| This is kept literally... +#| ... this too +# me too... not. +``` + + + +```md +Simple + This is kept literally... + ... this too +me too... not. ``` +
+ +- Tables +**Note:** In MD itself, there is little reason to indenting a table. Instead, indenting it too far would convert the table into a code block instead, +where the table source code is not processed as MD. +The literal md marker does not perform any extra checks to find out wether the pipe contained in `#|` is part of the table or not. The pipe character +connected to the comment hash will be stripped. As a consequence, writing tables requires a space between `#` and the left beginning `|` for regular +tables. Alternatively, if the table code should really be indented, 2 pipes must be used, with spaces in between. + + + + + + + + + + +
InputResult
+ +```bash +# @description Table +#| co1 | co2 | +# | abc | def | +#| | 123 | 456 | +#| | 123 | 456 | +``` + + + +```md +Table + co1 | co2 | +| abc | def | + | 123 | 456 | + | 123 | 456 | +``` + +
+ + +Takeaway: +1. If (for optical reasons only) you want to indent a table, don't use `#|`. +2. For regular tables, keep at least one space between `#` and `|` at the beginning of the line +3. Indenting table source requires `#|`, followed by the desired amount of spaces, then `|` to start the table row. + +### Parameter support + +`shdoc` can perform simple parameter replacement. It will recognize replacement tags in the form `%%%%` and +try to retrieve the value of an equally-named environment variable ``. If any value is found, the entire tag +is replaced. The tag is left untouched otherwise. + +This allows the documentation source comments to work like a simple template + +**Rules:** +- Only tags in comment blocks are recognized (lines starting with `[[:space:]]*#`). +- Tag replacement happens first, before any other processing is done. This allows to place annotations in tags. +- Multiple tags can be replaced per line (identical and different ones), but they will not be resolved recursively. + **Exception:** Tags are looped over in the order of definition. When the content of the first tag resolves to a + subsequent tag name, then this subsequent tag will be replaced in a later iteration. +- `` must be a valid name for a shell environment variable, consisting of `[a-Z0-9-_]+`. +- `shdoc` does not perform any parameter expansion. +- There is no way to 'mask' or 'quote' a tag, if it matches the form above. +- Example blocks or sections fenced in backticks or quotes are neither recognized, not treated specially. + ## Installation ### Arch Linux Arch Linux users can install shdoc using package in AUR: [shdoc-git](https://aur.archlinux.org/packages/shdoc-git) +**Note:** +The package in AUR installs the original version [reconquest/shdoc](https://github.com/reconquest/shdoc). This fork does not have a package. +However, shdoc itself is a single script file, it can just be downloaded directly from the repository. + ### Using Git NOTE: shdoc requires gawk: `apt-get install gawk` ```bash -git clone --recursive https://github.com/reconquest/shdoc +git clone --recursive https://github.com/GB609/shdoc cd shdoc sudo make install ``` ### Others -Unfortunately, there are no packages of shdoc for other distros, but we're looking for contributions. - -## Examples - -See example documentation on: - -* [tests.sh](https://github.com/reconquest/tests.sh/blob/master/REFERENCE.md) -* [coproc.bash](https://github.com/reconquest/coproc.bash/blob/master/REFERENCE.md) +There are no packages of shdoc for other distros. # LICENSE diff --git a/examples/readme-example.md b/examples/readme-example.md index 3f41e0a..742663a 100644 --- a/examples/readme-example.md +++ b/examples/readme-example.md @@ -13,19 +13,22 @@ The project solves lots of problems: ## Index * [say-hello](#say-hello) +* [Sub-section](#sub-section) + * [deeper-level](#deeper-level) +* [up-again](#up-again) -### say-hello +## say-hello My super function. Not thread-safe. -#### Example +### Example ```bash echo "test: $(say-hello World)" ``` -#### Options +### Options * **-h** | **--help** @@ -35,27 +38,52 @@ echo "test: $(say-hello World)" Set a value. -#### Arguments +### Arguments * **$1** (string): A value to print -#### Exit codes +### Exit codes * **0**: If successful. * **1**: If an empty string passed. -#### Output on stdout +### Output on stdout * Output 'Hello $1'. It hopes you say Hello back. -#### Output on stderr +### Output on stderr * Output 'Oups !' on error. It did it again. -#### See also +### See also * [validate()](#validate) * [shdoc](https://github.com/reconquest/shdoc). +## Sub-section + +Some grouped functions. +Sections allow a sub-set of other annotations and will ignore unsupported ones. + +### Example + +```bash +# @section example +# @see [some-link](./README.md) +# @example ... +``` + +### See also + +* [README](#readme) + +### deeper-level + +This is nested + +## up-again + +Back up again + diff --git a/examples/readme-example.sh b/examples/readme-example.sh index f5a3bab..b064167 100644 --- a/examples/readme-example.sh +++ b/examples/readme-example.sh @@ -38,3 +38,20 @@ say-hello() { echo "Hello $1" } + +# @section Sub-section +# @description Some grouped functions. +# Sections allow a sub-set of other annotations and will ignore unsupported ones. +# @see README +# @example +# # @section example +# # @see [some-link](./README.md) +# # @example ... + +# @description This is nested +deeper-level() { echo; } + +# @endsection + +# @description Back up again +up-again() { echo; } \ No newline at end of file diff --git a/shdoc b/shdoc index 5fbdad3..f013e6c 100755 --- a/shdoc +++ b/shdoc @@ -61,6 +61,8 @@ BEGIN { debug_fd = 2 } debug_file = "/dev/fd/" debug_fd + + function_nesting = 2 } # @description Display the given error message with its line number on stderr. @@ -108,6 +110,11 @@ function process_function(text) { } debug("→ function") + if (internal_section){ + debug("→ → function: contained in internal section, skip") + return + } + if (is_internal) { debug("→ → function: it is internal, skip") is_internal = 0 @@ -124,9 +131,9 @@ function process_function(text) { ) # Add function documentation to output. - doc = concat(doc, render_docblock(func_name, description, docblock)) + doc = concat(doc, render_docblock(func_name, description, docblock, function_nesting)) # Add function link to table of contents. - toc = concat(toc, render_toc_item(func_name)) + toc = concat(toc, render_toc_item(func_name, function_nesting)) } # Function document has been added to output. @@ -178,15 +185,16 @@ function render_toc_link(text) { # @see https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45 url = tolower(url) gsub(/[^[:alnum:] _-]/, "", url) - gsub(/_/, "", url) gsub(/ /, "-", url) } return "[" text "](#" url ")" } -function render_toc_item(title) { - return "* " render_toc_link(title) +function render_toc_item(title, nesting_level) { + nesting_depth = (nesting_level - 2) * 2 + 2 + print_format = "%" nesting_depth "s" + return sprintf(print_format, "* ") render_toc_link(title) } function unindent(text) { @@ -232,10 +240,23 @@ function unindent(text) { function reset() { debug("→ reset()") - delete docblock + # Make sure the variable exists before deleting. + # Without this, shdoc will fail for fully unannotated scripts. + # This might happen when batch-handling a list of files + if (typeof(docblock) != "unassigned"){ + delete docblock + } description = "" } +function reset_section() { + debug("→ reset_section()") + + delete docblock_filter + section = "" + section_description = "" +} + function handle_description() { debug("→ handle_description") @@ -250,14 +271,16 @@ function handle_description() { } if (section != "" && section_description == "") { - debug("→ → section description: added") - section_description = description - return; + debug("→ → section description: added") + section_description = description + description = "" + return; } - if (file_description == "") { + if (file_intro_enabled != 0 && file_description == "" && in_file_block == 0) { debug("→ → file description: added") - file_description = description + file_description = description + description = "" return; } } @@ -317,12 +340,10 @@ function docblock_concat(key, value) { function docblock_push(key, value) { new_item_index = length(docblock[key]) # Reinitialize docblock key value if it is empty to allow for array storage. - if(new_item_index == 0) - { + if(new_item_index == 0) { delete docblock[key] } - if(isarray(value)) - { + if(isarray(value)) { # Value is an array. Add its contents key by key to the docblock. # Note that is only allow for single dimension value array. @@ -361,8 +382,8 @@ function docblock_append(docblock_name, text) { # @param title Title of the rendered section. # # @stdout A unordered list of the dockblock entries. -function render_docblock_list(docblock, docblock_name, title) { - push(lines, render("h4", title)) +function render_docblock_list(docblock, docblock_name, title, nesting) { + push(lines, render(nesting, title)) # Initialize list item. item = "" # For each dockblock line. @@ -451,26 +472,66 @@ function process_at_option(text) { } } -function render_docblock(func_name, description, docblock) { +function process_section() { + debug("→ process_section") + if (!section_active || section == "") { + debug("→ → no valid section name - skip") + return; + } + if (is_internal){ + debug("→ → section marked as internal - skip") + internal_section = 1 + is_internal = 0 + reset_section(); + return; + } + + debug("→ → section: [" section "]") + debug("→ → section_description: [" section_description "]") + + # support a subset of additional annotations for sections themselves. + docblock_filter["example"] = 1 + docblock_filter["see"] = 1 + docblock_filter["set"] = 1 + + toc = concat(toc, render_toc_item(section, 2)) + + result = render_docblock(section, section_description, docblock, 2) + doc = concat(doc, result) + + reset_section(); +} + +function docblock_allows(key) { + debug("→ → check docblock filter for: [" key "]") + if (!isarray(docblock_filter) || length(docblock_filter) == 0) { + debug("→ → → no filters defined") + return 1 + } + if (key in docblock_filter) { + debug("→ → → filter is: [" docblock_filter[key] "]") + return docblock_filter[key] + } else { + debug("→ → → not allowed/defined: [" key "]") + return 0 + } +} + +function render_docblock(func_name, description, docblock, nesting) { debug("→ render_docblock") - debug("→ → func_name: [" func_name "]") + debug("→ → block_name: [" func_name "]") debug("→ → description: [" description "]") + debug("→ → header_depth: [" nesting "]") # Reset lines variable to allow for a new array creation. delete lines - if (section != "") { - lines[0] = render("h2", section) - if (section_description != "") { - push(lines, section_description) - # Add empty line to signal end of description. - push(lines, "") - } - section = "" - section_description = "" - push(lines, render("h3", func_name)) - } else { - lines[0] = render("h3", func_name) + nesting_top = "h" nesting; + nesting_one = "h" (nesting + 1); + nesting_two = "h" (nesting + 2); + + if (func_name) { + lines[0] = render(nesting_top, func_name) } if (description != "") { @@ -478,9 +539,11 @@ function render_docblock(func_name, description, docblock) { # Add empty line to signal end of description. push(lines, "") } + + filter_active = isarray(docblock_filter) - if ("example" in docblock) { - push(lines, render("h4", "Example")) + if (docblock_allows("example") && "example" in docblock) { + push(lines, render(nesting_one, "Example")) push(lines, render("code", "bash")) push(lines, unindent(docblock["example"])) push(lines, render("/code")) @@ -488,9 +551,9 @@ function render_docblock(func_name, description, docblock) { } if ("option" in docblock || "option-bad" in docblock) { - push(lines, render("h4", "Options")) + push(lines, render(nesting_one, "Options")) - if ("option" in docblock) { + if (docblock_allows("option") && "option" in docblock) { for (i in docblock["option"]) { # Add strong around options, but exclude pipes. term = render("strong", docblock["option"][i]["term"]) @@ -507,7 +570,7 @@ function render_docblock(func_name, description, docblock) { } } - if ("option-bad" in docblock) { + if (docblock_allows("option-bad") && "option-bad" in docblock) { for (i in docblock["option-bad"]) { item = render("li", docblock["option-bad"][i]) push(lines, item) @@ -518,8 +581,8 @@ function render_docblock(func_name, description, docblock) { } } - if ("arg" in docblock) { - push(lines, render("h4", "Arguments")) + if (docblock_allows("arg") && "arg" in docblock) { + push(lines, render(nesting_one, "Arguments")) # Sort args by indexes (i.e. by argument number.) asorti(docblock["arg"], sorted_indexes) @@ -537,15 +600,15 @@ function render_docblock(func_name, description, docblock) { push(lines, "") } - if ("noargs" in docblock) { + if (docblock_allows("noargs") && "noargs" in docblock) { push(lines, render("i", "Function has no arguments.")) # Add empty line to signal end of list in markdown. push(lines, "") } - if ("set" in docblock) { - push(lines, render("h4", "Variables set")) + if (docblock_allows("set") && "set" in docblock) { + push(lines, render(nesting_one, "Variables set")) for (i in docblock["set"]) { item = docblock["set"][i] item = render("set", item) @@ -557,8 +620,8 @@ function render_docblock(func_name, description, docblock) { push(lines, "") } - if ("exitcode" in docblock) { - push(lines, render("h4", "Exit codes")) + if (docblock_allows("exitcode") && "exitcode" in docblock) { + push(lines, render(nesting_one, "Exit codes")) for (i in docblock["exitcode"]) { item = render("li", render("exitcode", docblock["exitcode"][i])) push(lines, item) @@ -568,20 +631,20 @@ function render_docblock(func_name, description, docblock) { push(lines, "") } - if ("stdin" in docblock) { - render_docblock_list(docblock, "stdin", "Input on stdin") + if (docblock_allows("stdin") && "stdin" in docblock) { + render_docblock_list(docblock, "stdin", "Input on stdin", nesting_one) } - if ("stdout" in docblock) { - render_docblock_list(docblock, "stdout", "Output on stdout") + if (docblock_allows("stdout") && "stdout" in docblock) { + render_docblock_list(docblock, "stdout", "Output on stdout", nesting_one) } - if ("stderr" in docblock) { - render_docblock_list(docblock, "stderr", "Output on stderr") + if (docblock_allows("stderr") && "stderr" in docblock) { + render_docblock_list(docblock, "stderr", "Output on stderr", nesting_one) } - if ("see" in docblock) { - push(lines, render("h4", "See also")) + if (docblock_allows("see") && "see" in docblock) { + push(lines, render(nesting_one, "See also")) for (i in docblock["see"]) { item = render("li", render_toc_link(docblock["see"][i])) push(lines, item) @@ -605,17 +668,39 @@ function debug(msg) { debug("line: [" $0 "]") } +/^[[:space:]]*#.*?%%[a-zA-Z0-9_\-]+%%.*/ { + debug("→ found placeholders") + splitted[0] = "" + patsplit($0, splitted, /%%([a-zA-Z0-9_\-]+?)%%/) + + for(i in splitted) { + env_name = splitted[i] + gsub(/%%/, "", env_name) + if (ENVIRON[env_name] != ""){ + debug("→ → replace [" splitted[i] "] with " ENVIRON[env_name]) + gsub(splitted[i], ENVIRON[env_name]) + } + } +} + /^[[:space:]]*# @internal/ { debug("→ @internal") - is_internal = 1 + # ignore the flag while we are in an internal section + # to prevent dangling values not reset by function blocks + # because internal_section is checked first in process_function + if (!internal_section){ + is_internal = 1 + } next } /^[[:space:]]*# @(name|file)/ { debug("→ @name|@file") - sub(/^[[:space:]]*# @(name|file) /, "") + sub(/^[[:space:]]*# @(name|file)[[:space:]]*/, "") + file_intro_enabled = 1 file_title = $0 + in_file_block = 1 next } @@ -645,31 +730,50 @@ in_description { in_description = 0 handle_description() + } else if (match($0, /^[[:space:]]*#\|/)) { + debug("→ → in_description: literal [" $0 "] (keep spaces)") + sub(/^[[:space:]]*#\|/, "") + description = concat(description, $0) + next } else { - debug("→ → in_description: concat") sub(/^[[:space:]]*# @description[[:space:]]*/, "") sub(/^[[:space:]]*#[[:space:]]*/, "") sub(/^[[:space:]]*#$/, "") - + debug("→ → in_description: concat [" $0 "]") description = concat(description, $0) next } } +/^[[:space:]]*# @endsection/ { + debug("→ @endsection") + process_section() + section = "" + section_description = "" + section_active = 0 + function_nesting = 2 + internal_section = 0 + + next +} + /^[[:space:]]*# @section/ { debug("→ @section") sub(/^[[:space:]]*# @section /, "") + if (internal_section){ + internal_section = 0 + } section = $0 + section_active = 1 + function_nesting = 3 next } /^[[:space:]]*# @example/ { debug("→ @example") - in_example = 1 - next } @@ -719,10 +823,10 @@ in_example { # Test if @arg is a numbered item (or $@). if(match(arg_text, /^\$([0-9]+|@)[[:space:]]/, contents)) { - debug(" → → found arg $" arg_number) - # Fetch matched values. arg_number = contents[1] + + debug(" → → found arg $" arg_number) # Zero pad argument number for sorting. if(arg_number ~ /[0-9]+/){ @@ -835,14 +939,15 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: # - `function function_name () {` # - `function_name () {` # - `function_name {` -/^[[:blank:]]*(function[[:blank:]]+)?([a-zA-Z0-9_\-:-\\.]+)[[:blank:]]*(\([[:blank:]]*\))?[[:blank:]]*\{/ \ +/^[[:blank:]]*(function[[:blank:]]+)?([a-zA-Z0-9_\-:-\\.]+)[[:blank:]]*(\([[:blank:]]*\))?[[:blank:]]*[({]/ \ { process_function($0) + function_declaration_complete = 1 } # If line look like a function declaration but is missing opening bracket, /^[[:blank:]]*(function[[:blank:]]+)?([a-zA-Z0-9_\-:-\\.]+)[[:blank:]]*(\([[:blank:]]*\))?/ \ -{ + && !function_declaration_complete { # store it for future use debug("→ look like a function declaration, store line") function_declaration = $0 @@ -850,7 +955,7 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: } # Handle lone opening bracket if previous line is a function declaration. -/^[[:blank:]]*\{/ \ +/^[[:blank:]]*[({]/ \ && function_declaration != "" { debug("→ multi-line function declaration.") # Process function declaration. @@ -868,13 +973,26 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: # Handle non comment lines. /^[^#]*$/ { debug("→ break") + + if(file_intro_enabled && in_file_block && isarray(docblock) && length(docblock)){ + # support a subset of additional annotations for @file sections. + debug("handle file docblock") + docblock_filter["example"] = 1 + docblock_filter["see"] = 1 + docblock_filter["set"] = 1 + file_annoblock = render_docblock("", "", docblock, 1) + delete docblock_filter + } # Line is not an opening bracket, # this is not a function declaration. function_declaration = "" + function_declaration_complete = 0 + in_file_block = 0 # Add current (section) description to output. handle_description(); + process_section(); # Reset docblock. reset() @@ -895,8 +1013,10 @@ END { debug("→ → file_description: [" file_description "]") debug("→ END }") - if (file_title != "") { - print render("h1", file_title) + if (file_intro_enabled) { + if (file_title != "") { + print render("h1", file_title) + } if (file_brief != "") { print file_brief "\n" @@ -904,7 +1024,11 @@ END { if (file_description != "") { print render("h2", "Overview") - print file_description "\n" + print file_description "\n" + } + + if (file_annoblock != "") { + print file_annoblock } } diff --git a/tests/testcases/@endsection.test.sh b/tests/testcases/@endsection.test.sh new file mode 100644 index 0000000..9286680 --- /dev/null +++ b/tests/testcases/@endsection.test.sh @@ -0,0 +1,64 @@ + +tests:put input <0**: On failure * **5**: On some error. -#### Input on stdin +### Input on stdin * Path to something. -#### Output on stdout +### Output on stdout * Path to something. -#### Output on stderr +### Output on stderr * Stderr description. -#### See also +### See also * [some:other:func()](#someotherfunc) * Shell documation generator [shdoc](https://github.com/reconquest/shdoc). diff --git a/tests/testcases/issue-12-no-space-in-function-declaration.test.sh b/tests/testcases/issue-12-no-space-in-function-declaration.test.sh index ab44dfb..7709d9b 100644 --- a/tests/testcases/issue-12-no-space-in-function-declaration.test.sh +++ b/tests/testcases/issue-12-no-space-in-function-declaration.test.sh @@ -20,11 +20,11 @@ tests:put expected <