From ddfbb88a06765a1f274cc90fde6ae64149d20fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chastanet?= Date: Fri, 11 Aug 2023 22:37:21 +0200 Subject: [PATCH 1/4] new annotations - added warning with badge - added constraint - added require - added feature with badge - added deprecated with badge - added trap - added env - updated @example allowing to change default script language --- README.md | 183 +++++++++++++++++- shdoc | 127 +++++++++++- tests/testcases/@env.test.sh | 39 ++++ .../function-@example.format.test.sh | 33 ++++ ...cated-warning-require-trap-feature.test.sh | 60 ++++++ 5 files changed, 431 insertions(+), 11 deletions(-) create mode 100644 tests/testcases/@env.test.sh create mode 100644 tests/testcases/function-@example.format.test.sh create mode 100644 tests/testcases/function-deprecated-warning-require-trap-feature.test.sh diff --git a/README.md b/README.md index 5b9bc48..cd8ff33 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,37 @@ definitions, and creates a markdown file with ready to use documentation. ## Index -* [Example](#example) -* [Annotations](#annotations) -* [Usage](#usage) -* [Installation](#installation) -* [More examples](#examples) -* [License](#license) +- [Index](#index) +- [Example](#example) +- [Features](#features) + - [`@name`](#name) + - [`@file`](#file) + - [`@brief`](#brief) + - [`@description`](#description) + - [`@section`](#section) + - [`@example`](#example-1) + - [`@option`](#option) + - [`@arg`](#arg) + - [`@noargs`](#noargs) + - [`@set`](#set) + - [`@env`](#env) + - [`@exitcode`](#exitcode) + - [`@stdin`](#stdin) + - [`@stdout`](#stdout) + - [`@stderr`](#stderr) + - [`@see`](#see) + - [`@warning`](#warning) + - [`@require`](#require) + - [`@feature`](#feature) + - [`@trap`](#trap) + - [`@deprecated`](#deprecated) + - [`@internal`](#internal) +- [Usage](#usage) +- [Installation](#installation) + - [Arch Linux](#arch-linux) + - [Using Git](#using-git) + - [Others](#others) +- [Examples](#examples) ## Example @@ -40,7 +65,6 @@ _Output_: [examples/readme-example.md](examples/readme-example.md)

# * etc # @description My super function. -# Not thread-safe. # # @example # echo "test: $(say-hello World)" @@ -59,6 +83,20 @@ _Output_: [examples/readme-example.md](examples/readme-example.md)

# @exitcode 0 If successful. # @exitcode 1 If an empty string passed. # +# @warning Not thread-safe. +# +# @deprecated use yell-hello instead +# +# @require ubuntu>20 +# +# @trap INT EXIT HUP QUIT ABRT TERM to manage temp files removal +# +# @feature Retry::default +# +# @set HELLO_HAS_BEEN_SAID int set it to 1 if successful +# +# @env LANGUAGE string provide this variable to translate hello world in given language (default value: en-GB) +# # @see validate() # @see [shdoc](https://github.com/reconquest/shdoc). say-hello() { @@ -94,8 +132,11 @@ The project solves lots of problems: ### say-hello +[![Deprecated use yell-hello instead](https://img.shields.io/badge/Deprecated-use%20yell%2D%2Dhello%20instead-red.svg)](#) +[![Featuring Retry::default](https://img.shields.io/badge/Feature-Retry%3A%3A%3A%3Adefault-green.svg)](#) +[![1 Warning(s)](https://img.shields.io/badge/Warnings-1-yellow.svg)](#) + My super function. -Not thread-safe. #### Example @@ -117,6 +158,14 @@ echo "test: $(say-hello World)" * **$1** (string): A value to print +#### Environment variables + +* **LANGUAGE** (string): provide this variable to translate hello world in given language (default value: en-GB) + +#### Variables set + +* **HELLO_HAS_BEEN_SAID** (int): set it to 1 if successful + #### Exit codes * **0**: If successful. @@ -132,6 +181,22 @@ echo "test: $(say-hello World)" * Output 'Oups !' on error. It did it again. +#### Requires + +* ubuntu>20 + +#### Features + +* Retry::default + +#### Traps + +* INT EXIT HUP QUIT ABRT TERM to manage temp files removal + +#### Warnings + +* Not thread-safe. + #### See also * [validate()](#validate) @@ -214,6 +279,16 @@ say-hello() { } ``` +The annotation accepts one argument to override default bash language +**Example** +```bash +# @example text +# test: Hello World ! +say-hello() { + ... +} +``` + ### `@option` A description of an option expected to be passed while calling the function. @@ -277,6 +352,21 @@ set-hello() { } ``` +### `@env` + +A description of a global variable that is used during the call to this function. +Can be specified multiple times to describe any number of variables + +**Example** + +```bash +# @description Sets hello to the variable REPLY +# LANGUAGE string provide this variable to translate hello world in given language (default value: en-GB) +set-hello() { + ... +} +``` + ### `@exitcode` Describes an expected exitcode of the function. @@ -349,6 +439,83 @@ say-hello-world() { } ``` +### `@warning` + +Indicates some attention points related to the given function + +**Example** + +```bash +# @warning Performance : saying hello world to each people on Earth could lead to performance issues +say-hello-world() { + ... +} +``` + +Note that a badge will also be generated before function description indicating the number of warnings + +[![1 Warning(s)](https://img.shields.io/badge/Warnings-1-yellow.svg)](#warning) + +### `@require` + +Indicates some requirements needed by the given function + +**Example** + +```bash +# @require ubuntu>20 +say-hello-world() { + ... +} +``` + +### `@feature` + +Indicates some special features used by the given function + +**Example** + +```bash +# @feature Retry::default +# @feature sudo +say-hello-world() { + ... +} +``` + +### `@trap` + +Indicates that traps are used by the given function + +**Example** + +```bash +# @trap INT EXIT HUP QUIT ABRT TERM to manage temp files removal +say-hello-world() { + ... +} +``` + +### `@deprecated` + +Indicates that the function is deprecated + +**Example** + +```bash +# @deprecated use yell-hello-world instead +say-hello-world() { + ... +} +``` + +Note that a badge will also be generated before function description indicating the reason of the deprecation if specified + +[![Deprecated use yell-hello-world instead](https://img.shields.io/badge/Deprecated-use%20yell--hello--world%20instead-red.svg)](#warning) + +Or this simple badge if no reason is specified +[![Deprecated True](https://img.shields.io/badge/Deprecated-True-red.svg)](#warning) + ### `@internal` When you want to skip documentation generation for a particular function, you can specify this diff --git a/shdoc b/shdoc index 5fbdad3..3be1540 100755 --- a/shdoc +++ b/shdoc @@ -53,6 +53,16 @@ BEGIN { styles["github", "exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)" styles["github", "exitcode", "to"] = "**\\1**: \\2" + styles["github", "deprecated-badge", "from"] = "([^\t]+)\t(.*)" + styles["github", "deprecated-badge", "to"] = "[![Deprecated \\2](https://img.shields.io/badge/Deprecated-\\1-red.svg)](#)" + + styles["github", "feature-badge", "from"] = "([^\t]+)\t(.*)" + styles["github", "feature-badge", "to"] = "[![Featuring \\2](https://img.shields.io/badge/Feature-\\1-green.svg)](#)" + + styles["github", "warning-badge", "from"] = ".*" + styles["github", "warning-badge", "to"] = "[![& Warning(s)](https://img.shields.io/badge/Warnings-&-yellow.svg)](#)" + + stderr_section_flag = 0 debug_enable = ENVIRON["SHDOC_DEBUG"] == "1" @@ -61,8 +71,41 @@ BEGIN { debug_fd = 2 } debug_file = "/dev/fd/" debug_fd + + # declare empty arrays for url_encode + split("", url_encode_hex_tab) + split("", url_encode_ord) } +# @see https://searchcode.com/codesearch/view/77692066/ +function url_encode(str) { + if (length(url_encode_hex_tab) == 0) { + # lazy initialization + split ("1 2 3 4 5 6 7 8 9 A B C D E F", url_encode_hex_tab, " ") + url_encode_hex_tab[0] = 0 + for ( i=1; i<=255; ++i ) url_encode_ord[ sprintf ("%c", i) "" ] = i + 0 + } + len = length(str) + encoded = "" + for (i = 1; i <= len; i++) { + c = substr(str, i, 1); + if (c ~ /[0-9A-Za-z.]/) { + encoded = encoded c + } else if ( c == " " ) { + encoded = encoded "%20" # special handling + } else { + # unsafe character, encode it as a two-digit hex-number + encoded = encoded "%" sprintf("%02X", url_encode_ord[c]) + + lo = url_encode_ord [c] % 16 + hi = int (url_encode_ord [c] / 16); + encoded = encoded "%" url_encode_hex_tab[hi] url_encode_hex_tab[lo] + } + } + return encoded +} + + # @description Display the given error message with its line number on stderr. # and exit with error. # @arg $message string A error message. @@ -473,6 +516,36 @@ function render_docblock(func_name, description, docblock) { lines[0] = render("h3", func_name) } + atLeastOneBadge=0 + if ("deprecated" in docblock) { + for (i in docblock["deprecated"]) { + status=docblock["deprecated"][i] + if (status == "") { + status="True" + } + push(lines, render("deprecated-badge", url_encode(status) "\t" status)) + atLeastOneBadge=1 + } + } + if ("feature" in docblock) { + for (i in docblock["feature"]) { + status="True" + if (match(docblock["feature"][i], "^([^[:blank:]]+)", matches)) { + status=matches[1] + } + push(lines, render("feature-badge", url_encode(status) "\t" status)) + atLeastOneBadge=1 + } + } + if ("warning" in docblock) { + push(lines, render("warning-badge", length(docblock["warning"]))) + atLeastOneBadge=1 + } + if (atLeastOneBadge) { + # Add empty line to signal end of description. + push(lines, "") + } + if (description != "") { push(lines, description) # Add empty line to signal end of description. @@ -481,7 +554,7 @@ function render_docblock(func_name, description, docblock) { if ("example" in docblock) { push(lines, render("h4", "Example")) - push(lines, render("code", "bash")) + push(lines, render("code", docblock["example_format"])) push(lines, unindent(docblock["example"])) push(lines, render("/code")) push(lines, "") @@ -544,6 +617,19 @@ function render_docblock(func_name, description, docblock) { push(lines, "") } + if ("env" in docblock) { + push(lines, render("h4", "Environment variables")) + for (i in docblock["env"]) { + item = docblock["env"][i] + item = render("set", item) + item = render("li", item) + push(lines, item) + } + + # Add empty line to signal end of list in markdown. + push(lines, "") + } + if ("set" in docblock) { push(lines, render("h4", "Variables set")) for (i in docblock["set"]) { @@ -579,6 +665,11 @@ function render_docblock(func_name, description, docblock) { if ("stderr" in docblock) { render_docblock_list(docblock, "stderr", "Output on stderr") } + + render_list(docblock, "require", "Requires") + render_list(docblock, "feature", "Features") + render_list(docblock, "trap", "Traps") + render_list(docblock, "warning", "Warnings") if ("see" in docblock) { push(lines, render("h4", "See also")) @@ -595,6 +686,17 @@ function render_docblock(func_name, description, docblock) { return result } +function render_list(docblock, type, title) { + if (type in docblock) { + push(lines, render("h4", title)) + for (i in docblock[type]) { + push(lines, render("li", unindent(docblock[type][i]))) + } + # Add empty line to signal end of list in markdown. + push(lines, "") + } +} + function debug(msg) { if (debug_enable) { print "DEBUG: " msg > debug_file @@ -664,11 +766,15 @@ in_description { next } -/^[[:space:]]*# @example/ { +/^[[:space:]]*# @example.*/ { debug("→ @example") in_example = 1 - + in_example_format="bash" + if(match($0, /@example[[:space:]]*([a-z]+)/, matches)) { + debug(" → → example format " matches[1]) + in_example_format=matches[1] + } next } @@ -682,11 +788,26 @@ in_example { sub(/^[[:space:]]*#/, "") docblock_concat("example", $0) + docblock_set("example_format", in_example_format) next } } +# Select @warning lines with content. +match($0, /^[[:blank:]]*#[[:blank:]]+@(env|warning|constraint|require|feature|deprecated|trap)[[:blank:]]*(.*)$/, matches) { + type = matches[1] + # Trim text. + line = trim(matches[2]) + debug("→ @" type) + + # Add warning description to warning docblock. + docblock[type][length(docblock[type])+1] = line + + # Stop processing current line, and process next line. + next +} + # Select @option lines with content. /^[[:blank:]]*#[[:blank:]]+@option[[:blank:]]+[^[:blank:]]/ { debug("→ @option") diff --git a/tests/testcases/@env.test.sh b/tests/testcases/@env.test.sh new file mode 100644 index 0000000..30e1ba6 --- /dev/null +++ b/tests/testcases/@env.test.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +tests:put input <20 +# @trap INT EXIT HUP QUIT ABRT TERM to manage temp files removal +# @feature Retry::default +# @feature sudo +some:first:func() { +EOF + +tests:put expected <20 + +#### Features + +* Retry::default +* sudo + +#### Traps + +* INT EXIT HUP QUIT ABRT TERM to manage temp files removal + +#### Warnings + +* Performance : for multiple values to remove, prefer using some:second:func +EOF + +assert From 4e5529e8ea5b9564f1ed07ace0bb8e89514e2202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chastanet?= Date: Fri, 11 Aug 2023 23:47:51 +0200 Subject: [PATCH 2/4] update README.md example --- README.md | 23 ++++++++++------------- examples/readme-example.md | 29 ++++++++++++++++++++++++++++- examples/readme-example.sh | 17 +++++++++++++++-- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index cd8ff33..7a21088 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ definitions, and creates a markdown file with ready to use documentation. - [Index](#index) - [Example](#example) + - [Source file](#source-file) + - [Output](#output) - [Features](#features) - [`@name`](#name) - [`@file`](#file) @@ -42,17 +44,18 @@ 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)
-_Output_: [examples/readme-example.md](examples/readme-example.md)

+_Source_ [examples/readme-example.sh](examples/readme-example.sh) + +_Output_: [examples/readme-example.md](examples/readme-example.md) + +### Source file + ~~~bash #!/bin/bash # @file libexample @@ -109,9 +112,7 @@ say-hello() { } ~~~ - -
+### Output ~~~markdown # libexample @@ -204,10 +205,6 @@ echo "test: $(say-hello World)" ~~~ -
- - ## Features ### `@name` diff --git a/examples/readme-example.md b/examples/readme-example.md index 3f41e0a..b0dc37c 100644 --- a/examples/readme-example.md +++ b/examples/readme-example.md @@ -16,8 +16,11 @@ The project solves lots of problems: ### say-hello +[![Deprecated use yell-hello instead](https://img.shields.io/badge/Deprecated-use%20yell%2D%2Dhello%20instead-red.svg)](#) +[![Featuring Retry::default](https://img.shields.io/badge/Feature-Retry%3A%3A%3A%3Adefault-green.svg)](#) +[![1 Warning(s)](https://img.shields.io/badge/Warnings-1-yellow.svg)](#) + My super function. -Not thread-safe. #### Example @@ -39,6 +42,14 @@ echo "test: $(say-hello World)" * **$1** (string): A value to print +#### Environment variables + +* **LANGUAGE** (string): provide this variable to translate hello world in given language (default value: en-GB) + +#### Variables set + +* **HELLO_HAS_BEEN_SAID** (int): set it to 1 if successful + #### Exit codes * **0**: If successful. @@ -54,6 +65,22 @@ echo "test: $(say-hello World)" * Output 'Oups !' on error. It did it again. +#### Requires + +* ubuntu>20 + +#### Features + +* Retry::default + +#### Traps + +* INT EXIT HUP QUIT ABRT TERM to manage temp files removal + +#### Warnings + +* Not thread-safe. + #### See also * [validate()](#validate) diff --git a/examples/readme-example.sh b/examples/readme-example.sh index f5a3bab..3d5cc88 100644 --- a/examples/readme-example.sh +++ b/examples/readme-example.sh @@ -9,7 +9,6 @@ # * etc # @description My super function. -# Not thread-safe. # # @example # echo "test: $(say-hello World)" @@ -28,6 +27,20 @@ # @exitcode 0 If successful. # @exitcode 1 If an empty string passed. # +# @warning Not thread-safe. +# +# @deprecated use yell-hello instead +# +# @require ubuntu>20 +# +# @trap INT EXIT HUP QUIT ABRT TERM to manage temp files removal +# +# @feature Retry::default +# +# @set HELLO_HAS_BEEN_SAID int set it to 1 if successful +# +# @env LANGUAGE string provide this variable to translate hello world in given language (default value: en-GB) +# # @see validate() # @see [shdoc](https://github.com/reconquest/shdoc). say-hello() { @@ -37,4 +50,4 @@ say-hello() { fi echo "Hello $1" -} +} \ No newline at end of file From 8675e7eea5cce9c597d91c67fa345a3ff10d6452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chastanet?= Date: Sun, 3 Sep 2023 12:07:10 +0200 Subject: [PATCH 3/4] do not unindent too much in description --- shdoc | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/shdoc b/shdoc index 3be1540..a69eae9 100755 --- a/shdoc +++ b/shdoc @@ -71,7 +71,7 @@ BEGIN { debug_fd = 2 } debug_file = "/dev/fd/" debug_fd - + # declare empty arrays for url_encode split("", url_encode_hex_tab) split("", url_encode_ord) @@ -446,23 +446,23 @@ function render_docblock_list(docblock, docblock_name, title) { # long_option_regex = "--[[:alnum:]][[:alnum:]-]*((=|[[:blank:]]+)<[^>]+>)?" # pipe_separator_regex = "([[:blank:]]*\\|?[[:blank:]]+)" # description_regex = "([^[:blank:]|<-].*)?" -# +# # # Build regex matching all options # short_or_long_option_regex = sprintf("(%s|%s)", short_option_regex, long_option_regex) -# +# # # Build regex matching multiple options separated by spaces or pipe. # all_options_regex = sprintf("(%s%s)+", short_or_long_option_regex, pipe_separator_regex) -# +# # # Build final regex. # optional_arg_regex = sprintf("^(%s)%s$", all_options_regex, description_regex) # ``` -# +# # Final regex with non-matching groups (unsupported by gawk). -# +# # `^((?:(?:-[[:alnum:]](?:[[:blank:]]*<[^>]+>)?|--[[:alnum:]][[:alnum:]-]*(?:(?:=|[[:blank:]]+)<[^>]+>)?)(?:[[:blank:]]*\|?[[:blank:]]+))+)([^[:blank:]|<-].*)?$` # # @param text The text to process as an @option entry. -# +# # @set dockblock["option"] A docblock for correctly formated options. # @set dockblock["option-bad"] A docblock for badly formated options. function process_at_option(text) { @@ -665,7 +665,7 @@ function render_docblock(func_name, description, docblock) { if ("stderr" in docblock) { render_docblock_list(docblock, "stderr", "Output on stderr") } - + render_list(docblock, "require", "Requires") render_list(docblock, "feature", "Features") render_list(docblock, "trap", "Traps") @@ -750,7 +750,7 @@ in_description { } else { debug("→ → in_description: concat") sub(/^[[:space:]]*# @description[[:space:]]*/, "") - sub(/^[[:space:]]*#[[:space:]]*/, "") + sub(/^[[:space:]]*#[[:space:]]?/, "") sub(/^[[:space:]]*#$/, "") description = concat(description, $0) @@ -800,7 +800,7 @@ match($0, /^[[:blank:]]*#[[:blank:]]+@(env|warning|constraint|require|feature|de # Trim text. line = trim(matches[2]) debug("→ @" type) - + # Add warning description to warning docblock. docblock[type][length(docblock[type])+1] = line @@ -829,7 +829,7 @@ match($0, /^[[:blank:]]*#[[:blank:]]+@(env|warning|constraint|require|feature|de # Select @arg lines with content. /^[[:blank:]]*#[[:blank:]]+@arg[[:blank:]]+[^[:blank:]]/ { debug("→ @arg") - + arg_text = $0 # Remove '# @arg ' tag. @@ -911,9 +911,9 @@ multiple_line_docblock_name { # Check if current line indentation does match the previous line docblock item. if ($0 ~ multiple_line_identation_regex ) { debug("→ @" multiple_line_docblock_name " - new line") - + # Current line has the same indentation as the stderr section. - + # Remove indentation and trailing spaces. sub(/^[[:space:]]*#[[:space:]]+/, "") sub(/[[:space:]]+$/, "") @@ -1020,18 +1020,18 @@ END { print render("h1", file_title) if (file_brief != "") { - print file_brief "\n" + print file_brief "\n" } if (file_description != "") { print render("h2", "Overview") - print file_description "\n" + print file_description "\n" } } if (toc != "") { print render("h2", "Index") - print toc "\n" + print toc "\n" } print doc From 1f386d0e6e0dfca4baf1d8d1b05be1c6c74a7de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chastanet?= Date: Wed, 11 Oct 2023 00:33:51 +0200 Subject: [PATCH 4/4] fixed issue when just description and deprecated --- shdoc | 2 +- .../@description-and-@deprecated.test.sh | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/testcases/@description-and-@deprecated.test.sh diff --git a/shdoc b/shdoc index a69eae9..b120c62 100755 --- a/shdoc +++ b/shdoc @@ -741,7 +741,7 @@ function debug(msg) { } in_description { - if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]|^[[:space:]]*$/) { + if(match($0, /^[^[[:space:]]*#]|^[[:space:]]*# @([a-z]+)|^[[:space:]]*[^#]|^[[:space:]]*$/, matches) && matches[1] != "description") { debug("→ → in_description: leave") in_description = 0 diff --git a/tests/testcases/@description-and-@deprecated.test.sh b/tests/testcases/@description-and-@deprecated.test.sh new file mode 100644 index 0000000..782abb5 --- /dev/null +++ b/tests/testcases/@description-and-@deprecated.test.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +tests:put input <