From 248ff39dbd52a8fb00ba047d2bc617efa2b60d85 Mon Sep 17 00:00:00 2001 From: Anders Ingemann Date: Fri, 10 May 2024 15:18:45 +0200 Subject: [PATCH] Include comments showing what part a specific node is parsing --- README.adoc | 57 +++++++++++++++++++++++++++++++++++++++++++++ docopt_sh/docopt.sh | 7 +++++- docopt_sh/parser.py | 10 +++++--- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/README.adoc b/README.adoc index aa7de31..a46b2a1 100644 --- a/README.adoc +++ b/README.adoc @@ -73,6 +73,7 @@ Advanced usage:: - link:#exiting-with-a-usage-message[Exiting with a usage message] - link:#library-mode[Library mode] - link:#on-the-fly-parser-generation[On-the-fly parser generation] +- link:#understanding-the-parser[Understanding the parser] Developers:: - link:#testing[Testing] @@ -314,6 +315,62 @@ Since `docopt.sh` is not patching the script, you also avoid any line number jumps in your IDE. However, remember to replace this with the proper parser before you ship the script. +=== Understanding the parser + +You can turn of minifaction with `-n 0`. This outputs the parser in its full +form. The parser and the generated AST code is heavily documented and includes +references to the analyzed DOC, showing what each part does. + +e.g. `docopt.sh -n 0 naval_fate.sh` + +[source,sh] +---- +#!/usr/bin/env bash + +DOC="Naval Fate. + ... + --speed= Speed in knots [default: 10]." +# docopt parser below, refresh this parser with `docopt.sh naval_fate.library.sh` +# shellcheck disable=2016,2086,2317 +docopt() { + ... + # This is the AST representing the parsed doc. The last node is the root. + # Options are first, as mentioned above. The comments above each node is + # shows what part of the DOC it is parsing (with line numbers). + + # 03 naval_fate.sh move [--speed=] + # ~~~~~~~ + node_0(){ + value __speed 0 + } + + # 03 naval_fate.sh move [--speed=] + # ~~~~~~ + node_1(){ + value _name_ a + } + ... + # Unset exported variables from parent shell + # that may clash with names derived from the doc + for varname in "${varnames[@]}"; do + unset "$p$varname" + done + # Assign internal varnames to output varnames and set defaults + + eval $p'__speed=${var___speed:-10};'\ + ... + +} +# docopt parser above, complete command for generating this parser is `docopt.sh --line-length=0 naval_fate.library.sh` + +naval_fate() { + eval "$(docopt "$@")" + ... +} +naval_fate "$@" +---- + + === Developers ==== Testing diff --git a/docopt_sh/docopt.sh b/docopt_sh/docopt.sh index c6d3440..bf09496 100644 --- a/docopt_sh/docopt.sh +++ b/docopt_sh/docopt.sh @@ -27,9 +27,14 @@ docopt() { # * argcount (0 or 1) # The items are space separated. The order matches the AST node numbering options=("OPTIONS") - # This is the AST representing the parsed doc. + # This is the AST representing the parsed doc. The last node is the root. + # Options are first, as mentioned above. The comments above each node is + # shows what part of the DOC it is parsing (with line numbers). + "NODES" + # Exit function that is callable from the parent shell. It outputs an + # optional error message and then prints the usage part of the doc # shellcheck disable=2016 cat <<<' docopt_exit() { [[ -n $1 ]] && printf "%s\n" "$1" >&2 diff --git a/docopt_sh/parser.py b/docopt_sh/parser.py index 0485755..c225e9f 100644 --- a/docopt_sh/parser.py +++ b/docopt_sh/parser.py @@ -71,7 +71,9 @@ def generate(self, script): ), '"DOC DIGEST"': hashlib.sha256(script.doc.untrimmed_value.encode('utf-8')).hexdigest()[0:5], '"OPTIONS"': generate_options_array(leaf_nodes), - ' "NODES"': indent('\n'.join(map(str, map(lambda n: ast_cmd(n, nodes), nodes))), level=1), + ' "NODES"': indent('\n'.join( + map(str, map(lambda n: ast_cmd(n, nodes, script.doc.trimmed_value), nodes))), level=1 + ), '"VARNAMES"': ' '.join([bash_ifs_value(var_name(node)) for node in leaf_nodes]), ' "OUTPUT VARNAMES ASSIGNMENTS"': generate_default_assignments(leaf_nodes), ' "EARLY RETURN"\n': '' if leaf_nodes else ' return 0\n', @@ -220,7 +222,7 @@ def helper_name(node): return 'switch' -def ast_cmd(node, sorted_nodes): +def ast_cmd(node, sorted_nodes, doc): idx = sorted_nodes.index(node) if isinstance(node, P.Group): if len(sorted_nodes) == 1 and isinstance(node, P.Sequence): @@ -235,7 +237,9 @@ def ast_cmd(node, sorted_nodes): args += ' ' + bash_ifs_value(idx if type(node) is P.Option else f'a:{node.ident}') if type(node.default) in [list, int]: args += ' true' - return Code(f'node_{idx}(){{\n {helper_name(node)} {args}\n}}\n') + # Show where in the DOC the parsing node originates from + marked_source = '\n'.join(map(lambda line: f'# {line}', node.mark.show(doc).split('\n'))) + return Code(f'{marked_source}\nnode_{idx}(){{\n {helper_name(node)} {args}\n}}\n') def var_name(node):