├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── other.md ├── pull_request_template.md └── workflows │ ├── lint.yml │ ├── release.nu │ ├── release.yml │ ├── test-nur.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── commitlint.config.js ├── nur-tests └── nurfile ├── nurfile ├── scripts ├── nur-completions.bash ├── nur-completions.nu ├── nur-completions.zsh └── nurify.nu ├── src ├── args.rs ├── commands │ ├── mod.rs │ └── nur.rs ├── compat.rs ├── engine.rs ├── errors.rs ├── main.rs ├── names.rs ├── nu-scripts │ ├── default_nur_config.nu │ └── default_nur_env.nu ├── nu_version.rs ├── path.rs ├── scripts.rs └── state.rs └── wix ├── License.rtf └── main.wxs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ddanier 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. I have a task defined as '...' 16 | 2. When I do '....' 17 | 3. This happens '....' 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Version details (please complete the following information):** 26 | - OS: [e.g. Linux] 27 | - OS-Version [e.g. Debian 12] 28 | - nur Version [e.g. 0.1.2] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'improvement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other issue 3 | about: Describe this issue purpose in detail. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Run linter 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Setup Rust 13 | uses: actions-rust-lang/setup-rust-toolchain@v1 14 | with: 15 | toolchain: stable 16 | 17 | - name: Run Linter 18 | run: cargo clippy 19 | -------------------------------------------------------------------------------- /.github/workflows/release.nu: -------------------------------------------------------------------------------- 1 | let bin = "nur" 2 | let os = ($env.OS | parse "{name}-{version}" | first) 3 | let target = $env.TARGET 4 | let format = $env.FORMAT 5 | let src = $env.GITHUB_WORKSPACE 6 | let version = (open Cargo.toml | get package.version) 7 | let suffix = match $os.name { 8 | "windows" => ".exe" 9 | _ => "" 10 | } 11 | let target_path = $'target/($target)/release' 12 | let release_bin = $'($target_path)/($bin)($suffix)' 13 | let executables = $'($target_path)/($bin)*($suffix)' 14 | let dest = $'($bin)-($version)-($target)' 15 | let dist = $'($env.GITHUB_WORKSPACE)/output' 16 | 17 | def 'hr-line' [] { 18 | print $'(ansi g)----------------------------------------------------------------------------(ansi reset)' 19 | } 20 | 21 | print $'Config for this run is:' 22 | { 23 | bin: $bin 24 | os: $os 25 | target: $target 26 | format: $format 27 | src: $src 28 | version: $version 29 | suffix: $suffix 30 | target_path: $target_path 31 | release_bin: $release_bin 32 | executables: $executables 33 | dest: $dest 34 | dist: $dist 35 | } | table --expand | print 36 | 37 | print $'Packaging ($bin) v($version) for ($target) in ($src)...' 38 | 39 | hr-line 40 | print $'Preparing build dependencies for ($bin)...' 41 | match [$os.name, $format, $target] { 42 | ["ubuntu", "bin", "aarch64-unknown-linux-gnu"] => { 43 | sudo apt update 44 | sudo apt install -y gcc-aarch64-linux-gnu 45 | $env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 'aarch64-linux-gnu-gcc' 46 | } 47 | ["windows", "msi", _] => { 48 | cargo install cargo-wix 49 | } 50 | } 51 | 52 | hr-line 53 | print $'Start building ($bin)...' 54 | match [$os.name, $format] { 55 | ["windows", _] => { 56 | cargo build --release --all --target $target 57 | } 58 | [_, "bin"] => { 59 | cargo build --release --all --target $target --features=static-link-openssl 60 | } 61 | } 62 | 63 | hr-line 64 | print $'Check ($bin) version...' 65 | let built_version = do --ignore-errors { ^$release_bin --version | str join } | default "" 66 | if ($built_version | str trim | is-empty) { 67 | print $'(ansi r)Incompatible arch: cannot run ($release_bin)(ansi reset)' 68 | } else { 69 | print $" -> built version is: ($built_version)" 70 | } 71 | 72 | hr-line 73 | print $'Cleanup release target path...' 74 | rm -rf ...(glob $'($target_path)/*.d') 75 | 76 | hr-line 77 | print $'Copying ($bin) and other release files to ($dest)...' 78 | match [$os.name, $format] { 79 | ["windows", "msi"] => { 80 | print ' -> skipping for MSI build' 81 | } 82 | _ => { 83 | mkdir $dest 84 | [README.md LICENSE ...(glob $executables)] | each {|it| cp -rv $it $dest } | flatten 85 | } 86 | } 87 | 88 | hr-line 89 | print $'Creating release archive in ($dist)...' 90 | mkdir $dist 91 | mut archive = $'($dist)/($dest).tar.gz' 92 | match [$os.name, $format] { 93 | ["windows", "msi"] => { 94 | $archive = $'($dist)/($dest).msi' 95 | let main_version = if ("+" in $version) {$version | split row "+" | get 0} else {$version} 96 | cargo wix --no-build --nocapture --target $target --package $bin --output $archive --install-version $main_version 97 | } 98 | ["windows", "bin"] => { 99 | $archive = $'($dist)/($dest).zip' 100 | 7z a $archive $dest 101 | } 102 | _ => { 103 | tar -czf $archive $dest 104 | } 105 | } 106 | print $' -> archive: ($archive)' 107 | 108 | hr-line 109 | print $'Provide archive to GitHub...' 110 | match $os.name { 111 | "windows" => { 112 | # Workaround for https://github.com/softprops/action-gh-release/issues/280 113 | echo $"archive=($archive | str replace --all '\' '/')" | save --append $env.GITHUB_OUTPUT 114 | } 115 | _ => { 116 | echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create release as draft 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v[0-9]+.[0-9]+.[0-9]+*" 8 | 9 | jobs: 10 | release: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | target: 15 | - aarch64-apple-darwin 16 | - x86_64-apple-darwin 17 | - x86_64-pc-windows-msvc 18 | - aarch64-pc-windows-msvc 19 | - x86_64-unknown-linux-gnu 20 | - aarch64-unknown-linux-gnu 21 | format: ['bin'] 22 | include: 23 | - target: aarch64-apple-darwin 24 | os: macos-latest 25 | - target: x86_64-apple-darwin 26 | os: macos-latest 27 | - target: x86_64-pc-windows-msvc 28 | os: windows-latest 29 | - target: x86_64-pc-windows-msvc 30 | format: msi 31 | os: windows-latest 32 | - target: aarch64-pc-windows-msvc 33 | os: windows-latest 34 | - target: aarch64-pc-windows-msvc 35 | format: msi 36 | os: windows-latest 37 | - target: x86_64-unknown-linux-gnu 38 | os: ubuntu-latest 39 | - target: aarch64-unknown-linux-gnu 40 | os: ubuntu-latest 41 | 42 | runs-on: ${{matrix.os}} 43 | permissions: 44 | contents: write 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Setup rust toolchain and cache 50 | uses: actions-rust-lang/setup-rust-toolchain@v1 51 | with: 52 | toolchain: stable 53 | target: ${{ matrix.target }} 54 | rustflags: '' # Keep, as otherwise the defaults will be used 55 | 56 | - name: Setup nushell 57 | uses: hustcer/setup-nu@v3 58 | with: 59 | version: "*" 60 | 61 | - name: Build nur archive 62 | id: build 63 | run: nu .github/workflows/release.nu 64 | env: 65 | OS: ${{ matrix.os }} 66 | TARGET: ${{ matrix.target }} 67 | FORMAT: ${{ matrix.format }} 68 | 69 | - name: Publish archives into draft release 70 | uses: softprops/action-gh-release@v2 71 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 72 | with: 73 | draft: true 74 | name: "Release ${{ github.ref_name }}" 75 | generate_release_notes: true 76 | files: ${{ steps.build.outputs.archive }} 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | -------------------------------------------------------------------------------- /.github/workflows/test-nur.yml: -------------------------------------------------------------------------------- 1 | name: Run nur nuscript tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: true 9 | matrix: 10 | platform: [windows-latest, macos-latest, ubuntu-latest] 11 | runs-on: ${{ matrix.platform }} 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup Rust 17 | uses: actions-rust-lang/setup-rust-toolchain@v1 18 | with: 19 | toolchain: stable 20 | 21 | - name: Build nur release version to be used for nur nuscript tests 22 | run: cargo build --release 23 | 24 | - name: Run nur nuscript Tests 25 | run: | 26 | cd nur-tests 27 | ./../target/release/nur run-all 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: true 9 | matrix: 10 | platform: [windows-latest, macos-latest, ubuntu-latest] 11 | runs-on: ${{ matrix.platform }} 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Setup Rust 17 | uses: actions-rust-lang/setup-rust-toolchain@v1 18 | with: 19 | toolchain: stable 20 | 21 | - name: Run Tests 22 | run: cargo test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /target 3 | /_* 4 | /nurfile.local 5 | tarpaulin-report.html 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-toml 7 | - id: check-merge-conflict 8 | - id: end-of-file-fixer 9 | - repo: https://github.com/doublify/pre-commit-rust 10 | rev: v1.0 11 | hooks: 12 | - id: fmt 13 | - id: cargo-check 14 | - id: clippy 15 | - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook 16 | rev: v9.22.0 17 | hooks: 18 | - id: commitlint 19 | stages: [commit-msg] 20 | additional_dependencies: 21 | - "@team23/commitlint-config@1.0.0" 22 | 23 | default_stages: 24 | - pre-commit 25 | 26 | default_install_hook_types: 27 | - pre-commit 28 | - commit-msg 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you want to contribute to this project, feel free to just fork the project, create a dev 4 | branch in your fork and then create a pull request (PR). If you are unsure about whether 5 | your changes really suit the project please create an issue first, to talk about this. Also 6 | please check existing issues and/or issues first. 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nur" 3 | description = "nur - a taskrunner based on nu shell" 4 | version = "0.16.0+0.104.0" 5 | rust-version = "1.84.1" 6 | edition = "2021" 7 | license = "MIT" 8 | homepage = "https://nur-taskrunner.github.io/docs/" 9 | repository = "https://github.com/nur-taskrunner/nur" 10 | readme = "README.md" 11 | authors = ["David Danier "] 12 | keywords = ["nu", "taskrunner", "development", "command-line", "utility"] 13 | categories = ["command-line-utilities", "development-tools"] 14 | 15 | [dependencies] 16 | nu-cli = "0.104.0" 17 | nu-cmd-extra = "0.104.0" 18 | nu-cmd-lang = "0.104.0" 19 | nu-command = "0.104.0" 20 | nu-engine = "0.104.0" 21 | nu-explore = "0.104.0" 22 | nu-parser = "0.104.0" 23 | nu-protocol = "0.104.0" 24 | nu-std = "0.104.0" 25 | nu-utils = "0.104.0" 26 | thiserror = "2.0.9" 27 | miette = { version = "7.5", features = ["fancy-no-backtrace", "fancy"] } 28 | nu-ansi-term = "0.50.1" 29 | nu-path = "0.104.0" 30 | 31 | [target.'cfg(not(target_os = "windows"))'.dependencies] 32 | openssl = { version = "0.10", features = ["vendored"], optional = true } 33 | 34 | [features] 35 | default = [] 36 | static-link-openssl = ["dep:openssl", "nu-cmd-lang/static-link-openssl"] 37 | debug = [] 38 | 39 | [dev-dependencies] 40 | tempfile = "3.14.0" 41 | 42 | [profile.release] 43 | opt-level = "s" # Optimize for size 44 | strip = "debuginfo" 45 | lto = "thin" 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David Danier 4 | 5 | `nur` does borrow code or inspirations from `nu` itself and also `embed-nu` 6 | (https://github.com/Trivernis/embed-nu) by Trivernis. Thanks to both projects! 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nur - a taskrunner based on `nu` shell 2 | 3 | `nur` is a simple, yet very powerful task runner. It borrows ideas from [`b5`](https://github.com/team23/b5) 4 | and [`just`](https://github.com/casey/just), but uses [`nu` shell scripting](https://www.nushell.sh/book/programming_in_nu.md) 5 | to define the tasks. This allows for well-structured tasks while being able to use the super-powers of `nu` 6 | in your tasks. 7 | 8 | ## Quick overview and example 9 | 10 | `nur` allows you to execute tasks defined in a file called `nurfile`. It will look through your 11 | current working directory and all its parents to look for this file. When it has found the `nurfile` 12 | it will change to the directory the file was found in and then `source` the file into `nu` script. 13 | You can define tasks like this: 14 | 15 | ```nu-script 16 | # Just tell anybody or the "world" hello 17 | def "nur hello" [ 18 | name: string = "world" # The name to say hello to 19 | ] { 20 | print $"hello ($name)" 21 | } 22 | ``` 23 | 24 | The important bit is that you define your tasks as subcommands for "nur". If you then execute 25 | `nur hello` it will print "hello world", meaning it did execute the task `hello` in your `nurfile`. 26 | You can also use `nur --help` to get some details on how to use `nur` and `nur --help hello` to 27 | see what this `hello` task accepts as parameters. 28 | 29 | You may also pass arguments to your `nur` tasks, like using `nur hello bob` to pass "bob" 30 | as the name to the "hello" task. This supports all parameter variants normal `nu` scripts could also 31 | handle. You may use `nur --help ` to see the help for an available command. 32 | 33 | Your tasks then can do whatever you want them to do in `nu` script. This allows for very structured 34 | usage of for example docker to run/manage your project needs. But it can also execute simple commands 35 | like you would normally do in your shell (like `npm ci` or something). `nur` is not tied to any 36 | programming language, packaging system or anything. As in the end the `nurfile` is basically a 37 | normal `nu` script you can put into this whatever you like. 38 | 39 | I recommend reading [working with `nur`](https://nur-taskrunner.github.io/docs/working-with-nur/) to get an 40 | overview how to use `nur`. Also I recommend reading the `nu` documentation about 41 | [custom commands](https://www.nushell.sh/book/custom_commands.html) for details on how to define `nu` 42 | commands (and `nur` tasks) and at least read through the 43 | [nu quick tour](https://www.nushell.sh/book/quick_tour.html) to understand some basics and benefits 44 | about `nu` scripting. 45 | 46 | ## Installing `nur` 47 | 48 | You may use `cargo` to quickly install `nur` for your current user: 49 | 50 | ```shell 51 | > cargo install nur 52 | ``` 53 | 54 | The `nur` binary will be added in `$HOME/.cargo/bin` (or `$"($env.HOME)/.cargo/bin"` in `nu` shell). 55 | Make sure to add this to `$PATH` (or `$env.PATH` in `nu` shell). 56 | 57 | For more details see [the `nur` installation docs](https://nur-taskrunner.github.io/docs/installation.html). 58 | This also includes MacOS (using homebrew) and Windows (using `.msi` installer) installation methods. 59 | 60 | ## Working with `nur` 61 | 62 | `nur` uses a file called `nurfile` to define your tasks. This file is a normal `nu` script and may 63 | include any `nur` tasks defined as sub commands to `"nur"`. `nur` tasks may use the normal `nu` command 64 | features to define required arguments, their types and more. 65 | 66 | See the [working with `nur`](https://nur-taskrunner.github.io/docs/working-with-nur/) documentation 67 | for more details. 68 | 69 | ## Switching to `nur` 70 | 71 | Switching to `nur` on a large project or when having many projects can be some hassle. The recommended workflow 72 | is to create a `nurfile` that only calls the old task runner and then gradually convert your tasks to be rewritten 73 | as `nur` tasks. 74 | 75 | To simplify this process you may use the script [`nurify`](scripts/nurify.nu) to generate a `nurfile` from 76 | many existing task runners. 77 | 78 | For more details see the [switching to nur](https://nur-taskrunner.github.io/docs/switching-to-nur.html) 79 | documentation. 80 | 81 | ## Contributing 82 | 83 | See the [contributing](CONTRIBUTING.md) documentation for more details. 84 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@team23/commitlint-config'], 3 | }; 4 | -------------------------------------------------------------------------------- /nur-tests/nurfile: -------------------------------------------------------------------------------- 1 | use std 2 | 3 | let nurcmd = "./../target/release/nur" 4 | 5 | # Tests 6 | 7 | def "nur test-env" [] { 8 | std assert (($env.PWD | path join ".nur" "scripts") in $env.NU_LIB_DIRS) 9 | std assert ($env.NUR_VERSION | is-not-empty) 10 | std assert ($env.NUR_TASK_NAME | is-not-empty) 11 | std assert ($env.NUR_TASK_CALL | is-not-empty) 12 | } 13 | 14 | def "nur test-config" [] { 15 | try { 16 | $env.config 17 | } catch { 18 | error make {"msg": "Config does not exist"} 19 | } 20 | } 21 | 22 | def "nur test-nu" [] { 23 | std assert ($nu.config-path == ($env.PWD | path join ".nur" "config.nu")) 24 | std assert ($nu.env-path == ($env.PWD | path join ".nur" "env.nu")) 25 | if (is-windows) { 26 | std assert (($nu.current-exe | path basename) == "nur.exe") 27 | } else { 28 | std assert (($nu.current-exe | path basename) == "nur") 29 | } 30 | } 31 | 32 | def "nur test-nur" [] { 33 | std assert ($nur.task-name == "test-nur") 34 | std assert ($nur.run-path == $env.PWD) 35 | std assert ($nur.project-path == $env.PWD) 36 | std assert ($nur.default-lib-dir == ($env.PWD | path join ".nur" "scripts")) 37 | } 38 | 39 | def "nur exec-stdin" [] { 40 | lines | each { |it| print $"BEFORE ($it) AFTER" } 41 | } 42 | def "nur test-stdin" [] { 43 | std assert ("inner" | run-nur --stdin exec-stdin | str contains "BEFORE inner AFTER") 44 | } 45 | 46 | def "nur do-failed-execution" [] { 47 | if (is-windows) { 48 | cmd /c "exit 1" 49 | } else { 50 | ^false 51 | } 52 | } 53 | def "nur test-failed-execution" [] { 54 | try { 55 | run-nur do-failed-execution 56 | } catch { 57 | return # all ok 58 | } 59 | error make {"msg": "Did not fail, this is an error"} 60 | } 61 | 62 | def "nur do-invalid-executable" [] { 63 | ^does-not-exist-at-all-will-not-exist-ever 64 | } 65 | def "nur test-invalid-executable" [] { 66 | try { 67 | run-nur do-invalid-executable 68 | } catch { 69 | return # all ok 70 | } 71 | error make {"msg": "Did not fail, this is an error"} 72 | } 73 | 74 | def "nur do-sub-task" [] { print "ok" } 75 | def "nur do-sub-task sub" [] { print "sub-ok" } 76 | def "nur test-sub-task" [] { 77 | std assert ((run-nur do-sub-task) == "ok") 78 | std assert ((run-nur do-sub-task sub) == "sub-ok") 79 | } 80 | 81 | def "nur do-sub-task-without-parent sub" [] { print "sub-ok" } 82 | def "nur test-sub-task-without-parent" [] { 83 | std assert ((run-nur do-sub-task-without-parent sub) == "sub-ok") 84 | } 85 | 86 | def --wrapped "nur do-sub-task-with-any-args sub" [...args] { print "sub-ok" } 87 | def "nur test-sub-task-with-any-args" [] { 88 | std assert ((run-nur do-sub-task-with-any-args sub) == "sub-ok") 89 | std assert ((run-nur do-sub-task-with-any-args sub --foo bar bla) == "sub-ok") 90 | std assert ((run-nur do-sub-task-with-any-args sub some random args) == "sub-ok") 91 | } 92 | 93 | def "nur test-running-commands" [] { 94 | std assert ((run-nur --commands "print 'ok'") == "ok") 95 | std assert ((run-nur --commands "print $nurcmd") == $nurcmd) 96 | } 97 | 98 | def "nur test-invalid-calls" [] { 99 | assert exit-code { run-nur non-existing-task } 1 100 | assert exit-code { run-nur --commands some-command some-task-name } 1 101 | assert exit-code { run-nur --enter-shell some-task-nam } 1 102 | assert exit-code { run-nur --commands some-command --enter-shell } 1 103 | } 104 | 105 | def "nur do-test-preserve-exit-code" [] { exit 123 } 106 | def "nur test-preserve-exit-code" [] { 107 | assert exit-code { run-nur do-test-preserve-exit-code } 123 108 | } 109 | 110 | def "nur test-nur-list" [] { 111 | let nur_list = (run-nur --list | lines) 112 | std assert ($nur_list | is-not-empty) 113 | } 114 | 115 | # Utils and other commands 116 | 117 | def is-windows [] { 118 | $nu.os-info.name == 'windows' 119 | } 120 | 121 | def "assert exit-code" [ 122 | code: closure, 123 | exit_code: int, 124 | ] { 125 | try { do $code } catch { null } 126 | std assert (($env.LAST_EXIT_CODE | into int) == $exit_code) $"Expected exit code ($exit_code), but got ($env.LAST_EXIT_CODE)" --error-label { 127 | span: { 128 | start: (metadata $code).span.start 129 | end: (metadata $code).span.end 130 | } 131 | text: $"Did not return expected exit code ($exit_code)" 132 | } 133 | } 134 | 135 | def --wrapped run-nur [ 136 | ...args 137 | ] { 138 | ^$nurcmd --quiet ...$args 139 | } 140 | 141 | def "nur prepare" [] { 142 | cargo build --release 143 | } 144 | 145 | def "nur run-all" [] { 146 | let tests = (scope commands | filter { |it| $it.name starts-with 'nur test-' } | each { |it| $it.name | split row ' ' }) 147 | 148 | let failed_tests = ( 149 | $tests | each { 150 | |it| 151 | $it | str join " " | print -n 152 | let nur_call = (run-nur $it.1 | complete) 153 | let was_ok: bool = ($nur_call.exit_code == 0) 154 | if $was_ok { 155 | print $'...(ansi green)ok(ansi reset)' 156 | } else { 157 | print $'...(ansi red)failed(ansi reset)' 158 | print $nur_call 159 | } 160 | 161 | if $was_ok { null } else { $it.1 } 162 | } 163 | ) 164 | 165 | if ($failed_tests | is-not-empty) { 166 | error make {"msg": $'Some tests did fail, please fix those: ($failed_tests | str join ", ")'} 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /nurfile: -------------------------------------------------------------------------------- 1 | # Example, just for fun 2 | 3 | # Just tell anybody or the "world" hello 4 | def "nur hello" [ 5 | name: string = "world" # The name to say hello to 6 | ] { 7 | print $"hello ($name)" 8 | } 9 | 10 | # Actual useful tasks 11 | 12 | # Setup local environment and install deps 13 | def "nur install" [] { 14 | cargo fetch 15 | 16 | init-pre-commit 17 | } 18 | 19 | # Update local dev environment 20 | def "nur update" [] { 21 | cargo fetch 22 | } 23 | 24 | # Run cargo 25 | def --wrapped "nur cargo" [...args: string] { 26 | cargo ...$args 27 | } 28 | 29 | # Run cargo build 30 | def --wrapped "nur build" [...args: string] { 31 | cargo "build" ...$args 32 | } 33 | 34 | # Run cargo run 35 | def --wrapped "nur run" [...args: string] { 36 | cargo "run" ...$args 37 | } 38 | 39 | # Run tests 40 | def "nur test" [ 41 | --coverage 42 | ] { 43 | if $coverage { 44 | cargo tarpaulin --exclude-files _* 45 | } else { 46 | cargo test 47 | } 48 | } 49 | 50 | # Run nur nuscript tests 51 | def "nur test-nur" [] { 52 | cd nur-tests 53 | 54 | cargo run -- --quiet prepare 55 | cargo run -- --quiet run-all 56 | } 57 | 58 | # Run one task for all enabled features to see those compile 59 | def "nur test-features" [] { 60 | open Cargo.toml | get features | transpose key value | get key | each { 61 | |it| 62 | print $"Running 'nur hello ($it)' to check feature ($it)" 63 | cargo run -F $it -- --quiet hello $it 64 | } 65 | } 66 | 67 | # Run linter (clippy) 68 | def --wrapped "nur lint" [ 69 | ...args 70 | ] { 71 | cargo clippy ...$args 72 | } 73 | 74 | # Run all QA tasks 75 | def "nur qa" [] { 76 | print "Running clippy linter" 77 | nur lint 78 | print "Running cargo check" 79 | nur cargo check 80 | print "Running rust tests" 81 | nur test 82 | print "Running nur tests" 83 | nur test-nur 84 | print "Running feature tests" 85 | nur test-features 86 | print "Running cargo fmt" 87 | nur cargo fmt 88 | } 89 | 90 | # Update version in Cargo.toml 91 | def "nur version" [ 92 | version: string 93 | ] { 94 | let parsed_version = $version | parse --regex '^(?P[0-9]+)\.(?P[0-9]+)\.(?P[0-9]+)(-rc(?P[0-9]+))?(\+.*)?$' 95 | if ($parsed_version | is-empty) { 96 | error make { msg: "No valid version string provided" } 97 | } 98 | cargo set-version $version 99 | } 100 | 101 | # Publish to crates.io 102 | def "nur publish" [] { 103 | cargo publish 104 | } 105 | 106 | # Update version and release to crates.io 107 | def "nur release" [ 108 | version: string 109 | ] { 110 | let main_version = if ("+" in $version) {$version | split row "+" | get 0} else {$version} 111 | 112 | print $"Updating to version (ansi purple)($version)(ansi reset)" 113 | nur version $version 114 | 115 | print "" 116 | print $"Creating release commit and tag '(ansi purple)v($main_version)(ansi reset)'" 117 | git add Cargo.toml Cargo.lock 118 | git commit -m $"release: 🔖 v($main_version)" 119 | git tag $"v($main_version)" 120 | 121 | print "" 122 | print "Publishing to crates.io" 123 | nur publish 124 | 125 | print "" 126 | print $"(ansi yellow)Don't forget to push last commit + tags to github!(ansi reset)" 127 | } 128 | 129 | # Update nu dependencies to version 130 | def "nur upgrade-nu" [ 131 | version: string 132 | ] { 133 | print $"Updating all required nu packages to (ansi purple)($version)(ansi reset)" 134 | open Cargo.toml | get dependencies | transpose key value | each { 135 | |it| if ($it.key starts-with "nu-") and not ($it.key == "nu-ansi-term") { 136 | try { 137 | cargo add $"($it.key)@($version)" (if $it.value.optional {"--optional"}) 138 | } catch { 139 | cargo add $"($it.key)@($version)" 140 | } 141 | } 142 | } 143 | 144 | print "" 145 | print $"Storing version (ansi purple)($version)(ansi reset) in src/nu_version.rs as NU_VERSION constant" 146 | $"pub\(crate) const NU_VERSION: &str = \"($version)\";\n" | save -f src/nu_version.rs 147 | 148 | print "" 149 | print $"(ansi yellow)IMPORTANT: Please ensure all other packages are also upgraded accordingly:(ansi reset)" 150 | print $" -> (ansi cyan)nu-ansi-term(ansi reset): needs to be the same version as used in (ansi cyan)nu(ansi reset)" 151 | print $" -> (ansi cyan)miette(ansi reset): needs to be the same version as used in (ansi cyan)nu(ansi reset)" 152 | print $" -> (ansi cyan)openssl(ansi reset): needs to be the same version as used in (ansi cyan)nu(ansi reset)" 153 | print $" -> See nushell/Cargo.toml in section (ansi cyan)[workspace.dependencies](ansi reset) for details on used versions" 154 | print $" -> Also ensure (ansi cyan)rust-version(ansi reset) is set to the same version as used in (ansi cyan)nu(ansi reset)" 155 | } 156 | 157 | # Utility commands 158 | 159 | def init-pre-commit [] { 160 | if (which pre-commit | is-empty) { 161 | print -e $"(ansi red)You don't have pre-commit installed locally, pre-commit hooks cannot be initialized(ansi reset)" 162 | return null 163 | } 164 | 165 | pre-commit install --install-hooks 166 | } 167 | -------------------------------------------------------------------------------- /scripts/nur-completions.bash: -------------------------------------------------------------------------------- 1 | _comp_cmd_nur() 2 | { 3 | local cur prev words cword opts 4 | if type _get_comp_words_by_ref &>/dev/null; then 5 | _get_comp_words_by_ref -n : cur prev words cword 6 | else 7 | cur="${COMP_WORDS[COMP_CWORD]}" 8 | prev="${COMP_WORDS[COMP_CWORD-1]}" 9 | words=$COMP_WORDS 10 | cword=$COMP_CWORD 11 | fi 12 | 13 | local has_task=0 14 | for word in "${words[@]}" 15 | do 16 | case $word in 17 | -*|nur) 18 | ;; 19 | *) 20 | has_task=1 21 | ;; 22 | esac 23 | done 24 | 25 | if [[ $has_task -eq 0 ]] 26 | then 27 | if [[ ${cur} == -* ]] 28 | then 29 | opts=" -h --help -v --version -l --list -q --quiet --stdin -c --commands --enter-shell" 30 | COMPREPLY=( $( compgen -W "${opts}" -- "${cur}" ) ) 31 | return 0 32 | else 33 | local tasks 34 | IFS=$'\n' tasks=$( nur --list ) 35 | local tasks_string=$( printf "%s\t" "${tasks[@]}" ) 36 | COMPREPLY=( $( compgen -W "${tasks_string}" -- "${cur}" ) ) 37 | fi 38 | else 39 | COMPREPLY=("FUCK") 40 | fi 41 | } && 42 | complete -F _comp_cmd_nur nur 43 | -------------------------------------------------------------------------------- /scripts/nur-completions.nu: -------------------------------------------------------------------------------- 1 | def "nu-complete nur task-names" [] { 2 | ^nur --list | lines 3 | } 4 | 5 | # nur - a taskrunner based on nu shell. 6 | export extern nur [ 7 | --help(-h) # Display the help message for this command 8 | --version(-v) # Output version number and exit 9 | --list(-l) # List available tasks and then just exit 10 | --quiet(-q) # Do not output anything but what the task produces 11 | --stdin # Attach stdin to called nur task 12 | --commands(-c) # Run the given commands after nurfiles have been loaded 13 | --enter-shell # Enter a nu REPL shell after the nurfiles have been loaded (use only for debugging) 14 | task_name?: string@"nu-complete nur task-names" # Name of the task to run (optional) 15 | ...args # Parameters to the executed task 16 | ] 17 | -------------------------------------------------------------------------------- /scripts/nur-completions.zsh: -------------------------------------------------------------------------------- 1 | #compdef nur 2 | 3 | _nur_tasks() { 4 | [[ $PREFIX = -* ]] && return 1 5 | local tasks; tasks=( 6 | "${(@f)$(_call_program commands nur --list)}" 7 | ) 8 | 9 | _describe 'nur tasks' tasks 10 | } 11 | 12 | _nur() { 13 | local curcontext="$curcontext" state line ret=1 14 | typeset -A opt_args 15 | 16 | _arguments -C \ 17 | '-h[Display the help message for this command]' \ 18 | '--help[Display the help message for this command]' \ 19 | '-v[Output version number and exit]' \ 20 | '--version[Output version number and exit]' \ 21 | '-l[List available tasks and then just exit]' \ 22 | '--list[List available tasks and then just exit]' \ 23 | '-q[Do not output anything but what the task produces]' \ 24 | '--quiet[Do not output anything but what the task produces]' \ 25 | '--stdin[Attach stdin to called nur task]' \ 26 | '-c[Run the given commands after nurfiles have been loaded]' \ 27 | '--commands[Run the given commands after nurfiles have been loaded]' \ 28 | '--enter-shell[Enter a nu REPL shell after the nurfiles have been loaded (use only for debugging)]' \ 29 | '::optional arg:_nur_tasks' \ 30 | '*: :->args' \ 31 | && ret=0 32 | 33 | return ret 34 | } 35 | 36 | compdef _nur nur 37 | -------------------------------------------------------------------------------- /scripts/nurify.nu: -------------------------------------------------------------------------------- 1 | # Create nurfile from common task/command runner config 2 | # 3 | # Usage: 4 | # > cd into/the/project/path 5 | # > nurify # Will create nurfile 6 | # > nur --help 7 | # 8 | # The generated nurfile is not meant to replace your current task/commend runner, instead it 9 | # will create a nurfile allowing you to use nur instead of the projects task runner. nur 10 | # will then still execute the original task runner. 11 | # Still this allows you to gradually convert your project to nur by replacing your tasks 12 | # bit by bit with new nur variants. When switching to nur this is the recommended method: 13 | # Create new nurfile to run old tasks using the old task runner, then convert task for task. 14 | 15 | def prepare-nurfile [] { 16 | "# FILE GENERATED BY nurify COMMAND\n\n" | save -f nurfile 17 | } 18 | 19 | def nurify-from-b5 [] { 20 | let global_tasks = ( 21 | if (glob ~/.b5/Taskfile | first | path exists) { 22 | cat ~/.b5/Taskfile | lines | filter { 23 | |it| $it starts-with "task:" 24 | } | each { 25 | |it| $it | parse --regex "^task:(?P[^(]+).*" | get name | first 26 | } 27 | } else [] 28 | ) 29 | 30 | prepare-nurfile 31 | b5 --quiet help --tasks | lines | filter { 32 | |it| $it not-in $global_tasks 33 | } | each { 34 | |it| $"def --wrapped \"nur ($it | str replace --all ':' ' ')\" [...args] {\n ^b5 --quiet \"($it)\" ...$args\n}\n" 35 | } | save -f -a nurfile 36 | } 37 | 38 | def nurify-from-just [] { 39 | prepare-nurfile 40 | ^just --unsorted --dump --dump-format json 41 | | from json 42 | | get recipes 43 | | transpose k v 44 | | each { 45 | |it| $"def --wrapped \"nur ($it.k)\" [...args] {\n ^just \"($it.k)\" ...$args\n}\n" 46 | } | save -f -a nurfile 47 | } 48 | 49 | def nurify-from-package-json [] { 50 | prepare-nurfile 51 | open package.json 52 | | get scripts 53 | | transpose k v 54 | | each { 55 | |it| $"def --wrapped \"nur ($it.k)\" [...args] {\n ^npm run \"($it.k)\" ...$args\n}\n" 56 | } | save -f -a nurfile 57 | } 58 | 59 | def nurify-from-makefile [] { 60 | prepare-nurfile 61 | open ( glob "[Mm]akefile" | first ) 62 | | lines 63 | | find --regex '^[\w\.-]+\s*:' 64 | | where ($it | str trim -l) == $it 65 | | where ($it | str starts-with '.') == false 66 | | split column ':' target 67 | | get target 68 | | str trim 69 | | each { 70 | |it| $"def --wrapped \"nur ($it)\" [...args] {\n ^make \"($it)\" ...$args\n}\n" 71 | } | save -f -a nurfile 72 | } 73 | 74 | def nurify-from-toolkit-nu [] { 75 | # Output warning as we cannot support all features and the conversion may not cover all cases 76 | # 77 | # Example of a case not convertable: 78 | # We cannot handle (named) parameters of type "list" as the nur call will not accept a list of 79 | # strings. This is only possible with nu builtin or custom commands. But nur is neither of those, as it 80 | # is a separate executable using the nu libraries. So we cannot handle this case. Basically nu shell will 81 | # tell you that you cannot pass lists to external commands. 😉 82 | print $"(ansi red)Conversion from toolkit.nu may not cover all cases, you might need to extend/fix the generated nurfile(ansi reset)" 83 | print $"(ansi red)\(But be aware: Not all features nu shell commands would provide are available as nur is an external command\)(ansi reset)" 84 | 85 | let toolkit_commands = ( 86 | nu -c "use toolkit.nu ; scope modules | where name == 'toolkit' | first | get commands | where name != 'toolkit' | each { |it| print $it.name } ; null" 87 | ) | lines 88 | 89 | def get-toolkit-command-signature [command: string] { 90 | let command_signature = ( 91 | nu -c $"use toolkit.nu ; scope commands | where type == 'custom' and name == 'toolkit ($command)' | first | get signatures | to nuon" | from nuon 92 | ) 93 | 94 | $command_signature | transpose k v | get v | first 95 | } 96 | 97 | prepare-nurfile 98 | "use toolkit.nu\n\n" | save -f -a nurfile 99 | $toolkit_commands 100 | | each { 101 | |it| 102 | let signatures = get-toolkit-command-signature $it 103 | let arguments = $signatures | each { 104 | |param| 105 | let param_name = $param.parameter_name 106 | mut param_type = if ($param.syntax_shape | is-not-empty) and $param.syntax_shape != 'any' { $': ($param.syntax_shape)' } else '' 107 | if $param_type starts-with ': completable<' { 108 | $param_type = $": ($param_type | str substring 14..(($param_type | str length) - 2))" 109 | } 110 | let param_docs = if ($param.description | is-not-empty) { $' # ($param.description)' } 111 | match $param { 112 | {parameter_type: "positional"} => $" ($param_name)($param_type)($param_docs)", 113 | {parameter_type: "named"} => $" --($param_name)($param_type)($param_docs)", 114 | {parameter_type: "switch"} => $" --($param_name)($param_docs)", 115 | {parameter_type: "rest"} => $" ...($param_name | default 'rest')($param_type)($param_docs)", 116 | } 117 | } | str join "\n" 118 | let call_arguments = $signatures | each { 119 | |param| 120 | let param_name = $param.parameter_name 121 | match $param { 122 | {parameter_type: "positional"} => $"$($param_name | str replace --all '-' '_')", 123 | {parameter_type: "named"} => $"--($param_name) $($param_name | str replace --all '-' '_')", 124 | {parameter_type: "switch"} => $"--($param_name)=$($param_name | str replace --all '-' '_')", 125 | } 126 | } | str join " " 127 | let rest_arguments = $signatures | each { 128 | |param| 129 | let param_name = $param.parameter_name 130 | match $param { 131 | {parameter_type: "rest"} => $"...$($param_name | default 'rest' | str replace --all '-' '_')", 132 | } 133 | } | str join " " 134 | let has_rest_arguments = $signatures | any { 135 | |param| $param.parameter_type == "rest" 136 | } 137 | 138 | $"def( if $has_rest_arguments { ' --wrapped' }) \"nur ($it)\" [\n($arguments)\n] {\n toolkit ($it) ($call_arguments) (if $has_rest_arguments { $rest_arguments })\n}\n" 139 | } | save -f -a nurfile 140 | } 141 | 142 | def nurify-from-lets [] { 143 | prepare-nurfile 144 | open lets.yaml 145 | | get commands 146 | | transpose k v 147 | | each { 148 | |it| $"def --wrapped \"nur ($it.k)\" [...args] {\n ^lets \"($it.k)\" ...$args\n}\n" 149 | } | save -f -a nurfile 150 | } 151 | 152 | def nurify-from-task [] { 153 | prepare-nurfile 154 | open ( glob "[Tt]askfile.{yml,yaml}" | first ) 155 | | get tasks 156 | | transpose k v 157 | | each { 158 | |it| $"def --wrapped \"nur ($it.k)\" [...args] {\n ^task \"($it.k)\" ...$args\n}\n" 159 | } | save -f -a nurfile 160 | } 161 | 162 | def nurify-from-tusk [] { 163 | prepare-nurfile 164 | open tusk.yml 165 | | get tasks 166 | | transpose k v 167 | | each { 168 | |it| $"def --wrapped \"nur ($it.k)\" [...args] {\n ^tusk \"($it.k)\" ...$args\n}\n" 169 | } | save -f -a nurfile 170 | } 171 | 172 | # Create nurfile from different task/command runners. The nurfile will contain tasks to wrap 173 | # all tasks and then run original task/command runner. This can be used to gradually migrate to nur. 174 | # 175 | # Currently supports: 176 | # * b5 task runner (when build/Taskfile is found) 177 | # * just command runner (when justfile is found) 178 | # * npm package.json scripts (when package.json is found) 179 | # * make (when [Mm]akefile is found) 180 | # * nu toolkit module (when toolkit.nu is found) 181 | # * lets (when lets.yaml is found) 182 | # * task (when [Tt]askfile.{yml,yaml} is found) 183 | # * tusk (when tusk.yml is found) 184 | export def main [] { 185 | if ("build/Taskfile" | path exists) { 186 | print $"(ansi cyan)Found build/Taskfile, converting from b5 task runner(ansi reset)" 187 | nurify-from-b5 188 | } else if ("justfile" | path exists) { 189 | print $"(ansi cyan)Found justfile, converting from just command runner(ansi reset)" 190 | nurify-from-just 191 | } else if ("package.json" | path exists) { 192 | print $"(ansi cyan)Found package.json, converting from npm script(ansi reset)" 193 | nurify-from-package-json 194 | } else if (glob "[Mm]akefile" | is-not-empty) { 195 | print $"(ansi cyan)Found [Mm]akefile, converting from make(ansi reset)" 196 | nurify-from-makefile 197 | } else if ("toolkit.nu" | path exists) { 198 | print $"(ansi cyan)Found toolkit.nu, converting from toolkit.nu(ansi reset)" 199 | nurify-from-toolkit-nu 200 | } else if ("lets.yaml" | path exists) { 201 | print $"(ansi cyan)Found lets.yaml, converting from lets(ansi reset)" 202 | nurify-from-lets 203 | } else if (glob "[Tt]askfile.{yml,yaml}" | is-not-empty) { 204 | print $"(ansi cyan)Found [Tt]askfile.{yml,yaml}, converting from task(ansi reset)" 205 | nurify-from-task 206 | } else if ("tusk.yml" | path exists) { 207 | print $"(ansi cyan)Found tusk.yml, converting from tusk(ansi reset)" 208 | nurify-from-tusk 209 | } else { 210 | error make {"msg": "Could not find any existing task/command runner, please run nurify in project root"} 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::Nur; 2 | use crate::errors::{NurError, NurResult}; 3 | use crate::names::NUR_NAME; 4 | use nu_engine::{get_full_help, CallExt}; 5 | use nu_parser::escape_for_script_arg; 6 | use nu_parser::parse; 7 | use nu_protocol::ast::Expression; 8 | use nu_protocol::{ 9 | ast::Expr, 10 | engine::{EngineState, Stack, StateWorkingSet}, 11 | ShellError, 12 | }; 13 | use nu_protocol::{report_parse_error, Spanned}; 14 | use nu_utils::escape_quote_string; 15 | use nu_utils::stdout_write_all_and_flush; 16 | 17 | pub(crate) fn is_safe_taskname(name: &str) -> bool { 18 | // This is basically similar to string_should_be_quoted 19 | // in nushell/crates/nu-parser/src/deparse.rs:1, 20 | // BUT may change as the requirements are different. 21 | // Also I added "#" and "^", as seen in 22 | // nushell/crates/nu-parser/src/parse_keywords.rs:175 23 | !name.starts_with('$') 24 | && !(name.chars().any(|c| { 25 | c == ' ' 26 | || c == '(' 27 | || c == '\'' 28 | || c == '`' 29 | || c == '"' 30 | || c == '\\' 31 | || c == '#' 32 | || c == '^' 33 | })) 34 | } 35 | 36 | pub(crate) fn gather_commandline_args( 37 | args: Vec, 38 | ) -> NurResult<(Vec, bool, Vec)> { 39 | let mut args_to_nur = Vec::from([String::from(NUR_NAME)]); 40 | let mut task_call = Vec::from([String::from(NUR_NAME)]); 41 | let mut has_task_call = false; 42 | let mut args_iter = args.iter(); 43 | 44 | args_iter.next(); // Ignore own name 45 | #[allow(clippy::while_let_on_iterator)] 46 | while let Some(arg) = args_iter.next() { 47 | if !arg.starts_with('-') { 48 | // At least first non nur argument must be safe 49 | if !is_safe_taskname(arg) { 50 | eprintln!("{}", arg); 51 | return Err(NurError::InvalidTaskName(arg.clone())); 52 | } 53 | 54 | // Register task name and switch to task call parsing 55 | has_task_call = true; 56 | task_call.push(arg.clone()); 57 | break; 58 | } 59 | 60 | let flag_value = match arg.as_ref() { 61 | // "--some-file" => args.next().map(|a| escape_quote_string(&a)), 62 | "--commands" | "-c" => args_iter.next().map(|a| escape_quote_string(a)), 63 | _ => None, 64 | }; 65 | 66 | args_to_nur.push(arg.clone()); 67 | 68 | if let Some(flag_value) = flag_value { 69 | args_to_nur.push(flag_value); 70 | } 71 | } 72 | 73 | if has_task_call { 74 | // Consume remaining elements in iterator 75 | #[allow(clippy::while_let_on_iterator)] 76 | while let Some(arg) = args_iter.next() { 77 | task_call.push(escape_for_script_arg(arg)); 78 | } 79 | } else { 80 | // Also remove "nur" from task_call 81 | task_call.clear(); 82 | } 83 | 84 | Ok((args_to_nur, has_task_call, task_call)) 85 | } 86 | 87 | pub(crate) fn parse_commandline_args( 88 | commandline_args: &str, 89 | engine_state: &mut EngineState, 90 | ) -> Result { 91 | let (block, delta) = { 92 | let mut working_set = StateWorkingSet::new(engine_state); 93 | 94 | let output = parse(&mut working_set, None, commandline_args.as_bytes(), false); 95 | if let Some(err) = working_set.parse_errors.first() { 96 | report_parse_error(&working_set, err); 97 | 98 | std::process::exit(1); 99 | } 100 | 101 | (output, working_set.render()) 102 | }; 103 | 104 | engine_state.merge_delta(delta)?; 105 | 106 | let mut stack = Stack::new(); 107 | 108 | // We should have a successful parse now 109 | if let Some(pipeline) = block.pipelines.first() { 110 | if let Some(Expr::Call(call)) = pipeline.elements.first().map(|e| &e.expr.expr) { 111 | // let config_file = call.get_flag_expr("some-flag"); 112 | let list_tasks = call.has_flag(engine_state, &mut stack, "list")?; 113 | let quiet_execution = call.has_flag(engine_state, &mut stack, "quiet")?; 114 | let attach_stdin = call.has_flag(engine_state, &mut stack, "stdin")?; 115 | let show_help = call.has_flag(engine_state, &mut stack, "help")?; 116 | let run_commands = call.get_flag_expr("commands"); 117 | let enter_shell = call.has_flag(engine_state, &mut stack, "enter-shell")?; 118 | #[cfg(feature = "debug")] 119 | let debug_output = call.has_flag(engine_state, &mut stack, "debug")?; 120 | 121 | if call.has_flag(engine_state, &mut stack, "version")? { 122 | let version = env!("CARGO_PKG_VERSION").to_string(); 123 | let _ = std::panic::catch_unwind(move || { 124 | stdout_write_all_and_flush(format!("{version}\n")) 125 | }); 126 | 127 | std::process::exit(0); 128 | } 129 | 130 | fn extract_contents( 131 | expression: Option<&Expression>, 132 | ) -> Result>, ShellError> { 133 | if let Some(expr) = expression { 134 | let str = expr.as_string(); 135 | if let Some(str) = str { 136 | Ok(Some(Spanned { 137 | item: str, 138 | span: expr.span, 139 | })) 140 | } else { 141 | Err(ShellError::TypeMismatch { 142 | err_message: "string".into(), 143 | span: expr.span, 144 | }) 145 | } 146 | } else { 147 | Ok(None) 148 | } 149 | } 150 | 151 | let run_commands = extract_contents(run_commands)?; 152 | 153 | return Ok(NurArgs { 154 | list_tasks, 155 | quiet_execution, 156 | attach_stdin, 157 | show_help, 158 | run_commands, 159 | enter_shell, 160 | #[cfg(feature = "debug")] 161 | debug_output, 162 | }); 163 | } 164 | } 165 | 166 | // Just give the help and exit if the above fails 167 | let full_help = get_full_help(&Nur, engine_state, &mut stack); 168 | print!("{full_help}"); 169 | std::process::exit(1); 170 | } 171 | 172 | #[derive(Debug, Clone)] 173 | pub(crate) struct NurArgs { 174 | pub(crate) list_tasks: bool, 175 | pub(crate) quiet_execution: bool, 176 | pub(crate) attach_stdin: bool, 177 | pub(crate) show_help: bool, 178 | pub(crate) run_commands: Option>, 179 | pub(crate) enter_shell: bool, 180 | #[cfg(feature = "debug")] 181 | pub(crate) debug_output: bool, 182 | } 183 | 184 | #[cfg(test)] 185 | mod tests { 186 | use super::*; 187 | use crate::engine::init_engine_state; 188 | use tempfile::tempdir; 189 | 190 | #[test] 191 | fn test_gather_commandline_args_splits_on_task_name() { 192 | let args = vec![ 193 | String::from("nur"), 194 | String::from("--quiet"), 195 | String::from("some_task_name"), 196 | String::from("--task-option"), 197 | String::from("task-value"), 198 | ]; 199 | let (nur_args, has_task_call, task_call) = gather_commandline_args(args).unwrap(); 200 | assert_eq!(nur_args, vec![String::from("nur"), String::from("--quiet")]); 201 | assert_eq!(has_task_call, true); 202 | assert_eq!( 203 | task_call, 204 | vec![ 205 | String::from("nur"), 206 | String::from("some_task_name"), 207 | String::from("--task-option"), 208 | String::from("task-value") 209 | ] 210 | ); 211 | } 212 | 213 | #[test] 214 | fn test_gather_commandline_args_handles_missing_nur_args() { 215 | let args = vec![ 216 | String::from("nur"), 217 | String::from("some_task_name"), 218 | String::from("--task-option"), 219 | String::from("task-value"), 220 | ]; 221 | let (nur_args, has_task_call, task_call) = gather_commandline_args(args).unwrap(); 222 | assert_eq!(nur_args, vec![String::from("nur")]); 223 | assert_eq!(has_task_call, true); 224 | assert_eq!( 225 | task_call, 226 | vec![ 227 | String::from("nur"), 228 | String::from("some_task_name"), 229 | String::from("--task-option"), 230 | String::from("task-value") 231 | ] 232 | ); 233 | } 234 | 235 | #[test] 236 | fn test_gather_commandline_args_handles_missing_task_name() { 237 | let args = vec![String::from("nur"), String::from("--help")]; 238 | let (nur_args, has_task_call, task_call) = gather_commandline_args(args).unwrap(); 239 | assert_eq!(nur_args, vec![String::from("nur"), String::from("--help")]); 240 | assert_eq!(has_task_call, false); 241 | assert_eq!(task_call, vec![] as Vec); 242 | } 243 | 244 | #[test] 245 | fn test_gather_commandline_args_handles_missing_task_args() { 246 | let args = vec![ 247 | String::from("nur"), 248 | String::from("--quiet"), 249 | String::from("some_task_name"), 250 | ]; 251 | let (nur_args, has_task_call, task_call) = gather_commandline_args(args).unwrap(); 252 | assert_eq!(nur_args, vec![String::from("nur"), String::from("--quiet")]); 253 | assert_eq!(has_task_call, true); 254 | assert_eq!( 255 | task_call, 256 | vec![String::from("nur"), String::from("some_task_name")] 257 | ); 258 | } 259 | 260 | #[test] 261 | fn test_gather_commandline_args_handles_no_args_at_all() { 262 | let args = vec![String::from("nur")]; 263 | let (nur_args, has_task_call, task_call) = gather_commandline_args(args).unwrap(); 264 | assert_eq!(nur_args, vec![String::from("nur")]); 265 | assert_eq!(has_task_call, false); 266 | assert_eq!(task_call, vec![] as Vec); 267 | } 268 | 269 | fn _create_minimal_engine_for_erg_parsing() -> EngineState { 270 | let temp_dir = tempdir().unwrap(); 271 | let temp_dir_path = temp_dir.path().to_path_buf(); 272 | let engine_state = init_engine_state(&temp_dir_path).unwrap(); 273 | 274 | engine_state 275 | } 276 | 277 | #[test] 278 | fn test_parse_commandline_args_without_args() { 279 | let mut engine_state = _create_minimal_engine_for_erg_parsing(); 280 | 281 | let nur_args = parse_commandline_args("nur", &mut engine_state).unwrap(); 282 | assert_eq!(nur_args.list_tasks, false); 283 | assert_eq!(nur_args.quiet_execution, false); 284 | assert_eq!(nur_args.attach_stdin, false); 285 | assert_eq!(nur_args.show_help, false); 286 | assert!(nur_args.run_commands.is_none()); 287 | assert_eq!(nur_args.enter_shell, false); 288 | } 289 | 290 | #[test] 291 | fn test_parse_commandline_args_list() { 292 | let mut engine_state = _create_minimal_engine_for_erg_parsing(); 293 | 294 | let nur_args = parse_commandline_args("nur --list", &mut engine_state).unwrap(); 295 | assert_eq!(nur_args.list_tasks, true); 296 | } 297 | 298 | #[test] 299 | fn test_parse_commandline_args_quiet() { 300 | let mut engine_state = _create_minimal_engine_for_erg_parsing(); 301 | 302 | let nur_args = parse_commandline_args("nur --quiet", &mut engine_state).unwrap(); 303 | assert_eq!(nur_args.quiet_execution, true); 304 | } 305 | 306 | #[test] 307 | fn test_parse_commandline_args_stdin() { 308 | let mut engine_state = _create_minimal_engine_for_erg_parsing(); 309 | 310 | let nur_args = parse_commandline_args("nur --stdin", &mut engine_state).unwrap(); 311 | assert_eq!(nur_args.attach_stdin, true); 312 | } 313 | 314 | #[test] 315 | fn test_parse_commandline_args_help() { 316 | let mut engine_state = _create_minimal_engine_for_erg_parsing(); 317 | 318 | let nur_args = parse_commandline_args("nur --help", &mut engine_state).unwrap(); 319 | assert_eq!(nur_args.show_help, true); 320 | } 321 | 322 | #[test] 323 | fn test_parse_commandline_args_commands() { 324 | let mut engine_state = _create_minimal_engine_for_erg_parsing(); 325 | 326 | let nur_args = 327 | parse_commandline_args("nur --commands 'some_command'", &mut engine_state).unwrap(); 328 | assert!(nur_args.run_commands.is_some()); 329 | assert_eq!(nur_args.run_commands.unwrap().item, "some_command"); 330 | } 331 | 332 | #[test] 333 | fn test_parse_commandline_args_enter_shell() { 334 | let mut engine_state = _create_minimal_engine_for_erg_parsing(); 335 | 336 | let nur_args = parse_commandline_args("nur --enter-shell", &mut engine_state).unwrap(); 337 | assert_eq!(nur_args.enter_shell, true); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | mod nur; 2 | 3 | use nu_protocol::engine::{EngineState, StateWorkingSet}; 4 | pub(crate) use nur::Nur; 5 | 6 | pub(crate) fn create_nu_context(mut engine_state: EngineState) -> EngineState { 7 | // Custom additions only used in cli, normally registered in nu main() as "custom additions" 8 | let delta = { 9 | let mut working_set = StateWorkingSet::new(&engine_state); 10 | working_set.add_decl(Box::new(nu_cli::NuHighlight)); 11 | working_set.add_decl(Box::new(nu_cli::Print)); 12 | working_set.render() 13 | }; 14 | 15 | if let Err(err) = engine_state.merge_delta(delta) { 16 | eprintln!("Error creating nu command context: {err:?}"); 17 | } 18 | 19 | engine_state 20 | } 21 | 22 | pub(crate) fn create_nur_context(mut engine_state: EngineState) -> EngineState { 23 | // Add nur own commands 24 | let delta = { 25 | let mut working_set = StateWorkingSet::new(&engine_state); 26 | working_set.add_decl(Box::new(nur::Nur)); 27 | working_set.render() 28 | }; 29 | 30 | if let Err(err) = engine_state.merge_delta(delta) { 31 | eprintln!("Error creating nur command context: {err:?}"); 32 | } 33 | 34 | engine_state 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/nur.rs: -------------------------------------------------------------------------------- 1 | use nu_engine::get_full_help; 2 | use nu_protocol::engine::{Command, EngineState, Stack}; 3 | use nu_protocol::{ 4 | Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, 5 | }; 6 | 7 | #[derive(Clone)] 8 | pub(crate) struct Nur; 9 | 10 | impl Command for Nur { 11 | fn name(&self) -> &str { 12 | "nur" 13 | } 14 | 15 | fn signature(&self) -> Signature { 16 | let mut signature = Signature::build("nur"); 17 | 18 | signature = signature 19 | .description("nur - a taskrunner based on nu shell.") 20 | .switch("version", "Output version number and exit", Some('v')) 21 | .switch("list", "List available tasks and then just exit", Some('l')) 22 | .switch( 23 | "quiet", 24 | "Do not output anything but what the task produces", 25 | Some('q'), 26 | ) 27 | .switch("stdin", "Attach stdin to called nur task", None) 28 | .named( 29 | "commands", 30 | SyntaxShape::String, 31 | "Run the given commands after nurfiles have been loaded", 32 | Some('c'), 33 | ) 34 | .switch( 35 | "enter-shell", 36 | "Enter a nu REPL shell after the nurfiles have been loaded (use only for debugging)", 37 | None, 38 | ) 39 | .optional( 40 | "task name", 41 | SyntaxShape::String, 42 | "Name of the task to run (you may use sub tasks)", 43 | ) 44 | .rest( 45 | "task args", 46 | SyntaxShape::String, 47 | "Parameters for the executed task", 48 | ) 49 | .category(Category::Default); 50 | 51 | #[cfg(feature = "debug")] 52 | { 53 | signature = signature.switch("debug", "Show debug details", Some('d')); 54 | } 55 | 56 | signature 57 | } 58 | 59 | fn description(&self) -> &str { 60 | "nur - a taskrunner based on nu shell." 61 | } 62 | 63 | fn run( 64 | &self, 65 | engine_state: &EngineState, 66 | stack: &mut Stack, 67 | call: &nu_protocol::engine::Call, 68 | _input: PipelineData, 69 | ) -> Result { 70 | Ok(Value::string(get_full_help(&Nur, engine_state, stack), call.head).into_pipeline_data()) 71 | } 72 | 73 | fn examples(&self) -> Vec { 74 | vec![ 75 | Example { 76 | description: "Execute a task", 77 | example: "nur task-name", 78 | result: None, 79 | }, 80 | Example { 81 | description: "List available tasks", 82 | example: "nur --list", 83 | result: None, 84 | }, 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/compat.rs: -------------------------------------------------------------------------------- 1 | use nu_ansi_term::Color; 2 | use std::path::Path; 3 | 4 | pub(crate) fn show_nurscripts_hint>(project_path: P, use_color: bool) { 5 | // Give some hints if old ".nurscripts" exists 6 | let old_nur_lib_path = project_path.as_ref().join(".nurscripts"); 7 | if old_nur_lib_path.exists() && old_nur_lib_path.is_dir() { 8 | eprintln!( 9 | "{}WARNING: .nurscripts/ has moved to .nur/scripts/ -> please update your project{}", 10 | if use_color { 11 | Color::Red.prefix().to_string() 12 | } else { 13 | String::from("") 14 | }, 15 | if use_color { 16 | Color::Red.suffix().to_string() 17 | } else { 18 | String::from("") 19 | }, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::args::{is_safe_taskname, parse_commandline_args, NurArgs}; 2 | use crate::errors::NurError::EnteredShellError; 3 | use crate::errors::{NurError, NurResult}; 4 | use crate::names::{ 5 | NUR_ENV_NUR_TASK_CALL, NUR_ENV_NUR_TASK_NAME, NUR_ENV_NUR_VERSION, NUR_ENV_NU_LIB_DIRS, 6 | NUR_NAME, NUR_VAR_CONFIG_DIR, NUR_VAR_DEFAULT_LIB_DIR, NUR_VAR_PROJECT_PATH, NUR_VAR_RUN_PATH, 7 | NUR_VAR_TASK_NAME, 8 | }; 9 | use crate::nu_version::NU_VERSION; 10 | use crate::scripts::{get_default_nur_config, get_default_nur_env}; 11 | use crate::state::NurState; 12 | use nu_cli::{evaluate_repl, gather_parent_env_vars}; 13 | use nu_engine::get_full_help; 14 | use nu_protocol::ast::Block; 15 | use nu_protocol::engine::{Command, Stack, StateWorkingSet}; 16 | use nu_protocol::{ 17 | engine::EngineState, record, report_parse_error, report_shell_error, Config, IntoValue, 18 | PipelineData, Record, ShellError, Span, Type, Value, 19 | }; 20 | use nu_std::load_standard_library; 21 | use nu_utils::stdout_write_all_and_flush; 22 | use std::fs; 23 | use std::path::{Path, PathBuf}; 24 | use std::sync::Arc; 25 | 26 | pub(crate) fn init_engine_state>(project_path: P) -> NurResult { 27 | let engine_state = nu_cmd_lang::create_default_context(); 28 | let engine_state = nu_command::add_shell_command_context(engine_state); 29 | let engine_state = nu_cmd_extra::add_extra_command_context(engine_state); 30 | let engine_state = nu_cli::add_cli_context(engine_state); 31 | let engine_state = nu_explore::add_explore_context(engine_state); 32 | let engine_state = crate::commands::create_nu_context(engine_state); 33 | let engine_state = crate::commands::create_nur_context(engine_state); 34 | 35 | // Prepare engine state to be changed 36 | let mut engine_state = engine_state; 37 | 38 | // Setup default config 39 | engine_state.add_env_var( 40 | "config".into(), 41 | Config::default().into_value(Span::unknown()), 42 | ); 43 | engine_state.add_env_var( 44 | "ENV_CONVERSIONS".to_string(), 45 | Value::test_record(record! {}), 46 | ); 47 | 48 | // First, set up env vars as strings only 49 | gather_parent_env_vars(&mut engine_state, project_path.as_ref()); 50 | engine_state.add_env_var( 51 | "NU_VERSION".to_string(), 52 | Value::string(NU_VERSION, Span::unknown()), 53 | ); 54 | 55 | // Load std library 56 | if load_standard_library(&mut engine_state).is_err() { 57 | return Err(NurError::InitError(String::from( 58 | "Could not load std library", 59 | ))); 60 | } 61 | 62 | // Set some engine flags 63 | engine_state.is_interactive = false; 64 | engine_state.is_login = false; 65 | engine_state.history_enabled = false; 66 | 67 | Ok(engine_state) 68 | } 69 | 70 | #[derive(Clone)] 71 | pub(crate) struct NurEngine { 72 | pub(crate) engine_state: EngineState, 73 | pub(crate) stack: Stack, 74 | 75 | pub(crate) state: NurState, 76 | } 77 | 78 | impl NurEngine { 79 | pub(crate) fn new(engine_state: EngineState, nur_state: NurState) -> NurResult { 80 | let mut nur_engine = NurEngine { 81 | engine_state, 82 | stack: Stack::new(), 83 | 84 | state: nur_state, 85 | }; 86 | 87 | nur_engine._apply_nur_state()?; 88 | 89 | Ok(nur_engine) 90 | } 91 | 92 | fn _apply_nur_state(&mut self) -> NurResult<()> { 93 | // Set default scripts path 94 | self.engine_state.add_env_var( 95 | NUR_ENV_NU_LIB_DIRS.to_string(), 96 | Value::string(self.state.lib_dir_path.to_string_lossy(), Span::unknown()), 97 | ); 98 | 99 | // Set some generic nur ENV 100 | self.engine_state.add_env_var( 101 | NUR_ENV_NUR_VERSION.to_string(), 102 | Value::string(env!("CARGO_PKG_VERSION"), Span::unknown()), 103 | ); 104 | 105 | // Set config and env paths to .nur versions 106 | self.engine_state 107 | .set_config_path("env-path", self.state.env_path.clone()); 108 | self.engine_state 109 | .set_config_path("config-path", self.state.config_path.clone()); 110 | 111 | // Set up the $nu constant before evaluating any files 112 | // (those may need to have $nu available to execute them) 113 | self.engine_state.generate_nu_constant(); 114 | 115 | // Set up the $nur constant record (like $nu) 116 | let mut nur_record = Record::new(); 117 | nur_record.push( 118 | NUR_VAR_RUN_PATH, 119 | Value::string( 120 | String::from(self.state.run_path.to_str().unwrap()), 121 | Span::unknown(), 122 | ), 123 | ); 124 | nur_record.push( 125 | NUR_VAR_PROJECT_PATH, 126 | Value::string( 127 | String::from(self.state.project_path.to_str().unwrap()), 128 | Span::unknown(), 129 | ), 130 | ); 131 | if self.state.has_task_call { 132 | // TODO: Should we remove this? Will always only include the main task, no sub tasks 133 | nur_record.push( 134 | NUR_VAR_TASK_NAME, 135 | Value::string(self.state.task_call[1].clone(), Span::unknown()), // strip "nur " 136 | ); 137 | } 138 | nur_record.push( 139 | NUR_VAR_CONFIG_DIR, 140 | Value::string( 141 | String::from(self.state.config_dir.to_str().unwrap()), 142 | Span::unknown(), 143 | ), 144 | ); 145 | nur_record.push( 146 | NUR_VAR_DEFAULT_LIB_DIR, 147 | Value::string( 148 | String::from(self.state.lib_dir_path.to_str().unwrap()), 149 | Span::unknown(), 150 | ), 151 | ); 152 | let mut working_set = StateWorkingSet::new(&self.engine_state); 153 | let nur_var_id = working_set.add_variable( 154 | NUR_NAME.as_bytes().into(), 155 | Span::unknown(), 156 | Type::Any, 157 | false, 158 | ); 159 | self.stack 160 | .add_var(nur_var_id, Value::record(nur_record, Span::unknown())); 161 | self.engine_state.merge_delta(working_set.render())?; 162 | 163 | Ok(()) 164 | } 165 | 166 | fn _finalise_nur_state(&mut self) { 167 | // Set further state as ENV 168 | self.engine_state.add_env_var( 169 | NUR_ENV_NUR_TASK_CALL.to_string(), 170 | Value::string(self.state.task_call.join(" "), Span::unknown()), 171 | ); 172 | if self.state.task_name.is_some() { 173 | let task_name = self.get_short_task_name(); 174 | self.engine_state.add_env_var( 175 | NUR_ENV_NUR_TASK_NAME.to_string(), 176 | Value::string(task_name, Span::unknown()), 177 | ); 178 | } 179 | } 180 | 181 | pub(crate) fn parse_args(&mut self) -> NurArgs { 182 | parse_commandline_args(&self.state.args_to_nur.join(" "), &mut self.engine_state) 183 | .unwrap_or_else(|_| std::process::exit(1)) 184 | } 185 | 186 | pub(crate) fn load_env(&mut self) -> NurResult<()> { 187 | if self.state.env_path.exists() { 188 | self.source_and_merge_env(self.state.env_path.clone(), PipelineData::empty())?; 189 | } else { 190 | self.eval_and_merge_env(get_default_nur_env(), PipelineData::empty())?; 191 | } 192 | 193 | Ok(()) 194 | } 195 | 196 | pub(crate) fn load_config(&mut self) -> NurResult<()> { 197 | if self.state.config_path.exists() { 198 | self.source_and_merge_env(self.state.config_path.clone(), PipelineData::empty())?; 199 | } else { 200 | self.eval_and_merge_env(get_default_nur_config(), PipelineData::empty())?; 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | pub(crate) fn load_nurfiles(&mut self) -> NurResult<()> { 207 | if self.state.nurfile_path.exists() { 208 | self.source(self.state.nurfile_path.clone(), PipelineData::empty())?; 209 | } 210 | if self.state.local_nurfile_path.exists() { 211 | self.source(self.state.local_nurfile_path.clone(), PipelineData::empty())?; 212 | } 213 | 214 | self._find_task_name(); 215 | self._finalise_nur_state(); 216 | 217 | Ok(()) 218 | } 219 | 220 | fn _find_task_name(&mut self) { 221 | if !self.state.has_task_call { 222 | return; 223 | } 224 | 225 | let task_call_length = self.state.task_call.len(); 226 | 227 | let mut search_task_index = 2; // will start with main task 228 | let mut found_task_index = 0; // checked above 229 | while search_task_index <= task_call_length { 230 | // next sub task needs to be safe 231 | if !is_safe_taskname(&self.state.task_call[search_task_index - 1]) { 232 | break; 233 | } 234 | // Test if sub-task exists 235 | let next_possible_task_name = self.state.task_call[0..search_task_index].join(" "); 236 | if self.has_def(next_possible_task_name) { 237 | // If the sub-task exists, store found_task_index 238 | found_task_index = search_task_index; 239 | } 240 | search_task_index += 1; // check next argument, if it exists 241 | } 242 | 243 | // If we have not found any task name, abort 244 | if found_task_index == 0 { 245 | return; 246 | } 247 | 248 | self.state.task_name = Some(self.state.task_call[0..found_task_index].join(" ")); 249 | } 250 | 251 | pub(crate) fn get_task_def(&mut self) -> Option<&dyn Command> { 252 | let task_name = self.state.task_name.clone().unwrap(); 253 | 254 | self.get_def(task_name) 255 | } 256 | 257 | // Return task name without the "nur " prefix 258 | pub(crate) fn get_short_task_name(&self) -> String { 259 | let task_name = self.state.task_name.clone().unwrap(); 260 | 261 | String::from(&task_name[4..]) 262 | } 263 | 264 | fn _parse_nu_script( 265 | &mut self, 266 | file_path: Option<&str>, 267 | contents: String, 268 | ) -> NurResult> { 269 | if file_path.is_some() { 270 | self.engine_state.file = Some(PathBuf::from(file_path.unwrap())); 271 | } 272 | 273 | let mut working_set = StateWorkingSet::new(&self.engine_state); 274 | let block = nu_parser::parse(&mut working_set, file_path, &contents.into_bytes(), false); 275 | 276 | if working_set.parse_errors.is_empty() { 277 | let delta = working_set.render(); 278 | self.engine_state.merge_delta(delta)?; 279 | 280 | Ok(block) 281 | } else { 282 | if let Some(err) = working_set.parse_errors.first() { 283 | report_parse_error(&working_set, err); 284 | } 285 | 286 | Err(NurError::ParseErrors(working_set.parse_errors)) 287 | } 288 | } 289 | 290 | fn _execute_block(&mut self, block: &Block, input: PipelineData) -> NurResult { 291 | nu_engine::get_eval_block(&self.engine_state)( 292 | &self.engine_state, 293 | &mut self.stack, 294 | block, 295 | input, 296 | ) 297 | .map_err(|err| { 298 | report_shell_error(&self.engine_state, &err); 299 | std::process::exit(1); 300 | }) 301 | } 302 | 303 | fn _eval( 304 | &mut self, 305 | file_path: Option<&str>, 306 | contents: S, 307 | input: PipelineData, 308 | print: bool, 309 | merge_env: bool, 310 | ) -> NurResult { 311 | let str_contents = contents.to_string(); 312 | 313 | if str_contents.is_empty() { 314 | return Ok(0); 315 | } 316 | 317 | let block = self._parse_nu_script(file_path, str_contents)?; 318 | 319 | let result = self._execute_block(&block, input)?; 320 | 321 | // Merge env is requested 322 | if merge_env { 323 | match self.engine_state.cwd(Some(&self.stack)) { 324 | Ok(_cwd) => { 325 | if let Err(e) = self.engine_state.merge_env(&mut self.stack) { 326 | report_shell_error(&self.engine_state, &e); 327 | } 328 | } 329 | Err(e) => { 330 | report_shell_error(&self.engine_state, &e); 331 | } 332 | } 333 | } 334 | 335 | // Print result is requested 336 | let exit_details = if print { 337 | result.print_table(&self.engine_state, &mut self.stack, false, false) 338 | } else { 339 | result.drain() 340 | }; 341 | 342 | match exit_details { 343 | Ok(()) => Ok(0), 344 | Err(err) => { 345 | report_shell_error(&self.engine_state, &err); 346 | 347 | match err { 348 | ShellError::NonZeroExitCode { 349 | exit_code, 350 | span: _span, 351 | } => Ok(exit_code.into()), 352 | _ => Ok(1), 353 | } 354 | } 355 | } 356 | } 357 | 358 | // This is used in tests only currently 359 | #[allow(dead_code)] 360 | pub fn eval(&mut self, contents: S, input: PipelineData) -> NurResult { 361 | self._eval(None, contents, input, false, false) 362 | } 363 | 364 | pub(crate) fn eval_and_print( 365 | &mut self, 366 | contents: S, 367 | input: PipelineData, 368 | ) -> NurResult { 369 | self._eval(None, contents, input, true, false) 370 | } 371 | 372 | pub(crate) fn eval_and_merge_env( 373 | &mut self, 374 | contents: S, 375 | input: PipelineData, 376 | ) -> NurResult { 377 | self._eval(None, contents, input, false, true) 378 | } 379 | 380 | pub(crate) fn source>( 381 | &mut self, 382 | file_path: P, 383 | input: PipelineData, 384 | ) -> NurResult { 385 | let contents = fs::read_to_string(&file_path)?; 386 | 387 | self._eval(file_path.as_ref().to_str(), contents, input, false, false) 388 | } 389 | 390 | pub(crate) fn source_and_merge_env>( 391 | &mut self, 392 | file_path: P, 393 | input: PipelineData, 394 | ) -> NurResult { 395 | let contents = fs::read_to_string(&file_path)?; 396 | 397 | self._eval(file_path.as_ref().to_str(), contents, input, false, true) 398 | } 399 | 400 | pub(crate) fn has_def>(&self, name: S) -> bool { 401 | self.engine_state 402 | .find_decl(name.as_ref().as_bytes(), &[]) 403 | .is_some() 404 | } 405 | 406 | pub(crate) fn get_def>(&self, name: S) -> Option<&dyn Command> { 407 | if let Some(decl_id) = self.engine_state.find_decl(name.as_ref().as_bytes(), &[]) { 408 | Some(self.engine_state.get_decl(decl_id)) 409 | } else { 410 | None 411 | } 412 | } 413 | 414 | pub(crate) fn print_help(&mut self, command: &dyn Command) { 415 | let full_help = get_full_help(command, &self.engine_state, &mut self.stack); 416 | 417 | let _ = std::panic::catch_unwind(move || stdout_write_all_and_flush(full_help)); 418 | } 419 | 420 | pub(crate) fn run_repl(&mut self) -> NurResult<()> { 421 | match evaluate_repl( 422 | &mut self.engine_state, 423 | self.stack.clone(), 424 | None, 425 | None, 426 | std::time::Instant::now(), 427 | ) { 428 | Ok(_) => Ok(()), 429 | Err(_) => Err(EnteredShellError()), 430 | } 431 | } 432 | } 433 | 434 | #[cfg(test)] 435 | mod tests { 436 | use super::*; 437 | use crate::names::{ 438 | NUR_CONFIG_CONFIG_FILENAME, NUR_CONFIG_DIR, NUR_CONFIG_ENV_FILENAME, NUR_CONFIG_LIB_PATH, 439 | NUR_FILE, NUR_LOCAL_FILE, 440 | }; 441 | use std::fs::File; 442 | use std::io::Write; 443 | use tempfile::{tempdir, TempDir}; 444 | 445 | fn _has_decl>(engine_state: &mut EngineState, name: S) -> bool { 446 | engine_state 447 | .find_decl(name.as_ref().as_bytes(), &[]) 448 | .is_some() 449 | } 450 | 451 | #[test] 452 | fn test_init_engine_state_will_add_commands() { 453 | let temp_dir = tempdir().unwrap(); 454 | let temp_dir_path = temp_dir.path().to_path_buf(); 455 | let mut engine_state = init_engine_state(&temp_dir_path).unwrap(); 456 | 457 | assert!(_has_decl(&mut engine_state, "alias")); 458 | assert!(_has_decl(&mut engine_state, "do")); 459 | assert!(_has_decl(&mut engine_state, "uniq")); 460 | assert!(_has_decl(&mut engine_state, "help")); 461 | assert!(_has_decl(&mut engine_state, "str")); 462 | assert!(_has_decl(&mut engine_state, "format pattern")); 463 | assert!(_has_decl(&mut engine_state, "history")); 464 | assert!(_has_decl(&mut engine_state, "explore")); 465 | assert!(_has_decl(&mut engine_state, "print")); 466 | assert!(_has_decl(&mut engine_state, "nu-highlight")); 467 | assert!(_has_decl(&mut engine_state, "nur")); 468 | } 469 | 470 | #[test] 471 | fn test_init_engine_state_will_set_nu_version() { 472 | let temp_dir = tempdir().unwrap(); 473 | let temp_dir_path = temp_dir.path().to_path_buf(); 474 | let engine_state = init_engine_state(&temp_dir_path).unwrap(); 475 | 476 | assert!(engine_state.get_env_var("NU_VERSION").is_some()); 477 | } 478 | 479 | #[test] 480 | fn test_init_engine_state_will_set_flags() { 481 | let temp_dir = tempdir().unwrap(); 482 | let temp_dir_path = temp_dir.path().to_path_buf(); 483 | let engine_state = init_engine_state(&temp_dir_path).unwrap(); 484 | 485 | assert_eq!(engine_state.is_interactive, false); 486 | assert_eq!(engine_state.is_login, false); 487 | assert_eq!(engine_state.history_enabled, false); 488 | } 489 | 490 | fn _prepare_nur_engine(temp_dir: &TempDir) -> NurEngine { 491 | let temp_dir_path = temp_dir.path().to_path_buf(); 492 | let nurfile_path = temp_dir.path().join(NUR_FILE); 493 | File::create(&nurfile_path).unwrap(); 494 | 495 | let args = vec![ 496 | String::from("nur"), 497 | String::from("some-task"), 498 | String::from("sub-task"), 499 | ]; 500 | let nur_state = NurState::new(temp_dir_path.clone(), args).unwrap(); 501 | let engine_state = init_engine_state(temp_dir_path).unwrap(); 502 | 503 | NurEngine::new(engine_state, nur_state).unwrap() 504 | } 505 | 506 | fn _cleanup_nur_engine(temp_dir: &TempDir) { 507 | let nurfile_path = temp_dir.path().join(NUR_FILE); 508 | let nurfile_local_path = temp_dir.path().join(NUR_FILE); 509 | let config_dir = temp_dir.path().join(NUR_CONFIG_DIR); 510 | 511 | fs::remove_file(nurfile_path).unwrap(); 512 | if nurfile_local_path.exists() { 513 | fs::remove_file(nurfile_local_path).unwrap(); 514 | } 515 | if config_dir.exists() { 516 | fs::remove_dir_all(config_dir).unwrap(); 517 | } 518 | } 519 | 520 | fn _has_var>(nur_engine: &mut NurEngine, name: S) -> bool { 521 | let name = name.as_ref(); 522 | let dollar_name = format!("${name}"); 523 | let var_id = nur_engine 524 | .engine_state 525 | .active_overlays(&vec![]) 526 | .find_map(|o| { 527 | o.vars 528 | .get(dollar_name.as_bytes()) 529 | .or(o.vars.get(name.as_bytes())) 530 | }) 531 | .unwrap(); 532 | 533 | nur_engine.stack.get_var(*var_id, Span::unknown()).is_ok() 534 | } 535 | 536 | #[test] 537 | fn test_nur_engine_will_include_std_lib() { 538 | let temp_dir = tempdir().unwrap(); 539 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 540 | 541 | assert!(nur_engine.eval("use std", PipelineData::empty()).is_ok()); 542 | 543 | _cleanup_nur_engine(&temp_dir); 544 | } 545 | 546 | #[test] 547 | fn test_nur_engine_will_set_nur_variable() { 548 | let temp_dir = tempdir().unwrap(); 549 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 550 | 551 | assert!(_has_var(&mut nur_engine, "nur")); 552 | 553 | _cleanup_nur_engine(&temp_dir); 554 | } 555 | 556 | #[test] 557 | fn test_nur_engine_will_load_nurfiles() { 558 | let temp_dir = tempdir().unwrap(); 559 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 560 | 561 | let nurfile_path = temp_dir.path().join(NUR_FILE); 562 | let mut nurfile = File::create(&nurfile_path).unwrap(); 563 | nurfile.write_all(b"def nurfile-command [] {}").unwrap(); 564 | let nurfile_local_path = temp_dir.path().join(NUR_LOCAL_FILE); 565 | let mut nurfile_local = File::create(&nurfile_local_path).unwrap(); 566 | nurfile_local 567 | .write_all(b"def nurfile-local-command [] {}") 568 | .unwrap(); 569 | 570 | nur_engine.load_env().unwrap(); 571 | nur_engine.load_config().unwrap(); 572 | nur_engine.load_nurfiles().unwrap(); 573 | 574 | assert!(_has_decl(&mut nur_engine.engine_state, "nurfile-command")); 575 | assert!(_has_decl( 576 | &mut nur_engine.engine_state, 577 | "nurfile-local-command" 578 | )); 579 | 580 | _cleanup_nur_engine(&temp_dir); 581 | } 582 | 583 | #[test] 584 | fn test_nur_engine_will_load_env_and_config() { 585 | let temp_dir = tempdir().unwrap(); 586 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 587 | 588 | let config_dir = temp_dir.path().join(NUR_CONFIG_DIR); 589 | fs::create_dir(config_dir.clone()).unwrap(); 590 | let env_path = config_dir.join(NUR_CONFIG_ENV_FILENAME); 591 | let mut env_file = File::create(&env_path).unwrap(); 592 | env_file.write_all(b"def env-command [] {}").unwrap(); 593 | let config_path = config_dir.join(NUR_CONFIG_CONFIG_FILENAME); 594 | let mut config_file = File::create(&config_path).unwrap(); 595 | config_file.write_all(b"def config-command [] {}").unwrap(); 596 | 597 | nur_engine.load_env().unwrap(); 598 | nur_engine.load_config().unwrap(); 599 | nur_engine.load_nurfiles().unwrap(); 600 | 601 | assert!(_has_decl(&mut nur_engine.engine_state, "env-command")); 602 | assert!(_has_decl(&mut nur_engine.engine_state, "config-command")); 603 | 604 | _cleanup_nur_engine(&temp_dir); 605 | } 606 | 607 | #[test] 608 | fn test_nur_engine_will_allow_scripts() { 609 | let temp_dir = tempdir().unwrap(); 610 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 611 | 612 | let config_dir = temp_dir.path().join(NUR_CONFIG_DIR); 613 | fs::create_dir(config_dir.clone()).unwrap(); 614 | let scripts_dir = config_dir.join(NUR_CONFIG_LIB_PATH); 615 | fs::create_dir(scripts_dir.clone()).unwrap(); 616 | let module_path = scripts_dir.join("test-module.nu"); 617 | let mut module_file = File::create(&module_path).unwrap(); 618 | module_file 619 | .write_all(b"export def module-command [] {}") 620 | .unwrap(); 621 | 622 | nur_engine.load_env().unwrap(); 623 | nur_engine.load_config().unwrap(); 624 | nur_engine.load_nurfiles().unwrap(); 625 | 626 | nur_engine 627 | .eval("use test-module.nu *", PipelineData::empty()) 628 | .unwrap(); 629 | 630 | assert!(_has_decl(&mut nur_engine.engine_state, "module-command")); 631 | 632 | _cleanup_nur_engine(&temp_dir); 633 | } 634 | 635 | #[test] 636 | fn test_nur_engine_will_set_task_name() { 637 | let temp_dir = tempdir().unwrap(); 638 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 639 | 640 | let nurfile_path = temp_dir.path().join(NUR_FILE); 641 | let mut nurfile = File::create(&nurfile_path).unwrap(); 642 | nurfile.write_all(b"def \"nur some-task\" [] {}").unwrap(); 643 | 644 | nur_engine.load_env().unwrap(); 645 | nur_engine.load_config().unwrap(); 646 | nur_engine.load_nurfiles().unwrap(); 647 | 648 | assert!(nur_engine.state.task_name.is_some()); 649 | assert!(nur_engine.state.task_name.clone().unwrap() == "nur some-task"); 650 | assert!(nur_engine.get_short_task_name() == "some-task"); 651 | } 652 | 653 | #[test] 654 | fn test_nur_engine_will_check_task_name_exists() { 655 | let temp_dir = tempdir().unwrap(); 656 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 657 | 658 | let nurfile_path = temp_dir.path().join(NUR_FILE); 659 | File::create(&nurfile_path).unwrap(); 660 | 661 | nur_engine.load_env().unwrap(); 662 | nur_engine.load_config().unwrap(); 663 | nur_engine.load_nurfiles().unwrap(); 664 | 665 | assert!(nur_engine.state.task_name.is_none()); 666 | } 667 | 668 | #[test] 669 | fn test_nur_engine_will_allow_sub_tasks() { 670 | let temp_dir = tempdir().unwrap(); 671 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 672 | 673 | let nurfile_path = temp_dir.path().join(NUR_FILE); 674 | let mut nurfile = File::create(&nurfile_path).unwrap(); 675 | nurfile 676 | .write_all(b"def \"nur some-task sub-task\" [] {}") 677 | .unwrap(); 678 | 679 | nur_engine.load_env().unwrap(); 680 | nur_engine.load_config().unwrap(); 681 | nur_engine.load_nurfiles().unwrap(); 682 | 683 | assert!(nur_engine.state.task_name.is_some()); 684 | assert!(nur_engine.state.task_name.clone().unwrap() == "nur some-task sub-task"); 685 | assert!(nur_engine.get_short_task_name() == "some-task sub-task"); 686 | } 687 | 688 | #[test] 689 | fn test_nur_engine_will_set_env() { 690 | let temp_dir = tempdir().unwrap(); 691 | let mut nur_engine = _prepare_nur_engine(&temp_dir); 692 | 693 | let nurfile_path = temp_dir.path().join(NUR_FILE); 694 | let mut nurfile = File::create(&nurfile_path).unwrap(); 695 | nurfile.write_all(b"def \"nur some-task\" [] {}").unwrap(); 696 | 697 | assert!(nur_engine 698 | .engine_state 699 | .get_env_var(NUR_ENV_NUR_VERSION) 700 | .is_some()); 701 | 702 | nur_engine.load_env().unwrap(); 703 | nur_engine.load_config().unwrap(); 704 | nur_engine.load_nurfiles().unwrap(); 705 | 706 | assert!(nur_engine 707 | .engine_state 708 | .get_env_var(NUR_ENV_NUR_TASK_NAME) 709 | .is_some()); 710 | assert!(nur_engine 711 | .engine_state 712 | .get_env_var(NUR_ENV_NUR_TASK_CALL) 713 | .is_some()); 714 | } 715 | } 716 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use miette::Diagnostic; 2 | use nu_protocol::{ParseError, ShellError}; 3 | use thiserror::Error; 4 | 5 | pub(crate) type NurResult = Result; 6 | 7 | #[derive(Clone, Debug, Error, Diagnostic)] 8 | pub enum NurError { 9 | #[error("Init nu error {0}")] 10 | #[diagnostic()] 11 | InitError(String), 12 | 13 | #[error("IO Error {0}")] 14 | #[diagnostic()] 15 | IoError(String), 16 | 17 | #[error("Shell Error {0}")] 18 | #[diagnostic()] 19 | ShellError(#[from] ShellError), 20 | 21 | #[error("Parse Errors")] 22 | #[diagnostic()] 23 | ParseErrors(#[related] Vec), 24 | 25 | #[error("Invalid task name '{0}'")] 26 | #[diagnostic()] 27 | InvalidTaskName(String), 28 | 29 | #[error("Could not find the task for call '{0}'")] 30 | #[diagnostic()] 31 | TaskNotFound(String), 32 | 33 | #[error("Could not find nurfile in path and parents")] 34 | #[diagnostic()] 35 | NurfileNotFound(), 36 | 37 | #[error("Entered shell did raise an error")] 38 | #[diagnostic()] 39 | EnteredShellError(), 40 | 41 | #[error("You cannot use {0} and {1} together")] 42 | #[diagnostic()] 43 | InvalidNurCall(String, String), 44 | } 45 | 46 | impl From for NurError { 47 | fn from(_value: std::io::Error) -> NurError { 48 | NurError::IoError(String::from("Could not read file")) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod commands; 3 | mod compat; 4 | mod engine; 5 | mod errors; 6 | mod names; 7 | mod nu_version; 8 | mod path; 9 | mod scripts; 10 | mod state; 11 | 12 | use crate::commands::Nur; 13 | use crate::compat::show_nurscripts_hint; 14 | use crate::engine::init_engine_state; 15 | use crate::engine::NurEngine; 16 | use crate::errors::NurError; 17 | use crate::path::current_dir_from_environment; 18 | use crate::state::NurState; 19 | use miette::Result; 20 | use nu_ansi_term::Color; 21 | use nu_protocol::{ByteStream, PipelineData, Span}; 22 | use std::env; 23 | use std::process::ExitCode; 24 | 25 | fn main() -> Result { 26 | // Initialise nur state 27 | let run_path = current_dir_from_environment(); 28 | let nur_state = NurState::new(run_path, env::args().collect())?; 29 | 30 | // Create raw nu engine state 31 | let engine_state = init_engine_state(&nur_state.project_path)?; 32 | 33 | // Setup nur engine from engine state 34 | let mut nur_engine = NurEngine::new(engine_state, nur_state)?; 35 | let use_color = nur_engine 36 | .engine_state 37 | .get_config() 38 | .use_ansi_coloring 39 | .get(&nur_engine.engine_state); 40 | 41 | // Parse args 42 | let parsed_nur_args = nur_engine.parse_args(); 43 | 44 | #[cfg(feature = "debug")] 45 | if parsed_nur_args.debug_output { 46 | eprintln!("run path: {:?}", nur_engine.state.run_path); 47 | eprintln!("project path: {:?}", nur_engine.state.project_path); 48 | eprintln!(); 49 | eprintln!("nur args: {:?}", parsed_nur_args); 50 | eprintln!("task call: {:?}", nur_engine.state.task_call); 51 | eprintln!(); 52 | eprintln!("nur config dir: {:?}", nur_engine.state.config_dir); 53 | eprintln!( 54 | "nur lib path (scripts/): {:?}", 55 | nur_engine.state.lib_dir_path 56 | ); 57 | eprintln!("nur env path (env.nu): {:?}", nur_engine.state.env_path); 58 | eprintln!( 59 | "nur config path (config.nu): {:?}", 60 | nur_engine.state.config_path 61 | ); 62 | eprintln!(); 63 | eprintln!("nurfile path: {:?}", nur_engine.state.nurfile_path); 64 | eprintln!( 65 | "nurfile local path: {:?}", 66 | nur_engine.state.local_nurfile_path 67 | ); 68 | } 69 | 70 | // Show hints for compatibility issues 71 | if nur_engine.state.has_project_path { 72 | show_nurscripts_hint(nur_engine.state.project_path.clone(), use_color); 73 | } 74 | 75 | // Handle execution without project path, only allow to show help, abort otherwise 76 | if !nur_engine.state.has_project_path { 77 | if parsed_nur_args.show_help { 78 | nur_engine.print_help(&Nur); 79 | 80 | std::process::exit(0); 81 | } else { 82 | return Err(miette::ErrReport::from(NurError::NurfileNotFound())); 83 | } 84 | } 85 | 86 | // Load env and config 87 | nur_engine.load_env()?; 88 | nur_engine.load_config()?; 89 | 90 | // Load task files 91 | nur_engine.load_nurfiles()?; 92 | 93 | // Handle list tasks 94 | if parsed_nur_args.list_tasks { 95 | // TODO: Parse and handle commands without eval 96 | nur_engine.eval_and_print( 97 | r#"scope commands 98 | | where name starts-with "nur " and type == "custom" 99 | | get name 100 | | each { |it| $it | str substring 4.. } 101 | | sort 102 | | each { |it| print $it }; 103 | null"#, 104 | PipelineData::empty(), 105 | )?; 106 | 107 | std::process::exit(0); 108 | } 109 | 110 | // Show help if no task call was found 111 | // (error exit if --help was not passed) 112 | if !nur_engine.state.has_task_call 113 | && parsed_nur_args.run_commands.is_none() 114 | && !parsed_nur_args.enter_shell 115 | { 116 | nur_engine.print_help(&Nur); 117 | if parsed_nur_args.show_help { 118 | std::process::exit(0); 119 | } else { 120 | std::process::exit(1); 121 | } 122 | } 123 | 124 | // Handle help 125 | if parsed_nur_args.show_help { 126 | if !nur_engine.state.has_task_call { 127 | nur_engine.print_help(&Nur); 128 | std::process::exit(0); 129 | } 130 | 131 | if let Some(command) = nur_engine.clone().get_task_def() { 132 | nur_engine.clone().print_help(command); 133 | std::process::exit(0); 134 | } else { 135 | return Err(miette::ErrReport::from(NurError::TaskNotFound( 136 | nur_engine.state.task_call.join(" "), 137 | ))); 138 | } 139 | } 140 | 141 | // Ensure we only allow sane calls 142 | if nur_engine.state.has_task_call && parsed_nur_args.run_commands.is_some() { 143 | return Err(miette::ErrReport::from(NurError::InvalidNurCall( 144 | String::from("task call"), 145 | String::from("--commands/-c"), 146 | ))); 147 | } 148 | if nur_engine.state.has_task_call && parsed_nur_args.enter_shell { 149 | return Err(miette::ErrReport::from(NurError::InvalidNurCall( 150 | String::from("task call"), 151 | String::from("--enter-shell"), 152 | ))); 153 | } 154 | if parsed_nur_args.run_commands.is_some() && parsed_nur_args.enter_shell { 155 | return Err(miette::ErrReport::from(NurError::InvalidNurCall( 156 | String::from("--commands/-c"), 157 | String::from("--enter-shell"), 158 | ))); 159 | } 160 | if nur_engine.state.has_task_call && nur_engine.state.task_name.is_none() { 161 | return Err(miette::ErrReport::from(NurError::TaskNotFound( 162 | nur_engine.state.task_call.join(" "), 163 | ))); 164 | } 165 | 166 | // Prepare input data - if requested 167 | let input = if parsed_nur_args.attach_stdin { 168 | PipelineData::ByteStream(ByteStream::stdin(Span::unknown())?, None) 169 | } else { 170 | PipelineData::empty() 171 | }; 172 | 173 | // Execute the task 174 | let exit_code: i32; 175 | let run_command = if parsed_nur_args.run_commands.is_some() { 176 | parsed_nur_args.run_commands.clone().unwrap().item 177 | } else { 178 | nur_engine.state.task_call.join(" ") 179 | }; 180 | #[cfg(feature = "debug")] 181 | if parsed_nur_args.debug_output { 182 | eprintln!("full command call: {}", run_command); 183 | } 184 | if parsed_nur_args.enter_shell { 185 | exit_code = match nur_engine.run_repl() { 186 | Ok(_) => 0, 187 | Err(_) => 1, 188 | } 189 | } else if parsed_nur_args.quiet_execution { 190 | exit_code = nur_engine.eval_and_print(run_command, input)?; 191 | 192 | #[cfg(feature = "debug")] 193 | if parsed_nur_args.debug_output { 194 | println!("Exit code {:?}", exit_code); 195 | } 196 | } else { 197 | println!("nur version {}", env!("CARGO_PKG_VERSION")); 198 | println!( 199 | "Project path: {}", 200 | nur_engine.state.project_path.to_str().unwrap() 201 | ); 202 | if parsed_nur_args.run_commands.is_some() { 203 | println!("Running command: {}", run_command); 204 | } else { 205 | println!("Executing task: {}", nur_engine.get_short_task_name()); 206 | } 207 | println!(); 208 | exit_code = nur_engine.eval_and_print(run_command, input)?; 209 | #[cfg(feature = "debug")] 210 | if parsed_nur_args.debug_output { 211 | println!("Exit code {:?}", exit_code); 212 | } 213 | if exit_code == 0 { 214 | println!( 215 | "{}Task execution successful{}", 216 | if use_color { 217 | Color::Green.prefix().to_string() 218 | } else { 219 | String::from("") 220 | }, 221 | if use_color { 222 | Color::Green.suffix().to_string() 223 | } else { 224 | String::from("") 225 | }, 226 | ); 227 | } else { 228 | println!( 229 | "{}Task execution failed (exit code: {}){}", 230 | if use_color { 231 | Color::Red.prefix().to_string() 232 | } else { 233 | String::from("") 234 | }, 235 | exit_code, 236 | if use_color { 237 | Color::Red.suffix().to_string() 238 | } else { 239 | String::from("") 240 | }, 241 | ); 242 | } 243 | } 244 | 245 | Ok(ExitCode::from(exit_code as u8)) 246 | } 247 | -------------------------------------------------------------------------------- /src/names.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const NUR_NAME: &str = "nur"; 2 | 3 | // Config paths/files 4 | pub(crate) const NUR_CONFIG_DIR: &str = ".nur"; 5 | pub(crate) const NUR_CONFIG_LIB_PATH: &str = "scripts"; 6 | pub(crate) const NUR_CONFIG_CONFIG_FILENAME: &str = "config.nu"; 7 | pub(crate) const NUR_CONFIG_ENV_FILENAME: &str = "env.nu"; 8 | 9 | // $env variable names 10 | pub(crate) const NUR_ENV_NU_LIB_DIRS: &str = "NU_LIB_DIRS"; 11 | pub(crate) const NUR_ENV_NUR_VERSION: &str = "NUR_VERSION"; 12 | pub(crate) const NUR_ENV_NUR_TASK_CALL: &str = "NUR_TASK_CALL"; 13 | pub(crate) const NUR_ENV_NUR_TASK_NAME: &str = "NUR_TASK_NAME"; 14 | 15 | // $nur variable names 16 | pub(crate) const NUR_VAR_RUN_PATH: &str = "run-path"; 17 | pub(crate) const NUR_VAR_PROJECT_PATH: &str = "project-path"; 18 | pub(crate) const NUR_VAR_TASK_NAME: &str = "task-name"; 19 | pub(crate) const NUR_VAR_CONFIG_DIR: &str = "config-dir"; 20 | pub(crate) const NUR_VAR_DEFAULT_LIB_DIR: &str = "default-lib-dir"; 21 | 22 | // nurfile names 23 | pub(crate) const NUR_FILE: &str = "nurfile"; 24 | pub(crate) const NUR_LOCAL_FILE: &str = "nurfile.local"; 25 | -------------------------------------------------------------------------------- /src/nu-scripts/default_nur_config.nu: -------------------------------------------------------------------------------- 1 | # nur Config File 2 | 3 | # We don't set anything special here 4 | $env.config = {} 5 | -------------------------------------------------------------------------------- /src/nu-scripts/default_nur_env.nu: -------------------------------------------------------------------------------- 1 | # nur Environment File 2 | 3 | # Directories to search for scripts when calling source or use 4 | # The default for this is $nur.default-lib-dir which is $nur-project-path/.nur/scripts 5 | $env.NU_LIB_DIRS = [ 6 | $nur.default-lib-dir 7 | ] 8 | 9 | # To load from a custom file you can use: 10 | # source ($nur.project-path | path join 'custom.nu') 11 | -------------------------------------------------------------------------------- /src/nu_version.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const NU_VERSION: &str = "0.104.0"; 2 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | use crate::names::NUR_FILE; 2 | use std::path::{Path, PathBuf}; 3 | 4 | /// Get the directory where the Nushell executable is located. 5 | fn current_exe_directory() -> PathBuf { 6 | let mut path = std::env::current_exe().expect("current_exe() should succeed"); 7 | path.pop(); 8 | path 9 | } 10 | 11 | /// Get the current working directory from the environment. 12 | pub(crate) fn current_dir_from_environment() -> PathBuf { 13 | if let Ok(cwd) = std::env::current_dir() { 14 | return cwd; 15 | } 16 | if let Ok(cwd) = std::env::var("PWD") { 17 | return cwd.into(); 18 | } 19 | if let Some(home) = nu_path::home_dir() { 20 | return home.into_std_path_buf(); 21 | } 22 | current_exe_directory() 23 | } 24 | 25 | pub(crate) fn find_project_path>(cwd: P) -> Option { 26 | let mut path = cwd.as_ref(); 27 | 28 | loop { 29 | let taskfile_path = path.join(NUR_FILE); 30 | if taskfile_path.exists() { 31 | return Some(path.to_path_buf()); 32 | } 33 | 34 | if let Some(parent) = path.parent() { 35 | path = parent; 36 | } else { 37 | return None; 38 | } 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::*; 45 | use std::fs::{create_dir, File}; 46 | use tempfile::tempdir; 47 | 48 | #[test] 49 | fn test_find_project_path() { 50 | // Create a temporary directory and a "nurfile" inside it 51 | let temp_dir = tempdir().unwrap(); 52 | let temp_dir_path = temp_dir.path().to_path_buf(); 53 | let nurfile_path = temp_dir.path().join(NUR_FILE); 54 | File::create(&nurfile_path).unwrap(); 55 | 56 | // Test the function with the temporary directory as the current working directory 57 | let expected_path = temp_dir_path.clone(); 58 | let actual_path = find_project_path(&temp_dir_path).unwrap(); 59 | assert_eq!(expected_path, actual_path); 60 | 61 | // Clean up 62 | std::fs::remove_file(nurfile_path).unwrap(); 63 | } 64 | 65 | #[test] 66 | fn test_find_project_path_subdirectory() { 67 | // Create a temporary directory and a subdirectory inside it 68 | let temp_dir = tempdir().unwrap(); 69 | let temp_dir_path = temp_dir.path().to_path_buf(); 70 | let sub_dir = temp_dir_path.join("sub"); 71 | create_dir(&sub_dir).unwrap(); 72 | 73 | // Create a "nurfile" inside the temporary directory 74 | let nurfile_path = temp_dir_path.join(NUR_FILE); 75 | File::create(&nurfile_path).unwrap(); 76 | 77 | // Test the function with the subdirectory as the current working directory 78 | let expected_path = temp_dir_path.clone(); 79 | let actual_path = find_project_path(&sub_dir).unwrap(); 80 | assert_eq!(expected_path, actual_path); 81 | 82 | // Clean up 83 | std::fs::remove_file(nurfile_path).unwrap(); 84 | std::fs::remove_dir(sub_dir).unwrap(); 85 | } 86 | 87 | #[test] 88 | fn test_find_project_path_error() { 89 | // Create a temporary directory without a "nurfile" 90 | let temp_dir = tempdir().unwrap(); 91 | let temp_dir_path = temp_dir.path().to_path_buf(); 92 | 93 | // Test the function with the temporary directory as the current working directory 94 | match find_project_path(&temp_dir_path) { 95 | Some(_) => panic!("Expected an error, but got Ok"), 96 | None => (), 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/scripts.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn get_default_nur_env() -> &'static str { 2 | include_str!("nu-scripts/default_nur_env.nu") 3 | } 4 | 5 | pub(crate) fn get_default_nur_config() -> &'static str { 6 | include_str!("nu-scripts/default_nur_config.nu") 7 | } 8 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::args::gather_commandline_args; 2 | use crate::errors::NurResult; 3 | use crate::names::{ 4 | NUR_CONFIG_CONFIG_FILENAME, NUR_CONFIG_DIR, NUR_CONFIG_ENV_FILENAME, NUR_CONFIG_LIB_PATH, 5 | NUR_FILE, NUR_LOCAL_FILE, 6 | }; 7 | use crate::path::find_project_path; 8 | use std::path::PathBuf; 9 | 10 | #[derive(Clone)] 11 | pub(crate) struct NurState { 12 | pub(crate) run_path: PathBuf, 13 | pub(crate) has_project_path: bool, 14 | pub(crate) project_path: PathBuf, 15 | 16 | pub(crate) config_dir: PathBuf, 17 | pub(crate) lib_dir_path: PathBuf, 18 | pub(crate) env_path: PathBuf, 19 | pub(crate) config_path: PathBuf, 20 | 21 | pub(crate) nurfile_path: PathBuf, 22 | pub(crate) local_nurfile_path: PathBuf, 23 | 24 | pub(crate) args_to_nur: Vec, 25 | pub(crate) has_task_call: bool, 26 | pub(crate) task_call: Vec, 27 | pub(crate) task_name: Option, // full task name, like "nur some-task" 28 | } 29 | 30 | impl NurState { 31 | pub(crate) fn new(run_path: PathBuf, args: Vec) -> NurResult { 32 | // Get initial directory details 33 | let found_project_path = find_project_path(&run_path); 34 | let has_project_path = found_project_path.is_some(); 35 | let project_path = found_project_path.unwrap_or(run_path.clone()); 36 | 37 | // Set all paths 38 | let config_dir = project_path.join(NUR_CONFIG_DIR); 39 | let lib_dir_path = config_dir.join(NUR_CONFIG_LIB_PATH); 40 | let env_path = config_dir.join(NUR_CONFIG_ENV_FILENAME); 41 | let config_path = config_dir.join(NUR_CONFIG_CONFIG_FILENAME); 42 | 43 | // Set nurfiles 44 | let nurfile_path = project_path.join(NUR_FILE); 45 | let local_nurfile_path = project_path.join(NUR_LOCAL_FILE); 46 | 47 | // Parse args into bits 48 | let (args_to_nur, has_task_call, task_call) = gather_commandline_args(args)?; 49 | 50 | Ok(NurState { 51 | run_path, 52 | has_project_path, 53 | project_path, 54 | 55 | config_dir, 56 | lib_dir_path, 57 | env_path, 58 | config_path, 59 | 60 | nurfile_path, 61 | local_nurfile_path, 62 | 63 | args_to_nur, 64 | has_task_call, 65 | task_call, 66 | task_name: None, 67 | }) 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | use std::fs::File; 75 | use tempfile::tempdir; 76 | 77 | #[test] 78 | fn test_nur_state_with_project_path() { 79 | let temp_dir = tempdir().unwrap(); 80 | let temp_dir_path = temp_dir.path().to_path_buf(); 81 | let nurfile_path = temp_dir.path().join(NUR_FILE); 82 | File::create(&nurfile_path).unwrap(); 83 | 84 | // Setup test 85 | let args = vec![ 86 | String::from("nur"), 87 | String::from("--quiet"), 88 | String::from("some_task"), 89 | String::from("task_arg"), 90 | ]; 91 | let state = NurState::new(temp_dir_path.clone(), args).unwrap(); 92 | 93 | // Check everything works out 94 | assert_eq!(state.run_path, temp_dir_path); 95 | assert_eq!(state.project_path, temp_dir_path); 96 | assert_eq!(state.has_project_path, true); 97 | 98 | assert_eq!(state.config_dir, temp_dir_path.join(".nur")); 99 | assert_eq!(state.lib_dir_path, temp_dir_path.join(".nur/scripts")); 100 | assert_eq!(state.env_path, temp_dir_path.join(".nur/env.nu")); 101 | assert_eq!(state.config_path, temp_dir_path.join(".nur/config.nu")); 102 | 103 | assert_eq!(state.nurfile_path, temp_dir_path.join("nurfile")); 104 | assert_eq!( 105 | state.local_nurfile_path, 106 | temp_dir_path.join("nurfile.local") 107 | ); 108 | 109 | assert_eq!( 110 | state.args_to_nur, 111 | vec![String::from("nur"), String::from("--quiet"),] 112 | ); 113 | assert_eq!(state.has_task_call, true); 114 | assert_eq!( 115 | state.task_call, 116 | vec![ 117 | String::from("nur"), 118 | String::from("some_task"), 119 | String::from("task_arg") 120 | ] 121 | ); 122 | 123 | // Clean up 124 | std::fs::remove_file(nurfile_path).unwrap(); 125 | } 126 | 127 | #[test] 128 | fn test_nur_state_without_project_path() { 129 | let temp_dir = tempdir().unwrap(); 130 | let temp_dir_path = temp_dir.path().to_path_buf(); 131 | 132 | // Setup test 133 | let args = vec![ 134 | String::from("nur"), 135 | String::from("--quiet"), 136 | String::from("some_task"), 137 | String::from("task_arg"), 138 | ]; 139 | let state = NurState::new(temp_dir_path.clone(), args).unwrap(); 140 | 141 | // Check everything works out 142 | assert_eq!(state.run_path, temp_dir_path); 143 | assert_eq!(state.project_path, temp_dir_path); // same as run_path, as this is the fallback 144 | assert_eq!(state.has_project_path, false); 145 | 146 | assert_eq!(state.config_dir, temp_dir_path.join(".nur")); 147 | assert_eq!(state.lib_dir_path, temp_dir_path.join(".nur/scripts")); 148 | assert_eq!(state.env_path, temp_dir_path.join(".nur/env.nu")); 149 | assert_eq!(state.config_path, temp_dir_path.join(".nur/config.nu")); 150 | 151 | assert_eq!(state.nurfile_path, temp_dir_path.join("nurfile")); 152 | assert_eq!( 153 | state.local_nurfile_path, 154 | temp_dir_path.join("nurfile.local") 155 | ); 156 | 157 | assert_eq!( 158 | state.args_to_nur, 159 | vec![String::from("nur"), String::from("--quiet"),] 160 | ); 161 | assert_eq!(state.has_task_call, true); 162 | assert_eq!( 163 | state.task_call, 164 | vec![ 165 | String::from("nur"), 166 | String::from("some_task"), 167 | String::from("task_arg") 168 | ] 169 | ); 170 | } 171 | 172 | #[test] 173 | fn test_nur_state_without_task() { 174 | let temp_dir = tempdir().unwrap(); 175 | let temp_dir_path = temp_dir.path().to_path_buf(); 176 | 177 | // Setup test 178 | let args = vec![String::from("nur"), String::from("--help")]; 179 | let state = NurState::new(temp_dir_path.clone(), args).unwrap(); 180 | 181 | // Check everything works out 182 | assert_eq!(state.run_path, temp_dir_path); 183 | assert_eq!(state.project_path, temp_dir_path); // same as run_path, as this is the fallback 184 | assert_eq!(state.has_project_path, false); 185 | 186 | assert_eq!(state.config_dir, temp_dir_path.join(".nur")); 187 | assert_eq!(state.lib_dir_path, temp_dir_path.join(".nur/scripts")); 188 | assert_eq!(state.env_path, temp_dir_path.join(".nur/env.nu")); 189 | assert_eq!(state.config_path, temp_dir_path.join(".nur/config.nu")); 190 | 191 | assert_eq!(state.nurfile_path, temp_dir_path.join("nurfile")); 192 | assert_eq!( 193 | state.local_nurfile_path, 194 | temp_dir_path.join("nurfile.local") 195 | ); 196 | 197 | assert_eq!( 198 | state.args_to_nur, 199 | vec![String::from("nur"), String::from("--help"),] 200 | ); 201 | assert_eq!(state.has_task_call, false); 202 | assert_eq!(state.task_call, vec![] as Vec); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /wix/License.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2761 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw11900\paperh16840\vieww12000\viewh15840\viewkind0 6 | \deftab720 7 | \pard\pardeftab720\partightenfactor0 8 | 9 | \f0\fs24 \cf0 MIT License\ 10 | \ 11 | Copyright (c) 2024 David Danier \ 12 | \ 13 | `nur` does borrow code or inspirations from `nu` itself and also `embed-nu`\ 14 | (https://github.com/Trivernis/embed-nu) by Trivernis. Thanks to both projects!\ 15 | \ 16 | Permission is hereby granted, free of charge, to any person obtaining a copy\ 17 | of this software and associated documentation files (the "Software"), to deal\ 18 | in the Software without restriction, including without limitation the rights\ 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ 20 | copies of the Software, and to permit persons to whom the Software is\ 21 | furnished to do so, subject to the following conditions:\ 22 | \ 23 | The above copyright notice and this permission notice shall be included in all\ 24 | copies or substantial portions of the Software.\ 25 | \ 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ 32 | SOFTWARE.\ 33 | } 34 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 98 | 99 | 103 | 104 | 105 | 106 | 107 | 115 | 116 | 117 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 138 | 142 | 143 | 144 | 145 | 146 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 179 | 180 | 181 | 182 | 183 | 184 | 188 | 189 | 190 | 191 | 199 | 200 | 201 | 202 | 210 | 211 | 212 | 213 | 214 | 215 | --------------------------------------------------------------------------------