From 64d2d2348ab13f0af97112d69123a360214518f5 Mon Sep 17 00:00:00 2001 From: "Dr. Bryan Roessler" <38270216+cryobry@users.noreply.github.com> Date: Fri, 6 Dec 2024 00:00:08 -0500 Subject: [PATCH 01/19] Fix underscores in github markdown links Use the actual header name (do not remove underscores) --- shdoc | 1 - 1 file changed, 1 deletion(-) diff --git a/shdoc b/shdoc index 5fbdad3..013da72 100755 --- a/shdoc +++ b/shdoc @@ -178,7 +178,6 @@ 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) } From ebaf66d94e5cecaafb3fe75f5d715517e93c3b75 Mon Sep 17 00:00:00 2001 From: rafmartom Date: Wed, 26 Feb 2025 13:04:03 +0000 Subject: [PATCH 02/19] Fixed typo in README @option instead of @arg --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b9bc48..e701746 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,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() { ... } From ee2f086841e91e134eb765bdd8244705a2563e47 Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:08:12 +0000 Subject: [PATCH 03/19] Create .editorconfig --- .editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .editorconfig 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 From 686633aedcd58d423907d0b436de0348a7b741ad Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:28:46 +0000 Subject: [PATCH 04/19] Rework and enhance `@section` - Choose header level used for functions depending on section nesting: - h2 for top-level - h3 for functions contained in sections - sections themselves are h2 - sub-sections coming from annotations will be one level deeper than the corresponding function - Introduce `@endsection` to leave active section nesting again. - `@section` blocks now support `@see`, `@set` and `@example` - `@section` blocks also appear in TOC, contained functions will be nested in TOC as well - Sections are not bound to process_function/render_docblock anymore. They can also be used when there is no more function following after them. All that's needed is one none-comment line afterwards. - added some tests to verify the new behavior Bugfix: Reset variable "description" whenever its value was assigned (and thus consumed) to a `@section` or `@file` block to prevent it from being inherited/used by the next function as well. --- README.md | 109 +++++++++-- examples/readme-example.md | 44 ++++- examples/readme-example.sh | 17 ++ shdoc | 171 ++++++++++++------ tests/testcases/@endsection.test.sh | 64 +++++++ tests/testcases/@function-declaration.test.sh | 16 +- .../testcases/@name-and-@description.test.sh | 8 +- tests/testcases/@option.test.sh | 16 +- tests/testcases/@section-docblock.test.sh | 73 ++++++++ tests/testcases/@section.test.sh | 31 +++- tests/testcases/@see.test.sh | 4 +- tests/testcases/@set.test.sh | 4 +- tests/testcases/@stderr.test.sh | 8 +- tests/testcases/@stdin.test.sh | 12 +- tests/testcases/@stdout.test.sh | 10 +- tests/testcases/function-in-@example.test.sh | 8 +- tests/testcases/function-tags.test.sh | 18 +- ...2-no-space-in-function-declaration.test.sh | 4 +- ...ssing-trailing-newline-in-see-also.test.sh | 6 +- ...whitespace-in-function-declaration.test.sh | 36 ++-- ...n-opening-bracket-on-separate-line.test.sh | 4 +- tests/testcases/numbered-arguments.test.sh | 16 +- tests/testcases/table-of-contents.test.sh | 6 +- 23 files changed, 516 insertions(+), 169 deletions(-) create mode 100644 tests/testcases/@endsection.test.sh create mode 100644 tests/testcases/@section-docblock.test.sh diff --git a/README.md b/README.md index e701746..ba2a384 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,20 @@ definitions, and creates a markdown file with ready to use documentation. ## 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)

+ + + + @@ -193,7 +240,11 @@ 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. +The sections will also appear in the TOC, functions contained in it be nested here as well. +Use again to start the next section or `@endsection` to break out of the current section. +`@section` blocks need to be followed by a none-comment none-EOF line to be printed. **Example** ```bash @@ -201,9 +252,33 @@ The name of a section of the file. Can be used to group functions. # @description The following functions can be used to solve problems. ``` +### `@endsection` + +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. Has no effect on its own. + +**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. **Example** ```bash @@ -337,7 +412,7 @@ say-hello-world() { ### `@see` -Create a link on the given function in the "See Also" section. +Create a link on the given function in the "See Also" section of a function or block generated by `@section`. **Example** 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 013da72..85b6b3f 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. @@ -124,9 +126,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. @@ -184,8 +186,10 @@ function render_toc_link(text) { 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) { @@ -235,6 +239,15 @@ function reset() { description = "" } +function reset_section() { + debug("→ reset_section()") + + delete section_docblock + delete docblock_filter + section = "" + section_description = "" +} + function handle_description() { debug("→ handle_description") @@ -249,14 +262,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_title != "" && file_description == "") { debug("→ → file description: added") file_description = description + description = "" return; } } @@ -313,12 +328,16 @@ function docblock_concat(key, value) { # @param value Added item value. # # @set docblock[docblock_name] docblock with value added as its last item. -function docblock_push(key, value) { - new_item_index = length(docblock[key]) +function docblock_push(key, value, target) { + if (!isarray(target)) { + docblock_push(key, value, docblock) + return + } + new_item_index = length(target[key]) # Reinitialize docblock key value if it is empty to allow for array storage. if(new_item_index == 0) { - delete docblock[key] + delete target[key] } if(isarray(value)) { @@ -326,11 +345,11 @@ function docblock_push(key, value) { # Value is an array. Add its contents key by key to the docblock. # Note that is only allow for single dimension value array. for (i in value) { - docblock[key][new_item_index][i] = value[i] + target[key][new_item_index][i] = value[i] } } else { - docblock[key][new_item_index] = value + target[key][new_item_index] = value } } @@ -360,8 +379,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. @@ -450,36 +469,69 @@ 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; + } + + 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); + + lines[0] = render(nesting_top, func_name) if (description != "") { push(lines, description) # 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")) @@ -487,9 +539,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"]) @@ -506,7 +558,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) @@ -517,8 +569,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) @@ -536,15 +588,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) @@ -556,8 +608,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) @@ -567,20 +619,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) @@ -655,10 +707,22 @@ in_description { } } +/^[[:space:]]*# @endsection/ { + debug("→ @endsection") + section = "" + section_description = "" + section_active = 0 + function_nesting = 2 + + next +} + /^[[:space:]]*# @section/ { debug("→ @section") sub(/^[[:space:]]*# @section /, "") section = $0 + section_active = 1 + function_nesting = 3 next } @@ -834,14 +898,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 @@ -871,9 +936,11 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: # Line is not an opening bracket, # this is not a function declaration. function_declaration = "" + function_declaration_complete = 0 # Add current (section) description to output. handle_description(); + process_section(); # Reset docblock. reset() 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 < Date: Sun, 12 Oct 2025 22:57:34 +0200 Subject: [PATCH 05/19] allow to mark entire sections as `@internal` --- README.md | 7 +- shdoc | 26 +++++-- tests/testcases/@section-internal.test.sh | 85 +++++++++++++++++++++++ 3 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 tests/testcases/@section-internal.test.sh diff --git a/README.md b/README.md index ba2a384..f877379 100644 --- a/README.md +++ b/README.md @@ -426,10 +426,9 @@ 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. +When used in `@section` blocks, all functions until the next `@endsection` or `@section` will be considered internal. +It allows you to have the same style of doc comments across the script and keep internal functions hidden from users. **Example** diff --git a/shdoc b/shdoc index 85b6b3f..dc6dc69 100755 --- a/shdoc +++ b/shdoc @@ -110,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 @@ -242,7 +247,6 @@ function reset() { function reset_section() { debug("→ reset_section()") - delete section_docblock delete docblock_filter section = "" section_description = "" @@ -475,6 +479,13 @@ function process_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 "]") @@ -658,7 +669,12 @@ function debug(msg) { /^[[: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 } @@ -713,6 +729,7 @@ in_description { section_description = "" section_active = 0 function_nesting = 2 + internal_section = 0 next } @@ -720,6 +737,9 @@ in_description { /^[[:space:]]*# @section/ { debug("→ @section") sub(/^[[:space:]]*# @section /, "") + if (internal_section){ + internal_section = 0 + } section = $0 section_active = 1 function_nesting = 3 @@ -729,10 +749,8 @@ in_description { /^[[:space:]]*# @example/ { debug("→ @example") - in_example = 1 - next } diff --git a/tests/testcases/@section-internal.test.sh b/tests/testcases/@section-internal.test.sh new file mode 100644 index 0000000..064f0dc --- /dev/null +++ b/tests/testcases/@section-internal.test.sh @@ -0,0 +1,85 @@ +# Make an entire section internal. +# None of its functions should appear in doc or toc. +# The section itself also shouldn't appear. +# Internal flag should be cleared by starting a next section, or ending the current one. + +tests:put input < Date: Sun, 12 Oct 2025 22:10:29 +0000 Subject: [PATCH 06/19] fix potentially fatal delete statement --- shdoc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shdoc b/shdoc index dc6dc69..805a2d0 100755 --- a/shdoc +++ b/shdoc @@ -240,7 +240,12 @@ 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 = "" } From 9b8342ee1fb7c6f2592c77b60583112e03a83143 Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Mon, 13 Oct 2025 09:33:26 +0000 Subject: [PATCH 07/19] allow empty `@file` or `@name` to only get brief and description --- shdoc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shdoc b/shdoc index 805a2d0..1035b0c 100755 --- a/shdoc +++ b/shdoc @@ -686,7 +686,8 @@ function debug(msg) { /^[[:space:]]*# @(name|file)/ { debug("→ @name|@file") - sub(/^[[:space:]]*# @(name|file) /, "") + sub(/^[[:space:]]*# @(name|file)[[:space:]]*/, "") + file_intro_enabled = 1 file_title = $0 next @@ -984,8 +985,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" From 258757c77d9c890a4e4510f5be73beb8b4bdcb4f Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:08:26 +0200 Subject: [PATCH 08/19] fix `@file` --- shdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shdoc b/shdoc index 1035b0c..e0da5b6 100755 --- a/shdoc +++ b/shdoc @@ -277,7 +277,7 @@ function handle_description() { return; } - if (file_title != "" && file_description == "") { + if (file_intro_enabled != 0 && file_description == "") { debug("→ → file description: added") file_description = description description = "" From 1cd5c48407e00636d3a0b3812c0b8c0170779378 Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Sun, 9 Nov 2025 11:19:27 +0000 Subject: [PATCH 09/19] allow `@see`, `@set` and `@example` for `@file` blocks --- shdoc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/shdoc b/shdoc index e0da5b6..1dac86c 100755 --- a/shdoc +++ b/shdoc @@ -277,9 +277,13 @@ function handle_description() { return; } - if (file_intro_enabled != 0 && file_description == "") { + if (file_intro_enabled != 0 && file_description == "" && in_file_block == 0) { debug("→ → file description: added") - file_description = description + # support a subset of additional annotations for @file sections. + docblock_filter["example"] = 1 + docblock_filter["see"] = 1 + docblock_filter["set"] = 1 + file_description = file_description render_docblock("", description, docblock, 1) description = "" return; } @@ -536,7 +540,9 @@ function render_docblock(func_name, description, docblock, nesting) { nesting_one = "h" (nesting + 1); nesting_two = "h" (nesting + 2); - lines[0] = render(nesting_top, func_name) + if (func_name) { + lines[0] = render(nesting_top, func_name) + } if (description != "") { push(lines, description) @@ -689,6 +695,7 @@ function debug(msg) { sub(/^[[:space:]]*# @(name|file)[[:space:]]*/, "") file_intro_enabled = 1 file_title = $0 + in_file_block = 1 next } @@ -961,6 +968,7 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: # this is not a function declaration. function_declaration = "" function_declaration_complete = 0 + in_file_block = 0 # Add current (section) description to output. handle_description(); From 323398f77adf9ea67376766404a9364c6863727c Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:09:43 +0000 Subject: [PATCH 10/19] Update README.md --- README.md | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f877379..599a416 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ 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) +* [Annotations](#features) * [Usage](#usage) * [Installation](#installation) * [More examples](#examples) @@ -241,21 +241,37 @@ function super() { ### `@section` 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. -The sections will also appear in the TOC, functions contained in it be nested here as well. +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 a 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. Has no effect on its own. +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 @@ -280,6 +296,8 @@ A multiline example of the function usage. Can be specified only alongside the f 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 # @example @@ -340,7 +358,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** @@ -412,7 +432,9 @@ say-hello-world() { ### `@see` -Create a link on the given function in the "See Also" section of a function or block generated by `@section`. +Create a link on the given function in the "See Also" section. + +**fork-specific**: Also works for `@section` and `@file`. **Example** @@ -427,9 +449,11 @@ say-hello-world() { ### `@internal` When you want to skip documentation generation for a particular function, you can specify this `@internal` tag. -When used in `@section` blocks, all functions until the next `@endsection` or `@section` will be considered internal. 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** ```bash @@ -453,12 +477,16 @@ $ shdoc < your-shell-script.sh > doc.md 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 ``` From 1b10a5de82e73e02f7bad44255691436c25abb35 Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Sat, 29 Nov 2025 12:45:42 +0000 Subject: [PATCH 11/19] reset docblock_filter after creating file_description --- shdoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shdoc b/shdoc index 1dac86c..e6d59cd 100755 --- a/shdoc +++ b/shdoc @@ -285,6 +285,7 @@ function handle_description() { docblock_filter["set"] = 1 file_description = file_description render_docblock("", description, docblock, 1) description = "" + delete docblock_filter return; } } @@ -813,10 +814,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]+/){ From 0b0d46238a811c4da95138850df15c0cc912fda0 Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:05:25 +0100 Subject: [PATCH 12/19] fix tests and documentation related to `@file` descriptions --- README.md | 7 ++++++- tests/testcases/@stderr.test.sh | 1 + tests/testcases/@stdin.test.sh | 1 + tests/testcases/@stdout.test.sh | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 599a416..1153101 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,11 @@ Back up again 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 @@ -243,7 +248,7 @@ function super() { 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 a none-comment none-EOF line to be printed. +`@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. diff --git a/tests/testcases/@stderr.test.sh b/tests/testcases/@stderr.test.sh index 5e982d7..9b4de56 100644 --- a/tests/testcases/@stderr.test.sh +++ b/tests/testcases/@stderr.test.sh @@ -24,6 +24,7 @@ tests:put input < Date: Sun, 7 Dec 2025 23:05:54 +0100 Subject: [PATCH 13/19] new capability: parameter replacements --- README.md | 41 ++++++++++++++++++++++++++--------------- shdoc | 19 +++++++++++++++++-- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1153101..e136eee 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,20 @@ This fork adds a few features and bugfixes to the upstream at [reconquest/shdoc] ## Index +* [Usage](#usage) * [Example](#example) * [Annotations](#features) -* [Usage](#usage) * [Installation](#installation) -* [More examples](#examples) * [License](#license) +## 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: @@ -468,13 +475,24 @@ show-msg() { } ``` -## Usage +## Advanced features -shdoc has no args and expects a shell script with comments on stdin and will produce markdown as stdout. +### Parameter support -```bash -$ shdoc < your-shell-script.sh > doc.md -``` +**fork-specific** + +`shdoc` can perform simple parameter replacement. It will recognize replacment 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. + +**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, but they will not be resolved recursively. +- `` 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 @@ -498,14 +516,7 @@ 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/shdoc b/shdoc index e6d59cd..8a8f288 100755 --- a/shdoc +++ b/shdoc @@ -679,6 +679,21 @@ 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") # ignore the flag while we are in an internal section @@ -727,10 +742,10 @@ in_description { handle_description() } else { - debug("→ → in_description: concat") sub(/^[[:space:]]*# @description[[:space:]]*/, "") sub(/^[[:space:]]*#[[:space:]]*/, "") sub(/^[[:space:]]*#$/, "") + debug("→ → in_description: concat [" $0 "]") description = concat(description, $0) next @@ -1005,7 +1020,7 @@ END { if (file_description != "") { print render("h2", "Overview") - print file_description "\n" + print file_description } } From 74dc2d3c999ac242ffadd0659da8165a2e4e4a92 Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:45:17 +0100 Subject: [PATCH 14/19] decouple addtional annotations in `@file` blocks from `@description` --- shdoc | 22 +++++++++----- tests/testcases/parameter-replacement.test.sh | 29 +++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 tests/testcases/parameter-replacement.test.sh diff --git a/shdoc b/shdoc index 8a8f288..9c11511 100755 --- a/shdoc +++ b/shdoc @@ -279,13 +279,8 @@ function handle_description() { if (file_intro_enabled != 0 && file_description == "" && in_file_block == 0) { debug("→ → file description: added") - # support a subset of additional annotations for @file sections. - docblock_filter["example"] = 1 - docblock_filter["see"] = 1 - docblock_filter["set"] = 1 - file_description = file_description render_docblock("", description, docblock, 1) + file_description = description description = "" - delete docblock_filter return; } } @@ -979,6 +974,15 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: # Handle non comment lines. /^[^#]*$/ { debug("→ break") + + if(file_intro_enabled && in_file_block && length(docblock)){ + # support a subset of additional annotations for @file sections. + 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. @@ -1020,7 +1024,11 @@ END { if (file_description != "") { print render("h2", "Overview") - print file_description + print file_description "\n" + } + + if (file_annoblock != "") { + print file_annoblock } } diff --git a/tests/testcases/parameter-replacement.test.sh b/tests/testcases/parameter-replacement.test.sh new file mode 100644 index 0000000..130d3f9 --- /dev/null +++ b/tests/testcases/parameter-replacement.test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +tests:put input < Date: Sun, 7 Dec 2025 23:35:07 +0000 Subject: [PATCH 15/19] bugfix: make sure docblock length is only retrieved when its an array --- shdoc | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/shdoc b/shdoc index 9c11511..3eb6d09 100755 --- a/shdoc +++ b/shdoc @@ -337,28 +337,22 @@ function docblock_concat(key, value) { # @param value Added item value. # # @set docblock[docblock_name] docblock with value added as its last item. -function docblock_push(key, value, target) { - if (!isarray(target)) { - docblock_push(key, value, docblock) - return - } - new_item_index = length(target[key]) +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) - { - delete target[key] + 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. for (i in value) { - target[key][new_item_index][i] = value[i] + docblock[key][new_item_index][i] = value[i] } } else { - target[key][new_item_index] = value + docblock[key][new_item_index] = value } } @@ -975,8 +969,9 @@ match($0, /^([[:blank:]]*#[[:blank:]]+)@(stdin|stdout|stderr)[[:blank:]]+(.*[^[: /^[^#]*$/ { debug("→ break") - if(file_intro_enabled && in_file_block && length(docblock)){ + 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 From 1698074e9b152d2b791c156cbcfe9eb8b54d0369 Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:07:49 +0000 Subject: [PATCH 16/19] Allow raw MD in `@description` blocks (#5) * introduce literal blocks (no space trimming) * Create md-literal-lines.test.sh * Update README.md --- README.md | 87 +++++++++++++++++ shdoc | 7 +- tests/testcases/md-literal-lines.test.sh | 115 +++++++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 tests/testcases/md-literal-lines.test.sh diff --git a/README.md b/README.md index e136eee..b46cbda 100644 --- a/README.md +++ b/README.md @@ -477,6 +477,93 @@ show-msg() { ## Advanced features +### Literal md source + +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. +**Exception:** There is a simple detection if the line contains a table row definition (when it also ends with `|[ ]*`). +In that case, only `[ ]*#` at the beginning will be consumed and the pipe will be left untouched. + +**Examples** + +- Simple +
+ ~~~bash #!/bin/bash # @file libexample @@ -69,8 +72,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 +110,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 +135,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 +# @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 **fork-specific** diff --git a/shdoc b/shdoc index 3eb6d09..762e63d 100755 --- a/shdoc +++ b/shdoc @@ -730,12 +730,16 @@ 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 { sub(/^[[:space:]]*# @description[[:space:]]*/, "") sub(/^[[:space:]]*#[[:space:]]*/, "") sub(/^[[:space:]]*#$/, "") debug("→ → in_description: concat [" $0 "]") - description = concat(description, $0) next } @@ -743,6 +747,7 @@ in_description { /^[[:space:]]*# @endsection/ { debug("→ @endsection") + process_section() section = "" section_description = "" section_active = 0 diff --git a/tests/testcases/md-literal-lines.test.sh b/tests/testcases/md-literal-lines.test.sh new file mode 100644 index 0000000..5bcf3b5 --- /dev/null +++ b/tests/testcases/md-literal-lines.test.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +tests:put input < Date: Wed, 10 Dec 2025 06:41:18 +0000 Subject: [PATCH 17/19] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b46cbda..518a574 100644 --- a/README.md +++ b/README.md @@ -485,8 +485,7 @@ 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. -**Exception:** There is a simple detection if the line contains a table row definition (when it also ends with `|[ ]*`). -In that case, only `[ ]*#` at the beginning will be consumed and the pipe will be left untouched. +**Note:** This behaviour has implications for lines where tables are declared. **Examples** From 8d1e1ec09df21d2ddf5540de1f0e5a28a634da6a Mon Sep 17 00:00:00 2001 From: GB609 <39741460+GB609@users.noreply.github.com> Date: Wed, 10 Dec 2025 06:49:32 +0000 Subject: [PATCH 18/19] Update README.md --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 518a574..5052e42 100644 --- a/README.md +++ b/README.md @@ -477,7 +477,11 @@ show-msg() { ## Advanced features -### Literal md source +**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 @@ -565,16 +569,18 @@ Takeaway: ### Parameter support -**fork-specific** - -`shdoc` can perform simple parameter replacement. It will recognize replacment tags in the form `%%%%` and +`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, but they will not be resolved recursively. +- 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. From cde2263b83384747b5e80683567366dc4272cb08 Mon Sep 17 00:00:00 2001 From: Jeremy Brubaker Date: Thu, 10 Oct 2024 12:55:46 -0400 Subject: [PATCH 19/19] Add support for subshell functions Functions in the form `func () ( ... )` are documented just like the more common `{}` version. --- shdoc | 4 +- tests/testcases/@function-declaration.test.sh | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/shdoc b/shdoc index 762e63d..f013e6c 100755 --- a/shdoc +++ b/shdoc @@ -939,7 +939,7 @@ 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 @@ -955,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. diff --git a/tests/testcases/@function-declaration.test.sh b/tests/testcases/@function-declaration.test.sh index 8ca2f65..b25359b 100644 --- a/tests/testcases/@function-declaration.test.sh +++ b/tests/testcases/@function-declaration.test.sh @@ -35,6 +35,35 @@ function g function h() { echo "h"; } +# @description Subshell Function n°1 +sa() ( + echo "a" +) + +# @description Subshell Function n°2 +sb() ( echo "b" ; ) + +# @description Subshell Function n°3 +:sc() ( echo "c"; ) + +# @description Subshell Function n°4 +sd-method() +( echo "d"; ) + +# @description Subshell Function n°5 +function se:function ( echo "e"; ) + +# @description Subshell Function n°6 +function sf() ( echo "f"; ) + +# @description Subshell Function n°7 +function sg +( echo "g"; ) + +# @description Subshell Function n°8 +function sh() +( echo "h"; ) + a b :c @@ -43,6 +72,15 @@ e:function f g h + +sa +sb +:sc +sd-method +se:function +sf +sg +sh EOF tests:put expected <