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](57452337ff/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.
This commit is contained in:
Tim Martin 2024-06-28 18:11:48 -05:00 committed by GitHub
parent 4f8d82bb88
commit 153b45bc63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 5 deletions

View File

@ -204,7 +204,7 @@ More design guidelines:
### Useful Commands ### Useful Commands
- Run all unit tests for the standard library: - Run all unit tests for the standard library:
```nushell ```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** > **Note**
> this uses the debug version of NU interpreter from the same repo, which is > 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, - Run all tests for a specific test module, e.g,
`crates/nu-std/tests/test_foo.nu` `crates/nu-std/tests/test_foo.nu`
```nushell ```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 - Run a custom command with additional logging (assuming you have instrumented
the command with `log <level>`, as we recommend.) the command with `log <level>`, as we recommend.)

View File

@ -66,7 +66,7 @@ export def --env "path add" [
"record" => { $p | get --ignore-errors $nu.os-info.name }, "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) { if null in $paths or ($paths | is-empty) {
@ -80,7 +80,6 @@ export def --env "path add" [
$env $env
| get $path_name | get $path_name
| split row (char esep) | split row (char esep)
| path expand
| if $append { append $paths } else { prepend $paths } | if $append { append $paths } else { prepend $paths }
)} )}

View File

@ -38,12 +38,38 @@ def path_add [] {
std path add $target_paths std path add $target_paths
assert equal (get_path) ([($target_paths | get $nu.os-info.name)] | path expand) 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" std path add "~/foo"
assert equal (get_path) (["~/foo", "/foo", "/bar"] | path expand) 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] #[test]
def banner [] { def banner [] {
std assert ((std banner | lines | length) == 15) std assert ((std banner | lines | length) == 15)