Skip to content

Commit

Permalink
Include comments showing what part a specific node is parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
andsens committed May 10, 2024
1 parent 5159895 commit 248ff39
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 4 deletions.
57 changes: 57 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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=<kn> 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 <name> move <x> <y> [--speed=<kn>]
# ~~~~~~~
node_0(){
value __speed 0
}
# 03 naval_fate.sh <name> move <x> <y> [--speed=<kn>]
# ~~~~~~
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
Expand Down
7 changes: 6 additions & 1 deletion docopt_sh/docopt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions docopt_sh/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down

0 comments on commit 248ff39

Please sign in to comment.