├── .gitattributes ├── .gitignore ├── z_examples ├── 99_strip_markdown │ ├── example.nu │ ├── nushell_readme.nu │ ├── error-with-try.nu │ ├── raw_strings_test.nu │ ├── simple_markdown_first_block.nu │ ├── simple_markdown.nu │ ├── simple_nu_table.nu │ ├── numd_commands_explanations.nu │ ├── types_of_data.nu │ └── working_with_lists.nu ├── 6_edge_cases │ ├── error_customized.md │ ├── simple_markdown_first_block.md │ ├── raw_strings_test.md │ ├── error-with-try.md_intermed.nu │ ├── error-with-try.md │ ├── raw_strings_test.md_intermed.nu │ └── simple_markdown_first_block.md_intermed.nu ├── 8_parse_frontmatter │ ├── example.md │ ├── example.md_intermed.nu │ └── dotnu-test.nu ├── 999_numd_internals │ ├── create-execution-code_0.nu │ └── gen-execute-code_0.nu ├── 1_simple_markdown │ ├── simple_markdown.md │ ├── simple_markdown_with_no_output.md │ └── simple_markdown.md_intermed.nu ├── 9_other │ ├── parse-markdown.nu │ ├── nushell_readme.md_intermed.nu │ └── nushell_readme.md ├── 5_simple_nu_table │ ├── simple_nu_table_customized_width20.md │ ├── simple_nu_table.md │ ├── simple_nu_table_customized_example_config.md │ └── simple_nu_table.md_intermed.nu ├── 2_numd_commands_explanations │ ├── numd_commands_explanations.md_intermed.nu │ └── numd_commands_explanations.md └── 4_book_working_with_lists │ ├── working_with_lists.md │ └── working_with_lists.md_intermed.nu ├── .editorconfig ├── numd ├── nu-utils │ ├── mod.nu │ ├── str repeat.nu │ ├── parse.nu │ └── cprint.nu ├── plumbing.nu ├── mod.nu ├── parse.nu ├── parse-help.nu ├── capture.nu └── commands.nu ├── nupm.nuon ├── numd_config_example2.nu ├── numd_config_example1.nu ├── .github └── workflows │ ├── ci.yml │ └── claude.yml ├── LICENSE ├── todo └── 20251216-163414-debloat.md ├── toolkit.nu ├── CLAUDE.md ├── README.md └── tests └── test_commands.nu /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zzz_md_backups 2 | .claude/ 3 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/example.nu: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/nushell_readme.nu: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | -------------------------------------------------------------------------------- /z_examples/6_edge_cases/error_customized.md: -------------------------------------------------------------------------------- 1 | ```nushell 2 | lssomething 3 | ``` 4 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/error-with-try.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nushell try, new-instance 3 | lssomething 4 | -------------------------------------------------------------------------------- /numd/nu-utils/mod.nu: -------------------------------------------------------------------------------- 1 | export module "cprint.nu" 2 | export module "str repeat.nu" 3 | export module "numd-internals.nu" 4 | -------------------------------------------------------------------------------- /z_examples/8_parse_frontmatter/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: real example 3 | date: 2025-11-01 4 | --- 5 | 6 | # Hello, world 7 | 8 | It's me. I love you. 9 | -------------------------------------------------------------------------------- /numd/nu-utils/str repeat.nu: -------------------------------------------------------------------------------- 1 | export def main [ 2 | $n: int 3 | ]: string -> string { 4 | let text = $in 5 | seq 1 $n | each { $text } | str join 6 | } 7 | -------------------------------------------------------------------------------- /nupm.nuon: -------------------------------------------------------------------------------- 1 | { 2 | name: numd, 3 | type: module, 4 | version: "0.2.2", 5 | description: "reproducible Nushell Markdown documents", 6 | license: Unlicense 7 | } -------------------------------------------------------------------------------- /numd/plumbing.nu: -------------------------------------------------------------------------------- 1 | # Low-level pipeline commands for advanced usage and scripting 2 | # Use: `use numd/plumbing.nu` 3 | 4 | export use commands.nu [ 5 | parse-file 6 | strip-outputs 7 | execute-blocks 8 | to-markdown 9 | to-numd-script 10 | ] 11 | -------------------------------------------------------------------------------- /numd/nu-utils/parse.nu: -------------------------------------------------------------------------------- 1 | alias core_parse = parse 2 | 3 | export def main [ 4 | file: path 5 | ] { 6 | open $file 7 | | lines 8 | | enumerate 9 | | each {|i| $i.item | parse -r '^(?:(?#+) (?.*))?(?```)?' | get 0 | merge $i } 10 | } 11 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/raw_strings_test.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nu 3 | let $two_single_lines_text = r#'"High up in the mountains, a Snake crawled and lay in a damp gorge, coiled 4 | into a knot, staring out at the sea.'# 5 | 6 | 7 | # ```nu 8 | $two_single_lines_text 9 | -------------------------------------------------------------------------------- /z_examples/999_numd_internals/create-execution-code_0.nu: -------------------------------------------------------------------------------- 1 | "ls | sort-by modified -r" | nu-highlight | print 2 | 3 | "```\n```output-numd" | print 4 | 5 | ls | sort-by modified -r | table | lines | each {$'// ($in)' | str trim --right} | str join (char nl) | table | print; print '' 6 | -------------------------------------------------------------------------------- /z_examples/999_numd_internals/gen-execute-code_0.nu: -------------------------------------------------------------------------------- 1 | "ls | sort-by modified -r" | nu-highlight | print 2 | 3 | "```\n```output-numd" | print 4 | 5 | ls | sort-by modified -r | table | into string | lines | each {$'// ($in)' | str trim --right} | str join (char nl) | print; print '' 6 | -------------------------------------------------------------------------------- /numd/mod.nu: -------------------------------------------------------------------------------- 1 | export use commands.nu [ 2 | run 3 | clear-outputs 4 | list-fence-options 5 | ] 6 | 7 | export use capture.nu [ 8 | 'capture start' 9 | 'capture stop' 10 | ] 11 | 12 | export use parse-help.nu 13 | export use parse.nu [ 'parse-frontmatter' 'to md-with-frontmatter' ] 14 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/simple_markdown_first_block.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nu 3 | let $var1 = 'foo' 4 | 5 | 6 | # ```nu 7 | # This block will produce some output in a separate block 8 | $var1 | path join 'baz' 'bar' 9 | 10 | 11 | # ```nu 12 | # This block will output results inline 13 | whoami 14 | 15 | 2 + 2 16 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/simple_markdown.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nu 3 | let $var1 = 'foo' 4 | 5 | 6 | # ```nu separate-block 7 | # This block will produce some output in a separate block 8 | $var1 | path join 'baz' 'bar' 9 | 10 | 11 | # ```nu 12 | # This block will output results inline 13 | whoami 14 | 15 | 2 + 2 16 | -------------------------------------------------------------------------------- /numd_config_example2.nu: -------------------------------------------------------------------------------- 1 | # numd config example 2 2 | # This file is prepended to the intermediate script before execution 3 | 4 | $env.config.footer_mode = 'never' 5 | $env.config.table.header_on_separator = true 6 | $env.config.table.mode = 'rounded' 7 | $env.config.table.abbreviated_row_count = 10000 8 | 9 | # Set custom table width (overrides default 120) 10 | $env.numd.table-width = 120 11 | -------------------------------------------------------------------------------- /z_examples/6_edge_cases/simple_markdown_first_block.md: -------------------------------------------------------------------------------- 1 | ```nu 2 | let $var1 = 'foo' 3 | ``` 4 | 5 | ## Example 2 6 | 7 | ```nu 8 | # This block will produce some output in a separate block 9 | $var1 | path join 'baz' 'bar' 10 | # => foo/baz/bar 11 | ``` 12 | 13 | ## Example 3 14 | 15 | ```nu 16 | # This block will output results inline 17 | whoami 18 | # => user 19 | 20 | 2 + 2 21 | # => 4 22 | ``` 23 | -------------------------------------------------------------------------------- /z_examples/6_edge_cases/raw_strings_test.md: -------------------------------------------------------------------------------- 1 | raw strings test 2 | 3 | ```nu 4 | let $two_single_lines_text = r#'"High up in the mountains, a Snake crawled and lay in a damp gorge, coiled 5 | into a knot, staring out at the sea.'# 6 | ``` 7 | 8 | ```nu 9 | $two_single_lines_text 10 | # => "High up in the mountains, a Snake crawled and lay in a damp gorge, coiled 11 | # => into a knot, staring out at the sea. 12 | ``` 13 | -------------------------------------------------------------------------------- /numd_config_example1.nu: -------------------------------------------------------------------------------- 1 | # numd config example 1 2 | # This file is prepended to the intermediate script before execution 3 | 4 | $env.config.footer_mode = 'always' 5 | $env.config.table = { 6 | mode: rounded 7 | index_mode: never 8 | show_empty: false 9 | padding: {left: 1, right: 1} 10 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 11 | header_on_separator: true 12 | abbreviated_row_count: 1000 13 | } 14 | -------------------------------------------------------------------------------- /z_examples/1_simple_markdown/simple_markdown.md: -------------------------------------------------------------------------------- 1 | # This is a simple markdown example 2 | 3 | ## Example 1 4 | 5 | the block below will be executed as it is, but won't yield any output 6 | 7 | ```nu 8 | let $var1 = 'foo' 9 | ``` 10 | 11 | ## Example 2 12 | 13 | ```nu separate-block 14 | # This block will produce some output in a separate block 15 | $var1 | path join 'baz' 'bar' 16 | ``` 17 | 18 | Output: 19 | 20 | ``` 21 | # => foo/baz/bar 22 | ``` 23 | 24 | ## Example 3 25 | 26 | ```nu 27 | # This block will output results inline 28 | whoami 29 | # => user 30 | 31 | 2 + 2 32 | # => 4 33 | ``` 34 | 35 | ## Example 4 36 | 37 | ``` 38 | # This block doesn't have a language identifier in the opening fence 39 | ``` 40 | -------------------------------------------------------------------------------- /z_examples/9_other/parse-markdown.nu: -------------------------------------------------------------------------------- 1 | use std/iter scan 2 | 3 | open nushell_readme.md 4 | | lines 5 | | each { str substring ..20 } 6 | | wrap content 7 | | enumerate 8 | | flatten 9 | | insert code {|i| 10 | $i.content 11 | | parse -r '^\s*(?```\w*)' 12 | | get 0? 13 | } 14 | | insert h {|i| 15 | $i.content 16 | | parse -r '^(?#+)\s?(.*)$' 17 | | get 0? 18 | } 19 | | flatten 20 | | scan {in_code: false} {|line state| 21 | let new_state = if ($line.code? != null) { 22 | {in_code: (not $state.in_code)} 23 | } else { 24 | $state 25 | } 26 | $line | insert in_code $new_state.in_code 27 | } 28 | | update in_code {|i| if $i.code? == '```' { true } else $i.in_code? } 29 | -------------------------------------------------------------------------------- /z_examples/9_other/nushell_readme.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | -------------------------------------------------------------------------------- /z_examples/1_simple_markdown/simple_markdown_with_no_output.md: -------------------------------------------------------------------------------- 1 | # This is a simple markdown example 2 | 3 | ## Example 1 4 | 5 | the block below will be executed as it is, but won't yield any output 6 | 7 | ```nu 8 | let $var1 = 'foo' 9 | ``` 10 | 11 | ## Example 2 12 | 13 | ```nu separate-block 14 | # This block will produce some output in a separate block 15 | $var1 | path join 'baz' 'bar' 16 | ``` 17 | 18 | Output: 19 | 20 | ``` 21 | # => foo/baz/bar 22 | ``` 23 | 24 | ## Example 3 25 | 26 | ```nu 27 | # This block will output results inline 28 | whoami 29 | # => user 30 | 31 | 2 + 2 32 | # => 4 33 | ``` 34 | 35 | ## Example 4 36 | 37 | ``` 38 | # This block doesn't have a language identifier in the opening fence 39 | ``` 40 | -------------------------------------------------------------------------------- /z_examples/8_parse_frontmatter/example.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | -------------------------------------------------------------------------------- /z_examples/8_parse_frontmatter/dotnu-test.nu: -------------------------------------------------------------------------------- 1 | use ../../numd 2 | 3 | let parse = open example.md 4 | | numd parse-frontmatter 5 | 6 | $parse | print $in 7 | 8 | # => ╭─────────┬──────────────────────╮ 9 | # => │ title │ real example │ 10 | # => │ date │ 2025-11-01 │ 11 | # => │ content │ │ 12 | # => │ │ # Hello, world │ 13 | # => │ │ │ 14 | # => │ │ It's me. I love you. │ 15 | # => │ │ │ 16 | # => ╰─────────┴──────────────────────╯ 17 | 18 | $parse | numd to md-with-frontmatter | print $in 19 | 20 | # => --- 21 | # => title: real example 22 | # => date: 2025-11-01 23 | # => --- 24 | # => 25 | # => # Hello, world 26 | # => 27 | # => It's me. I love you. 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**.md' 8 | - 'LICENSE' 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Nushell 26 | uses: hustcer/setup-nu@v3 27 | with: 28 | version: "*" 29 | 30 | - name: Install nutest 31 | run: git clone https://github.com/vyadh/nutest.git ../nutest 32 | 33 | - name: Run tests 34 | run: nu toolkit.nu test --json 35 | 36 | - name: Show diff of changed files 37 | if: always() 38 | run: git diff 39 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/simple_nu_table.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nushell 3 | $env.numd? 4 | 5 | 6 | # ```nushell 7 | [[a b c]; [1 2 3]] 8 | 9 | 10 | # ```nushell 11 | [[column long_text]; 12 | ['value_1' ('Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.' + 13 | 'Eu aute lorem ullamco sed ipsum incididunt irure. Lorem ullamco sed ipsum incididunt.' + 14 | 'Sed ipsum incididunt irure, culpa. Irure, culpa labore sit sunt.')] 15 | ['value_2' ('Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip ' + 16 | 'elit lorem, ut quis nostrud. Lorem ut quis, nostrud commodo non. Nostrud commodo non ' + 17 | 'cillum exercitation dolore fugiat nulla. Non cillum exercitation dolore fugiat nulla ' + 18 | 'ut. Exercitation dolore fugiat nulla ut adipiscing laboris elit. Fugiat nulla ut ' + 19 | 'adipiscing, laboris elit quis pariatur. Adipiscing laboris elit quis pariatur. ' + 20 | 'Elit quis pariatur, in ut anim anim ut.')]] 21 | -------------------------------------------------------------------------------- /z_examples/6_edge_cases/error-with-try.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | 22 | "#code-block-marker-open-2 23 | ```nushell try, new-instance" | print 24 | "lssomething" | nu-highlight | print 25 | 26 | /Users/user/.cargo/bin/nu -c "lssomething" | complete | if ($in.exit_code != 0) {get stderr} else {get stdout} | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 27 | print '' 28 | "```" | print 29 | -------------------------------------------------------------------------------- /numd/parse.nu: -------------------------------------------------------------------------------- 1 | # Parse frontmatter from markdown, output record 2 | export def 'parse-frontmatter' [ 3 | file?: path # path to a markdow file. Might be ommited if markdown content is piped in 4 | ]: [string -> record nothing -> record] { 5 | let input = if $file == null { } else { open $file } 6 | | if $in != null { } else { 7 | error make {msg: 'no path or content of file were provided'} 8 | } 9 | 10 | let list = $input | split row "---\n" --number 3 11 | 12 | # it means there is no frontmatter 13 | if $list.0 != '' { return {content: $input} } 14 | 15 | let yaml = $list.1 | from yaml 16 | 17 | $yaml | insert content $list.2 18 | } 19 | 20 | alias core_to_md = to md 21 | 22 | # produce markdown from record. All fields except of `content` go to frontmatter. 23 | # And content becomes the body 24 | export def 'to md-with-frontmatter' []: record -> string { 25 | let input = $in 26 | 27 | $input | columns | if $in == ['content'] { return $input.content } 28 | 29 | let frontmatter = $input | reject --optional content | to yaml 30 | 31 | '' 32 | | append $frontmatter 33 | | append ($input.content? | default '') 34 | | str join "---\n" 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /z_examples/6_edge_cases/error-with-try.md: -------------------------------------------------------------------------------- 1 | ```nushell no-run 2 | lssomething 3 | ╭───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ 4 | │ msg │ External command failed │ 5 | │ debug │ ExternalCommand { label: "Command `lssomething` not found", help: "`lssomething` is neither a Nushell built-in or a known external command", span: Span { start: 1967901, end: 1967912 } } │ 6 | │ raw │ ExternalCommand { label: "Command `lssomething` not found", help: "`lssomething` is neither a Nushell built-in or a known external command", span: Span { start: 1967901, end: 1967912 } } │ 7 | ╰───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 8 | ``` 9 | 10 | ```nushell try, new-instance 11 | lssomething 12 | # => Error: nu::shell::external_command 13 | # => 14 | # => x External command failed 15 | # => ,-[source:1:1] 16 | # => 1 | lssomething 17 | # => : ^^^^^|^^^^^ 18 | # => : `-- Command `lssomething` not found 19 | # => `---- 20 | # => help: `lssomething` is neither a Nushell built-in or a known external 21 | # => command 22 | # => 23 | ``` 24 | -------------------------------------------------------------------------------- /z_examples/6_edge_cases/raw_strings_test.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | 22 | "#code-block-marker-open-1 23 | ```nu" | print 24 | "let $two_single_lines_text = r#'\"High up in the mountains, a Snake crawled and lay in a damp gorge, coiled 25 | into a knot, staring out at the sea.'#" | nu-highlight | print 26 | 27 | let $two_single_lines_text = r#'"High up in the mountains, a Snake crawled and lay in a damp gorge, coiled 28 | into a knot, staring out at the sea.'# 29 | print '' 30 | "```" | print 31 | 32 | "#code-block-marker-open-3 33 | ```nu" | print 34 | "$two_single_lines_text" | nu-highlight | print 35 | 36 | $two_single_lines_text | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 37 | print '' 38 | "```" | print 39 | -------------------------------------------------------------------------------- /z_examples/5_simple_nu_table/simple_nu_table_customized_width20.md: -------------------------------------------------------------------------------- 1 | ```nushell 2 | $env.numd? 3 | # => ╭─────────────┬────╮ 4 | # => │ table-width │ 20 │ 5 | # => ╰─────────────┴────╯ 6 | ``` 7 | 8 | ```nushell 9 | [[a b c]; [1 2 3]] 10 | # => ╭───┬───┬───┬───╮ 11 | # => │ # │ a │ b │ c │ 12 | # => ├───┼───┼───┼───┤ 13 | # => │ 0 │ 1 │ 2 │ 3 │ 14 | # => ╰───┴───┴───┴───╯ 15 | ``` 16 | 17 | ```nushell 18 | [[column long_text]; 19 | ['value_1' ('Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.' + 20 | 'Eu aute lorem ullamco sed ipsum incididunt irure. Lorem ullamco sed ipsum incididunt.' + 21 | 'Sed ipsum incididunt irure, culpa. Irure, culpa labore sit sunt.')] 22 | ['value_2' ('Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip ' + 23 | 'elit lorem, ut quis nostrud. Lorem ut quis, nostrud commodo non. Nostrud commodo non ' + 24 | 'cillum exercitation dolore fugiat nulla. Non cillum exercitation dolore fugiat nulla ' + 25 | 'ut. Exercitation dolore fugiat nulla ut adipiscing laboris elit. Fugiat nulla ut ' + 26 | 'adipiscing, laboris elit quis pariatur. Adipiscing laboris elit quis pariatur. ' + 27 | 'Elit quis pariatur, in ut anim anim ut.')]] 28 | # => ╭───┬────────┬─────╮ 29 | # => │ # │ column │ ... │ 30 | # => ├───┼────────┼─────┤ 31 | # => │ 0 │ value_ │ ... │ 32 | # => │ │ 1 │ │ 33 | # => │ 1 │ value_ │ ... │ 34 | # => │ │ 2 │ │ 35 | # => ╰───┴────────┴─────╯ 36 | ``` 37 | -------------------------------------------------------------------------------- /z_examples/5_simple_nu_table/simple_nu_table.md: -------------------------------------------------------------------------------- 1 | ```nushell 2 | $env.numd? 3 | ``` 4 | 5 | ```nushell 6 | [[a b c]; [1 2 3]] 7 | # => ╭─a─┬─b─┬─c─╮ 8 | # => │ 1 │ 2 │ 3 │ 9 | # => ╰─a─┴─b─┴─c─╯ 10 | ``` 11 | 12 | ```nushell 13 | [[column long_text]; 14 | ['value_1' ('Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.' + 15 | 'Eu aute lorem ullamco sed ipsum incididunt irure. Lorem ullamco sed ipsum incididunt.' + 16 | 'Sed ipsum incididunt irure, culpa. Irure, culpa labore sit sunt.')] 17 | ['value_2' ('Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip ' + 18 | 'elit lorem, ut quis nostrud. Lorem ut quis, nostrud commodo non. Nostrud commodo non ' + 19 | 'cillum exercitation dolore fugiat nulla. Non cillum exercitation dolore fugiat nulla ' + 20 | 'ut. Exercitation dolore fugiat nulla ut adipiscing laboris elit. Fugiat nulla ut ' + 21 | 'adipiscing, laboris elit quis pariatur. Adipiscing laboris elit quis pariatur. ' + 22 | 'Elit quis pariatur, in ut anim anim ut.')]] 23 | # => ╭─column──┬─────────────────────────────────────────────────long_text──────────────────────────────────────────────────╮ 24 | # => │ value_1 │ Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.Eu aute lorem ullamco sed ipsu... │ 25 | # => │ value_2 │ Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip elit lorem, ut quis n... │ 26 | # => ╰─column──┴─────────────────────────────────────────────────long_text──────────────────────────────────────────────────╯ 27 | ``` 28 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/numd_commands_explanations.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nu 3 | # This setting is for overriding the author's usual small number of `abbreviated_row_count`. 4 | $env.config.table.abbreviated_row_count = 100 5 | # The `$init_numd_pwd_const` constant points to the current working directory from where the `numd` command was initiated. 6 | # It is added by `numd` in every intermediate script to make it available in cases like below. 7 | # We use `path join` here to construct working paths for both Windows and Unix 8 | use ($init_numd_pwd_const | path join numd commands.nu) * 9 | 10 | 11 | # ```nu 12 | # Here we set the `$file` variable (which will be used in several commands throughout this script) to point to `z_examples/1_simple_markdown/simple_markdown.md`. 13 | let $file = $init_numd_pwd_const | path join z_examples 1_simple_markdown simple_markdown.md 14 | let $md_orig = open -r $file | convert-output-fences 15 | let $original_md_table = $md_orig | parse-markdown-to-blocks 16 | 17 | $original_md_table | table -e --width 120 18 | 19 | 20 | # ```nu 21 | # Here we emulate that the `$intermed_script_path` options is not set 22 | let $intermediate_script_path = $file 23 | | build-modified-path --prefix $'numd-temp-(generate-timestamp)' --suffix '.nu' 24 | 25 | decorate-original-code-blocks $original_md_table 26 | | generate-intermediate-script 27 | | save -f $intermediate_script_path 28 | 29 | open $intermediate_script_path 30 | 31 | 32 | # ```nu 33 | # the flag `$no_fail_on_error` is set to false 34 | let $no_fail_on_error = false 35 | let $nu_res_stdout_lines = execute-intermediate-script $intermediate_script_path $no_fail_on_error false false 36 | rm $intermediate_script_path 37 | 38 | $nu_res_stdout_lines 39 | 40 | 41 | # ```nu 42 | let $md_res = $nu_res_stdout_lines 43 | | str join (char nl) 44 | | clean-markdown 45 | 46 | $md_res 47 | 48 | 49 | # ```nu 50 | compute-change-stats $file $md_orig $md_res 51 | -------------------------------------------------------------------------------- /todo/20251216-163414-debloat.md: -------------------------------------------------------------------------------- 1 | --- 2 | status: draft 3 | created: 20251216-163414 4 | updated: 20251216-163414 5 | --- 6 | I would like to make the numd module more lean and to adhere closer to unix philosopy. 7 | I've identfied that we can get rid of the flags: 8 | 9 | ## Flags to remove from `numd run` 10 | 11 | ### 1. `--save-ansi` ✅ CORRECT 12 | --save-ansi # save ANSI formatted version 13 | 14 | Replace with: 15 | `numd run test.md --echo | save test.ans` 16 | 17 | **Analysis**: Logic is sound. `--echo` already outputs ANSI to stdout. 18 | 19 | --- 20 | 21 | ### 2. `--no-save` ⚠️ REQUIRES CODE CHANGE 22 | --no-save # do not save changes to the `.md` file 23 | 24 | Replace with: 25 | `numd run save.md --echo` 26 | 27 | **Analysis**: Currently `--echo` does NOT block saving (lines 62-65 in commands.nu). 28 | This requires a design change: when `--echo` is used, numd should NOT save to file. 29 | This is the right unix approach (output to stdout OR file, not both). 30 | 31 | --- 32 | 33 | ### 3. `--no-backup` ✅ CORRECT 34 | --no-backup # overwrite the existing `.md` file without backup 35 | 36 | Get rid of backup functionality completely. Instead, check that: 37 | - File is tracked by git 38 | - File has no uncommitted changes 39 | 40 | If either check fails, warn the user. No backup directory (`zzz_md_backups`) needed. 41 | 42 | **Analysis**: Sound approach. Git is the backup system. 43 | 44 | --- 45 | 46 | ### 4. `--result-md-path (-o)` ✅ CORRECT 47 | --result-md-path (-o): path # path to a resulting `.md` file; if omitted, updates the original file 48 | 49 | Replace with: 50 | `numd run test.md --echo | ansi strip | save test_diff.md` 51 | 52 | **Analysis**: Once `--echo` implies no-save, this is achievable via piping. 53 | 54 | --- 55 | 56 | 57 | ### 7. `--result-md-path` in `clear-outputs` - REMOVE 58 | --result-md-path (-o): path # in clear-outputs command 59 | 60 | Same logic applies: 61 | `numd clear-outputs file.md --echo | save other.md` 62 | 63 | -------------------------------------------------------------------------------- /z_examples/6_edge_cases/simple_markdown_first_block.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | 22 | "#code-block-marker-open-0 23 | ```nu" | print 24 | "let $var1 = 'foo'" | nu-highlight | print 25 | 26 | let $var1 = 'foo' 27 | print '' 28 | "```" | print 29 | 30 | "#code-block-marker-open-2 31 | ```nu" | print 32 | "# This block will produce some output in a separate block 33 | $var1 | path join 'baz' 'bar'" | nu-highlight | print 34 | 35 | # This block will produce some output in a separate block 36 | $var1 | path join 'baz' 'bar' | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 37 | print '' 38 | "```" | print 39 | 40 | "#code-block-marker-open-4 41 | ```nu" | print 42 | "# This block will output results inline 43 | whoami" | nu-highlight | print 44 | 45 | # This block will output results inline 46 | whoami | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 47 | print '' 48 | "2 + 2" | nu-highlight | print 49 | 50 | 2 + 2 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 51 | print '' 52 | "```" | print 53 | -------------------------------------------------------------------------------- /z_examples/1_simple_markdown/simple_markdown.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | 22 | "#code-block-marker-open-1 23 | ```nu" | print 24 | "let $var1 = 'foo'" | nu-highlight | print 25 | 26 | let $var1 = 'foo' 27 | print '' 28 | "```" | print 29 | 30 | "#code-block-marker-open-3 31 | ```nu separate-block" | print 32 | "# This block will produce some output in a separate block 33 | $var1 | path join 'baz' 'bar'" | nu-highlight | print 34 | 35 | "```\n```output-numd" | print 36 | 37 | # This block will produce some output in a separate block 38 | $var1 | path join 'baz' 'bar' | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 39 | print '' 40 | "```" | print 41 | 42 | "#code-block-marker-open-6 43 | ```nu" | print 44 | "# This block will output results inline 45 | whoami" | nu-highlight | print 46 | 47 | # This block will output results inline 48 | whoami | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 49 | print '' 50 | "2 + 2" | nu-highlight | print 51 | 52 | 2 + 2 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 53 | print '' 54 | "```" | print 55 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: write # Allow creating commits/branches 23 | pull-requests: write # Allow commenting and creating PRs 24 | issues: write # Allow commenting on issues 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@v1 36 | with: 37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 | 39 | # This is an optional setting that allows Claude to read CI results on PRs 40 | additional_permissions: | 41 | actions: read 42 | 43 | # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. 44 | # prompt: 'Update the pull request description to include a summary of changes.' 45 | 46 | # Optional: Add claude_args to customize behavior and configuration 47 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 48 | # or https://code.claude.com/docs/en/cli-reference for available options 49 | # claude_args: '--allowed-tools Bash(gh pr:*)' 50 | -------------------------------------------------------------------------------- /numd/parse-help.nu: -------------------------------------------------------------------------------- 1 | # Beautify and adapt the standard `--help` for markdown output 2 | export def main [ 3 | --sections: list # filter to only include these sections (e.g., ['Usage', 'Flags']) 4 | --record # return result as a record instead of formatted string 5 | ]: string -> any { 6 | let help_lines = split row '======================' 7 | | first # quick fix for https://github.com/nushell/nushell/issues/13470 8 | | ansi strip 9 | | str replace --all 'Search terms:' "Search terms:\n" 10 | | str replace --all ': (optional)' ' (optional)' 11 | | lines 12 | | str trim 13 | | if ($in.0 != 'Usage:') { prepend 'Description:' } else { } 14 | 15 | let regex = [ 16 | Description 17 | "Search terms" 18 | Usage 19 | Subcommands 20 | Flags 21 | Parameters 22 | "Input/output types" 23 | Examples 24 | ] 25 | | str join '|' 26 | | '^(' + $in + '):' 27 | 28 | let existing_sections = $help_lines 29 | | where $it =~ $regex 30 | | str trim --right --char ':' 31 | | wrap chapter 32 | 33 | let elements = $help_lines 34 | | split list -r $regex 35 | | skip 36 | | wrap elements 37 | 38 | $existing_sections 39 | | merge $elements 40 | | transpose --as-record --ignore-titles --header-row 41 | | if ($in.Flags? == null) { } else { update 'Flags' { where $it !~ '-h, --help' } } 42 | | if ($in.Flags? | length) == 1 { reject 'Flags' } else { } # todo now flags contain fields with empty row 43 | | if ($in.Description? | default '' | split list '' | length) > 1 { 44 | let input = $in 45 | 46 | $input 47 | | update Description ($input.Description | take until { $in == '' } | append '') 48 | | upsert Examples {|i| $i.Examples? | append ($input.Description | skip until { $in == '' } | skip) } 49 | } else { } 50 | | if $sections == null { } else { select -o ...$sections } 51 | | if $record { 52 | items {|k v| 53 | {$k: ($v | str join (char nl))} 54 | } 55 | | into record 56 | } else { 57 | items {|k v| 58 | $v 59 | | str replace -r '^\s*(\S)' ' $1' # add two spaces before description lines 60 | | str join (char nl) 61 | | $"($k):\n($in)" 62 | } 63 | | str join (char nl) 64 | | str replace -ar '\s+$' '' # empty trailing new lines 65 | | str replace -arm '^' '# => ' 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /z_examples/5_simple_nu_table/simple_nu_table_customized_example_config.md: -------------------------------------------------------------------------------- 1 | ```nushell 2 | $env.numd? 3 | # => ╭─────────────┬─────╮ 4 | # => │ table-width │ 120 │ 5 | # => ╰─────────────┴─────╯ 6 | ``` 7 | 8 | ```nushell 9 | [[a b c]; [1 2 3]] 10 | # => ╭─#─┬─a─┬─b─┬─c─╮ 11 | # => │ 0 │ 1 │ 2 │ 3 │ 12 | # => ╰───┴───┴───┴───╯ 13 | ``` 14 | 15 | ```nushell 16 | [[column long_text]; 17 | ['value_1' ('Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.' + 18 | 'Eu aute lorem ullamco sed ipsum incididunt irure. Lorem ullamco sed ipsum incididunt.' + 19 | 'Sed ipsum incididunt irure, culpa. Irure, culpa labore sit sunt.')] 20 | ['value_2' ('Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip ' + 21 | 'elit lorem, ut quis nostrud. Lorem ut quis, nostrud commodo non. Nostrud commodo non ' + 22 | 'cillum exercitation dolore fugiat nulla. Non cillum exercitation dolore fugiat nulla ' + 23 | 'ut. Exercitation dolore fugiat nulla ut adipiscing laboris elit. Fugiat nulla ut ' + 24 | 'adipiscing, laboris elit quis pariatur. Adipiscing laboris elit quis pariatur. ' + 25 | 'Elit quis pariatur, in ut anim anim ut.')]] 26 | # => ╭─#─┬─column──┬───────────────────────────────────────────────long_text────────────────────────────────────────────────╮ 27 | # => │ 0 │ value_1 │ Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.Eu aute lorem ullamco sed │ 28 | # => │ │ │ ipsum incididunt irure. Lorem ullamco sed ipsum incididunt.Sed ipsum incididunt irure, culpa. Irure, │ 29 | # => │ │ │ culpa labore sit sunt. │ 30 | # => │ 1 │ value_2 │ Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip elit lorem, ut quis │ 31 | # => │ │ │ nostrud. Lorem ut quis, nostrud commodo non. Nostrud commodo non cillum exercitation dolore fugiat │ 32 | # => │ │ │ nulla. Non cillum exercitation dolore fugiat nulla ut. Exercitation dolore fugiat nulla ut adipiscing │ 33 | # => │ │ │ laboris elit. Fugiat nulla ut adipiscing, laboris elit quis pariatur. Adipiscing laboris elit quis │ 34 | # => │ │ │ pariatur. Elit quis pariatur, in ut anim anim ut. │ 35 | # => ╰───┴─────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────╯ 36 | ``` 37 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/types_of_data.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nushell 3 | 42 | describe 4 | 5 | 6 | # ```nushell 7 | "-5" | into int 8 | 9 | 10 | # ```nushell 11 | "1.2" | into float 12 | 13 | 14 | # ```nushell 15 | let mybool = 2 > 1 16 | $mybool 17 | 18 | let mybool = ($nu.home-path | path exists) 19 | $mybool 20 | 21 | 22 | # ```nushell 23 | 3.14day 24 | 25 | 26 | # ```nushell 27 | 30day / 1sec # How many seconds in 30 days? 28 | 29 | 30 | # ```nushell 31 | 1Gb / 1b 32 | 33 | 1Gib / 1b 34 | 35 | (1Gib / 1b) == 2 ** 30 36 | 37 | 38 | # ```nushell 39 | 0x[1F FF] # Hexadecimal 40 | 41 | 0b[1 1010] # Binary 42 | 43 | 0o[377] # Octal 44 | 45 | 46 | # ```nushell 47 | {name: sam rank: 10} 48 | 49 | 50 | # ```nushell 51 | {x:3 y:1} | insert z 0 52 | 53 | 54 | # ```nushell 55 | {name: sam, rank: 10} | transpose key value 56 | 57 | 58 | # ```nushell 59 | {x:12 y:4}.x 60 | 61 | 62 | # ```nushell 63 | {"1":true " ":false}." " 64 | 65 | 66 | # ```nushell 67 | let data = { name: alice, age: 50 } 68 | { ...$data, hobby: cricket } 69 | 70 | 71 | # ```nushell 72 | [sam fred george] 73 | 74 | 75 | # ```nushell 76 | [bell book candle] | where ($it =~ 'b') 77 | 78 | 79 | # ```nushell 80 | [a b c].1 81 | 82 | 83 | # ```nushell 84 | [a b c d e f] | slice 1..3 85 | 86 | 87 | # ```nushell 88 | let x = [1 2] 89 | [...$x 3 ...(4..7 | take 2)] 90 | 91 | 92 | # ```nushell 93 | [[column1, column2]; [Value1, Value2] [Value3, Value4]] 94 | 95 | 96 | # ```nushell 97 | [{name: sam, rank: 10}, {name: bob, rank: 7}] 98 | 99 | 100 | # ```nushell 101 | [{x:12, y:5}, {x:3, y:6}] | get 0 102 | 103 | 104 | # ```nushell 105 | [[x,y];[12,5],[3,6]] | get 0 106 | 107 | 108 | # ```nushell 109 | [{x:12 y:5} {x:4 y:7} {x:2 y:2}].x 110 | 111 | 112 | # ```nushell 113 | [{x:0 y:5 z:1} {x:4 y:7 z:3} {x:2 y:2 z:0}] | select y z 114 | 115 | 116 | # ```nushell 117 | [{x:0 y:5 z:1} {x:4 y:7 z:3} {x:2 y:2 z:0}] | select 1 2 118 | 119 | 120 | # ```nushell 121 | [{foo: 123}, {}].foo? 122 | 123 | 124 | # ```nushell 125 | # Assign a closure to a variable 126 | let greet = { |name| print $"Hello ($name)"} 127 | 128 | do $greet "Julian" 129 | 130 | 131 | # ```nushell 132 | mut x = 1 133 | if true { 134 | $x += 1000 135 | } 136 | 137 | print $x 138 | 1001 139 | 1001 140 | 1001 141 | 142 | 143 | # ```nushell try,new-instance 144 | [{a:1 b:2} {b:1}] 145 | 146 | [{a:1 b:2} {b:1}].1.a 147 | -------------------------------------------------------------------------------- /numd/capture.nu: -------------------------------------------------------------------------------- 1 | use nu-utils/cprint.nu 2 | use commands.nu clean-markdown 3 | 4 | # start capturing commands and their outputs into a file 5 | export def --env 'capture start' [ 6 | file: path = 'numd_capture.md' 7 | --separate-blocks # create separate code blocks for each pipeline instead of inline `# =>` output 8 | ]: nothing -> nothing { 9 | cprint $'numd commands capture has been started. 10 | Commands and their outputs of the current nushell instance 11 | will be appended to the *($file)* file. 12 | 13 | Beware that your `display_output` hook has been changed. 14 | It will be reverted when you use `numd capture stop`' 15 | 16 | $env.numd.status = 'running' 17 | $env.numd.path = ($file | path expand) 18 | $env.numd.separate-blocks = $separate_blocks 19 | 20 | if not $separate_blocks { "```nushell\n" | save -a $env.numd.path } 21 | 22 | $env.backup.hooks.display_output = ( 23 | $env.config.hooks?.display_output? 24 | | default { 25 | if (term size).columns >= 100 { table -e } else { table } 26 | } 27 | ) 28 | 29 | $env.config.hooks.display_output = { 30 | let input = $in 31 | let command = history | last | get command 32 | 33 | $input 34 | | default '' 35 | | if (term size).columns >= 100 { table -e } else { table } 36 | | into string 37 | | ansi strip 38 | | default (char nl) 39 | | if $env.numd.separate-blocks { 40 | $"```nushell\n($command)\n```\n```output-numd\n($in)\n```\n\n" 41 | | str replace --regex --all "[\n\r ]+```\n" "\n```\n" 42 | } else { 43 | # inline output format: command followed by `# =>` prefixed output 44 | let output_lines = $in | lines | each { $'# => ($in)' } | str join (char nl) 45 | $"($command)\n($output_lines)\n\n" 46 | } 47 | | str replace --regex "\n{3,}$" "\n\n" 48 | | if ($in !~ 'numd capture') { 49 | # don't save numd capture managing commands 50 | save --append --raw $env.numd.path 51 | } 52 | 53 | print -n $input # without the `-n` flag new line is added to an output 54 | } 55 | } 56 | 57 | # stop capturing commands and their outputs 58 | export def --env 'capture stop' []: nothing -> nothing { 59 | $env.config.hooks.display_output = $env.backup.hooks.display_output 60 | 61 | let file = $env.numd.path 62 | 63 | if not $env.numd.separate-blocks { 64 | $"(open $file)```\n" 65 | | clean-markdown 66 | | save --force $file 67 | } 68 | 69 | cprint $'numd commands capture to the *($file)* file has been stopped.' 70 | 71 | $env.numd.status = 'stopped' 72 | } 73 | -------------------------------------------------------------------------------- /numd/nu-utils/cprint.nu: -------------------------------------------------------------------------------- 1 | # export def main [] {} 2 | 3 | use 'str repeat.nu' 4 | 5 | # Print the string colorfully with bells and whistles. 6 | export def main [ 7 | ...text_args 8 | --color (-c): any = 'default' 9 | --highlight_color (-h): any = 'green_bold' 10 | --frame_color (-r): any = 'dark_gray' 11 | --frame (-f): string = '' # A symbol (or a string) to frame a text 12 | --before (-b): int = 0 # A number of new lines before a text 13 | --after (-a): int = 1 # A number of new lines after a text 14 | --echo (-e) # Echo text string instead of printing 15 | --keep_single_breaks # Don't remove single line breaks 16 | --width (-w): int = 80 # The width of text to format it 17 | --indent (-i): int = 0 18 | --err_msg # produce a record with an error message 19 | ] { 20 | let width_safe = ( 21 | term size 22 | | get columns 23 | | [$in $width] | math min 24 | | [$in 40] | math max # term size gives 0 in tests 25 | ) 26 | 27 | def wrapit [] { 28 | $in 29 | | str replace -r -a '(?m)^[\t ]+' '' 30 | | if $keep_single_breaks { } else { 31 | str replace -r -a '(\n[\t ]*(\n[\t ]*)+)' '⏎' 32 | | str replace -r -a '\n' ' ' # remove single line breaks used for code formatting 33 | | str replace -a '⏎' "\n\n" 34 | } 35 | | str replace -r -a '[\t ]+$' '' 36 | | str replace -r -a $"\(.{1,($width_safe - $indent)}\)\(\\s|$\)|\(.{1,($width_safe - $indent)}\)" "$1$3\n" 37 | | str replace -r $'(char nl)$' '' # trailing new line 38 | | str replace -r -a '(?m)^(.)' $'((char sp) | str repeat $indent)$1' 39 | } 40 | 41 | def colorit [] { 42 | str replace -r -a '\*([\s\S]+?)\*' $'(ansi reset)(ansi $highlight_color)$1(ansi reset)(ansi $color)' 43 | | $'(ansi $color)($in)(ansi reset)' 44 | } 45 | 46 | def frameit [] { 47 | let text = $in; 48 | let width_frame = ( 49 | $width_safe 50 | | ($in // ($frame | str length)) 51 | | [$in 1] | math max 52 | ) 53 | 54 | let frame_line = ( 55 | ' ' 56 | | fill -a r -w $width_frame -c $frame 57 | | $'(ansi $frame_color)($in)(ansi reset)' 58 | ) 59 | 60 | ( 61 | $frame_line + "\n" + $text + "\n" + $frame_line 62 | ) 63 | } 64 | 65 | def newlineit [] { 66 | $"((char nl) | str repeat $before)($in)((char nl) | str repeat $after)" 67 | } 68 | 69 | ( 70 | $text_args 71 | | str join ' ' 72 | | wrapit 73 | | colorit 74 | | if $frame != '' { 75 | frameit 76 | } else { } 77 | | newlineit 78 | | if $err_msg { 79 | {msg: $in} 80 | } else if $echo { 81 | } else { 82 | print -n $in 83 | } 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /z_examples/5_simple_nu_table/simple_nu_table.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | 22 | "#code-block-marker-open-0 23 | ```nushell" | print 24 | "$env.numd?" | nu-highlight | print 25 | 26 | $env.numd? | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 27 | print '' 28 | "```" | print 29 | 30 | "#code-block-marker-open-2 31 | ```nushell" | print 32 | "[[a b c]; [1 2 3]]" | nu-highlight | print 33 | 34 | [[a b c]; [1 2 3]] | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 35 | print '' 36 | "```" | print 37 | 38 | "#code-block-marker-open-4 39 | ```nushell" | print 40 | "[[column long_text]; 41 | ['value_1' ('Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.' + 42 | 'Eu aute lorem ullamco sed ipsum incididunt irure. Lorem ullamco sed ipsum incididunt.' + 43 | 'Sed ipsum incididunt irure, culpa. Irure, culpa labore sit sunt.')] 44 | ['value_2' ('Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip ' + 45 | 'elit lorem, ut quis nostrud. Lorem ut quis, nostrud commodo non. Nostrud commodo non ' + 46 | 'cillum exercitation dolore fugiat nulla. Non cillum exercitation dolore fugiat nulla ' + 47 | 'ut. Exercitation dolore fugiat nulla ut adipiscing laboris elit. Fugiat nulla ut ' + 48 | 'adipiscing, laboris elit quis pariatur. Adipiscing laboris elit quis pariatur. ' + 49 | 'Elit quis pariatur, in ut anim anim ut.')]]" | nu-highlight | print 50 | 51 | [[column long_text]; 52 | ['value_1' ('Veniam cillum et et. Et et qui enim magna. Qui enim, magna eu aute lorem.' + 53 | 'Eu aute lorem ullamco sed ipsum incididunt irure. Lorem ullamco sed ipsum incididunt.' + 54 | 'Sed ipsum incididunt irure, culpa. Irure, culpa labore sit sunt.')] 55 | ['value_2' ('Irure quis magna ipsum anim. Magna ipsum anim aliquip elit lorem ut. Anim aliquip ' + 56 | 'elit lorem, ut quis nostrud. Lorem ut quis, nostrud commodo non. Nostrud commodo non ' + 57 | 'cillum exercitation dolore fugiat nulla. Non cillum exercitation dolore fugiat nulla ' + 58 | 'ut. Exercitation dolore fugiat nulla ut adipiscing laboris elit. Fugiat nulla ut ' + 59 | 'adipiscing, laboris elit quis pariatur. Adipiscing laboris elit quis pariatur. ' + 60 | 'Elit quis pariatur, in ut anim anim ut.')]] | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 61 | print '' 62 | "```" | print 63 | -------------------------------------------------------------------------------- /z_examples/99_strip_markdown/working_with_lists.nu: -------------------------------------------------------------------------------- 1 | 2 | # ```nu 3 | [bell book candle] | where ($it =~ 'b') 4 | 5 | 6 | # ```nu 7 | [1, 2, 3, 4] | insert 2 10 8 | 9 | # [1, 2, 10, 3, 4] 10 | 11 | 12 | # ```nu 13 | [1, 2, 3, 4] | update 1 10 14 | 15 | # [1, 10, 3, 4] 16 | 17 | 18 | # ```nu 19 | let colors = [yellow green] 20 | let colors = ($colors | prepend red) 21 | let colors = ($colors | append purple) 22 | let colors = ("black" | append $colors) 23 | $colors # [black red yellow green purple blue] 24 | 25 | 26 | # ```nu 27 | let colors = [red yellow green purple] 28 | let colors = ($colors | skip 1) 29 | let colors = ($colors | drop 2) 30 | $colors # [yellow] 31 | 32 | 33 | # ```nu 34 | let colors = [red yellow green purple black magenta] 35 | let colors = ($colors | last 3) 36 | $colors # [purple black magenta] 37 | 38 | 39 | # ```nu 40 | let colors = [yellow green purple] 41 | let colors = ($colors | first 2) 42 | $colors # [yellow green] 43 | 44 | 45 | # ```nu 46 | let x = [1 2] 47 | [ ...$x 3 ...(4..7 | take 2) ] 48 | 49 | 50 | # ```nu 51 | let names = [Mark Tami Amanda Jeremy] 52 | $names | each { |elt| $"Hello, ($elt)!" } 53 | # Outputs "Hello, Mark!" and three more similar lines. 54 | 55 | $names | enumerate | each { |elt| $"($elt.index + 1) - ($elt.item)" } 56 | # Outputs "1 - Mark", "2 - Tami", etc. 57 | 58 | 59 | # ```nu 60 | let colors = [red orange yellow green blue purple] 61 | $colors | where ($it | str ends-with 'e') 62 | # The block passed to `where` must evaluate to a boolean. 63 | # This outputs the list [orange blue purple]. 64 | 65 | 66 | # ```nu 67 | let scores = [7 10 8 6 7] 68 | $scores | where $it > 7 # [10 8] 69 | 70 | 71 | # ```nu 72 | let scores = [3 8 4] 73 | $"total = ($scores | reduce { |elt, acc| $acc + $elt })" # total = 15 74 | $"total = ($scores | math sum)" # easier approach, same result 75 | $"product = ($scores | reduce --fold 1 { |elt, acc| $acc * $elt })" # product = 96 76 | $scores | enumerate | reduce --fold 0 { |elt, acc| $acc + $elt.index * $elt.item } # 0*3 + 1*8 + 2*4 = 16 77 | 78 | 79 | # ```nu 80 | let names = [Mark Tami Amanda Jeremy] 81 | $names.1 # gives Tami 82 | 83 | 84 | # ```nu 85 | let names = [Mark Tami Amanda Jeremy] 86 | let index = 1 87 | $names | get $index # gives Tami 88 | 89 | 90 | # ```nu 91 | let colors = [red green blue] 92 | $colors | is-empty # false 93 | 94 | let colors = [] 95 | $colors | is-empty # true 96 | 97 | 98 | # ```nu 99 | let colors = [red green blue] 100 | 'blue' in $colors # true 101 | 'yellow' in $colors # false 102 | 'gold' not-in $colors # true 103 | 104 | 105 | # ```nu 106 | let colors = [red green blue] 107 | # Do any color names end with "e"? 108 | $colors | any {|elt| $elt | str ends-with "e" } # true 109 | 110 | # Is the length of any color name less than 3? 111 | $colors | any {|elt| ($elt | str length) < 3 } # false 112 | 113 | let scores = [3 8 4] 114 | # Are any scores greater than 7? 115 | $scores | any {|elt| $elt > 7 } # true 116 | 117 | # Are any scores odd? 118 | $scores | any {|elt| $elt mod 2 == 1 } # true 119 | 120 | 121 | # ```nu 122 | let colors = [red green blue] 123 | # Do all color names end with "e"? 124 | $colors | all {|elt| $elt | str ends-with "e" } # false 125 | 126 | # Is the length of all color names greater than or equal to 3? 127 | $colors | all {|elt| ($elt | str length) >= 3 } # true 128 | 129 | let scores = [3 8 4] 130 | # Are all scores greater than 7? 131 | $scores | all {|elt| $elt > 7 } # false 132 | 133 | # Are all scores even? 134 | $scores | all {|elt| $elt mod 2 == 0 } # false 135 | 136 | 137 | # ```nu 138 | [1 [2 3] 4 [5 6]] | flatten # [1 2 3 4 5 6] 139 | 140 | [[1 2] [3 [4 5 [6 7 8]]]] | flatten | flatten | flatten # [1 2 3 4 5 6 7 8] 141 | 142 | 143 | # ```nu 144 | let zones = [UTC CET Europe/Moscow Asia/Yekaterinburg] 145 | # Show world clock for selected time zones 146 | let base_time = '2024-01-15 12:00:00' | into datetime --timezone UTC 147 | $zones | wrap 'Zone' | upsert Time {|row| ($base_time | date to-timezone $row.Zone | format date '%Y.%m.%d %H:%M')} 148 | -------------------------------------------------------------------------------- /toolkit.nu: -------------------------------------------------------------------------------- 1 | const numdinternals = ([numd commands.nu] | path join) 2 | use $numdinternals [ build-modified-path compute-change-stats ] 3 | 4 | export def main [] { } 5 | 6 | # Run all tests (unit + integration) 7 | export def 'main test' [ 8 | --json # output results as JSON for external consumption 9 | ] { 10 | let unit = main test-unit --quiet=$json 11 | let integration = main test-integration 12 | 13 | {unit: $unit integration: $integration} 14 | | if $json { to json --raw } else { } 15 | } 16 | 17 | # Run unit tests using nutest 18 | export def 'main test-unit' [ 19 | --json # output results as JSON for external consumption 20 | --quiet # suppress terminal output (for use when called from main test) 21 | ] { 22 | use ../nutest/nutest 23 | 24 | let display = if ($json or $quiet) { 'nothing' } else { 'terminal' } 25 | nutest run-tests --path tests/ --returns summary --display $display 26 | | if $json { to json --raw } else { } 27 | } 28 | 29 | # Run integration tests (execute example markdown files) 30 | export def 'main test-integration' [ 31 | --json # output results as JSON for external consumption 32 | ] { 33 | use numd 34 | 35 | # will be executed if dotnu-embeds-are-available 36 | update-dotnu-embeds 37 | 38 | # path join is used for windows compatability 39 | let path_simple_table = [z_examples 5_simple_nu_table simple_nu_table.md] | path join 40 | 41 | # clear outputs from simple markdown 42 | let simple_md = ['z_examples' '1_simple_markdown' 'simple_markdown.md'] | path join 43 | numd clear-outputs $simple_md --echo 44 | | save -f ($simple_md | build-modified-path --suffix '_with_no_output') 45 | 46 | # I use a long chain of `append` here to obtain a table with statistics on updates upon exit. 47 | 48 | # Strip markdown and run main set of .md files in one loop 49 | glob z_examples/*/*.md --exclude [ 50 | */*_with_no_output* 51 | */*_customized* 52 | */8_parse_frontmatter 53 | ] 54 | | par-each --keep-order {|file| 55 | # Strip markdown 56 | let strip_markdown_path = $file 57 | | path parse 58 | | get stem 59 | | $in + '.nu' 60 | | [z_examples 99_strip_markdown $in] 61 | | path join 62 | 63 | numd clear-outputs $file --strip-markdown --echo 64 | | save -f $strip_markdown_path 65 | 66 | # Run files with config set 67 | ( 68 | numd run $file --save-intermed-script $'($file)_intermed.nu' 69 | --config-path numd_config_example1.nu 70 | ) 71 | } 72 | # Run file with customized width of table 73 | | append (do { 74 | let target = $path_simple_table | build-modified-path --suffix '_customized_width20' 75 | let orig = open $path_simple_table 76 | numd run $path_simple_table --echo --no-stats --table-width 20 77 | | ansi strip 78 | | save -f $target 79 | compute-change-stats $target $orig (open $target) 80 | }) 81 | # Run file with another config 82 | | append (do { 83 | let target = $path_simple_table | build-modified-path --suffix '_customized_example_config' 84 | let orig = open $path_simple_table 85 | numd run $path_simple_table --echo --no-stats --config-path 'numd_config_example2.nu' 86 | | ansi strip 87 | | save -f $target 88 | compute-change-stats $target $orig (open $target) 89 | }) 90 | # Run readme 91 | | append ( 92 | numd run README.md --config-path numd_config_example1.nu 93 | ) 94 | | if $json { to json --raw } else { } 95 | } 96 | 97 | def update-dotnu-embeds [] { 98 | scope modules 99 | | where name == 'dotnu' 100 | | is-empty 101 | | if $in { return } 102 | 103 | dotnu embeds-update z_examples/8_parse_frontmatter/dotnu-test.nu 104 | } 105 | 106 | export def 'main release' [ 107 | --major (-M) # Bump major version (X.0.0) 108 | --minor (-m) # Bump minor version (x.Y.0) 109 | ] { 110 | git checkout main 111 | 112 | let description = gh repo view --json description | from json | get description 113 | let parts = git tag | lines | sort --natural | last | split row '.' | into int 114 | let tag = if $major { 115 | [($parts.0 + 1) 0 0] 116 | } else if $minor { 117 | [$parts.0 ($parts.1 + 1) 0] 118 | } else { 119 | [$parts.0 $parts.1 ($parts.2 + 1)] 120 | } | str join '.' 121 | 122 | open nupm.nuon 123 | | update description ($description | str replace 'numd - ' '') 124 | | update version $tag 125 | | to nuon --indent 2 126 | | save --force --raw nupm.nuon 127 | 128 | git add nupm.nuon 129 | git commit -m $'($tag) nupm version' 130 | git tag $tag 131 | git push origin main --tags 132 | } 133 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | numd is a Nushell module for creating reproducible Markdown documents. It executes Nushell code blocks within markdown files and writes results back to the document. 8 | 9 | ## Common Commands 10 | 11 | ```nushell 12 | # Run numd on a markdown file (updates file with execution results) 13 | use numd; numd run README.md 14 | 15 | # Preview mode (output to stdout, don't save) 16 | use numd; numd run README.md --echo 17 | 18 | # Run tests (executes all example files and reports changes) 19 | nu toolkit.nu test --json 20 | 21 | # Clear outputs from a markdown file 22 | use numd; numd clear-outputs path/to/file.md 23 | 24 | # Strip markdown to get pure Nushell script 25 | use numd; numd clear-outputs path/to/file.md --strip-markdown --echo 26 | ``` 27 | 28 | ## Architecture 29 | 30 | ### Module Structure (`numd/`) 31 | 32 | - **mod.nu**: Entry point exporting user-friendly commands (`run`, `clear-outputs`, etc.) 33 | - **plumbing.nu**: Low-level pipeline commands for advanced usage/scripting 34 | - **commands.nu**: Core implementation containing all command logic 35 | - **capture.nu**: `capture start/stop` commands for interactive session recording 36 | - **parse-help.nu**: `parse-help` command for formatting --help output 37 | - **parse.nu**: Frontmatter parsing utilities (`parse-frontmatter`, `to md-with-frontmatter`) 38 | - **nu-utils/**: Helper utilities (`cprint.nu`, `str repeat.nu`) 39 | 40 | ### Plumbing Commands 41 | 42 | Low-level composable commands (import via `use numd/plumbing.nu`): 43 | 44 | ```nushell 45 | use numd/plumbing.nu 46 | 47 | # Parse markdown file into blocks table 48 | plumbing parse-file file.md 49 | 50 | # Strip output lines (# =>) from blocks 51 | plumbing parse-file file.md | plumbing strip-outputs 52 | 53 | # Execute code blocks and update with results 54 | plumbing parse-file file.md | plumbing execute-blocks --save-intermed-script temp.nu 55 | 56 | # Render blocks table back to markdown 57 | plumbing parse-file file.md | plumbing strip-outputs | plumbing to-markdown 58 | 59 | # Extract pure Nushell script (no markdown) 60 | plumbing parse-file file.md | plumbing strip-outputs | plumbing to-numd-script 61 | ``` 62 | 63 | The high-level commands use these internally: 64 | - `run` = `parse-file | execute-blocks | to-markdown` 65 | - `clear-outputs` = `parse-file | strip-outputs | to-markdown` 66 | 67 | ### Core Processing Pipeline (in `commands.nu`) 68 | 69 | 1. **`parse-markdown-to-blocks`**: Parses markdown into a table classifying each block by type (`text`, ` ```nushell `, ` ```output-numd `) and action (`execute`, `print-as-it-is`, `delete`) 70 | 71 | 2. **`decorate-original-code-blocks`** + **`generate-intermediate-script`**: Transforms executable code blocks into a temporary `.nu` script with markers for output capture 72 | 73 | 3. **`execute-intermediate-script`**: Runs the generated script in a new Nushell instance, capturing stdout 74 | 75 | 4. **`extract-block-index`** + **`merge-markdown`**: Parses execution results using `#code-block-marker-open-N` markers and merges them back into the original markdown structure 76 | 77 | ### Fence Options 78 | 79 | Blocks support fence options (e.g., ` ```nushell try, no-output `): 80 | - `no-run` / `N`: Skip execution 81 | - `no-output` / `O`: Execute but hide output 82 | - `try` / `t`: Wrap in try-catch 83 | - `new-instance` / `n`: Execute in separate Nushell instance 84 | - `separate-block` / `s`: Output results in separate code block instead of inline `# =>` 85 | 86 | ### Output Format Conventions 87 | 88 | - Code blocks are split by blank lines (double newlines) into command groups 89 | - Each command group is executed separately via `split-by-blank-lines` 90 | - Lines starting with `# =>` contain output from previous command group 91 | - Plain `#` comments are preserved; `# =>` output lines are regenerated on each run 92 | - Use `separate-block` fence option to output results in a separate code block instead of inline `# =>` 93 | 94 | ## Testing 95 | 96 | ```nushell 97 | # Run all tests (unit + integration) 98 | nu toolkit.nu test 99 | 100 | # Run only unit tests (nutest-based, tests internal functions) 101 | nu toolkit.nu test-unit 102 | 103 | # Run only integration tests (executes example markdown files) 104 | nu toolkit.nu test-integration 105 | 106 | # All commands support --json for CI 107 | nu toolkit.nu test --json 108 | ``` 109 | 110 | ### Unit Tests (`tests/`) 111 | 112 | Unit tests use [nutest](https://github.com/vyadh/nutest) framework. Tests import internal functions via `use ../numd/commands.nu *` to test parsing and transformation logic directly. 113 | 114 | ### Integration Tests (`z_examples/`) 115 | 116 | The `test-integration` command: 117 | 1. Runs all example files in `z_examples/` through numd 118 | 2. Generates stripped `.nu` versions in `z_examples/99_strip_markdown/` 119 | 3. Runs `numd run README.md` to update README with latest outputs 120 | 4. Reports Levenshtein distance and diff stats to detect changes 121 | 122 | Example files serve as integration tests - use both the Levenshtein stats and `git diff` to verify changes. 123 | 124 | ### Expected Non-Zero Diffs 125 | 126 | Some files legitimately differ on each run due to: 127 | - **Dynamic content**: `git tag` output in README.md (version changes over time) 128 | - **Nushell version changes**: Error message formatting, table rendering differences 129 | 130 | A zero `levenshtein_dist` for most files + expected diffs in dynamic content files = passing tests. 131 | 132 | ## Configuration 133 | 134 | By default, numd runs intermediate scripts with `nu -n` (no config files) for reproducible output across different systems. Use `--use-host-config` to load host's env, config, and plugin files when needed. 135 | 136 | numd supports `.nu` config files (see `numd_config_example1.nu`). The config file is a Nushell script that gets prepended to the intermediate script: 137 | ```nushell 138 | # numd_config_example1.nu 139 | $env.config.table.mode = 'rounded' 140 | $env.numd.table-width = 100 # optional: set custom table width 141 | ``` 142 | 143 | Pass via `--config-path` or use `--prepend-code` / `--table-width` flags directly. Flags override config file settings. 144 | 145 | ## Git Workflow 146 | 147 | - Do not squash commits when merging PRs - preserve individual commit history 148 | -------------------------------------------------------------------------------- /z_examples/2_numd_commands_explanations/numd_commands_explanations.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | 22 | "#code-block-marker-open-1 23 | ```nu" | print 24 | "# This setting is for overriding the author's usual small number of `abbreviated_row_count`. 25 | $env.config.table.abbreviated_row_count = 100 26 | # The `$init_numd_pwd_const` constant points to the current working directory from where the `numd` command was initiated. 27 | # It is added by `numd` in every intermediate script to make it available in cases like below. 28 | # We use `path join` here to construct working paths for both Windows and Unix 29 | use ($init_numd_pwd_const | path join numd commands.nu) *" | nu-highlight | print 30 | 31 | # This setting is for overriding the author's usual small number of `abbreviated_row_count`. 32 | $env.config.table.abbreviated_row_count = 100 33 | # The `$init_numd_pwd_const` constant points to the current working directory from where the `numd` command was initiated. 34 | # It is added by `numd` in every intermediate script to make it available in cases like below. 35 | # We use `path join` here to construct working paths for both Windows and Unix 36 | use ($init_numd_pwd_const | path join numd commands.nu) * 37 | print '' 38 | "```" | print 39 | 40 | "#code-block-marker-open-3 41 | ```nu" | print 42 | "# Here we set the `$file` variable (which will be used in several commands throughout this script) to point to `z_examples/1_simple_markdown/simple_markdown.md`. 43 | let $file = $init_numd_pwd_const | path join z_examples 1_simple_markdown simple_markdown.md 44 | let $md_orig = open -r $file | convert-output-fences 45 | let $original_md_table = $md_orig | parse-markdown-to-blocks" | nu-highlight | print 46 | 47 | # Here we set the `$file` variable (which will be used in several commands throughout this script) to point to `z_examples/1_simple_markdown/simple_markdown.md`. 48 | let $file = $init_numd_pwd_const | path join z_examples 1_simple_markdown simple_markdown.md 49 | let $md_orig = open -r $file | convert-output-fences 50 | let $original_md_table = $md_orig | parse-markdown-to-blocks 51 | print '' 52 | "$original_md_table | table -e --width 120" | nu-highlight | print 53 | 54 | $original_md_table | table -e --width 120 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 55 | print '' 56 | "```" | print 57 | 58 | "#code-block-marker-open-5 59 | ```nu" | print 60 | "# Here we emulate that the `$intermed_script_path` options is not set 61 | let $intermediate_script_path = $file 62 | | build-modified-path --prefix $'numd-temp-(generate-timestamp)' --suffix '.nu'" | nu-highlight | print 63 | 64 | # Here we emulate that the `$intermed_script_path` options is not set 65 | let $intermediate_script_path = $file 66 | | build-modified-path --prefix $'numd-temp-(generate-timestamp)' --suffix '.nu' 67 | print '' 68 | "decorate-original-code-blocks $original_md_table 69 | | generate-intermediate-script 70 | | save -f $intermediate_script_path" | nu-highlight | print 71 | 72 | decorate-original-code-blocks $original_md_table 73 | | generate-intermediate-script 74 | | save -f $intermediate_script_path | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 75 | print '' 76 | "open $intermediate_script_path" | nu-highlight | print 77 | 78 | open $intermediate_script_path | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 79 | print '' 80 | "```" | print 81 | 82 | "#code-block-marker-open-7 83 | ```nu" | print 84 | "# the flag `$no_fail_on_error` is set to false 85 | let $no_fail_on_error = false 86 | let $nu_res_stdout_lines = execute-intermediate-script $intermediate_script_path $no_fail_on_error false false 87 | rm $intermediate_script_path" | nu-highlight | print 88 | 89 | # the flag `$no_fail_on_error` is set to false 90 | let $no_fail_on_error = false 91 | let $nu_res_stdout_lines = execute-intermediate-script $intermediate_script_path $no_fail_on_error false false 92 | rm $intermediate_script_path | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 93 | print '' 94 | "$nu_res_stdout_lines" | nu-highlight | print 95 | 96 | $nu_res_stdout_lines | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 97 | print '' 98 | "```" | print 99 | 100 | "#code-block-marker-open-9 101 | ```nu" | print 102 | "let $md_res = $nu_res_stdout_lines 103 | | str join (char nl) 104 | | clean-markdown" | nu-highlight | print 105 | 106 | let $md_res = $nu_res_stdout_lines 107 | | str join (char nl) 108 | | clean-markdown 109 | print '' 110 | "$md_res" | nu-highlight | print 111 | 112 | $md_res | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 113 | print '' 114 | "```" | print 115 | 116 | "#code-block-marker-open-11 117 | ```nu" | print 118 | "compute-change-stats $file $md_orig $md_res" | nu-highlight | print 119 | 120 | compute-change-stats $file $md_orig $md_res | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 121 | print '' 122 | "```" | print 123 | -------------------------------------------------------------------------------- /z_examples/4_book_working_with_lists/working_with_lists.md: -------------------------------------------------------------------------------- 1 | # Working with Lists 2 | 3 | :::tip 4 | Lists are equivalent to the individual columns of tables. You can think of a list as essentially being a "one-column table" (with no column name). Thus, any command which operates on a column _also_ operates on a list. For instance, [`where`](/commands/docs/where.md) can be used with lists: 5 | 6 | ```nu 7 | [bell book candle] | where ($it =~ 'b') 8 | # => ╭──────╮ 9 | # => │ bell │ 10 | # => │ book │ 11 | # => ╰──────╯ 12 | ``` 13 | 14 | ::: 15 | 16 | ## Creating lists 17 | 18 | A list is an ordered collection of values. 19 | A list is created using square brackets surrounding values separated by spaces, linebreaks, and/or commas. 20 | For example, `[foo bar baz]` or `[foo, bar, baz]`. 21 | 22 | ::: tip 23 | Nushell lists are similar to JSON arrays. The same `[ "Item1", "Item2", "Item3" ]` that represents a JSON array can also be used to create a Nushell list. 24 | ::: 25 | 26 | ## Updating lists 27 | 28 | We can [`insert`](/commands/docs/insert.md) values into lists as they flow through the pipeline, for example let's insert the value `10` into the middle of a list: 29 | 30 | ```nu 31 | [1, 2, 3, 4] | insert 2 10 32 | # => ╭────╮ 33 | # => │ 1 │ 34 | # => │ 2 │ 35 | # => │ 10 │ 36 | # => │ 3 │ 37 | # => │ 4 │ 38 | # => ╰────╯ 39 | 40 | # [1, 2, 10, 3, 4] 41 | ``` 42 | 43 | We can also use [`update`](/commands/docs/update.md) to replace the 2nd element with the value `10`. 44 | 45 | ```nu 46 | [1, 2, 3, 4] | update 1 10 47 | # => ╭────╮ 48 | # => │ 1 │ 49 | # => │ 10 │ 50 | # => │ 3 │ 51 | # => │ 4 │ 52 | # => ╰────╯ 53 | 54 | # [1, 10, 3, 4] 55 | ``` 56 | 57 | ## Removing or Adding Items from List 58 | 59 | In addition to [`insert`](/commands/docs/insert.md) and [`update`](/commands/docs/update.md), we also have [`prepend`](/commands/docs/prepend.md) and [`append`](/commands/docs/append.md). These let you insert to the beginning of a list or at the end of the list, respectively. 60 | 61 | For example: 62 | 63 | ```nu 64 | let colors = [yellow green] 65 | let colors = ($colors | prepend red) 66 | let colors = ($colors | append purple) 67 | let colors = ("black" | append $colors) 68 | $colors # [black red yellow green purple blue] 69 | # => ╭────────╮ 70 | # => │ black │ 71 | # => │ red │ 72 | # => │ yellow │ 73 | # => │ green │ 74 | # => │ purple │ 75 | # => ╰────────╯ 76 | ``` 77 | 78 | In case you want to remove items from list, there are many ways. [`skip`](/commands/docs/skip.md) allows you skip first rows from input, while [`drop`](/commands/docs/drop.md) allows you to skip specific numbered rows from end of list. 79 | 80 | ```nu 81 | let colors = [red yellow green purple] 82 | let colors = ($colors | skip 1) 83 | let colors = ($colors | drop 2) 84 | $colors # [yellow] 85 | # => ╭────────╮ 86 | # => │ yellow │ 87 | # => ╰────────╯ 88 | ``` 89 | 90 | We also have [`last`](/commands/docs/last.md) and [`first`](/commands/docs/first.md) which allow you to [`take`](/commands/docs/take.md) from the end or beginning of the list, respectively. 91 | 92 | ```nu 93 | let colors = [red yellow green purple black magenta] 94 | let colors = ($colors | last 3) 95 | $colors # [purple black magenta] 96 | # => ╭─────────╮ 97 | # => │ purple │ 98 | # => │ black │ 99 | # => │ magenta │ 100 | # => ╰─────────╯ 101 | ``` 102 | 103 | And from the beginning of a list, 104 | 105 | ```nu 106 | let colors = [yellow green purple] 107 | let colors = ($colors | first 2) 108 | $colors # [yellow green] 109 | # => ╭────────╮ 110 | # => │ yellow │ 111 | # => │ green │ 112 | # => ╰────────╯ 113 | ``` 114 | 115 | ### Using the Spread Operator 116 | 117 | To append one or more lists together, optionally with values interspersed in between, you can also use the 118 | [spread operator](/book/operators#spread-operator) (`...`): 119 | 120 | ```nu 121 | let x = [1 2] 122 | [ ...$x 3 ...(4..7 | take 2) ] 123 | # => ╭───╮ 124 | # => │ 1 │ 125 | # => │ 2 │ 126 | # => │ 3 │ 127 | # => │ 4 │ 128 | # => │ 5 │ 129 | # => ╰───╯ 130 | ``` 131 | 132 | ## Iterating over Lists 133 | 134 | To iterate over the items in a list, use the [`each`](/commands/docs/each.md) command with a [block](types_of_data.html#blocks) 135 | of Nu code that specifies what to do to each item. The block parameter (e.g. `|elt|` in `{ |elt| print $elt }`) is the current list 136 | item, but the [`enumerate`](/commands/docs/enumerate.md) filter can be used to provide `index` and `item` values if needed. For example: 137 | 138 | ```nu 139 | let names = [Mark Tami Amanda Jeremy] 140 | $names | each { |elt| $"Hello, ($elt)!" } 141 | # Outputs "Hello, Mark!" and three more similar lines. 142 | # => ╭────────────────╮ 143 | # => │ Hello, Mark! │ 144 | # => │ Hello, Tami! │ 145 | # => │ Hello, Amanda! │ 146 | # => │ Hello, Jeremy! │ 147 | # => ╰────────────────╯ 148 | 149 | $names | enumerate | each { |elt| $"($elt.index + 1) - ($elt.item)" } 150 | # Outputs "1 - Mark", "2 - Tami", etc. 151 | # => ╭────────────╮ 152 | # => │ 1 - Mark │ 153 | # => │ 2 - Tami │ 154 | # => │ 3 - Amanda │ 155 | # => │ 4 - Jeremy │ 156 | # => ╰────────────╯ 157 | ``` 158 | 159 | The [`where`](/commands/docs/where.md) command can be used to create a subset of a list, effectively filtering the list based on a condition. 160 | 161 | The following example gets all the colors whose names end in "e". 162 | 163 | ```nu 164 | let colors = [red orange yellow green blue purple] 165 | $colors | where ($it | str ends-with 'e') 166 | # The block passed to `where` must evaluate to a boolean. 167 | # This outputs the list [orange blue purple]. 168 | ``` 169 | 170 | In this example, we keep only values higher than `7`. 171 | 172 | ```nu 173 | let scores = [7 10 8 6 7] 174 | $scores | where $it > 7 # [10 8] 175 | # => ╭────╮ 176 | # => │ 10 │ 177 | # => │ 8 │ 178 | # => ╰────╯ 179 | ``` 180 | 181 | The [`reduce`](/commands/docs/reduce.md) command computes a single value from a list. 182 | It uses a block which takes 2 parameters: the current item (conventionally named `elt`) and an accumulator 183 | (conventionally named `acc`). To specify an initial value for the accumulator, use the `--fold` (`-f`) flag. 184 | To change `elt` to have `index` and `item` values, use the [`enumerate`](/commands/docs/enumerate.md) filter. 185 | For example: 186 | 187 | ```nu 188 | let scores = [3 8 4] 189 | $"total = ($scores | reduce { |elt, acc| $acc + $elt })" # total = 15 190 | $"total = ($scores | math sum)" # easier approach, same result 191 | $"product = ($scores | reduce --fold 1 { |elt, acc| $acc * $elt })" # product = 96 192 | $scores | enumerate | reduce --fold 0 { |elt, acc| $acc + $elt.index * $elt.item } # 0*3 + 1*8 + 2*4 = 16 193 | # => 16 194 | ``` 195 | 196 | ## Accessing the List 197 | 198 | ::: tip Note 199 | The following is a basic overview. For a more in-depth discussion of this topic, see the chapter, [Navigating and Accessing Structured Data](/book/navigating_structured_data.md). 200 | ::: 201 | 202 | To access a list item at a given index, use the `$name.index` form where `$name` is a variable that holds a list. 203 | 204 | For example, the second element in the list below can be accessed with `$names.1`. 205 | 206 | ```nu 207 | let names = [Mark Tami Amanda Jeremy] 208 | $names.1 # gives Tami 209 | # => Tami 210 | ``` 211 | 212 | If the index is in some variable `$index` we can use the `get` command to extract the item from the list. 213 | 214 | ```nu 215 | let names = [Mark Tami Amanda Jeremy] 216 | let index = 1 217 | $names | get $index # gives Tami 218 | # => Tami 219 | ``` 220 | 221 | The [`length`](/commands/docs/length.md) command returns the number of items in a list. 222 | For example, `[red green blue] | length` outputs `3`. 223 | 224 | The [`is-empty`](/commands/docs/is-empty.md) command determines whether a string, list, or table is empty. 225 | It can be used with lists as follows: 226 | 227 | ```nu 228 | let colors = [red green blue] 229 | $colors | is-empty # false 230 | # => false 231 | 232 | let colors = [] 233 | $colors | is-empty # true 234 | # => true 235 | ``` 236 | 237 | The `in` and `not-in` operators are used to test whether a value is in a list. For example: 238 | 239 | ```nu 240 | let colors = [red green blue] 241 | 'blue' in $colors # true 242 | 'yellow' in $colors # false 243 | 'gold' not-in $colors # true 244 | # => true 245 | ``` 246 | 247 | The [`any`](/commands/docs/any.md) command determines if any item in a list 248 | matches a given condition. 249 | For example: 250 | 251 | ```nu 252 | let colors = [red green blue] 253 | # Do any color names end with "e"? 254 | $colors | any {|elt| $elt | str ends-with "e" } # true 255 | # => true 256 | 257 | # Is the length of any color name less than 3? 258 | $colors | any {|elt| ($elt | str length) < 3 } # false 259 | # => false 260 | 261 | let scores = [3 8 4] 262 | # Are any scores greater than 7? 263 | $scores | any {|elt| $elt > 7 } # true 264 | # => true 265 | 266 | # Are any scores odd? 267 | $scores | any {|elt| $elt mod 2 == 1 } # true 268 | # => true 269 | ``` 270 | 271 | The [`all`](/commands/docs/all.md) command determines if every item in a list 272 | matches a given condition. 273 | For example: 274 | 275 | ```nu 276 | let colors = [red green blue] 277 | # Do all color names end with "e"? 278 | $colors | all {|elt| $elt | str ends-with "e" } # false 279 | # => false 280 | 281 | # Is the length of all color names greater than or equal to 3? 282 | $colors | all {|elt| ($elt | str length) >= 3 } # true 283 | # => true 284 | 285 | let scores = [3 8 4] 286 | # Are all scores greater than 7? 287 | $scores | all {|elt| $elt > 7 } # false 288 | # => false 289 | 290 | # Are all scores even? 291 | $scores | all {|elt| $elt mod 2 == 0 } # false 292 | # => false 293 | ``` 294 | 295 | ## Converting the List 296 | 297 | The [`flatten`](/commands/docs/flatten.md) command creates a new list from an existing list 298 | by adding items in nested lists to the top-level list. 299 | This can be called multiple times to flatten lists nested at any depth. 300 | For example: 301 | 302 | ```nu 303 | [1 [2 3] 4 [5 6]] | flatten # [1 2 3 4 5 6] 304 | # => ╭───╮ 305 | # => │ 1 │ 306 | # => │ 2 │ 307 | # => │ 3 │ 308 | # => │ 4 │ 309 | # => │ 5 │ 310 | # => │ 6 │ 311 | # => ╰───╯ 312 | 313 | [[1 2] [3 [4 5 [6 7 8]]]] | flatten | flatten | flatten # [1 2 3 4 5 6 7 8] 314 | # => ╭───╮ 315 | # => │ 1 │ 316 | # => │ 2 │ 317 | # => │ 3 │ 318 | # => │ 4 │ 319 | # => │ 5 │ 320 | # => │ 6 │ 321 | # => │ 7 │ 322 | # => │ 8 │ 323 | # => ╰───╯ 324 | ``` 325 | 326 | The [`wrap`](/commands/docs/wrap.md) command converts a list to a table. Each list value will 327 | be converted to a separate row with a single column: 328 | 329 | ```nu 330 | let zones = [UTC CET Europe/Moscow Asia/Yekaterinburg] 331 | # Show world clock for selected time zones 332 | let base_time = '2024-01-15 12:00:00' | into datetime --timezone UTC 333 | $zones | wrap 'Zone' | upsert Time {|row| ($base_time | date to-timezone $row.Zone | format date '%Y.%m.%d %H:%M')} 334 | # => ╭────────Zone────────┬───────Time───────╮ 335 | # => │ UTC │ 2024.01.15 15:00 │ 336 | # => │ CET │ 2024.01.15 16:00 │ 337 | # => │ Europe/Moscow │ 2024.01.15 18:00 │ 338 | # => │ Asia/Yekaterinburg │ 2024.01.15 20:00 │ 339 | # => ╰────────Zone────────┴───────Time───────╯ 340 | ``` 341 | -------------------------------------------------------------------------------- /z_examples/9_other/nushell_readme.md: -------------------------------------------------------------------------------- 1 | # Nushell 2 | [![Crates.io](https://img.shields.io/crates/v/nu.svg)](https://crates.io/crates/nu) 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/nushell/nushell/ci.yml?branch=main)](https://github.com/nushell/nushell/actions) 4 | [![Nightly Build](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml/badge.svg)](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml) 5 | [![Discord](https://img.shields.io/discord/601130461678272522.svg?logo=discord)](https://discord.gg/NtAbbGn) 6 | [![The Changelog #363](https://img.shields.io/badge/The%20Changelog-%23363-61c192.svg)](https://changelog.com/podcast/363) 7 | [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/nushell/nushell)](https://github.com/nushell/nushell/graphs/commit-activity) 8 | [![GitHub contributors](https://img.shields.io/github/contributors/nushell/nushell)](https://github.com/nushell/nushell/graphs/contributors) 9 | 10 | A new type of shell. 11 | 12 | ![Example of nushell](assets/nushell-autocomplete6.gif "Example of nushell") 13 | 14 | ## Table of Contents 15 | 16 | - [Status](#status) 17 | - [Learning About Nu](#learning-about-nu) 18 | - [Installation](#installation) 19 | - [Configuration](#configuration) 20 | - [Philosophy](#philosophy) 21 | - [Pipelines](#pipelines) 22 | - [Opening files](#opening-files) 23 | - [Plugins](#plugins) 24 | - [Goals](#goals) 25 | - [Officially Supported By](#officially-supported-by) 26 | - [Contributing](#contributing) 27 | - [License](#license) 28 | 29 | ## Status 30 | 31 | This project has reached a minimum-viable-product level of quality. Many people use it as their daily driver, but it may be unstable for some commands. Nu's design is subject to change as it matures. 32 | 33 | ## Learning About Nu 34 | 35 | The [Nushell book](https://www.nushell.sh/book/) is the primary source of Nushell documentation. You can find [a full list of Nu commands in the book](https://www.nushell.sh/commands/), and we have many examples of using Nu in our [cookbook](https://www.nushell.sh/cookbook/). 36 | 37 | We're also active on [Discord](https://discord.gg/NtAbbGn); come and chat with us! 38 | 39 | ## Installation 40 | 41 | To quickly install Nu: 42 | 43 | ```bash 44 | # Linux and macOS 45 | brew install nushell 46 | # Windows 47 | winget install nushell 48 | ``` 49 | 50 | To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/actions/setup-nu) for more detail. 51 | 52 | Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers: 53 | 54 | [![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg?columns=3)](https://repology.org/project/nushell/versions) 55 | 56 | For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md). 57 | 58 | ## Configuration 59 | 60 | The default configurations can be found at [sample_config](crates/nu-utils/src/default_files) 61 | which are the configuration files one gets when they startup Nushell for the first time. 62 | 63 | It sets all of the default configuration to run Nushell. From here one can 64 | then customize this file for their specific needs. 65 | 66 | To see where *config.nu* is located on your system simply type this command. 67 | 68 | ```rust 69 | $nu.config-path 70 | ``` 71 | 72 | Please see our [book](https://www.nushell.sh) for all of the Nushell documentation. 73 | 74 | 75 | ## Philosophy 76 | 77 | Nu draws inspiration from projects like PowerShell, functional programming languages, and modern CLI tools. 78 | Rather than thinking of files and data as raw streams of text, Nu looks at each input as something with structure. 79 | For example, when you list the contents of a directory what you get back is a table of rows, where each row represents an item in that directory. 80 | These values can be piped through a series of steps, in a series of commands called a 'pipeline'. 81 | 82 | ### Pipelines 83 | 84 | In Unix, it's common to pipe between commands to split up a sophisticated command over multiple steps. 85 | Nu takes this a step further and builds heavily on the idea of _pipelines_. 86 | As in the Unix philosophy, Nu allows commands to output to stdout and read from stdin. 87 | Additionally, commands can output structured data (you can think of this as a third kind of stream). 88 | Commands that work in the pipeline fit into one of three categories: 89 | 90 | - Commands that produce a stream (e.g., `ls`) 91 | - Commands that filter a stream (e.g., `where type == "dir"`) 92 | - Commands that consume the output of the pipeline (e.g., `table`) 93 | 94 | Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right. 95 | 96 | ```shell 97 | ls | where type == "dir" | table 98 | # => ╭────┬──────────┬──────┬─────────┬───────────────╮ 99 | # => │ # │ name │ type │ size │ modified │ 100 | # => ├────┼──────────┼──────┼─────────┼───────────────┤ 101 | # => │ 0 │ .cargo │ dir │ 0 B │ 9 minutes ago │ 102 | # => │ 1 │ assets │ dir │ 0 B │ 2 weeks ago │ 103 | # => │ 2 │ crates │ dir │ 4.0 KiB │ 2 weeks ago │ 104 | # => │ 3 │ docker │ dir │ 0 B │ 2 weeks ago │ 105 | # => │ 4 │ docs │ dir │ 0 B │ 2 weeks ago │ 106 | # => │ 5 │ images │ dir │ 0 B │ 2 weeks ago │ 107 | # => │ 6 │ pkg_mgrs │ dir │ 0 B │ 2 weeks ago │ 108 | # => │ 7 │ samples │ dir │ 0 B │ 2 weeks ago │ 109 | # => │ 8 │ src │ dir │ 4.0 KiB │ 2 weeks ago │ 110 | # => │ 9 │ target │ dir │ 0 B │ a day ago │ 111 | # => │ 10 │ tests │ dir │ 4.0 KiB │ 2 weeks ago │ 112 | # => │ 11 │ wix │ dir │ 0 B │ 2 weeks ago │ 113 | # => ╰────┴──────────┴──────┴─────────┴───────────────╯ 114 | ``` 115 | 116 | Because most of the time you'll want to see the output of a pipeline, `table` is assumed. 117 | We could have also written the above: 118 | 119 | ```shell 120 | ls | where type == "dir" 121 | ``` 122 | 123 | Being able to use the same commands and compose them differently is an important philosophy in Nu. 124 | For example, we could use the built-in `ps` command to get a list of the running processes, using the same `where` as above. 125 | 126 | ```shell 127 | ps | where cpu > 0 128 | # => ╭───┬───────┬───────────┬───────┬───────────┬───────────╮ 129 | # => │ # │ pid │ name │ cpu │ mem │ virtual │ 130 | # => ├───┼───────┼───────────┼───────┼───────────┼───────────┤ 131 | # => │ 0 │ 2240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │ 132 | # => │ 1 │ 16948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │ 133 | # => │ 2 │ 17700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │ 134 | # => ╰───┴───────┴───────────┴───────┴───────────┴───────────╯ 135 | ``` 136 | 137 | ### Opening files 138 | 139 | Nu can load file and URL contents as raw text or structured data (if it recognizes the format). 140 | For example, you can load a .toml file as structured data and explore it: 141 | 142 | ```shell 143 | open Cargo.toml 144 | # => ╭──────────────────┬────────────────────╮ 145 | # => │ bin │ [table 1 row] │ 146 | # => │ dependencies │ {record 25 fields} │ 147 | # => │ dev-dependencies │ {record 8 fields} │ 148 | # => │ features │ {record 10 fields} │ 149 | # => │ package │ {record 13 fields} │ 150 | # => │ patch │ {record 1 field} │ 151 | # => │ profile │ {record 3 fields} │ 152 | # => │ target │ {record 3 fields} │ 153 | # => │ workspace │ {record 1 field} │ 154 | # => ╰──────────────────┴────────────────────╯ 155 | ``` 156 | 157 | We can pipe this into a command that gets the contents of one of the columns: 158 | 159 | ```shell 160 | open Cargo.toml | get package 161 | # => ╭───────────────┬────────────────────────────────────╮ 162 | # => │ authors │ [list 1 item] │ 163 | # => │ default-run │ nu │ 164 | # => │ description │ A new type of shell │ 165 | # => │ documentation │ https://www.nushell.sh/book/ │ 166 | # => │ edition │ 2018 │ 167 | # => │ exclude │ [list 1 item] │ 168 | # => │ homepage │ https://www.nushell.sh │ 169 | # => │ license │ MIT │ 170 | # => │ metadata │ {record 1 field} │ 171 | # => │ name │ nu │ 172 | # => │ repository │ https://github.com/nushell/nushell │ 173 | # => │ rust-version │ 1.60 │ 174 | # => │ version │ 0.72.0 │ 175 | # => ╰───────────────┴────────────────────────────────────╯ 176 | ``` 177 | 178 | And if needed we can drill down further: 179 | 180 | ```shell 181 | open Cargo.toml | get package.version 182 | # => 0.72.0 183 | ``` 184 | 185 | ### Plugins 186 | 187 | Nu supports plugins that offer additional functionality to the shell and follow the same structured data model that built-in commands use. There are a few examples in the `crates/nu_plugins_*` directories. 188 | 189 | Plugins are binaries that are available in your path and follow a `nu_plugin_*` naming convention. 190 | These binaries interact with nu via a simple JSON-RPC protocol where the command identifies itself and passes along its configuration, making it available for use. 191 | If the plugin is a filter, data streams to it one element at a time, and it can stream data back in return via stdin/stdout. 192 | If the plugin is a sink, it is given the full vector of final data and is given free reign over stdin/stdout to use as it pleases. 193 | 194 | The [awesome-nu repo](https://github.com/nushell/awesome-nu#plugins) lists a variety of nu-plugins while the [showcase repo](https://github.com/nushell/showcase) *shows* off informative blog posts that have been written about Nushell along with videos that highlight technical 195 | topics that have been presented. 196 | 197 | ## Goals 198 | 199 | Nu adheres closely to a set of goals that make up its design philosophy. As features are added, they are checked against these goals. 200 | 201 | - First and foremost, Nu is cross-platform. Commands and techniques should work across platforms and Nu has [first-class support for Windows, macOS, and Linux](devdocs/PLATFORM_SUPPORT.md). 202 | 203 | - Nu ensures compatibility with existing platform-specific executables. 204 | 205 | - Nu's workflow and tools should have the usability expected of modern software in 2022 (and beyond). 206 | 207 | - Nu views data as either structured or unstructured. It is a structured shell like PowerShell. 208 | 209 | - Finally, Nu views data functionally. Rather than using mutation, pipelines act as a means to load, change, and save data without mutable state. 210 | 211 | ## Officially Supported By 212 | 213 | Please submit an issue or PR to be added to this list. 214 | 215 | - [zoxide](https://github.com/ajeetdsouza/zoxide) 216 | - [starship](https://github.com/starship/starship) 217 | - [oh-my-posh](https://ohmyposh.dev) 218 | - [Couchbase Shell](https://couchbase.sh) 219 | - [virtualenv](https://github.com/pypa/virtualenv) 220 | - [atuin](https://github.com/ellie/atuin) 221 | - [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell) 222 | - [Dorothy](http://github.com/bevry/dorothy) 223 | - [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell) 224 | - [x-cmd](https://x-cmd.com/mod/nu) 225 | - [vfox](https://github.com/version-fox/vfox) 226 | 227 | ## Contributing 228 | 229 | See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed! 230 | 231 | 232 | 233 | 234 | 235 | ## License 236 | 237 | The project is made available under the MIT license. See the `LICENSE` file for more information. 238 | -------------------------------------------------------------------------------- /z_examples/2_numd_commands_explanations/numd_commands_explanations.md: -------------------------------------------------------------------------------- 1 | # numd commands explanation 2 | 3 | In the code block below, we set settings and variables for executing this entire document. 4 | 5 | ```nu 6 | # This setting is for overriding the author's usual small number of `abbreviated_row_count`. 7 | $env.config.table.abbreviated_row_count = 100 8 | # The `$init_numd_pwd_const` constant points to the current working directory from where the `numd` command was initiated. 9 | # It is added by `numd` in every intermediate script to make it available in cases like below. 10 | # We use `path join` here to construct working paths for both Windows and Unix 11 | use ($init_numd_pwd_const | path join numd commands.nu) * 12 | ``` 13 | 14 | ## numd-internals.nu 15 | 16 | ### parse-markdown-to-blocks 17 | 18 | This command is used for parsing initial markdown to detect executable code blocks. 19 | 20 | ```nu 21 | # Here we set the `$file` variable (which will be used in several commands throughout this script) to point to `z_examples/1_simple_markdown/simple_markdown.md`. 22 | let $file = $init_numd_pwd_const | path join z_examples 1_simple_markdown simple_markdown.md 23 | let $md_orig = open -r $file | convert-output-fences 24 | let $original_md_table = $md_orig | parse-markdown-to-blocks 25 | 26 | $original_md_table | table -e --width 120 27 | # => ╭─block_index─┬───────row_type───────┬───────────────────────────────────line────────────────────────────────────┬─act─╮ 28 | # => │ 0 │ text │ ╭───────────────────────────────────────────────────────────────────────╮ │ pri │ 29 | # => │ │ │ │ # This is a simple markdown example │ │ nt- │ 30 | # => │ │ │ │ │ │ as- │ 31 | # => │ │ │ │ ## Example 1 │ │ it- │ 32 | # => │ │ │ │ │ │ is │ 33 | # => │ │ │ │ the block below will be executed as it is, but won't yield any output │ │ │ 34 | # => │ │ │ │ │ │ │ 35 | # => │ │ │ ╰───────────────────────────────────────────────────────────────────────╯ │ │ 36 | # => │ 1 │ ```nu │ ╭───────────────────╮ │ exe │ 37 | # => │ │ │ │ ```nu │ │ cut │ 38 | # => │ │ │ │ let $var1 = 'foo' │ │ e │ 39 | # => │ │ │ │ ``` │ │ │ 40 | # => │ │ │ ╰───────────────────╯ │ │ 41 | # => │ 2 │ text │ ╭──────────────╮ │ pri │ 42 | # => │ │ │ │ │ │ nt- │ 43 | # => │ │ │ │ ## Example 2 │ │ as- │ 44 | # => │ │ │ │ │ │ it- │ 45 | # => │ │ │ ╰──────────────╯ │ is │ 46 | # => │ 3 │ ```nu separate-block │ ╭───────────────────────────────────────────────────────────╮ │ exe │ 47 | # => │ │ │ │ ```nu separate-block │ │ cut │ 48 | # => │ │ │ │ # This block will produce some output in a separate block │ │ e │ 49 | # => │ │ │ │ $var1 | path join 'baz' 'bar' │ │ │ 50 | # => │ │ │ │ ``` │ │ │ 51 | # => │ │ │ ╰───────────────────────────────────────────────────────────╯ │ │ 52 | # => │ 4 │ ```output-numd │ ╭──────────────────╮ │ del │ 53 | # => │ │ │ │ ```output-numd │ │ ete │ 54 | # => │ │ │ │ # => foo/baz/bar │ │ │ 55 | # => │ │ │ │ ``` │ │ │ 56 | # => │ │ │ ╰──────────────────╯ │ │ 57 | # => │ 5 │ text │ ╭──────────────╮ │ pri │ 58 | # => │ │ │ │ │ │ nt- │ 59 | # => │ │ │ │ ## Example 3 │ │ as- │ 60 | # => │ │ │ │ │ │ it- │ 61 | # => │ │ │ ╰──────────────╯ │ is │ 62 | # => │ 6 │ ```nu │ ╭─────────────────────────────────────────╮ │ exe │ 63 | # => │ │ │ │ ```nu │ │ cut │ 64 | # => │ │ │ │ # This block will output results inline │ │ e │ 65 | # => │ │ │ │ whoami │ │ │ 66 | # => │ │ │ │ # => user │ │ │ 67 | # => │ │ │ │ │ │ │ 68 | # => │ │ │ │ 2 + 2 │ │ │ 69 | # => │ │ │ │ # => 4 │ │ │ 70 | # => │ │ │ │ ``` │ │ │ 71 | # => │ │ │ ╰─────────────────────────────────────────╯ │ │ 72 | # => │ 7 │ text │ ╭──────────────╮ │ pri │ 73 | # => │ │ │ │ │ │ nt- │ 74 | # => │ │ │ │ ## Example 4 │ │ as- │ 75 | # => │ │ │ │ │ │ it- │ 76 | # => │ │ │ ╰──────────────╯ │ is │ 77 | # => │ 8 │ ``` │ ╭──────────────────────────────────────────────────────────────────────╮ │ pri │ 78 | # => │ │ │ │ ``` │ │ nt- │ 79 | # => │ │ │ │ # This block doesn't have a language identifier in the opening fence │ │ as- │ 80 | # => │ │ │ │ ``` │ │ it- │ 81 | # => │ │ │ ╰──────────────────────────────────────────────────────────────────────╯ │ is │ 82 | # => ╰─block_index─┴───────row_type───────┴───────────────────────────────────line────────────────────────────────────┴─act─╯ 83 | ``` 84 | 85 | ## generate-intermediate-script 86 | 87 | The `generate-intermediate-script` command generates a script that contains code from all executable blocks and `numd` service commands used for capturing outputs. 88 | 89 | ```nu 90 | # Here we emulate that the `$intermed_script_path` options is not set 91 | let $intermediate_script_path = $file 92 | | build-modified-path --prefix $'numd-temp-(generate-timestamp)' --suffix '.nu' 93 | 94 | decorate-original-code-blocks $original_md_table 95 | | generate-intermediate-script 96 | | save -f $intermediate_script_path 97 | 98 | open $intermediate_script_path 99 | # => # this script was generated automatically using numd 100 | # => # https://github.com/nushell-prophet/numd 101 | # => 102 | # => const init_numd_pwd_const = '/Users/user/git/numd' 103 | # => 104 | # => "#code-block-marker-open-1 105 | # => ```nu" | print 106 | # => "let $var1 = 'foo'" | nu-highlight | print 107 | # => 108 | # => let $var1 = 'foo' 109 | # => print '' 110 | # => "```" | print 111 | # => 112 | # => "#code-block-marker-open-3 113 | # => ```nu separate-block" | print 114 | # => "# This block will produce some output in a separate block 115 | # => $var1 | path join 'baz' 'bar'" | nu-highlight | print 116 | # => 117 | # => "```\n```output-numd" | print 118 | # => 119 | # => # This block will produce some output in a separate block 120 | # => $var1 | path join 'baz' 'bar' | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 121 | # => print '' 122 | # => "```" | print 123 | # => 124 | # => "#code-block-marker-open-6 125 | # => ```nu" | print 126 | # => "# This block will output results inline 127 | # => whoami" | nu-highlight | print 128 | # => 129 | # => # This block will output results inline 130 | # => whoami | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 131 | # => print '' 132 | # => "2 + 2" | nu-highlight | print 133 | # => 134 | # => 2 + 2 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 135 | # => print '' 136 | # => "```" | print 137 | ``` 138 | 139 | ## execute-intermediate-script 140 | 141 | The `execute-intermediate-script` command runs and captures outputs of the executed intermediate script. 142 | 143 | ```nu 144 | # the flag `$no_fail_on_error` is set to false 145 | let $no_fail_on_error = false 146 | let $nu_res_stdout_lines = execute-intermediate-script $intermediate_script_path $no_fail_on_error false false 147 | rm $intermediate_script_path 148 | 149 | $nu_res_stdout_lines 150 | # => #code-block-marker-open-1 151 | # => ```nu 152 | # => let $var1 = 'foo' 153 | # => 154 | # => ``` 155 | # => #code-block-marker-open-3 156 | # => ```nu separate-block 157 | # => # This block will produce some output in a separate block 158 | # => $var1 | path join 'baz' 'bar' 159 | # => ``` 160 | # => ```output-numd 161 | # => # => foo/baz/bar 162 | # => 163 | # => 164 | # => 165 | # => ``` 166 | # => #code-block-marker-open-6 167 | # => ```nu 168 | # => # This block will output results inline 169 | # => whoami 170 | # => # => user 171 | # => 172 | # => 173 | # => 174 | # => 2 + 2 175 | # => # => 4 176 | # => 177 | # => 178 | # => 179 | # => ``` 180 | ``` 181 | 182 | ```nu 183 | let $md_res = $nu_res_stdout_lines 184 | | str join (char nl) 185 | | clean-markdown 186 | 187 | $md_res 188 | # => #code-block-marker-open-1 189 | # => ```nu 190 | # => let $var1 = 'foo' 191 | # => 192 | # => ``` 193 | # => #code-block-marker-open-3 194 | # => ```nu separate-block 195 | # => # This block will produce some output in a separate block 196 | # => $var1 | path join 'baz' 'bar' 197 | # => ``` 198 | # => ```output-numd 199 | # => # => foo/baz/bar 200 | # => 201 | # => ``` 202 | # => #code-block-marker-open-6 203 | # => ```nu 204 | # => # This block will output results inline 205 | # => whoami 206 | # => # => user 207 | # => 208 | # => 2 + 2 209 | # => # => 4 210 | # => 211 | # => ``` 212 | ``` 213 | 214 | ## compute-change-stats 215 | 216 | The `compute-change-stats` command displays stats on the changes made. 217 | 218 | ```nu 219 | compute-change-stats $file $md_orig $md_res 220 | # => ╭──────────────────┬────────────────────╮ 221 | # => │ filename │ simple_markdown.md │ 222 | # => │ nushell_blocks │ 3 │ 223 | # => │ levenshtein_dist │ 248 │ 224 | # => │ diff_lines │ -12 (-33.3%) │ 225 | # => │ diff_words │ -24 (-30.8%) │ 226 | # => │ diff_chars │ -163 (-32.5%) │ 227 | # => ╰──────────────────┴────────────────────╯ 228 | ``` 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/nushell-prophet/numd/actions/workflows/ci.yml/badge.svg)](https://github.com/nushell-prophet/numd/actions/workflows/ci.yml) 2 | 3 | # numd - reproducible Nushell Markdown documents 4 | 5 | Execute blocks of nushell code within markdown documents, write results back to your `.md` document, or output them to the terminal. 6 | 7 | ## Quickstart 8 | 9 | ```nushell no-run 10 | # clone the repo and `cd` into it 11 | git clone https://github.com/nushell-prophet/numd 12 | cd numd 13 | 14 | # use definitions from the module 15 | use numd 16 | 17 | # run it on any file to check (--echo outputs to stdout without saving) 18 | numd run z_examples/1_simple_markdown/simple_markdown.md --echo 19 | ``` 20 | 21 | ## How it works 22 | 23 | `numd run` parses the initial file ([example](/z_examples/1_simple_markdown/simple_markdown.md)), generates a script to execute the found commands ([example](/z_examples/1_simple_markdown/simple_markdown.md_intermed.nu)), executes this script in a new nushell instance, captures the results, updates the initial document accordingly, and/or outputs the resulting document into the terminal along with basic changes [stats](#stats-of-changes). 24 | 25 | Experienced nushell users can understand the logic better by looking at [examples](./z_examples/). Especially, seeing [numd in action describing its own commands](./z_examples/2_numd_commands_explanations/numd_commands_explanations.md). 26 | 27 | ### Details on parsing code blocks and displaying the output 28 | 29 | 1. `numd` looks for code blocks marked with ` ```nushell ` or ` ```nu `. 30 | 2. Code blocks are split into command groups by blank lines (double newlines). Each command group is executed separately. 31 | 3. Output from each command group is displayed inline with `# =>` prefix immediately after the command. 32 | 4. Multiline commands (pipelines split across lines without blank lines) are treated as a single command group. 33 | 5. Plain `#` comments are preserved; `# =>` output lines are regenerated on each run. 34 | 6. Use the `separate-block` fence option to output results in a separate code block instead of inline. 35 | 36 | > [!NOTE] 37 | > This readme is a live `numd` document 38 | 39 | ### `numd run` flags and params 40 | 41 | ```nushell 42 | use numd 43 | numd run --help 44 | # => Run Nushell code blocks in a markdown file, output results back to the `.md`, and optionally to terminal 45 | # => 46 | # => Usage: 47 | # => > run {flags} 48 | # => 49 | # => Flags: 50 | # => -h, --help: Display the help message for this command 51 | # => --config-path : path to a .nu config file (Nushell code prepended to script) (default: '') 52 | # => --echo: output resulting markdown to stdout instead of saving to file 53 | # => --no-fail-on-error: skip errors (markdown is never saved on error) 54 | # => --no-stats: do not output stats of changes (is activated via --echo by default) 55 | # => --prepend-code : additional code to prepend (applied after config file) 56 | # => --print-block-results: print blocks one by one as they are executed, useful for long running scripts 57 | # => --save-intermed-script : optional path for keeping intermediate script (useful for debugging purposes). If not set, the temporary intermediate script will be deleted. 58 | # => --table-width : set $env.numd.table-width (overrides config file) 59 | # => --use-host-config: load host's env, config, and plugin files (default: run with nu -n for reproducibility) 60 | # => 61 | # => Parameters: 62 | # => file : path to a `.md` file containing Nushell code to be executed 63 | # => 64 | # => Input/output types: 65 | # => ╭───┬─────────┬─────────╮ 66 | # => │ # │ input │ output │ 67 | # => ├───┼─────────┼─────────┤ 68 | # => │ 0 │ nothing │ string │ 69 | # => │ 1 │ nothing │ nothing │ 70 | # => │ 2 │ nothing │ record │ 71 | # => ╰───┴─────────┴─────────╯ 72 | # => 73 | # => Examples: 74 | # => update readme 75 | # => > numd run README.md 76 | # => 77 | ``` 78 | 79 | ### Supported fence options 80 | 81 | `numd` understands the following fence options. Several comma-separated fence options can be combined together. Fence options are placed in the [infostring](https://github.github.com/gfm/#info-string) of the opening code fence, for example: ` ```nushell try, new-instance ` 82 | 83 | ```nushell 84 | numd list-fence-options 85 | # => ╭──────long──────┬─short─┬───────────────────────────description────────────────────────────╮ 86 | # => │ no-output │ O │ execute code without outputting results │ 87 | # => │ no-run │ N │ do not execute code in block │ 88 | # => │ try │ t │ execute block inside `try {}` for error handling │ 89 | # => │ new-instance │ n │ execute block in new Nushell instance (useful with `try` block) │ 90 | # => │ separate-block │ s │ output results in a separate code block instead of inline `# =>` │ 91 | # => ╰──────long──────┴─short─┴───────────────────────────description────────────────────────────╯ 92 | ``` 93 | 94 | ### Stats of changes 95 | 96 | By default, `numd` provides basic stats on changes made (when not using `--echo`). 97 | 98 | ```nushell 99 | # Running without --echo saves the file and returns stats 100 | let path = [z_examples 1_simple_markdown simple_markdown_with_no_output.md] | path join 101 | numd run $path 102 | # => ╭──────────────────┬───────────────────────────────────╮ 103 | # => │ filename │ simple_markdown_with_no_output.md │ 104 | # => │ nushell_blocks │ 3 │ 105 | # => │ levenshtein_dist │ 52 │ 106 | # => │ diff_lines │ +8 (25.8%) │ 107 | # => │ diff_words │ +6 (8.5%) │ 108 | # => │ diff_chars │ +52 (11.6%) │ 109 | # => ╰──────────────────┴───────────────────────────────────╯ 110 | ``` 111 | 112 | ### Styling outputs 113 | 114 | It is possible to set Nushell visual settings (and all the others) using the `--prepend-code` option. Just pass a code there to be prepended into our save-intermed-script.nu and executed before all parts of the code. 115 | 116 | ```nushell 117 | let path = $nu.temp-path | path join simple_nu_table.md 118 | 119 | # let's generate some markdown and save it to the `simple_nu_table.md` file in the temp directory 120 | "```nushell\n[[a b c]; [1 2 3]]\n```\n" | save -f $path 121 | 122 | # let's run this file to see its outputs (--echo outputs to stdout without saving) 123 | numd run $path --echo --no-stats --prepend-code " 124 | $env.config.footer_mode = 'never' 125 | $env.config.table.header_on_separator = false 126 | $env.config.table.index_mode = 'never' 127 | $env.config.table.mode = 'basic_compact' 128 | " 129 | # => ```nushell 130 | # => [[a b c]; [1 2 3]] 131 | # => # => +---+---+---+ 132 | # => # => | a | b | c | 133 | # => # => | 1 | 2 | 3 | 134 | # => # => +---+---+---+ 135 | # => ``` 136 | ``` 137 | 138 | ### `numd clear-outputs` 139 | 140 | ```nu 141 | numd clear-outputs --help 142 | # => Remove numd execution outputs from the file 143 | # => 144 | # => Usage: 145 | # => > clear-outputs {flags} 146 | # => 147 | # => Flags: 148 | # => -h, --help: Display the help message for this command 149 | # => --echo: output resulting markdown to stdout instead of writing to file 150 | # => --strip-markdown: keep only Nushell script, strip all markdown tags 151 | # => 152 | # => Parameters: 153 | # => file : path to a `.md` file containing numd output to be cleared 154 | # => 155 | # => Input/output types: 156 | # => ╭───┬─────────┬─────────╮ 157 | # => │ # │ input │ output │ 158 | # => ├───┼─────────┼─────────┤ 159 | # => │ 0 │ nothing │ string │ 160 | # => │ 1 │ nothing │ nothing │ 161 | # => ╰───┴─────────┴─────────╯ 162 | # => 163 | ``` 164 | 165 | ### `numd capture` 166 | 167 | `numd` can use the `display_output` hook to write the current session prompts together with their output into a specified markdown file. There are corresponding commands `numd capture start` and `numd capture stop`. 168 | 169 | ```nushell 170 | numd capture start --help 171 | # => start capturing commands and their outputs into a file 172 | # => 173 | # => Usage: 174 | # => > capture start {flags} (file) 175 | # => 176 | # => Flags: 177 | # => -h, --help: Display the help message for this command 178 | # => --separate-blocks: create separate code blocks for each pipeline instead of inline `# =>` output 179 | # => 180 | # => Parameters: 181 | # => file : (optional, default: 'numd_capture.md') 182 | # => 183 | # => Input/output types: 184 | # => ╭───┬─────────┬─────────╮ 185 | # => │ # │ input │ output │ 186 | # => ├───┼─────────┼─────────┤ 187 | # => │ 0 │ nothing │ nothing │ 188 | # => ╰───┴─────────┴─────────╯ 189 | # => 190 | ``` 191 | 192 | ```nushell 193 | numd capture stop --help 194 | # => stop capturing commands and their outputs 195 | # => 196 | # => Usage: 197 | # => > capture stop 198 | # => 199 | # => Flags: 200 | # => -h, --help: Display the help message for this command 201 | # => 202 | # => Input/output types: 203 | # => ╭───┬─────────┬─────────╮ 204 | # => │ # │ input │ output │ 205 | # => ├───┼─────────┼─────────┤ 206 | # => │ 0 │ nothing │ nothing │ 207 | # => ╰───┴─────────┴─────────╯ 208 | # => 209 | ``` 210 | 211 | ### Some random familiar examples 212 | 213 | ```nushell 214 | ls z_examples | sort-by name | reject modified size 215 | # => ╭──────────────────name───────────────────┬─type─╮ 216 | # => │ z_examples/1_simple_markdown │ dir │ 217 | # => │ z_examples/2_numd_commands_explanations │ dir │ 218 | # => │ z_examples/3_book_types_of_data │ dir │ 219 | # => │ z_examples/4_book_working_with_lists │ dir │ 220 | # => │ z_examples/5_simple_nu_table │ dir │ 221 | # => │ z_examples/6_edge_cases │ dir │ 222 | # => │ z_examples/7_image_output │ dir │ 223 | # => │ z_examples/8_parse_frontmatter │ dir │ 224 | # => │ z_examples/999_numd_internals │ dir │ 225 | # => │ z_examples/99_strip_markdown │ dir │ 226 | # => │ z_examples/9_other │ dir │ 227 | # => ╰──────────────────name───────────────────┴─type─╯ 228 | 229 | 'hello world' | str length 230 | # => 11 231 | 232 | 2 + 2 233 | # => 4 234 | 235 | git tag | lines | sort -n | last 236 | # => 0.2.2 237 | ``` 238 | 239 | ## Real fight examples to try 240 | 241 | ```nushell no-output 242 | # output the result of execution to terminal without updating the file (--echo implies no save) 243 | [z_examples 1_simple_markdown simple_markdown.md] 244 | | path join 245 | | numd run $in --echo 246 | ``` 247 | 248 | ## Development and testing 249 | 250 | Nushell Markdown documents used together with Git could often serve as a convenient way to test custom and built-in Nushell commands. 251 | 252 | Testing of the `numd` module is done via `toolkit.nu`: 253 | 254 | ```nushell no-run 255 | # Run all tests (unit + integration) 256 | nu toolkit.nu test 257 | 258 | # Run only unit tests (uses nutest framework) 259 | nu toolkit.nu test-unit 260 | 261 | # Run only integration tests (executes example markdown files) 262 | nu toolkit.nu test-integration 263 | ``` 264 | 265 | ### Unit tests 266 | 267 | Unit tests in `tests/` use the [nutest](https://github.com/vyadh/nutest) framework to test internal functions like `parse-markdown-to-blocks`, `classify-block-action`, `extract-fence-options`, etc. 268 | 269 | ### Integration tests 270 | 271 | Integration tests run all example files in `z_examples/` through numd and report changes via Levenshtein distance. Whatever changes are made in the module - it can be easily seen if they break anything (both by the Levenshtein distance metric or by `git diff` of the updated example files versus their initial versions). 272 | 273 | ```nushell no-run 274 | nu toolkit.nu test-integration 275 | # => ╭───────────────────────────────────────────────┬─────────────────┬───────────────────┬────────────┬──────────────┬─────╮ 276 | # => │ filename │ nushell_blocks │ levenshtein_dist │ diff_lines │ diff_words │ ... │ 277 | # => ├───────────────────────────────────────────────┼─────────────────┼───────────────────┼────────────┼──────────────┼─────┤ 278 | # => │ types_of_data.md │ 30 │ 204 │ 0% │ -29 (-1.1%) │ ... │ 279 | # => │ working_with_lists.md │ 20 │ 4 │ 0% │ 0% │ ... │ 280 | # => │ numd_commands_explanations.md │ 6 │ 0 │ 0% │ 0% │ ... │ 281 | # => │ simple_markdown.md │ 3 │ 0 │ 0% │ 0% │ ... │ 282 | # => │ error-with-try.md │ 1 │ 13 │ -1 (-4.3%) │ 0% │ ... │ 283 | # => │ simple_markdown_first_block.md │ 3 │ 0 │ 0% │ 0% │ ... │ 284 | # => │ raw_strings_test.md │ 2 │ 0 │ 0% │ 0% │ ... │ 285 | # => │ simple_nu_table.md │ 3 │ 0 │ 0% │ 0% │ ... │ 286 | # => │ simple_nu_table_customized_width20.md │ 3 │ 458 │ 0% │ -42 (-23.7%) │ ... │ 287 | # => │ simple_nu_table_customized_example_config.md │ 3 │ 56 │ 0% │ -4 (-2.3%) │ ... │ 288 | # => │ README.md │ 9 │ 0 │ 0% │ 0% │ ... │ 289 | # => ╰───────────────────────────────────────────────┴─────────────────┴───────────────────┴────────────┴──────────────┴─────╯ 290 | ``` 291 | -------------------------------------------------------------------------------- /z_examples/4_book_working_with_lists/working_with_lists.md_intermed.nu: -------------------------------------------------------------------------------- 1 | # this script was generated automatically using numd 2 | # https://github.com/nushell-prophet/numd 3 | 4 | const init_numd_pwd_const = '/Users/user/git/numd' 5 | 6 | # numd config loaded from `numd_config_example1.nu` 7 | 8 | # numd config example 1 9 | # This file is prepended to the intermediate script before execution 10 | 11 | $env.config.footer_mode = 'always' 12 | $env.config.table = { 13 | mode: rounded 14 | index_mode: never 15 | show_empty: false 16 | padding: {left: 1, right: 1} 17 | trim: {methodology: truncating, wrapping_try_keep_words: false, truncating_suffix: '...'} 18 | header_on_separator: true 19 | abbreviated_row_count: 1000 20 | } 21 | 22 | "#code-block-marker-open-1 23 | ```nu" | print 24 | "[bell book candle] | where ($it =~ 'b')" | nu-highlight | print 25 | 26 | [bell book candle] | where ($it =~ 'b') | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 27 | print '' 28 | "```" | print 29 | 30 | "#code-block-marker-open-3 31 | ```nu" | print 32 | "[1, 2, 3, 4] | insert 2 10" | nu-highlight | print 33 | 34 | [1, 2, 3, 4] | insert 2 10 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 35 | print '' 36 | "# [1, 2, 10, 3, 4]" | nu-highlight | print 37 | 38 | 39 | "```" | print 40 | 41 | "#code-block-marker-open-5 42 | ```nu" | print 43 | "[1, 2, 3, 4] | update 1 10" | nu-highlight | print 44 | 45 | [1, 2, 3, 4] | update 1 10 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 46 | print '' 47 | "# [1, 10, 3, 4]" | nu-highlight | print 48 | 49 | 50 | "```" | print 51 | 52 | "#code-block-marker-open-7 53 | ```nu" | print 54 | "let colors = [yellow green] 55 | let colors = ($colors | prepend red) 56 | let colors = ($colors | append purple) 57 | let colors = (\"black\" | append $colors) 58 | $colors # [black red yellow green purple blue]" | nu-highlight | print 59 | 60 | let colors = [yellow green] 61 | let colors = ($colors | prepend red) 62 | let colors = ($colors | append purple) 63 | let colors = ("black" | append $colors) 64 | $colors | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 65 | print '' 66 | "```" | print 67 | 68 | "#code-block-marker-open-9 69 | ```nu" | print 70 | "let colors = [red yellow green purple] 71 | let colors = ($colors | skip 1) 72 | let colors = ($colors | drop 2) 73 | $colors # [yellow]" | nu-highlight | print 74 | 75 | let colors = [red yellow green purple] 76 | let colors = ($colors | skip 1) 77 | let colors = ($colors | drop 2) 78 | $colors | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 79 | print '' 80 | "```" | print 81 | 82 | "#code-block-marker-open-11 83 | ```nu" | print 84 | "let colors = [red yellow green purple black magenta] 85 | let colors = ($colors | last 3) 86 | $colors # [purple black magenta]" | nu-highlight | print 87 | 88 | let colors = [red yellow green purple black magenta] 89 | let colors = ($colors | last 3) 90 | $colors | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 91 | print '' 92 | "```" | print 93 | 94 | "#code-block-marker-open-13 95 | ```nu" | print 96 | "let colors = [yellow green purple] 97 | let colors = ($colors | first 2) 98 | $colors # [yellow green]" | nu-highlight | print 99 | 100 | let colors = [yellow green purple] 101 | let colors = ($colors | first 2) 102 | $colors | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 103 | print '' 104 | "```" | print 105 | 106 | "#code-block-marker-open-15 107 | ```nu" | print 108 | "let x = [1 2] 109 | [ ...$x 3 ...(4..7 | take 2) ]" | nu-highlight | print 110 | 111 | let x = [1 2] 112 | [ ...$x 3 ...(4..7 | take 2) ] | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 113 | print '' 114 | "```" | print 115 | 116 | "#code-block-marker-open-17 117 | ```nu" | print 118 | "let names = [Mark Tami Amanda Jeremy] 119 | $names | each { |elt| $\"Hello, ($elt)!\" } 120 | # Outputs \"Hello, Mark!\" and three more similar lines." | nu-highlight | print 121 | 122 | let names = [Mark Tami Amanda Jeremy] 123 | $names | each { |elt| $"Hello, ($elt)!" } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 124 | print '' 125 | "$names | enumerate | each { |elt| $\"($elt.index + 1) - ($elt.item)\" } 126 | # Outputs \"1 - Mark\", \"2 - Tami\", etc." | nu-highlight | print 127 | 128 | $names | enumerate | each { |elt| $"($elt.index + 1) - ($elt.item)" } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 129 | print '' 130 | "```" | print 131 | 132 | "#code-block-marker-open-19 133 | ```nu" | print 134 | "let colors = [red orange yellow green blue purple] 135 | $colors | where ($it | str ends-with 'e') 136 | # The block passed to `where` must evaluate to a boolean. 137 | # This outputs the list [orange blue purple]." | nu-highlight | print 138 | 139 | let colors = [red orange yellow green blue purple] 140 | $colors | where ($it | str ends-with 'e') 141 | # The block passed to `where` must evaluate to a boolean. 142 | print '' 143 | "```" | print 144 | 145 | "#code-block-marker-open-21 146 | ```nu" | print 147 | "let scores = [7 10 8 6 7] 148 | $scores | where $it > 7 # [10 8]" | nu-highlight | print 149 | 150 | let scores = [7 10 8 6 7] 151 | $scores | where $it > 7 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 152 | print '' 153 | "```" | print 154 | 155 | "#code-block-marker-open-23 156 | ```nu" | print 157 | "let scores = [3 8 4] 158 | $\"total = ($scores | reduce { |elt, acc| $acc + $elt })\" # total = 15 159 | $\"total = ($scores | math sum)\" # easier approach, same result 160 | $\"product = ($scores | reduce --fold 1 { |elt, acc| $acc * $elt })\" # product = 96 161 | $scores | enumerate | reduce --fold 0 { |elt, acc| $acc + $elt.index * $elt.item } # 0*3 + 1*8 + 2*4 = 16" | nu-highlight | print 162 | 163 | let scores = [3 8 4] 164 | $"total = ($scores | reduce { |elt, acc| $acc + $elt })" # total = 15 165 | $"total = ($scores | math sum)" # easier approach, same result 166 | $"product = ($scores | reduce --fold 1 { |elt, acc| $acc * $elt })" # product = 96 167 | $scores | enumerate | reduce --fold 0 { |elt, acc| $acc + $elt.index * $elt.item } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 168 | print '' 169 | "```" | print 170 | 171 | "#code-block-marker-open-25 172 | ```nu" | print 173 | "let names = [Mark Tami Amanda Jeremy] 174 | $names.1 # gives Tami" | nu-highlight | print 175 | 176 | let names = [Mark Tami Amanda Jeremy] 177 | $names.1 | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 178 | print '' 179 | "```" | print 180 | 181 | "#code-block-marker-open-27 182 | ```nu" | print 183 | "let names = [Mark Tami Amanda Jeremy] 184 | let index = 1 185 | $names | get $index # gives Tami" | nu-highlight | print 186 | 187 | let names = [Mark Tami Amanda Jeremy] 188 | let index = 1 189 | $names | get $index | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 190 | print '' 191 | "```" | print 192 | 193 | "#code-block-marker-open-29 194 | ```nu" | print 195 | "let colors = [red green blue] 196 | $colors | is-empty # false" | nu-highlight | print 197 | 198 | let colors = [red green blue] 199 | $colors | is-empty | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 200 | print '' 201 | "let colors = [] 202 | $colors | is-empty # true" | nu-highlight | print 203 | 204 | let colors = [] 205 | $colors | is-empty | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 206 | print '' 207 | "```" | print 208 | 209 | "#code-block-marker-open-31 210 | ```nu" | print 211 | "let colors = [red green blue] 212 | 'blue' in $colors # true 213 | 'yellow' in $colors # false 214 | 'gold' not-in $colors # true" | nu-highlight | print 215 | 216 | let colors = [red green blue] 217 | 'blue' in $colors # true 218 | 'yellow' in $colors # false 219 | 'gold' not-in $colors | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 220 | print '' 221 | "```" | print 222 | 223 | "#code-block-marker-open-33 224 | ```nu" | print 225 | "let colors = [red green blue] 226 | # Do any color names end with \"e\"? 227 | $colors | any {|elt| $elt | str ends-with \"e\" } # true" | nu-highlight | print 228 | 229 | let colors = [red green blue] 230 | # Do any color names end with "e"? 231 | $colors | any {|elt| $elt | str ends-with "e" } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 232 | print '' 233 | "# Is the length of any color name less than 3? 234 | $colors | any {|elt| ($elt | str length) < 3 } # false" | nu-highlight | print 235 | 236 | # Is the length of any color name less than 3? 237 | $colors | any {|elt| ($elt | str length) < 3 } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 238 | print '' 239 | "let scores = [3 8 4] 240 | # Are any scores greater than 7? 241 | $scores | any {|elt| $elt > 7 } # true" | nu-highlight | print 242 | 243 | let scores = [3 8 4] 244 | # Are any scores greater than 7? 245 | $scores | any {|elt| $elt > 7 } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 246 | print '' 247 | "# Are any scores odd? 248 | $scores | any {|elt| $elt mod 2 == 1 } # true" | nu-highlight | print 249 | 250 | # Are any scores odd? 251 | $scores | any {|elt| $elt mod 2 == 1 } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 252 | print '' 253 | "```" | print 254 | 255 | "#code-block-marker-open-35 256 | ```nu" | print 257 | "let colors = [red green blue] 258 | # Do all color names end with \"e\"? 259 | $colors | all {|elt| $elt | str ends-with \"e\" } # false" | nu-highlight | print 260 | 261 | let colors = [red green blue] 262 | # Do all color names end with "e"? 263 | $colors | all {|elt| $elt | str ends-with "e" } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 264 | print '' 265 | "# Is the length of all color names greater than or equal to 3? 266 | $colors | all {|elt| ($elt | str length) >= 3 } # true" | nu-highlight | print 267 | 268 | # Is the length of all color names greater than or equal to 3? 269 | $colors | all {|elt| ($elt | str length) >= 3 } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 270 | print '' 271 | "let scores = [3 8 4] 272 | # Are all scores greater than 7? 273 | $scores | all {|elt| $elt > 7 } # false" | nu-highlight | print 274 | 275 | let scores = [3 8 4] 276 | # Are all scores greater than 7? 277 | $scores | all {|elt| $elt > 7 } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 278 | print '' 279 | "# Are all scores even? 280 | $scores | all {|elt| $elt mod 2 == 0 } # false" | nu-highlight | print 281 | 282 | # Are all scores even? 283 | $scores | all {|elt| $elt mod 2 == 0 } | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 284 | print '' 285 | "```" | print 286 | 287 | "#code-block-marker-open-37 288 | ```nu" | print 289 | "[1 [2 3] 4 [5 6]] | flatten # [1 2 3 4 5 6]" | nu-highlight | print 290 | 291 | [1 [2 3] 4 [5 6]] | flatten | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 292 | print '' 293 | "[[1 2] [3 [4 5 [6 7 8]]]] | flatten | flatten | flatten # [1 2 3 4 5 6 7 8]" | nu-highlight | print 294 | 295 | [[1 2] [3 [4 5 [6 7 8]]]] | flatten | flatten | flatten | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 296 | print '' 297 | "```" | print 298 | 299 | "#code-block-marker-open-39 300 | ```nu" | print 301 | "let zones = [UTC CET Europe/Moscow Asia/Yekaterinburg] 302 | # Show world clock for selected time zones 303 | let base_time = '2024-01-15 12:00:00' | into datetime --timezone UTC 304 | $zones | wrap 'Zone' | upsert Time {|row| ($base_time | date to-timezone $row.Zone | format date '%Y.%m.%d %H:%M')}" | nu-highlight | print 305 | 306 | let zones = [UTC CET Europe/Moscow Asia/Yekaterinburg] 307 | # Show world clock for selected time zones 308 | let base_time = '2024-01-15 12:00:00' | into datetime --timezone UTC 309 | $zones | wrap 'Zone' | upsert Time {|row| ($base_time | date to-timezone $row.Zone | format date '%Y.%m.%d %H:%M')} | table --width ($env.numd?.table-width? | default 120) | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\s*$' "\n" | print; print '' 310 | print '' 311 | "```" | print 312 | -------------------------------------------------------------------------------- /tests/test_commands.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use std/testing * 3 | 4 | # Import all functions from commands.nu (including internals not re-exported via mod.nu) 5 | use ../numd/commands.nu * 6 | 7 | # ============================================================================= 8 | # Tests for parse-markdown-to-blocks 9 | # ============================================================================= 10 | 11 | @test 12 | def "parse-markdown-to-blocks detects nushell block" [] { 13 | let result = "```nushell\necho hello\n```" | parse-markdown-to-blocks 14 | 15 | assert equal ($result | length) 1 16 | assert equal $result.0.row_type "```nushell" 17 | assert equal $result.0.action "execute" 18 | } 19 | 20 | @test 21 | def "parse-markdown-to-blocks detects nu block" [] { 22 | let result = "```nu\nls\n```" | parse-markdown-to-blocks 23 | 24 | assert equal ($result | length) 1 25 | assert equal $result.0.row_type "```nu" 26 | assert equal $result.0.action "execute" 27 | } 28 | 29 | @test 30 | def "parse-markdown-to-blocks handles no-run option" [] { 31 | let result = "```nushell no-run\necho hello\n```" | parse-markdown-to-blocks 32 | 33 | assert equal $result.0.action "print-as-it-is" 34 | } 35 | 36 | @test 37 | def "parse-markdown-to-blocks handles text blocks" [] { 38 | let result = "Some text\nMore text" | parse-markdown-to-blocks 39 | 40 | assert equal ($result | length) 1 41 | assert equal $result.0.row_type "text" 42 | assert equal $result.0.action "print-as-it-is" 43 | } 44 | 45 | @test 46 | def "parse-markdown-to-blocks handles mixed content" [] { 47 | let md = "# Header 48 | 49 | ```nushell 50 | ls 51 | ``` 52 | 53 | Some text 54 | 55 | ```nu no-run 56 | echo skip 57 | ```" 58 | 59 | let result = $md | parse-markdown-to-blocks 60 | 61 | assert equal ($result | length) 4 62 | assert equal ($result | where action == "execute" | length) 1 63 | assert equal ($result | where action == "print-as-it-is" | length) 3 64 | } 65 | 66 | # ============================================================================= 67 | # Tests for classify-block-action 68 | # ============================================================================= 69 | 70 | @test 71 | def "classify-block-action returns execute for nushell" [] { 72 | assert equal (classify-block-action "```nushell") "execute" 73 | } 74 | 75 | @test 76 | def "classify-block-action returns execute for nu" [] { 77 | assert equal (classify-block-action "```nu") "execute" 78 | } 79 | 80 | @test 81 | def "classify-block-action returns print-as-it-is for no-run" [] { 82 | assert equal (classify-block-action "```nushell no-run") "print-as-it-is" 83 | } 84 | 85 | @test 86 | def "classify-block-action returns delete for output-numd" [] { 87 | assert equal (classify-block-action "```output-numd") "delete" 88 | } 89 | 90 | @test 91 | def "classify-block-action returns print-as-it-is for text" [] { 92 | assert equal (classify-block-action "text") "print-as-it-is" 93 | } 94 | 95 | @test 96 | def "classify-block-action returns print-as-it-is for other languages" [] { 97 | assert equal (classify-block-action "```python") "print-as-it-is" 98 | assert equal (classify-block-action "```rust") "print-as-it-is" 99 | } 100 | 101 | # ============================================================================= 102 | # Tests for extract-fence-options 103 | # ============================================================================= 104 | 105 | @test 106 | def "extract-fence-options parses single option" [] { 107 | let result = "```nu no-run" | extract-fence-options 108 | 109 | assert equal $result ["no-run"] 110 | } 111 | 112 | @test 113 | def "extract-fence-options parses multiple options" [] { 114 | let result = "```nushell try, no-output" | extract-fence-options 115 | 116 | assert equal ($result | length) 2 117 | assert ("try" in $result) 118 | assert ("no-output" in $result) 119 | } 120 | 121 | @test 122 | def "extract-fence-options expands short options" [] { 123 | let result = "```nu t, O" | extract-fence-options 124 | 125 | assert ("try" in $result) 126 | assert ("no-output" in $result) 127 | } 128 | 129 | @test 130 | def "extract-fence-options handles empty options" [] { 131 | let result = "```nushell" | extract-fence-options 132 | 133 | assert equal ($result | length) 0 134 | } 135 | 136 | # ============================================================================= 137 | # Tests for convert-short-options 138 | # ============================================================================= 139 | 140 | @test 141 | def "convert-short-options expands O" [] { 142 | assert equal (convert-short-options "O") "no-output" 143 | } 144 | 145 | @test 146 | def "convert-short-options expands N" [] { 147 | assert equal (convert-short-options "N") "no-run" 148 | } 149 | 150 | @test 151 | def "convert-short-options expands t" [] { 152 | assert equal (convert-short-options "t") "try" 153 | } 154 | 155 | @test 156 | def "convert-short-options expands n" [] { 157 | assert equal (convert-short-options "n") "new-instance" 158 | } 159 | 160 | @test 161 | def "convert-short-options keeps long options unchanged" [] { 162 | assert equal (convert-short-options "no-output") "no-output" 163 | assert equal (convert-short-options "try") "try" 164 | } 165 | 166 | # ============================================================================= 167 | # Tests for code-block-marker 168 | # ============================================================================= 169 | 170 | @test 171 | def "code-block-marker generates open marker" [] { 172 | assert equal (code-block-marker 5) "#code-block-marker-open-5" 173 | } 174 | 175 | @test 176 | def "code-block-marker generates close marker" [] { 177 | assert equal (code-block-marker 3 --end) "#code-block-marker-close-3" 178 | } 179 | 180 | # ============================================================================= 181 | # Tests for clean-markdown 182 | # ============================================================================= 183 | 184 | @test 185 | def "clean-markdown removes empty output blocks" [] { 186 | let input = "text\n```output-numd\n \n```\nmore" 187 | let result = $input | clean-markdown 188 | 189 | assert ($result !~ "output-numd") 190 | } 191 | 192 | @test 193 | def "clean-markdown collapses multiple newlines" [] { 194 | let input = "text\n\n\n\nmore" 195 | let result = $input | clean-markdown 196 | 197 | assert ($result !~ "\n{3,}") 198 | } 199 | 200 | @test 201 | def "clean-markdown removes trailing spaces" [] { 202 | let input = "text \nmore \n" 203 | let result = $input | clean-markdown 204 | 205 | assert ($result !~ " \n") 206 | } 207 | 208 | @test 209 | def "clean-markdown ensures single trailing newline" [] { 210 | let input = "text\n\n\n" 211 | let result = $input | clean-markdown 212 | 213 | # Result should be "text\n" - single trailing newline 214 | assert equal $result "text\n" 215 | } 216 | 217 | # ============================================================================= 218 | # Tests for convert-output-fences 219 | # ============================================================================= 220 | 221 | @test 222 | def "convert-output-fences converts output format" [] { 223 | let input = "```nu\n123\n```\n\nOutput:\n\n```\n456\n```" 224 | let result = $input | convert-output-fences 225 | 226 | assert ($result =~ "output-numd") 227 | assert ($result !~ "Output:") 228 | } 229 | 230 | @test 231 | def "convert-output-fences restores expanded format" [] { 232 | let input = "```nu\n123\n```\n```output-numd\n456\n```" 233 | let result = $input | convert-output-fences --restore 234 | 235 | assert ($result =~ "Output:") 236 | assert ($result !~ "output-numd") 237 | } 238 | 239 | # ============================================================================= 240 | # Tests for quote-for-print 241 | # ============================================================================= 242 | 243 | @test 244 | def "quote-for-print escapes quotes" [] { 245 | let result = 'hello "world"' | quote-for-print 246 | 247 | assert equal $result '"hello \"world\""' 248 | } 249 | 250 | @test 251 | def "quote-for-print escapes backslashes" [] { 252 | let result = 'path\to\file' | quote-for-print 253 | 254 | assert equal $result '"path\\to\\file"' 255 | } 256 | 257 | # ============================================================================= 258 | # Tests for can-append-print 259 | # ============================================================================= 260 | 261 | @test 262 | def "can-append-print returns true for simple commands" [] { 263 | assert equal (can-append-print "ls") true 264 | assert equal (can-append-print "echo hello") true 265 | } 266 | 267 | @test 268 | def "can-append-print returns false for let statements" [] { 269 | assert equal (can-append-print "let a = 1") false 270 | assert equal (can-append-print "let a = ls") false 271 | } 272 | 273 | @test 274 | def "can-append-print returns false for mut statements" [] { 275 | assert equal (can-append-print "mut a = 1") false 276 | } 277 | 278 | @test 279 | def "can-append-print returns false for def statements" [] { 280 | assert equal (can-append-print "def foo [] {}") false 281 | } 282 | 283 | @test 284 | def "can-append-print returns false for semicolon ending" [] { 285 | assert equal (can-append-print "ls;") false 286 | } 287 | 288 | @test 289 | def "can-append-print returns false for print ending" [] { 290 | assert equal (can-append-print "ls | print") false 291 | } 292 | 293 | @test 294 | def "can-append-print returns false for source statements" [] { 295 | assert equal (can-append-print "source a.nu") false 296 | assert equal (can-append-print "overlay use foo") false 297 | assert equal (can-append-print "alias ll = ls -la") false 298 | } 299 | 300 | @test 301 | def "can-append-print handles multi-statement commands" [] { 302 | # Last span is echo - should be true 303 | assert equal (can-append-print "source a.nu; echo abc") true 304 | # Last span is source - should be false 305 | assert equal (can-append-print "echo abc; source a.nu") false 306 | # Multi-line: last is echo 307 | assert equal (can-append-print "source a.nu\necho hello") true 308 | # Last span is use - should be false 309 | assert equal (can-append-print "ls; use std") false 310 | } 311 | 312 | # ============================================================================= 313 | # Tests for get-last-span 314 | # ============================================================================= 315 | 316 | @test 317 | def "get-last-span returns whole command for simple commands" [] { 318 | assert equal (get-last-span "ls") "ls" 319 | assert equal (get-last-span "echo hello") "echo hello" 320 | } 321 | 322 | @test 323 | def "get-last-span returns last statement after semicolon" [] { 324 | assert equal (get-last-span "source a.nu; echo abc") "echo abc" 325 | assert equal (get-last-span "echo abc; source a.nu") "source a.nu" 326 | } 327 | 328 | @test 329 | def "get-last-span handles multi-line commands" [] { 330 | assert equal (get-last-span "source a.nu\necho hello") "echo hello" 331 | } 332 | 333 | # ============================================================================= 334 | # Tests for build-modified-path 335 | # ============================================================================= 336 | 337 | @test 338 | def "build-modified-path adds prefix" [] { 339 | let result = "file.nu" | build-modified-path --prefix "test_" 340 | 341 | assert equal $result "test_file.nu" 342 | } 343 | 344 | @test 345 | def "build-modified-path adds suffix" [] { 346 | let result = "file.nu" | build-modified-path --suffix "_backup" 347 | 348 | assert equal $result "file_backup.nu" 349 | } 350 | 351 | @test 352 | def "build-modified-path changes extension" [] { 353 | let result = "file.nu" | build-modified-path --extension ".md" 354 | 355 | assert equal $result "file.nu.md" 356 | } 357 | 358 | @test 359 | def "build-modified-path combines all options" [] { 360 | let result = "file.nu" | build-modified-path --prefix "pre_" --suffix "_suf" --extension ".md" 361 | 362 | assert equal $result "pre_file_suf.nu.md" 363 | } 364 | 365 | # ============================================================================= 366 | # Tests for generate-timestamp 367 | # ============================================================================= 368 | 369 | @test 370 | def "generate-timestamp returns correct format" [] { 371 | let result = generate-timestamp 372 | 373 | # Format should be YYYYMMDD_HHMMSS (15 chars) 374 | assert equal ($result | str length) 15 375 | assert ($result =~ '^\d{8}_\d{6}$') 376 | } 377 | 378 | # ============================================================================= 379 | # Tests for strip-outputs 380 | # ============================================================================= 381 | 382 | @test 383 | def "strip-outputs removes output lines from blocks" [] { 384 | let blocks = "```nu\necho hello\n# => hello\n```" | parse-markdown-to-blocks 385 | 386 | let result = $blocks | strip-outputs 387 | 388 | # The line list should not contain '# => hello' 389 | let code_block = $result | where action == "execute" | first 390 | assert (($code_block.line | str join "\n") !~ '# =>') 391 | } 392 | 393 | @test 394 | def "strip-outputs preserves plain comments" [] { 395 | let blocks = "```nu\n# this is a comment\necho hello\n# => hello\n```" | parse-markdown-to-blocks 396 | 397 | let result = $blocks | strip-outputs 398 | 399 | let code_block = $result | where action == "execute" | first 400 | let code = $code_block.line | str join "\n" 401 | # Plain comment should be preserved 402 | assert ($code =~ '# this is a comment') 403 | # Output line should be removed 404 | assert ($code !~ '# => hello') 405 | } 406 | 407 | @test 408 | def "strip-outputs handles multiple output lines" [] { 409 | let blocks = "```nu\nls\n# => file1\n# => file2\n# => file3\n```" | parse-markdown-to-blocks 410 | 411 | let result = $blocks | strip-outputs 412 | 413 | let code_block = $result | where action == "execute" | first 414 | let code = $code_block.line | str join "\n" 415 | assert ($code !~ '# =>') 416 | assert ($code =~ 'ls') 417 | } 418 | 419 | @test 420 | def "strip-outputs preserves text blocks unchanged" [] { 421 | let blocks = "# Header\n\nSome text" | parse-markdown-to-blocks 422 | 423 | let result = $blocks | strip-outputs 424 | 425 | assert equal ($result | length) 1 426 | assert equal $result.0.action "print-as-it-is" 427 | } 428 | 429 | # ============================================================================= 430 | # Tests for to-markdown 431 | # ============================================================================= 432 | 433 | @test 434 | def "to-markdown renders text blocks" [] { 435 | let blocks = "# Header\n\nSome text" | parse-markdown-to-blocks 436 | 437 | let result = $blocks | to-markdown 438 | 439 | assert ($result =~ '# Header') 440 | assert ($result =~ 'Some text') 441 | } 442 | 443 | @test 444 | def "to-markdown renders code blocks with fences" [] { 445 | let blocks = "```nu\necho hello\n```" | parse-markdown-to-blocks 446 | 447 | let result = $blocks | to-markdown 448 | 449 | assert ($result =~ '```nu') 450 | assert ($result =~ 'echo hello') 451 | assert ($result =~ '```') 452 | } 453 | 454 | @test 455 | def "to-markdown skips delete blocks" [] { 456 | let blocks = "```nu\necho hello\n```\n```output-numd\nold output\n```" | parse-markdown-to-blocks 457 | 458 | let result = $blocks | to-markdown 459 | 460 | # output-numd block should be deleted 461 | assert ($result !~ 'output-numd') 462 | assert ($result !~ 'old output') 463 | } 464 | 465 | @test 466 | def "to-markdown preserves block order" [] { 467 | let md = "# Title\n\n```nu\nls\n```\n\nMiddle text\n\n```nu\necho hi\n```" 468 | let blocks = $md | parse-markdown-to-blocks 469 | 470 | let result = $blocks | to-markdown 471 | 472 | # Check order is preserved 473 | let title_pos = $result | str index-of '# Title' 474 | let ls_pos = $result | str index-of 'ls' 475 | let middle_pos = $result | str index-of 'Middle text' 476 | let echo_pos = $result | str index-of 'echo hi' 477 | 478 | assert ($title_pos < $ls_pos) 479 | assert ($ls_pos < $middle_pos) 480 | assert ($middle_pos < $echo_pos) 481 | } 482 | 483 | # ============================================================================= 484 | # Tests for to-numd-script 485 | # ============================================================================= 486 | 487 | @test 488 | def "to-numd-script extracts code from blocks" [] { 489 | let blocks = "```nu\necho hello\n```" | parse-markdown-to-blocks 490 | 491 | let result = $blocks | to-numd-script 492 | 493 | assert ($result =~ 'echo hello') 494 | } 495 | 496 | @test 497 | def "to-numd-script removes markdown fences" [] { 498 | let blocks = "```nu\necho hello\n```" | parse-markdown-to-blocks 499 | 500 | let result = $blocks | to-numd-script 501 | 502 | # Should not contain raw fence markers 503 | assert ($result !~ '^```') 504 | } 505 | 506 | @test 507 | def "to-numd-script includes infostring as comment" [] { 508 | let blocks = "```nu separate-block\necho hello\n```" | parse-markdown-to-blocks 509 | 510 | let result = $blocks | to-numd-script 511 | 512 | # Infostring should be preserved as comment 513 | assert ($result =~ '# ```nu separate-block') 514 | } 515 | 516 | @test 517 | def "to-numd-script only includes executable blocks" [] { 518 | let blocks = "# Header\n\n```nu\necho hello\n```\n\n```python\nprint('hi')\n```" | parse-markdown-to-blocks 519 | 520 | let result = $blocks | to-numd-script 521 | 522 | assert ($result =~ 'echo hello') 523 | assert ($result !~ 'Header') 524 | assert ($result !~ 'print') 525 | } 526 | 527 | @test 528 | def "to-numd-script handles multiple code blocks" [] { 529 | let blocks = "```nu\nlet a = 1\n```\n\n```nu\necho $a\n```" | parse-markdown-to-blocks 530 | 531 | let result = $blocks | to-numd-script 532 | 533 | assert ($result =~ 'let a = 1') 534 | assert ($result =~ 'echo \$a') 535 | } 536 | -------------------------------------------------------------------------------- /numd/commands.nu: -------------------------------------------------------------------------------- 1 | use std/iter scan 2 | 3 | # Run Nushell code blocks in a markdown file, output results back to the `.md`, and optionally to terminal 4 | @example "update readme" { 5 | numd run README.md 6 | } 7 | export def run [ 8 | file: path # path to a `.md` file containing Nushell code to be executed 9 | --config-path: path = '' # path to a .nu config file (Nushell code prepended to script) 10 | --echo # output resulting markdown to stdout instead of saving to file 11 | --no-fail-on-error # skip errors (markdown is never saved on error) 12 | --no-stats # do not output stats of changes (is activated via --echo by default) 13 | --prepend-code: string # additional code to prepend (applied after config file) 14 | --print-block-results # print blocks one by one as they are executed, useful for long running scripts 15 | --save-intermed-script: path # optional path for keeping intermediate script (useful for debugging purposes). If not set, the temporary intermediate script will be deleted. 16 | --table-width: int # set $env.numd.table-width (overrides config file) 17 | --use-host-config # load host's env, config, and plugin files (default: run with nu -n for reproducibility) 18 | ]: [nothing -> string nothing -> nothing nothing -> record] { 19 | let original_md = open -r $file 20 | 21 | let intermediate_script_path = $save_intermed_script 22 | | default ($file | build-modified-path --prefix $'numd-temp-(generate-timestamp)' --extension '.nu') 23 | 24 | let result = parse-file $file 25 | | execute-blocks --config-path $config_path --no-fail-on-error=$no_fail_on_error --prepend-code $prepend_code --print-block-results=$print_block_results --save-intermed-script $intermediate_script_path --table-width $table_width --use-host-config=$use_host_config 26 | 27 | # if $save_intermed_script param wasn't set - remove the temporary intermediate script 28 | if $save_intermed_script == null { rm $intermediate_script_path } 29 | 30 | # Check for empty output (no code blocks executed) 31 | if ($result | where action == 'execute' | is-empty) { 32 | return { 33 | filename: $file 34 | comment: "the script didn't produce any output" 35 | } 36 | } 37 | 38 | let updated_md = $result | to-markdown 39 | 40 | if $echo { 41 | $updated_md 42 | } else { 43 | check-git-clean $file 44 | $updated_md | ansi strip | save -f $file 45 | 46 | if not $no_stats { 47 | compute-change-stats $file $original_md $updated_md 48 | } 49 | } 50 | } 51 | 52 | # Remove numd execution outputs from the file 53 | export def clear-outputs [ 54 | file: path # path to a `.md` file containing numd output to be cleared 55 | --echo # output resulting markdown to stdout instead of writing to file 56 | --strip-markdown # keep only Nushell script, strip all markdown tags 57 | ]: [nothing -> string nothing -> nothing] { 58 | parse-file $file 59 | | strip-outputs 60 | | if $strip_markdown { 61 | to-numd-script 62 | } else { 63 | to-markdown 64 | } 65 | | if $echo { } else { 66 | let result = $in 67 | check-git-clean $file 68 | $result | save -f $file 69 | } 70 | } 71 | 72 | # Extract pure Nushell script from blocks table (strip markdown fences) 73 | export def to-numd-script []: table -> string { 74 | where action == 'execute' 75 | | each {|block| 76 | $block.line 77 | | update 0 { $'(char nl) # ($in)' } # keep infostring as comment 78 | | drop # remove closing fence 79 | | str join (char nl) 80 | | str replace -r '\s*$' (char nl) 81 | } 82 | | str join (char nl) 83 | } 84 | 85 | # Execute code blocks and return updated blocks table 86 | export def execute-blocks [ 87 | --config-path: path = '' # path to a .nu config file 88 | --no-fail-on-error # skip errors 89 | --prepend-code: string # additional code to prepend 90 | --print-block-results # print blocks as they execute 91 | --save-intermed-script: path # path for intermediate script 92 | --table-width: int # set table width 93 | --use-host-config # load host's env, config, and plugin files 94 | ]: table -> table { 95 | let original = $in 96 | 97 | load-config $config_path --prepend_code $prepend_code --table_width $table_width 98 | 99 | decorate-original-code-blocks $original 100 | | generate-intermediate-script 101 | | save -f $save_intermed_script 102 | 103 | let execution_output = execute-intermediate-script $save_intermed_script $no_fail_on_error $print_block_results $use_host_config 104 | 105 | if $execution_output == '' { 106 | return $original 107 | } 108 | 109 | let results = $execution_output 110 | | str replace -ar "\n{2,}```\n" "\n```\n" 111 | | lines 112 | | extract-block-index 113 | 114 | # Update original table with execution results 115 | let result_indices = $results | get block_index 116 | $original 117 | | each {|block| 118 | if $block.block_index in $result_indices { 119 | let result = $results | where block_index == $block.block_index | first 120 | $block | update line { $result.line | lines } 121 | } else { 122 | $block 123 | } 124 | } 125 | } 126 | 127 | # Parse a markdown file into a blocks table 128 | export def parse-file [ 129 | file: path # path to a markdown file 130 | ]: nothing -> table, action: string> { 131 | open -r $file 132 | | if $nu.os-info.family == windows { 133 | str replace --all (char crlf) "\n" 134 | } else { } 135 | | convert-output-fences 136 | | parse-markdown-to-blocks 137 | } 138 | 139 | # Strip numd output lines (# =>) from code blocks 140 | export def strip-outputs []: table -> table { 141 | update line {|block| 142 | if $block.action == 'execute' { 143 | $block.line | where $it !~ '^# => ?' 144 | } else { 145 | $block.line 146 | } 147 | } 148 | } 149 | 150 | # Render blocks table back to markdown string 151 | export def to-markdown []: table -> string { 152 | where action != 'delete' 153 | | each { $in.line | str join (char nl) } 154 | | str join (char nl) 155 | | clean-markdown 156 | | convert-output-fences --restore 157 | } 158 | 159 | #### 160 | # I keep commands here with `export` to make them available for testing script, yet I do not export them in the mod.nu 161 | ### 162 | 163 | # Detect code blocks in a markdown string and return a table with their line numbers and info strings. 164 | export def parse-markdown-to-blocks []: string -> table, action: string> { 165 | let file_lines = $in | lines 166 | let row_type = $file_lines 167 | | each { 168 | str trim --right 169 | | if $in =~ '^```' { } else { 'text' } 170 | } 171 | | scan --noinit 'text' {|curr_fence prev_fence| 172 | match $curr_fence { 173 | 'text' => { if $prev_fence == 'closing-fence' { 'text' } else { $prev_fence } } 174 | '```' => { if $prev_fence == 'text' { '```' } else { 'closing-fence' } } 175 | _ => { $curr_fence } 176 | } 177 | } 178 | | scan --noinit 'text' {|curr_fence prev_fence| 179 | if $curr_fence == 'closing-fence' { $prev_fence } else { $curr_fence } 180 | } 181 | 182 | let block_index = $row_type 183 | | window --remainder 2 184 | | scan 0 {|window index| 185 | if $window.0 == $window.1? { $index } else { $index + 1 } 186 | } 187 | 188 | # Wrap lists into columns because the `window` command was used previously 189 | $file_lines | wrap line 190 | | merge ($row_type | wrap row_type) 191 | | merge ($block_index | wrap block_index) 192 | | if ($in | last | $in.row_type =~ '^```nu' and $in.line != '```') { 193 | error make { 194 | msg: 'A closing code block fence (```) is missing; the markdown might be invalid.' 195 | } 196 | } else { } 197 | | group-by block_index --to-table 198 | | insert row_type { $in.items.row_type.0 } 199 | | update items { get line } 200 | | rename block_index line row_type 201 | | select block_index row_type line 202 | | into int block_index 203 | | insert action { classify-block-action $in.row_type } 204 | } 205 | 206 | export def classify-block-action [ 207 | $row_type: string 208 | ]: nothing -> string { 209 | match $row_type { 210 | 'text' => { 'print-as-it-is' } 211 | '```output-numd' => { 'delete' } 212 | 213 | $i if ($i =~ '^```nu(shell)?(\s|$)') => { 214 | if $i =~ 'no-run' { 'print-as-it-is' } else { 'execute' } 215 | } 216 | 217 | _ => { 'print-as-it-is' } 218 | } 219 | } 220 | 221 | # Apply output formatting based on fence options (separate-block vs inline `# =>`). 222 | def format-command-output [fence_options: list]: string -> string { 223 | if 'no-output' in $fence_options { return $in } else { } 224 | | if 'separate-block' in $fence_options { generate-separate-block-fence } else { } 225 | | if (can-append-print $in) { 226 | generate-inline-output-pipeline 227 | | generate-print-statement 228 | } else { } 229 | } 230 | 231 | # Generate code for execution in the intermediate script within a given code fence. 232 | export def generate-block-execution [ 233 | fence_options: list 234 | ]: string -> string { 235 | let code_content = $in 236 | 237 | let highlighted_command = $code_content | generate-highlight-print 238 | 239 | let code_execution = $code_content 240 | | trim-trailing-comments 241 | | if 'try' in $fence_options { 242 | wrap-in-try-catch --new-instance=('new-instance' in $fence_options) 243 | } else { } 244 | | format-command-output $fence_options 245 | | $in + (char nl) 246 | # Always print a blank line after each command group to preserve visual separation 247 | | $in + "print ''" 248 | 249 | $highlighted_command + $code_execution 250 | } 251 | 252 | # Generate additional service code necessary for execution and capturing results, while preserving the original code. 253 | export def decorate-original-code-blocks [ 254 | md_classified: table, action: string> # classified markdown table from parse-markdown-to-blocks 255 | ]: nothing -> table, action: string, code: string> { 256 | $md_classified 257 | | where action == 'execute' 258 | | insert code {|i| 259 | $i.line 260 | | process-code-block-content ($i.row_type | extract-fence-options) 261 | | generate-block-markers $i.block_index $i.row_type 262 | } 263 | } 264 | 265 | # Generate an intermediate script from a table of classified markdown code blocks. 266 | # 267 | # Takes decorated code blocks and produces a complete Nushell script ready for execution. 268 | export def generate-intermediate-script []: table, action: string, code: string> -> string { 269 | get code -o 270 | | if $env.numd?.prepend-code? != null { 271 | prepend $"($env.numd?.prepend-code?)\n" 272 | | if $env.numd.config-path? != null { 273 | prepend ($"# numd config loaded from `($env.numd.config-path)`\n") 274 | } else { } 275 | } else { } 276 | | prepend $"const init_numd_pwd_const = '(pwd)'\n" # initialize it here so it will be available in intermediate scripts 277 | | prepend ( 278 | '# this script was generated automatically using numd' + 279 | "\n# https://github.com/nushell-prophet/numd\n" 280 | ) 281 | | flatten 282 | | str join (char nl) 283 | | str replace -r "\\s*$" "\n" 284 | } 285 | 286 | # Split code block content by blank lines into command groups and generate execution code for each. 287 | export def process-code-block-content [ 288 | fence_options: list # options from the code fence (e.g., 'no-output', 'try') 289 | ]: list -> list { 290 | skip | drop # skip code fences 291 | | where $it !~ '^# =>' # strip existing `# =>` output lines (keep plain `#` comments) 292 | | str join (char nl) 293 | | split-by-blank-lines 294 | | each {|group| 295 | let trimmed = $group | str trim 296 | if ($trimmed | is-empty) { 297 | '' 298 | } else if ($trimmed | lines | all { $in =~ '^#' }) { 299 | $group | generate-highlight-print 300 | } else { 301 | $group | generate-block-execution $fence_options 302 | } 303 | } 304 | } 305 | 306 | # Split string by blank lines (double newlines) into command groups. 307 | # Preserves multiline commands that don't have blank lines between them. 308 | export def split-by-blank-lines []: string -> list { 309 | split row "\n\n" 310 | | each { str trim -c "\n" } 311 | } 312 | 313 | # Parse block indices from Nushell output lines and return a table with the original markdown line numbers. 314 | export def extract-block-index []: list -> table { 315 | let clean_lines = skip until { $in =~ (code-block-marker) } 316 | 317 | let block_index = $clean_lines 318 | | each { 319 | if $in =~ $"^(code-block-marker)\\d+$" { 320 | split row '-' | last | into int 321 | } else { 322 | -1 323 | } 324 | } 325 | | scan --noinit 0 {|curr_index prev_index| 326 | if $curr_index == -1 { $prev_index } else { $curr_index } 327 | } 328 | | wrap block_index 329 | 330 | $clean_lines 331 | | wrap 'nu_out' 332 | | merge $block_index 333 | | group-by block_index --to-table 334 | | upsert items {|i| 335 | $i.items.nu_out 336 | | skip 337 | | take until { $in =~ (code-block-marker --end) } 338 | | str join (char nl) 339 | } 340 | | rename block_index line 341 | | into int block_index 342 | } 343 | 344 | # Assemble the final markdown by merging the original classified markdown with parsed results of the generated script. 345 | export def merge-markdown [ 346 | md_classified: table, action: string> 347 | nu_res_with_block_index: table 348 | ]: nothing -> string { 349 | $md_classified 350 | | where action == 'print-as-it-is' 351 | | update line { str join (char nl) } 352 | | append $nu_res_with_block_index 353 | | sort-by block_index 354 | | get line 355 | | str join (char nl) 356 | } 357 | 358 | # Prettify markdown by removing unnecessary empty lines and trailing spaces. 359 | export def clean-markdown []: string -> string { 360 | str replace --all --regex "\n```output-numd\\s+```\n" "\n" # empty output-numd blocks 361 | | str replace --all --regex "\n{3,}" "\n\n" # multiple newlines 362 | | str replace --all --regex " +\n" "\n" # remove trailing spaces 363 | | str replace --all --regex "\\s*$" "\n" # ensure a single trailing newline 364 | } 365 | 366 | # Replacement is needed to distinguish the blocks with outputs from blocks with just ```. 367 | # `parse-markdown-to-blocks` works only with lines without knowing the previous lines. 368 | @example "convert output fence to compact format" { 369 | "```nu\n123\n```\n\nOutput:\n\n```\n123" | convert-output-fences 370 | } --result "```nu\n123\n```\n```output-numd\n123" 371 | export def convert-output-fences [ 372 | expanded_format = "\n```\n\nOutput:\n\n```\n" # default params to prevent collecting $in 373 | compact_format = "\n```\n```output-numd\n" 374 | --restore # convert back from compact to expanded format 375 | ]: string -> string { 376 | if $restore { 377 | str replace --all $compact_format $expanded_format 378 | } else { 379 | str replace --all $expanded_format $compact_format 380 | } 381 | } 382 | 383 | # Calculate changes between the original and updated markdown files and return a record with the differences. 384 | export def compute-change-stats [ 385 | filename: path 386 | orig_file: string 387 | new_file: string 388 | ]: nothing -> record { 389 | let original_file_content = $orig_file | ansi strip 390 | let new_file_content = $new_file | ansi strip 391 | 392 | let nushell_blocks = $new_file_content 393 | | parse-markdown-to-blocks 394 | | where action == 'execute' 395 | | get block_index 396 | | uniq 397 | | length 398 | 399 | $new_file_content | str stats | transpose metric new 400 | | merge ($original_file_content | str stats | transpose metric old) 401 | | insert change_percentage {|metric_stats| 402 | let change_value = $metric_stats.new - $metric_stats.old 403 | 404 | ($change_value / $metric_stats.old) * 100 405 | | math round --precision 1 406 | | if $in < 0 { 407 | $"(ansi red)($change_value) \(($in)%\)(ansi reset)" 408 | } else if ($in > 0) { 409 | $"(ansi blue)+($change_value) \(($in)%\)(ansi reset)" 410 | } else { '0%' } 411 | } 412 | | update metric { $'diff_($in)' } 413 | | select metric change_percentage 414 | | transpose --as-record --ignore-titles --header-row 415 | | insert filename ($filename | path basename) 416 | | insert levenshtein_dist ($original_file_content | str distance $new_file_content) 417 | | insert nushell_blocks $nushell_blocks 418 | | select filename nushell_blocks levenshtein_dist diff_lines diff_words diff_chars 419 | } 420 | 421 | # Fence options data: short form, long form, description 422 | const fence_options = [ 423 | [short long description]; 424 | 425 | [O no-output "execute code without outputting results"] 426 | [N no-run "do not execute code in block"] 427 | [t try "execute block inside `try {}` for error handling"] 428 | [n new-instance "execute block in new Nushell instance (useful with `try` block)"] 429 | [s separate-block "output results in a separate code block instead of inline `# =>`"] 430 | ] 431 | 432 | # List fence options for execution and output customization. 433 | export def list-fence-options []: nothing -> table { 434 | $fence_options | select long short description 435 | } 436 | 437 | # Expand short options for code block execution to their long forms. 438 | @example "expand short option to long form" { 439 | convert-short-options 'O' 440 | } --result 'no-output' 441 | export def convert-short-options [ 442 | option: string 443 | ]: nothing -> string { 444 | let options_dict = $fence_options | select short long | transpose -r -d 445 | let result = $options_dict | get -o $option | default $option 446 | 447 | if $result not-in ($options_dict | values) { 448 | print $'(ansi red)($result) is unknown option(ansi reset)' 449 | } 450 | 451 | $result 452 | } 453 | 454 | # Escape symbols to be printed unchanged inside a `print "something"` statement. 455 | @example "escape quotes for print statement" { 456 | 'abcd"dfdaf" "' | quote-for-print 457 | } --result '"abcd\"dfdaf\" \""' 458 | export def quote-for-print []: string -> string { 459 | # `to json` might give similar results, yet it replaces new lines 460 | # which spoils readability of intermediate scripts 461 | str replace --all --regex '(\\|\")' '\$1' 462 | | $'"($in)"' 463 | } 464 | 465 | # Run the intermediate script and return its output as a string. 466 | export def execute-intermediate-script [ 467 | intermed_script_path: path # path to the generated intermediate script 468 | no_fail_on_error: bool # if true, return empty string on error instead of failing 469 | print_block_results: bool # print blocks one by one as they execute 470 | use_host_config: bool # if true, load host's env, config, and plugin files 471 | ]: nothing -> string { 472 | let args = if $use_host_config { 473 | [ 474 | ...(if ($nu.env-path | path exists) { [--env-config $nu.env-path] } else { [] }) 475 | ...(if ($nu.config-path | path exists) { [--config $nu.config-path] } else { [] }) 476 | ...(if ($nu.plugin-path | path exists) { [--plugin-config $nu.plugin-path] } else { [] }) 477 | ] 478 | } else { 479 | [-n] 480 | } 481 | 482 | (^$nu.current-exe ...$args $intermed_script_path) 483 | | if $print_block_results { tee { print } } else { } 484 | | complete 485 | | if $in.exit_code == 0 { 486 | get stdout 487 | } else { 488 | if $no_fail_on_error { 489 | '' 490 | } else { 491 | error make --unspanned {msg: ($in.stderr? | default '' | into string)} # default '' - to refactor later 492 | } 493 | } 494 | } 495 | 496 | # Generate a unique identifier for code blocks in markdown to distinguish their output. 497 | @example "generate marker for block 3" { 498 | code-block-marker 3 499 | } --result "#code-block-marker-open-3" 500 | export def code-block-marker [ 501 | index?: int 502 | --end 503 | ]: nothing -> string { 504 | $"#code-block-marker-open-($index)" 505 | | if $end { str replace 'open' 'close' } else { } 506 | } 507 | # TODO NUON can be used in code-block-markers to set display options 508 | 509 | # Generate a command to highlight code using Nushell syntax highlighting. 510 | @example "generate syntax highlighting command" { 511 | 'ls' | generate-highlight-print 512 | } --result "\"ls\" | nu-highlight | print\n\n" 513 | export def generate-highlight-print []: string -> string { 514 | quote-for-print 515 | | $"($in) | nu-highlight | print(char nl)(char nl)" 516 | } 517 | 518 | # Trim comments and extra whitespace from code blocks for use in the generated script. 519 | export def trim-trailing-comments []: string -> string { 520 | str replace -r '[\s\n]+$' '' # trim newlines and spaces from the end of a line 521 | | str replace -r '\s+#.*$' '' # remove comments from the last line. Might spoil code blocks with the # symbol, used not for commenting 522 | } 523 | 524 | # Extract the last span from a command to determine if `| print` can be appended. 525 | @example "pipeline ending with command" { get-last-span 'let a = 1..10; $a | length' } --result 'length' 526 | @example "pipeline ending with assignment" { get-last-span 'let a = 1..10; let b = $a | length' } --result 'let b = $a | length' 527 | @example "statement ending with semicolon" { get-last-span 'let a = 1..10; ($a | length);' } --result 'let a = 1..10; ($a | length);' 528 | @example "expression in parentheses" { get-last-span 'let a = 1..10; ($a | length)' } --result '($a | length)' 529 | @example "single assignment" { get-last-span 'let a = 1..10' } --result 'let a = 1..10' 530 | @example "string literal" { get-last-span '"abc"' } --result '"abc"' 531 | export def get-last-span [ 532 | command: string 533 | ]: nothing -> string { 534 | let command = $command | str trim 535 | let spans = ast $command --json 536 | | get block 537 | | from json 538 | | to yaml 539 | | parse -r 'span:\n\s+start:(.*)\n\s+end:(.*)' 540 | | rename start end 541 | | into int start end 542 | 543 | # I just brute-forced AST filter parameters in nu 0.97, as `ast` awaits a better replacement or improvement. 544 | let last_span_end = $spans.end | math max 545 | let longest_last_span_start = $spans 546 | | where end == $last_span_end 547 | | get start 548 | | if ($in | length) == 1 { } else { sort | skip } 549 | | first 550 | 551 | let offset = $longest_last_span_start - $last_span_end 552 | 553 | $command 554 | | str substring $offset.. 555 | } 556 | 557 | # Check if the command can have `| print` appended by analyzing its last span for semicolons or declaration keywords. 558 | @example "assignment cannot have print appended" { can-append-print 'let a = ls' } --result false 559 | @example "command can have print appended" { can-append-print 'ls' } --result true 560 | @example "mutation cannot have print appended" { can-append-print 'mut a = 1; $a = 2' } --result false 561 | export def can-append-print [ 562 | command: string 563 | ]: nothing -> bool { 564 | let last_span = get-last-span $command 565 | 566 | ( 567 | $last_span !~ '(;|print|null)$' 568 | and $last_span !~ '\b(let|mut|def|use|source|overlay|alias)\b' 569 | and $last_span !~ '(^|;|\n) ?(?` prefix for inline display. 574 | @example "generate inline output capture pipeline" { 575 | 'ls' | generate-inline-output-pipeline 576 | } --result "ls | table --width 120 | default '' | into string | lines | each {$'# => ($in)' | str trim --right} | str join (char nl) | str replace -r '\\s*$' \"\\n\"" 577 | export def generate-inline-output-pipeline [ 578 | --indent: string = '# => ' # prefix string for each output line 579 | ]: string -> string { 580 | generate-table-statement 581 | | $"($in) | default '' | into string | lines | each {$'($indent)\($in\)' | str trim --right} | str join \(char nl\) | str replace -r '\\s*$' \"\\n\"" 582 | } 583 | 584 | # Generate a print statement for capturing command output. 585 | @example "wrap command with print" { 'ls' | generate-print-statement } --result "ls | print; print ''" 586 | export def generate-print-statement []: string -> string { 587 | $"($in) | print; print ''" # The last `print ''` is for newlines after executed commands 588 | } 589 | 590 | # Generate a table statement with width evaluated at runtime from $env.numd.table-width. 591 | @example "default table width" { 'ls' | generate-table-statement } --result 'ls | table --width ($env.numd?.table-width? | default 120)' 592 | export def generate-table-statement []: string -> string { 593 | $in + ' | table --width ($env.numd?.table-width? | default 120)' 594 | } 595 | 596 | # Wrap code in a try-catch block to handle errors gracefully. 597 | @example "wrap command in try-catch" { 'ls' | wrap-in-try-catch } --result 'try {ls} catch {|error| $error}' 598 | export def wrap-in-try-catch [ 599 | --new-instance # execute in a separate Nushell instance to get formatted error messages 600 | ]: string -> string { 601 | if $new_instance { 602 | quote-for-print 603 | | ( 604 | $'($nu.current-exe) -c ($in)' + 605 | " | complete | if ($in.exit_code != 0) {get stderr} else {get stdout}" 606 | ) 607 | } else { 608 | $"try {($in)} catch {|error| $error}" 609 | } 610 | } 611 | 612 | # Generate a fenced code block for output with a specific format. 613 | export def generate-separate-block-fence []: string -> string { 614 | # We use a combination of "\n" and (char nl) here for intermediate script formatting aesthetics 615 | $"\"```\\n```output-numd\" | print(char nl)(char nl)($in)" 616 | } 617 | 618 | # Join a list of strings and generate a print statement for the combined output. 619 | export def join-and-print []: list -> string { 620 | str join (char nl) 621 | | quote-for-print 622 | | $'($in) | print' 623 | } 624 | 625 | # Generate marker tags and code block delimiters for tracking output in the intermediate script. 626 | export def generate-block-markers [ 627 | block_number: int # index of the code block in the markdown 628 | fence: string # the original fence line (e.g., '```nushell') 629 | ]: list -> string { 630 | let input = $in 631 | 632 | code-block-marker $block_number 633 | | append $fence 634 | | join-and-print 635 | | append $input 636 | | append '"```" | print' 637 | | append '' 638 | | str join (char nl) 639 | } 640 | 641 | # Parse options from a code fence and return them as a list. 642 | @example "parse fence options with short forms" { '```nu no-run, t' | extract-fence-options } --result ['no-run' 'try'] 643 | export def extract-fence-options []: string -> list { 644 | str replace -r '```nu(shell)?\s*' '' 645 | | split row ',' 646 | | str trim 647 | | compact --empty 648 | | each { convert-short-options $in } 649 | } 650 | 651 | # Modify a path by adding a prefix, suffix, extension, or parent directory. 652 | @example "build path with all modifiers" { 653 | 'numd/capture.nu' | build-modified-path --extension '.md' --prefix 'pref_' --suffix '_suf' --parent_dir abc 654 | } --result 'numd/abc/pref_capture_suf.nu.md' 655 | export def build-modified-path [ 656 | --prefix: string 657 | --suffix: string 658 | --extension: string 659 | --parent_dir: string 660 | ]: path -> path { 661 | path parse 662 | | update stem { $'($prefix)($in)($suffix)' } 663 | | if $extension != null { update extension { $in + $extension } } else { } 664 | | if $parent_dir != null { 665 | update parent { path join $parent_dir | $'(mkdir $in)($in)' } 666 | } else { } 667 | | path join 668 | } 669 | 670 | # Check if file is safely tracked in git before overwriting. 671 | # Warns if file has uncommitted changes or is not tracked by git. 672 | export def check-git-clean [ 673 | file_path: path # path to the file to check 674 | ]: nothing -> nothing { 675 | let file = $file_path | path expand 676 | 677 | # Check if we're in a git repo 678 | let in_git_repo = (do { git rev-parse --git-dir } | complete).exit_code == 0 679 | if not $in_git_repo { return } 680 | 681 | # Check if file is tracked by git 682 | let is_tracked = (do { git ls-files --error-unmatch $file } | complete).exit_code == 0 683 | if not $is_tracked { 684 | print -e $"(ansi yellow)Warning: ($file_path) is not tracked by git(ansi reset)" 685 | return 686 | } 687 | 688 | # Check if file has uncommitted changes 689 | let has_changes = (git diff --name-only $file | str trim) != '' 690 | let is_staged = (git diff --staged --name-only $file | str trim) != '' 691 | if $has_changes or $is_staged { 692 | print -e $"(ansi yellow)Warning: ($file_path) has uncommitted changes(ansi reset)" 693 | } 694 | } 695 | 696 | # TODO: make config an env record 697 | 698 | # Load numd configuration from a .nu file or command-line options into the environment. 699 | # The config file is a Nushell script that gets prepended to the intermediate script. 700 | # Flags override config file settings (applied after config file content). 701 | export def --env load-config [ 702 | path: path # path to a .nu config file (Nushell code to prepend) 703 | --prepend_code: string # additional code to prepend (applied after config file) 704 | --table_width: int # width for table output (sets $env.numd.table-width, applied last) 705 | ]: nothing -> nothing { 706 | # Build prepend code: config file → flag code → table-width override 707 | let config_code = if $path != '' { open -r $path } else { '' } 708 | let flag_code = $prepend_code | default '' 709 | let table_width_code = if $table_width != null { $"\n$env.numd.table-width = ($table_width)" } else { '' } 710 | 711 | let combined_code = [$config_code $flag_code $table_width_code] 712 | | where $it != '' 713 | | str join "\n" 714 | | str trim 715 | 716 | $env.numd = if $combined_code == '' { 717 | {} 718 | } else { 719 | { 720 | prepend-code: $combined_code 721 | config-path: (if $path != '' { $path }) 722 | } 723 | } 724 | } 725 | 726 | # Generate a timestamp string in the format YYYYMMDD_HHMMSS. 727 | export def generate-timestamp []: nothing -> string { 728 | date now | format date "%Y%m%d_%H%M%S" 729 | } 730 | --------------------------------------------------------------------------------