# get all command names from a clean scope def command-names [] { const PLUGINS = [ nu_plugin_inc, nu_plugin_gstat, nu_plugin_query, nu_plugin_polars, nu_plugin_formats, ] let nu_dir = (which nu) | get path.0 | path dirname mut plugins = [] for plugin in $PLUGINS { if (sys host | get name) == 'Windows' { $plugins ++= $'($nu_dir | path join $plugin).exe' } else { $plugins ++= $'($nu_dir | path join $plugin)' } } nu --no-config-file --plugins ($plugins | to nuon) --commands $'scope commands | select name | to json' | from json } def html-escape [] { to html | parse --regex '(?.*)' | get html.0 } # remove invalid characters from a path # # # Examples # using the standard library # ```nushell # use std.nu # # std assert eq ("foo/bar baz/foooo" | safe-path) "foo/bar_baz/foooo" # std assert eq ("invalid ? path" | safe-path) "invalid__path" # ``` def safe-path [] { $in | str replace --all '\?' '' | str replace --all ' ' '_' } # generate the YAML frontmatter of a command # # # Examples # - the `bits` command in `commands/docs/bits.md` # ```yaml # --- # title: bits # categories: | # bits # version: 0.76.1 # bits: | # Various commands for working with bits. # usage: | # Various commands for working with bits. # --- # ``` # - the `dfr min` command in `commands/docs/dfr_min.md` # ```yaml # --- # title: dfr min # categories: | # expression # lazyframe # version: 0.76.0 # expression: | # Creates a min expression # lazyframe: | # Aggregates columns to their min value # usage: | # Creates a min expression # Aggregates columns to their min value # --- # ``` def command-frontmatter [commands_group, command_name] { let commands_list = ($commands_group | get $command_name) let category_list = ($commands_list | get category | str join $"(char newline) " ) let nu_version = (version).version let category_matter = ( $commands_list | get category | each { |category| let usage = ($commands_list | where category == $category | get usage | str join (char newline)) $'($category | str snake-case): |(char newline) ($usage)' } | str join (char newline) ) let indented_usage = ( $commands_list | get usage | each {|it| $" ($it)"} | str join (char newline) ) let feature = if $command_name =~ '^dfr' { "dataframe" } else { "default" } # This is going in the frontmatter as a multiline YAML string, so indentation matters $"--- title: ($command_name) categories: | ($category_list) version: ($nu_version) ($category_matter) usage: | ($indented_usage) ---" } # generate the whole command documentation # # TODO: be more detailed here def command-doc [command] { let top = $" # `($command.name)` for [($command.category)]\(/commands/categories/($command.category).md\)
($command.usage | html-escape)
" let columns = ($command.signatures | columns) let no_sig = ($command | get signatures | is-empty) let sig = if $no_sig { '' } else { ($command.signatures | get $columns.0 | each { |param| if $param.parameter_type == "positional" { $"('(')($param.parameter_name)(')')" } else if $param.parameter_type == "rest" { $"...rest" } } | str join " ") } let signatures = $"## Signature ```> ($command.name) {flags} ($sig)``` " let flag_types = ['named', 'switch'] let no_flags = if $no_sig { true } else { $command.signatures | get $columns.0 | where parameter_type in $flag_types | is-empty } let flags = if $no_flags { '' } else { ($command.signatures | get $columns.0 | each { |param| if $param.parameter_type == "switch" { $" - `--($param.parameter_name), -($param.short_flag)`: ($param.description)" } else if $param.parameter_type == "named" { $" - `--($param.parameter_name), -($param.short_flag) {($param.syntax_shape)}`: ($param.description)" } } | str join (char newline)) } let flags = if $no_flags { "" } else { $"## Flags ($flags) " } let param_types = ['rest', 'positional'] let no_param = if $no_sig { true } else { $command.signatures | get $columns.0 | where parameter_type in $param_types | is-empty } let params = if $no_param { '' } else { ($command.signatures | get $columns.0 | each { |param| if $param.parameter_type == "positional" { $" - `($param.parameter_name)`: ($param.description)" } else if $param.parameter_type == "rest" { $" - `...rest`: ($param.description)" } } | str join (char newline)) } let parameters = if $no_param { "" } else { $"## Parameters ($params) " } let ex = $command.extra_usage # Certain commands' extra_usage is wrapped in code block markup to prevent their code from # being interpreted as markdown. This is strictly hard-coded for now. let extra_usage = if $ex == "" { "" } else if $command.name in ['def-env' 'export def-env' 'as-date' 'as-datetime' ansi] { $"## Notes ```text ($ex) ``` " } else { $"## Notes ($ex) " } let sigs = scope commands | where name == $command.name | select signatures | get 0 | get signatures | values mut input_output = [] for s in $sigs { let input = $s | where parameter_type == 'input' | get 0 | get syntax_shape let output = $s | where parameter_type == 'output' | get 0 | get syntax_shape # FIXME: Parentheses are required here to mutate $input_output, otherwise it won't work, maybe a bug? $input_output = ($input_output | append [[input output]; [$input $output]]) } let in_out = if ($input_output | length) > 0 { let markdown = ($input_output | sort-by input | to md --pretty | str replace -a '<' '\<' | str replace -a '>' '\>') ['', '## Input/output types:', '', $markdown, ''] | str join (char newline) } else { '' } let examples = if ($command.examples | length) > 0 { let example_top = $"## Examples(char newline)(char newline)" let $examples = ( $command.examples | each { |example| let result = (do -i { $example.result | try { table --expand } catch { $in } } ) $"($example.description) ```nu > ($example.example) ($result | if ($result | describe) == "string" { ansi strip } else { $in }) ``` " } | str join ) $example_top + $examples } else { "" } # Typically a root command that has sub commands should be one word command let one_word_cmd = ($command.name | split row ' ' | length) == 1 let sub_commands = if $one_word_cmd { scope commands | where name =~ $'^($command.name) ' } else { [] } let sub_commands = if $one_word_cmd and ($sub_commands | length) > 0 { let commands = $sub_commands | select name usage type | update name {|it| $"[`($it.name)`]\(/commands/docs/($it.name | safe-path).md\)" } | upsert usage {|it| $it.usage | str replace -a '<' '\<' | str replace -a '>' '\>' } | to md --pretty ['', '## Subcommands:', '', $commands, ''] | str join (char newline) } else { '' } let features = if $command.name =~ '^dfr' { $'::: warning(char nl)Dataframe commands were not shipped in the official binaries by default, you have to build it with `--features=dataframe` flag(char nl):::(char nl)(char nl)' } else { '' } let plugins = if $command.name in ['from ini', 'from ics', 'from eml', 'from vcf'] { $"::: warning(char nl)Command `($command.name)` resides in [plugin]\(/book/plugins.html) [`nu_plugin_formats`]\(https://crates.io/crates/nu_plugin_formats). To use this command, you must install/compile and register nu_plugin_formats(char nl):::(char nl)(char nl)" } else if $command.name in ['query', 'query xml', 'query json', 'query web'] { $"::: warning(char nl)Command `($command.name)` resides in [plugin]\(/book/plugins.html) [`nu_plugin_query`]\(https://crates.io/crates/nu_plugin_query). To use this command, you must install/compile and register nu_plugin_query(char nl):::(char nl)(char nl)" } else { '' } let doc = ( ($top + $plugins + $features + $signatures + $flags + $parameters + $in_out + $examples + $extra_usage + $sub_commands) | lines | each {|it| ($it | str trim -r) } | str join (char newline) ) $doc } # generate the full documentation page of a given command # # this command will # 1. compute the frontmatter of the command, i.e. the YAML header # 2. compute the actual content of the documentation # 3. concatenate them # 4. save that to `commands/docs/.md` # # # Examples # - the `bits` command at https://nushell.sh/commands/docs/bits.html # - the `bits and` subcommand at https://nushell.sh/commands/docs/bits_and.html def generate-command [commands_group command_name] { let safe_name = ($command_name | safe-path) let doc_path = (['.', 'commands', 'docs', $'($safe_name).md'] | path join) let frontmatter = (command-frontmatter $commands_group $command_name) let note = "" let doc = ( $commands_group | get $command_name | each { |command| command-doc $command } | str join ) [$frontmatter $note $doc] | str join "\n" | save --raw --force $doc_path $doc_path } # generate the list of all categories in a TS file used by `vuepress` # # this will modify `.vuepress/configs/sidebar/command_categories.ts` # # # Example # the sidebar file has following format # ```typescript # export const commandCategories = [ # '/commands/categories/.md', # '/commands/categories/.md', # ... # ]; # ``` # and contains all the categories given by `scope commands | get category | uniq` # # this file is responsible for the sidebar containing the categories that one can see in # # https://nushell.sh/commands/ def generate-category-sidebar [unique_categories] { let sidebar_path = (['.', '.vuepress', 'configs', "sidebar", "command_categories.ts"] | path join) let list_content = ( $unique_categories | each { || safe-path } | each { |category| $" '/commands/categories/($category).md',"} | str join (char newline) ) $"export const commandCategories = [ ($list_content) ];" | save --raw --force $sidebar_path } # generate one category file in `commands/categories/` # # # Example # for the `bits` category, that might look, once rendered, like # # https://nushell.sh/commands/categories/bits.html def generate-category [category] { let safe_name = ($category | safe-path) let doc_path = (['.', 'commands', 'categories', $'($safe_name).md'] | path join) $"# ($category | str title-case)
Command Description
{{ command.title }} {{ command.frontmatter.usage }}
" | save --raw --force $doc_path $doc_path } def main [] { # Old commands are currently not deleted because some of them # are platform-specific (currently `exec`, `registry query`), and a single run of this script will not regenerate # all of them. #do -i { rm commands/docs/*.md } let commands = ( scope commands | join (command-names) name | sort-by category ) let commands_group = ($commands | group-by name) let unique_commands = ($commands_group | columns) let unique_categories = ($commands | get category | uniq) let number_generated_commands = ( $unique_commands | par-each { |command_name| generate-command $commands_group $command_name } | length ) print $"($number_generated_commands) commands written" generate-category-sidebar $unique_categories let number_generated_categories = ( $unique_categories | each { |category| generate-category $category } | length ) print $"($number_generated_categories) categories written" }