nu-command 0.115.0

Nushell's built-in commands
Documentation
use nu_test_support::{fs::Stub::FileWithContentToBeTrimmed, prelude::*};

#[test]
fn which_ls() -> Result {
    test()
        .run("which ls | get type.0")
        .expect_value_eq("built-in")
}

#[test]
fn which_alias_ls() -> Result {
    test()
        .run("alias ls = ls -a; which ls | get path.0 | str trim")
        .expect_value_eq("nu-tester-0")
}

#[test]
fn which_custom_alias() -> Result {
    test()
        .run(r#"alias foo = print "foo!"; which foo | to nuon"#)
        .expect_value_eq(
            r#"[[command, path, type, definition]; [foo, "nu-tester-0", alias, "print \"foo!\""]]"#,
        )
}

#[test]
fn which_def_ls() -> Result {
    test()
        .run("def ls [] {echo def}; which ls | get type.0")
        .expect_value_eq("custom")
}

#[test]
fn correct_precedence_alias_def_custom() -> Result {
    // aliases shadow custom commands; `which` only reports the winning
    // declaration. there is no way to invoke the underlying custom command
    // when an alias exists, so returning both entries would be misleading.
    test()
        .run("def ls [] {echo def}; alias ls = echo alias; which ls | get path.0 | str trim")
        .expect_value_eq("nu-tester-0")
}

// `get_aliases_with_name` and `get_custom_commands_with_name` don't return the correct count of
// values
// I suspect this is due to the ScopeFrame getting discarded at '}' and the command is then
// executed in the parent scope
// See: parse_definition, line 2187 for reference.
#[ignore]
#[test]
fn correctly_report_of_shadowed_alias() -> Result {
    let code = "
        alias xaz = echo alias1
        def helper [] {
            alias xaz = echo alias2
            which -a xaz
        }
        helper | get path | str contains alias2
    ";

    test().run(code).expect_value_eq(true)
}

#[test]
fn one_report_of_multiple_defs() -> Result {
    let code = "
        def xaz [] { echo def1 }
        def helper [] {
            def xaz [] { echo def2 }
            which -a xaz
        }
        helper | length
    ";

    test().run(code).expect_value_eq(1)
}

#[test]
fn def_only_seen_once() -> Result {
    let code = "def xaz [] {echo def1}; which -a xaz | length";
    test().run(code).expect_value_eq(1)
}

#[test]
fn do_not_show_hidden_aliases() -> Result {
    let code = "
        alias foo = echo foo
        hide foo
        which foo | length
    ";

    test().run(code).expect_value_eq(0)
}

#[test]
fn do_not_show_hidden_commands() -> Result {
    let code = "
        def foo [] { echo foo }
        hide foo
        which foo | length
    ";

    test().run(code).expect_value_eq(0)
}

#[test]
fn which_accepts_spread_list() -> Result {
    let code = "
        let apps = [ls];
        $apps | which ...$in | get command.0
    ";

    test().run(code).expect_value_eq("ls")
}

#[test]
fn which_dedup_is_less_than_all() -> Result {
    let all: i32 = test().run("which -a | length")?;
    let dedup: i32 = test().run("which | length")?;

    assert!(all >= dedup);
    Ok(())
}

#[test]
fn which_custom_command_reports_file() -> Result {
    Playground::setup("which_file_1", |dirs, sandbox| {
        sandbox.with_files(&[FileWithContentToBeTrimmed(
            "foo.nu",
            "
                def foo [] { echo hi }
            ",
        )]);

        let code = "
            source foo.nu
            which foo
        ";

        #[derive(Debug, FromValue)]
        struct Outcome {
            path: String,
        }

        let outcome: (Outcome,) = test().cwd(dirs.test()).run(code)?;
        assert_contains("foo.nu", outcome.0.path);
        Ok(())
    })
}

#[cfg(feature = "plugin")]
#[test]
#[deps(NU_PLUGIN_EXAMPLE)]
fn which_plugin_reports_executable() -> Result {
    // `example` is the root command provided by nu_plugin_example.
    // `which example` should resolve via plugin_identity to the plugin binary path,
    // which contains "nu_plugin_example" in its filename.
    test()
        .run("which example | get 0.path")
        .expect_value_eq(NU_PLUGIN_EXAMPLE.path())
}

#[test]
#[deps(NU)]
fn which_external_command_reports_path() -> Result {
    #[derive(Debug, FromValue)]
    struct Outcome {
        path: String,
    }

    // `nu` itself should be on PATH; PATH-found binaries report a non-empty path.
    let outcome: (Outcome,) = test().run("which nu")?;
    // The path value should be non-empty (not just an empty string)
    assert!(!outcome.0.path.is_empty());

    Ok(())
}

// Regression test for #18419: on Windows, `which` must honor in-shell changes to
// `$env.PATHEXT`, the same way it already honors changes to `$env.PATH`. `.qqq`
// is not a normal executable extension, so `foo.qqq` is only found when the
// shell's `$env.PATHEXT` includes `.QQQ`.
#[cfg(windows)]
#[test]
fn which_respects_pathext_from_env() -> Result {
    Playground::setup("which_pathext", |dirs, sandbox| {
        sandbox.with_files(&[FileWithContentToBeTrimmed("foo.qqq", "")]);
        let dir = dirs.test().display().to_string();

        let found: i32 = test().cwd(dirs.test()).run(format!(
            "with-env {{ Path: '{dir}', PATHEXT: '.QQQ' }} {{ which foo | length }}"
        ))?;
        assert_eq!(
            found, 1,
            "which should find foo.qqq when $env.PATHEXT has .QQQ"
        );

        let not_found: i32 = test().cwd(dirs.test()).run(format!(
            "with-env {{ Path: '{dir}', PATHEXT: '.EXE' }} {{ which foo | length }}"
        ))?;
        assert_eq!(
            not_found, 0,
            "which should not find foo.qqq when $env.PATHEXT lacks .QQQ"
        );

        Ok(())
    })
}

// A `PATHEXT` removed with `hide-env` must stay hidden: `which` must not fall
// back to the process `PATHEXT`, which would resurrect the hidden value (the
// process `PATHEXT` includes `.CMD` by default, so `foo.cmd` would be found
// again).
#[cfg(windows)]
#[test]
fn which_respects_hidden_pathext() -> Result {
    Playground::setup("which_pathext_hidden", |dirs, sandbox| {
        sandbox.with_files(&[FileWithContentToBeTrimmed("foo.cmd", "")]);
        let dir = dirs.test().display().to_string();

        let found: i32 = test().cwd(dirs.test()).run(format!(
            "with-env {{ Path: '{dir}', PATHEXT: '.CMD' }} {{ which foo | length }}"
        ))?;
        assert_eq!(
            found, 1,
            "which should find foo.cmd when $env.PATHEXT has .CMD"
        );

        let hidden: i32 = test().cwd(dirs.test()).run(format!(
            "with-env {{ Path: '{dir}', PATHEXT: '.CMD' }} {{ hide-env PATHEXT; which foo | length }}"
        ))?;
        assert_eq!(
            hidden, 0,
            "which should not resurrect a hidden $env.PATHEXT from the process environment"
        );

        Ok(())
    })
}