From 153b45bc63b6b65a34e3efa966a2fb4c8af999ab Mon Sep 17 00:00:00 2001 From: Tim Martin Date: Fri, 28 Jun 2024 18:11:48 -0500 Subject: [PATCH] Surprising symlink resolution for std `path add` (#13258) # Description The standard library's `path add` function has some surprising side effects that I attempt to address in this PR: 1. Paths added, if they are symbolic links, should not be resolved to their targets. Currently, resolution happens. Imagine the following: ```nu # Some time earlier, perhaps even not by the user, a symlink is created mkdir real-dir ln -s real-dir link-dir # Then, step to now, with link-dir that we want in our PATHS variable use std path add link-dir ``` In the current implementation of `path add`, it is _not_ `link-dir` that will be added, as has been stated in the command. It is instead `real-dir`. This is surprising. Users have the agency to do this resolution if they wish with `path expand` (sans a `--no-symlink` flag): for example, `path add (link-dir | path expand)` In particular, when I was trying to set up [fnm](https://github.com/Schniz/fnm), a Node.js version manager, I was bitten by this fact when `fnm` told me that an expected path had not been added to the PATHS variable. It was looking for the non-resolved link. The user in [this comment](https://github.com/Schniz/fnm/issues/463#issuecomment-1710050737) was likely affected by this too. Shells, such as nushell, can handle path symlinks just fine. Binary lookup is unaffected. Let resolution be opt-in. Lastly, there is some convention already in place for **not** resolving path symlinks in the [default $env.ENV_CONVERSIONS table](https://github.com/nushell/nushell/blob/57452337ff4e228102433e99b4c6000700a9b3b2/crates/nu-utils/src/sample_config/default_env.nu#L65). 2. All existing paths in the path variable should be left untouched. Currently, they are `path expand`-ed (including symbolic link resolution). Path add should mean just that: prepend/append this path. Instead, it currently means that, _plus mutate all other paths in the variable_. Again, users have the agency to do this with something like `$env.PATH = $env.PATH | split row (char esep) | path expand`. 3. Minorly, I update documentation on running tests in `crates/nu-std/CONTRIBUTING.md`. The offered command to run the standard library test suite was no longer functional. Thanks to @weirdan in [this Discord conversation](https://discord.com/channels/601130461678272522/614593951969574961/1256029201119576147) for the context. # User-Facing Changes (Written from the perspective of release notes) - The standard library's `path add` function no longer resolves symlinks in either the newly added paths, nor the other paths already in the variable. # Tests + Formatting A test for the changes working correctly has been added to `crates/nu-std/tests/test_std.nu` under the test named `path_add_expand`. You can quickly verify this new test and the existing `path add` test with the following command: ```nu cargo run -- -c 'use crates/nu-std/testing.nu; NU_LOG_LEVEL=INFO testing run-tests --path crates/nu-std --test path_add' ``` All commands suggested in the issue template have been run and complete without error. # After Submitting I'll add a release note to [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged. --- crates/nu-std/CONTRIBUTING.md | 4 ++-- crates/nu-std/std/mod.nu | 3 +-- crates/nu-std/tests/test_std.nu | 28 +++++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/crates/nu-std/CONTRIBUTING.md b/crates/nu-std/CONTRIBUTING.md index 1677a31de1..e866cc9206 100644 --- a/crates/nu-std/CONTRIBUTING.md +++ b/crates/nu-std/CONTRIBUTING.md @@ -204,7 +204,7 @@ More design guidelines: ### Useful Commands - Run all unit tests for the standard library: ```nushell - cargo run -- -c 'use std testing; testing run-tests --path crates/nu-std' + cargo run -- -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std' ``` > **Note** > this uses the debug version of NU interpreter from the same repo, which is @@ -216,7 +216,7 @@ More design guidelines: - Run all tests for a specific test module, e.g, `crates/nu-std/tests/test_foo.nu` ```nushell - cargo run -- -c 'use std testing; testing run-tests --path crates/nu-std --module test_foo' + cargo run -- -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std --module test_foo' ``` - Run a custom command with additional logging (assuming you have instrumented the command with `log `, as we recommend.) diff --git a/crates/nu-std/std/mod.nu b/crates/nu-std/std/mod.nu index 15bad0e8e5..4d4f2bb0f1 100644 --- a/crates/nu-std/std/mod.nu +++ b/crates/nu-std/std/mod.nu @@ -66,7 +66,7 @@ export def --env "path add" [ "record" => { $p | get --ignore-errors $nu.os-info.name }, } - $p | path expand + $p | path expand --no-symlink } if null in $paths or ($paths | is-empty) { @@ -80,7 +80,6 @@ export def --env "path add" [ $env | get $path_name | split row (char esep) - | path expand | if $append { append $paths } else { prepend $paths } )} diff --git a/crates/nu-std/tests/test_std.nu b/crates/nu-std/tests/test_std.nu index 45633719a0..6ac7c6b9e0 100644 --- a/crates/nu-std/tests/test_std.nu +++ b/crates/nu-std/tests/test_std.nu @@ -38,12 +38,38 @@ def path_add [] { std path add $target_paths assert equal (get_path) ([($target_paths | get $nu.os-info.name)] | path expand) - load-env {$path_name: [$"/foo(char esep)/bar"]} + load-env {$path_name: [$"(["/foo", "/bar"] | path expand | str join (char esep))"]} std path add "~/foo" assert equal (get_path) (["~/foo", "/foo", "/bar"] | path expand) } } +#[test] +def path_add_expand [] { + use std assert + + # random paths to avoid collision, especially if left dangling on failure + let real_dir = $nu.temp-path | path join $"real-dir-(random chars)" + let link_dir = $nu.temp-path | path join $"link-dir-(random chars)" + mkdir $real_dir + let path_name = if $nu.os-info.family == 'windows' { + mklink /D $link_dir $real_dir + "Path" + } else { + ln -s $real_dir $link_dir | ignore + "PATH" + } + + with-env {$path_name: []} { + def get_path [] { $env | get $path_name } + + std path add $link_dir + assert equal (get_path) ([$link_dir]) + } + + rm $real_dir $link_dir +} + #[test] def banner [] { std assert ((std banner | lines | length) == 15)