Created
August 10, 2024 06:36
-
-
Save mofosyne/fd6a43116c9e1e224ff1bd8f7068c662 to your computer and use it in GitHub Desktop.
Autogenerated Documentation For Justfiles
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # Autogenerated Documentation For Justfiles | |
| # This was created to support this issue ticket https://github.com/casey/just/issues/2033#issuecomment-2278336973 | |
| import json | |
| import subprocess | |
| from typing import Any | |
| # just --dump --dump-format json --unstable | jq > test.json | |
| json_output = subprocess.run( | |
| ["just", "--dump", "--dump-format", "json", "--unstable"], | |
| check=True, | |
| capture_output=True, | |
| ).stdout | |
| def markdown_table_with_alignment_support(header_map: list[dict[str, str]], data: list[dict[str, Any]]): | |
| # JSON to Markdown table formatting: https://stackoverflow.com/a/72983854/2850957 | |
| # Alignment Utility Function | |
| def strAlign(padding: int, alignMode: str | None, strVal: str): | |
| if alignMode == 'center': | |
| return strVal.center(padding) | |
| elif alignMode == 'right': | |
| return strVal.rjust(padding - 1) + ' ' | |
| elif alignMode == 'left': | |
| return ' ' + strVal.ljust(padding - 1) | |
| else: # default left | |
| return ' ' + strVal.ljust(padding - 1) | |
| def dashAlign(padding: int, alignMode: str | None): | |
| if alignMode == 'center': | |
| return ':' + '-' * (padding - 2) + ':' | |
| elif alignMode == 'right': | |
| return '-' * (padding - 1) + ':' | |
| elif alignMode == 'left': | |
| return ':' + '-' * (padding - 1) | |
| else: # default left | |
| return '-' * (padding) | |
| # Calculate Padding For Each Column Based On Header and Data Length | |
| rowsPadding = {} | |
| for index, columnEntry in enumerate(header_map): | |
| padCount = max([len(str(v)) for d in data for k, v in d.items() if k == columnEntry['key_name']], default=0) + 2 | |
| headerPadCount = len(columnEntry['header_name']) + 2 | |
| rowsPadding[index] = headerPadCount if padCount <= headerPadCount else padCount | |
| # Render Markdown Header | |
| rows = [] | |
| rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(columnEntry['header_name'])) for index, columnEntry in enumerate(header_map))) | |
| rows.append('|'.join(dashAlign(rowsPadding[index], columnEntry.get('align')) for index, columnEntry in enumerate(header_map))) | |
| # Render Tabular Data | |
| for item in data: | |
| rows.append('|'.join(strAlign(rowsPadding[index], columnEntry.get('align'), str(item[columnEntry['key_name']])) for index, columnEntry in enumerate(header_map))) | |
| # Convert Tabular String Rows Into String | |
| tableString = "" | |
| for row in rows: | |
| tableString += f'|{row}|\n' | |
| return tableString | |
| def recipe_one_line_short(stuff): | |
| command_args = stuff["name"] | |
| if len(stuff["parameters"]) > 0: | |
| for parameter_entry in stuff["parameters"]: | |
| parameter_name = parameter_entry["name"].upper() | |
| if parameter_entry["kind"] == "singular": | |
| if parameter_entry["default"] is not None: | |
| # Singular Argument. Default value used if missing | |
| command_args += f" {{{parameter_name}}}" | |
| else: | |
| # Singular Argument. Must provide value | |
| command_args += f" <{parameter_name}>" | |
| elif parameter_entry["kind"] == "plus": | |
| # One or more arguments | |
| command_args += f" [{parameter_name}... 1 or more]" | |
| elif parameter_entry["kind"] == "star": | |
| # Zero or more arguments (optional option) | |
| command_args += f" [{parameter_name}... 0 or more]" | |
| return command_args | |
| def captialise_sentences(text: str): | |
| lines = text.split('. ') | |
| for index, line in enumerate(lines): | |
| if len(line) > 1: | |
| lines[index] = line[0].upper() + line[1:] | |
| return '. '.join(lines) | |
| data = json.loads(json_output) | |
| recipes = data["recipes"] | |
| print("# Justfile Autogenerated Documentation") | |
| print("") | |
| print("## Recipe List") | |
| print("- Legend") | |
| print(" - `{ARG}` : Singular Argument. Default Value Avaliable If Missing") | |
| print(" - `<ARG>` : Singular Argument. Must Provide Value") | |
| print(" - `[ARG... 1 or more]` : Varidict Argument. Must Provide At Least One Value") | |
| print(" - `[ARG... 0 or more]` : Varidict Argument. Optionally Provide Multiple Values") | |
| # Main Recipes | |
| print("") | |
| print("### Main Recipe") | |
| recipe_list_table: list[dict[str, str | int]] = [] | |
| for recipe_name, stuff in recipes.items(): | |
| if recipe_name[0] == "_": | |
| continue | |
| recipe_list_table.append({"recipe_name":f"[`{recipe_one_line_short(stuff)}`](#{recipe_name})", "doc":captialise_sentences(stuff.get('doc') or "")}) | |
| recipe_list_header_map = [ | |
| {'key_name':'recipe_name', 'header_name':'Recipe', 'align':'left'}, | |
| {'key_name':'doc', 'header_name':'Description', 'align':'left'} | |
| ] | |
| print(markdown_table_with_alignment_support(recipe_list_header_map, recipe_list_table)) | |
| # Internal Recipes | |
| print("") | |
| print("### Internal Recipe") | |
| recipe_list_table: list[dict[str, str | int]] = [] | |
| for recipe_name, stuff in recipes.items(): | |
| if recipe_name[0] != "_": | |
| continue | |
| recipe_list_table.append({"recipe_name":f"[`{recipe_one_line_short(stuff)}`](#{recipe_name})", "doc":stuff.get('doc') or ""}) | |
| recipe_list_header_map = [ | |
| {'key_name':'recipe_name', 'header_name':'Recipe', 'align':'left'}, | |
| {'key_name':'doc', 'header_name':'Description', 'align':'left'} | |
| ] | |
| print(markdown_table_with_alignment_support(recipe_list_header_map, recipe_list_table)) | |
| print("") | |
| print("---") | |
| print("") | |
| print("## Recipe Details") | |
| for recipe_name, stuff in recipes.items(): | |
| print("") | |
| print(f"## <a id=\"{recipe_name}\"> {recipe_name} </a>") | |
| print(f"- Command: `{recipe_one_line_short(stuff)}`") | |
| if len(stuff["parameters"]) > 0: | |
| print(f"- Arguments:") | |
| for parameter_entry in stuff["parameters"]: | |
| print(f' - `{parameter_entry["name"].upper()}`') | |
| if parameter_entry["kind"] == "plus": | |
| print(f' - One or more arguments') | |
| elif parameter_entry["kind"] == "star": | |
| print(f' - Zero or more arguments') | |
| if parameter_entry["default"] is not None: | |
| if isinstance(parameter_entry["default"], str): | |
| print(f' - Default Value: {parameter_entry["default"]}') | |
| elif isinstance(parameter_entry["default"], list): | |
| if parameter_entry["default"][0] == "evaluate": | |
| print(f' - Default Value: evaluate(`{parameter_entry["default"][1]}`)') | |
| else: | |
| print(f' - Default Value: ??(`{parameter_entry["default"][1]}`)') | |
| else: | |
| print(f' - Default Value: `{parameter_entry["default"]}`)') | |
| if len(stuff["dependencies"]) > 0: | |
| print("- Dependencies:") | |
| for dependent_recipe_name in [d["recipe"] for d in recipes.get(recipe_name)["dependencies"]]: | |
| print(f' - [{dependent_recipe_name}](#{dependent_recipe_name})') | |
| # Print description | |
| if stuff["doc"] is not None: | |
| print("") | |
| print(captialise_sentences(stuff.get("doc", ""))) | |
| elif (stuff["body"] is None or len(stuff["body"]) <= 0) and (len(stuff["dependencies"]) > 0): | |
| # No code but has dependencies, likely a meta command | |
| print("") | |
| print(f"Run all {recipe_name} recipies") | |
| if stuff["body"] is not None and len(stuff["body"]) > 0: | |
| print("") | |
| print("```bash") | |
| for body_line in stuff["body"]: | |
| line = "" | |
| for piece in body_line: | |
| if isinstance(piece, str): | |
| line += piece | |
| elif piece[0][0] == "variable": | |
| line += f"{{{{{piece[0][1]}}}}}" | |
| elif piece[0][0] == "call": | |
| line += f"{{{{{piece[0][1]}()}}}}" | |
| else: | |
| line += f"{piece}" | |
| print(line) | |
| print("```") | |
| if len(stuff["dependencies"]) > 0: | |
| print("") | |
| print("```mermaid") | |
| print("graph LR") | |
| def call_graph_gen(recipe_name): | |
| if recipe_name not in recipes: | |
| return | |
| stuff = recipes.get(recipe_name) | |
| dependencies = [d["recipe"] for d in stuff["dependencies"]] | |
| for d in dependencies: | |
| print(f' {recipe_name} --> {d}') | |
| print(f' click {d} "#{d}"') | |
| call_graph_gen(d) | |
| call_graph_gen(recipe_name) | |
| print("```") |
Author
This is nice. After trying, it appears this currently does not recognise modules or groups, am I correct?
A workaround can be to build documentation for each module individually.
Author
Well it's a proof of concept, so not expecting this to cover everything. But yeah, all these are good ideas. But that would require bit more standardization of the justfile json output I think.
If you want more progress, best to throw your effort at this ticket casey/just#2033 (comment) @almereyda
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is how the above will generate the below document from just's justfile itself:
Justfile Autogenerated Documentation
Recipe List
{ARG}: Singular Argument. Default Value Avaliable If Missing<ARG>: Singular Argument. Must Provide Value[ARG... 1 or more]: Varidict Argument. Must Provide At Least One Value[ARG... 0 or more]: Varidict Argument. Optionally Provide Multiple ValuesMain Recipe
buildbuild-bookcheckciclippyconvert-integration-test <TEST>done {BRANCH}filter <PATTERN>fmtforbidfuzzgenerate-completionsinstallinstall-dev-depsmanpolyglotprpublishpushpwdquinereadme-version-notesrender-readmereplace <FROM> <TO>runshellchecksloctesttest-completionstest-quineupdate-changelogupdate-contributorsview-manwatch [ARGS... 1 or more]watch-readmeInternal Recipe
_js_nu_perl_python_ruby_shRecipe Details
_js
_js_nu
_nu_perl
_perl_python
_python_ruby
_ruby_sh
_shbuild
buildbuild-book
build-bookcheck
checkgraph LR check --> fmt click fmt "#fmt" check --> clippy click clippy "#clippy" check --> test click test "#test" check --> forbid click forbid "#forbid"ci
cicargo test --all cargo clippy --all --all-targets -- --deny warnings cargo fmt --all -- --check ./bin/forbid cargo update --locked --package justgraph LR ci --> build-book click build-book "#build-book"clippy
clippyEveryone's favorite animate paper clip
convert-integration-test
convert-integration-test <TEST>TESTcargo expand --test integration {{test}} | \ sed \ -E \ -e 's/#\[cfg\(test\)\]/#\[test\]/' \ -e 's/^ *let test = //' \ -e 's/^ *test[.]/./' \ -e 's/;$//' \ -e 's/crate::test::Test/Test/' \ -e 's/\.run\(\)/.run();/'done
done {BRANCH}BRANCHgit rev-parse --abbrev-ref HEAD)Clean up feature branch BRANCH
git checkout master git diff --no-ext-diff --quiet --exit-code git pull --rebase github master git diff --no-ext-diff --quiet --exit-code {{BRANCH}} git branch -D {{BRANCH}}filter
filter <PATTERN>PATTERNOnly run tests matching PATTERN
cargo test {{PATTERN}}fmt
fmtforbid
forbidfuzz
fuzzgenerate-completions
generate-completionsinstall
installInstall just from crates.io
install-dev-deps
install-dev-depsInstall development dependencies
man
mancargo run -- --man > man/just.1polyglot
polyglotRun all polyglot recipes
graph LR polyglot --> _python click _python "#_python" polyglot --> _js click _js "#_js" polyglot --> _perl click _perl "#_perl" polyglot --> _sh click _sh "#_sh" polyglot --> _ruby click _ruby "#_ruby"pr
prgraph LR pr --> push click push "#push" push --> check click check "#check" check --> fmt click fmt "#fmt" check --> clippy click clippy "#clippy" check --> test click test "#test" check --> forbid click forbid "#forbid"publish
publishPublish current GitHub master branch
push
pushgraph LR push --> check click check "#check" check --> fmt click fmt "#fmt" check --> clippy click clippy "#clippy" check --> test click test "#test" check --> forbid click forbid "#forbid"pwd
pwdPrint working directory, for demonstration purposes!
quine
quineMake a quine, compile it, and verify it
readme-version-notes
readme-version-notesgrep '<sup>master</sup>' README.mdrender-readme
render-readmereplace
replace <FROM> <TO>FROMTOrun
runshellcheck
shellchecksloc
slocCount non-empty lines of code
test
testcargo testtest-completions
test-completionstest-quine
test-quineupdate-changelog
update-changelogAdd git log messages to changelog
update-contributors
update-contributorsview-man
view-mangraph LR view-man --> man click man "#man"watch
watch [ARGS... 1 or more]ARGScargo watch --clear --exec '{{args}}'watch-readme
watch-readmejust render-readme fswatch -ro README.adoc | xargs -n1 -I{} just render-readme