Add extern for nu command (#16119)

# Description
Adds an `extern` definition (`KnownExternal`) for the `nu` command. This
way you can do `help nu` and get tab completions for flags on the `nu`
executable

# User-Facing Changes
* You can now view the flags for Nushell itself with `help nu`, and tab
completion for flags on `nu` works
This commit is contained in:
132ikl
2025-07-14 19:26:40 -04:00
committed by GitHub
parent 202d3b2d11
commit 316d2d6af2
12 changed files with 143 additions and 88 deletions

View File

@ -22,7 +22,7 @@ fn basic_string_fails() {
#[test] #[test]
fn short_stream_binary() { fn short_stream_binary() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 5 | bytes ends-with 0x[010101] ^nu --testbin repeater (0x[01]) 5 | bytes ends-with 0x[010101]
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -31,7 +31,7 @@ fn short_stream_binary() {
#[test] #[test]
fn short_stream_mismatch() { fn short_stream_mismatch() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[010203]) 5 | bytes ends-with 0x[010204] ^nu --testbin repeater (0x[010203]) 5 | bytes ends-with 0x[010204]
"#); "#);
assert_eq!(actual.out, "false"); assert_eq!(actual.out, "false");
@ -40,7 +40,7 @@ fn short_stream_mismatch() {
#[test] #[test]
fn short_stream_binary_overflow() { fn short_stream_binary_overflow() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 5 | bytes ends-with 0x[010101010101] ^nu --testbin repeater (0x[01]) 5 | bytes ends-with 0x[010101010101]
"#); "#);
assert_eq!(actual.out, "false"); assert_eq!(actual.out, "false");
@ -49,7 +49,7 @@ fn short_stream_binary_overflow() {
#[test] #[test]
fn long_stream_binary() { fn long_stream_binary() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 32768 | bytes ends-with 0x[010101] ^nu --testbin repeater (0x[01]) 32768 | bytes ends-with 0x[010101]
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -59,7 +59,7 @@ fn long_stream_binary() {
fn long_stream_binary_overflow() { fn long_stream_binary_overflow() {
// .. ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow // .. ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 32768 | bytes ends-with (0..32768 | each {|| 0x[01] } | bytes collect) ^nu --testbin repeater (0x[01]) 32768 | bytes ends-with (0..32768 | each {|| 0x[01] } | bytes collect)
"#); "#);
assert_eq!(actual.out, "false"); assert_eq!(actual.out, "false");
@ -69,7 +69,7 @@ fn long_stream_binary_overflow() {
fn long_stream_binary_exact() { fn long_stream_binary_exact() {
// ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01020304]) 8192 | bytes ends-with (0..<8192 | each {|| 0x[01020304] } | bytes collect) ^nu --testbin repeater (0x[01020304]) 8192 | bytes ends-with (0..<8192 | each {|| 0x[01020304] } | bytes collect)
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -79,7 +79,7 @@ fn long_stream_binary_exact() {
fn long_stream_string_exact() { fn long_stream_string_exact() {
// ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater hell 8192 | bytes ends-with (0..<8192 | each {|| "hell" | into binary } | bytes collect) ^nu --testbin repeater hell 8192 | bytes ends-with (0..<8192 | each {|| "hell" | into binary } | bytes collect)
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -92,7 +92,7 @@ fn long_stream_mixed_exact() {
let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect)
let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect)
nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes ends-with (bytes build $binseg $strseg) ^nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes ends-with (bytes build $binseg $strseg)
"#); "#);
assert_eq!( assert_eq!(
@ -109,7 +109,7 @@ fn long_stream_mixed_overflow() {
let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect)
let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect)
nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes ends-with (bytes build 0x[01] $binseg $strseg) ^nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes ends-with (bytes build 0x[01] $binseg $strseg)
"#); "#);
assert_eq!( assert_eq!(

View File

@ -22,7 +22,7 @@ fn basic_string_fails() {
#[test] #[test]
fn short_stream_binary() { fn short_stream_binary() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 5 | bytes starts-with 0x[010101] ^nu --testbin repeater (0x[01]) 5 | bytes starts-with 0x[010101]
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -31,7 +31,7 @@ fn short_stream_binary() {
#[test] #[test]
fn short_stream_mismatch() { fn short_stream_mismatch() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[010203]) 5 | bytes starts-with 0x[010204] ^nu --testbin repeater (0x[010203]) 5 | bytes starts-with 0x[010204]
"#); "#);
assert_eq!(actual.out, "false"); assert_eq!(actual.out, "false");
@ -40,7 +40,7 @@ fn short_stream_mismatch() {
#[test] #[test]
fn short_stream_binary_overflow() { fn short_stream_binary_overflow() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 5 | bytes starts-with 0x[010101010101] ^nu --testbin repeater (0x[01]) 5 | bytes starts-with 0x[010101010101]
"#); "#);
assert_eq!(actual.out, "false"); assert_eq!(actual.out, "false");
@ -49,7 +49,7 @@ fn short_stream_binary_overflow() {
#[test] #[test]
fn long_stream_binary() { fn long_stream_binary() {
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 32768 | bytes starts-with 0x[010101] ^nu --testbin repeater (0x[01]) 32768 | bytes starts-with 0x[010101]
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -59,7 +59,7 @@ fn long_stream_binary() {
fn long_stream_binary_overflow() { fn long_stream_binary_overflow() {
// .. ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow // .. ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01]) 32768 | bytes starts-with (0..32768 | each {|| 0x[01] } | bytes collect) ^nu --testbin repeater (0x[01]) 32768 | bytes starts-with (0..32768 | each {|| 0x[01] } | bytes collect)
"#); "#);
assert_eq!(actual.out, "false"); assert_eq!(actual.out, "false");
@ -69,7 +69,7 @@ fn long_stream_binary_overflow() {
fn long_stream_binary_exact() { fn long_stream_binary_exact() {
// ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater (0x[01020304]) 8192 | bytes starts-with (0..<8192 | each {|| 0x[01020304] } | bytes collect) ^nu --testbin repeater (0x[01020304]) 8192 | bytes starts-with (0..<8192 | each {|| 0x[01020304] } | bytes collect)
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -79,7 +79,7 @@ fn long_stream_binary_exact() {
fn long_stream_string_exact() { fn long_stream_string_exact() {
// ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow // ranges are inclusive..inclusive, so we don't need to +1 to check for an overflow
let actual = nu!(r#" let actual = nu!(r#"
nu --testbin repeater hell 8192 | bytes starts-with (0..<8192 | each {|| "hell" | into binary } | bytes collect) ^nu --testbin repeater hell 8192 | bytes starts-with (0..<8192 | each {|| "hell" | into binary } | bytes collect)
"#); "#);
assert_eq!(actual.out, "true"); assert_eq!(actual.out, "true");
@ -92,7 +92,7 @@ fn long_stream_mixed_exact() {
let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect)
let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect)
nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes starts-with (bytes build $binseg $strseg) ^nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes starts-with (bytes build $binseg $strseg)
"#); "#);
assert_eq!( assert_eq!(
@ -109,7 +109,7 @@ fn long_stream_mixed_overflow() {
let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect) let binseg = (0..<2048 | each {|| 0x[003d9fbf] } | bytes collect)
let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect) let strseg = (0..<2048 | each {|| "hell" | into binary } | bytes collect)
nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes starts-with (bytes build $binseg $strseg 0x[01]) ^nu --testbin repeat_bytes 003d9fbf 2048 68656c6c 2048 | bytes starts-with (bytes build $binseg $strseg 0x[01])
"#); "#);
assert_eq!( assert_eq!(

View File

@ -4,6 +4,14 @@ use nu_test_support::playground::Playground;
use rstest::rstest; use rstest::rstest;
use rstest_reuse::*; use rstest_reuse::*;
// Get full path to nu, so we don't use the nu extern
fn nu_path(prefix: &str) -> String {
let binary = nu_test_support::fs::executable_path()
.to_string_lossy()
.to_string();
format!("{prefix}{}", binary)
}
// Template for run-external test to ensure tests work when calling // Template for run-external test to ensure tests work when calling
// the binary directly, using the caret operator, and when using // the binary directly, using the caret operator, and when using
// the run-external command // the run-external command
@ -18,8 +26,8 @@ fn run_external_prefixes(#[case] prefix: &str) {}
fn better_empty_redirection(prefix: &str) { fn better_empty_redirection(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: "tests/fixtures/formats", cwd: "tests/fixtures/formats",
"ls | each {{ |it| {}nu `--testbin` cococo $it.name }} | ignore", "ls | each {{ |it| {} `--testbin` cococo $it.name }} | ignore",
prefix nu_path(prefix)
); );
eprintln!("out: {}", actual.out); eprintln!("out: {}", actual.out);
@ -39,9 +47,9 @@ fn explicit_glob(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo ('*.txt' | into glob) {} `--testbin` cococo ('*.txt' | into glob)
"#, "#,
prefix nu_path(prefix)
); );
assert!(actual.out.contains("D&D_volume_1.txt")); assert!(actual.out.contains("D&D_volume_1.txt"));
@ -61,9 +69,9 @@ fn bare_word_expand_path_glob(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
" "
{}nu `--testbin` cococo *.txt {} `--testbin` cococo *.txt
", ",
prefix nu_path(prefix)
); );
assert!(actual.out.contains("D&D_volume_1.txt")); assert!(actual.out.contains("D&D_volume_1.txt"));
@ -83,9 +91,9 @@ fn backtick_expand_path_glob(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo `*.txt` {} `--testbin` cococo `*.txt`
"#, "#,
prefix nu_path(prefix)
); );
assert!(actual.out.contains("D&D_volume_1.txt")); assert!(actual.out.contains("D&D_volume_1.txt"));
@ -105,9 +113,9 @@ fn single_quote_does_not_expand_path_glob(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo '*.txt' {} `--testbin` cococo '*.txt'
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, "*.txt"); assert_eq!(actual.out, "*.txt");
@ -126,9 +134,9 @@ fn double_quote_does_not_expand_path_glob(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo "*.txt" {} `--testbin` cococo "*.txt"
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, "*.txt"); assert_eq!(actual.out, "*.txt");
@ -141,9 +149,9 @@ fn failed_command_with_semicolon_will_not_execute_following_cmds(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
" "
{}nu `--testbin` fail; echo done {} `--testbin` fail; echo done
", ",
prefix nu_path(prefix)
); );
assert!(!actual.out.contains("done")); assert!(!actual.out.contains("done"));
@ -156,9 +164,9 @@ fn external_args_with_quoted(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo "foo=bar 'hi'" {} `--testbin` cococo "foo=bar 'hi'"
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, "foo=bar 'hi'"); assert_eq!(actual.out, "foo=bar 'hi'");
@ -198,9 +206,9 @@ fn external_arg_with_non_option_like_embedded_quotes(#[case] prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo foo='bar' 'foo'=bar {} `--testbin` cococo foo='bar' 'foo'=bar
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, "foo=bar foo=bar"); assert_eq!(actual.out, "foo=bar foo=bar");
@ -217,9 +225,9 @@ fn external_arg_with_string_interpolation(#[case] prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo foo=(2 + 2) $"foo=(2 + 2)" foo=$"(2 + 2)" {} `--testbin` cococo foo=(2 + 2) $"foo=(2 + 2)" foo=$"(2 + 2)"
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, "foo=4 foo=4 foo=4"); assert_eq!(actual.out, "foo=4 foo=4 foo=4");
@ -233,9 +241,9 @@ fn external_arg_with_variable_name(prefix: &str) {
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
let dump_command = "PGPASSWORD='db_secret' pg_dump -Fc -h 'db.host' -p '$db.port' -U postgres -d 'db_name' > '/tmp/dump_name'"; let dump_command = "PGPASSWORD='db_secret' pg_dump -Fc -h 'db.host' -p '$db.port' -U postgres -d 'db_name' > '/tmp/dump_name'";
{}nu `--testbin` nonu $dump_command {} `--testbin` nonu $dump_command
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!( assert_eq!(
@ -251,9 +259,9 @@ fn external_command_escape_args(prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo "\"abcd" {} `--testbin` cococo "\"abcd"
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, r#""abcd"#); assert_eq!(actual.out, r#""abcd"#);
@ -264,9 +272,9 @@ fn external_command_escape_args(prefix: &str) {
fn external_command_ndots_args(prefix: &str) { fn external_command_ndots_args(prefix: &str) {
let actual = nu!( let actual = nu!(
r#" r#"
{}nu `--testbin` cococo foo/. foo/.. foo/... foo/./bar foo/../bar foo/.../bar ./bar ../bar .../bar {} `--testbin` cococo foo/. foo/.. foo/... foo/./bar foo/../bar foo/.../bar ./bar ../bar .../bar
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!( assert_eq!(
@ -286,9 +294,9 @@ fn external_command_ndots_leading_dot_slash(prefix: &str) {
// Don't expand ndots with a leading `./` // Don't expand ndots with a leading `./`
let actual = nu!( let actual = nu!(
r#" r#"
{}nu `--testbin` cococo ./... ./.... {} `--testbin` cococo ./... ./....
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, "./... ./...."); assert_eq!(actual.out, "./... ./....");
@ -300,9 +308,9 @@ fn external_command_url_args(prefix: &str) {
// here // here
let actual = nu!( let actual = nu!(
r#" r#"
{}nu `--testbin` cococo http://example.com http://example.com/.../foo //foo {} `--testbin` cococo http://example.com http://example.com/.../foo //foo
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!( assert_eq!(
@ -359,9 +367,9 @@ fn external_arg_expand_tilde(#[case] prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
{}nu `--testbin` cococo ~/foo ~/(2 + 2) {} `--testbin` cococo ~/foo ~/(2 + 2)
"#, "#,
prefix nu_path(prefix)
); );
let home = dirs::home_dir().expect("failed to find home dir"); let home = dirs::home_dir().expect("failed to find home dir");
@ -382,7 +390,7 @@ fn external_command_not_expand_tilde_with_quotes(prefix: &str) {
Playground::setup( Playground::setup(
"external command not expand tilde with quotes", "external command not expand tilde with quotes",
|dirs, _| { |dirs, _| {
let actual = nu!(cwd: dirs.test(), r#"{}nu `--testbin` nonu "~""#, prefix); let actual = nu!(cwd: dirs.test(), r#"{} `--testbin` nonu "~""#, nu_path(prefix));
assert_eq!(actual.out, r#"~"#); assert_eq!(actual.out, r#"~"#);
}, },
) )
@ -393,7 +401,7 @@ fn external_command_expand_tilde_with_back_quotes(prefix: &str) {
Playground::setup( Playground::setup(
"external command not expand tilde with quotes", "external command not expand tilde with quotes",
|dirs, _| { |dirs, _| {
let actual = nu!(cwd: dirs.test(), r#"{}nu `--testbin` nonu `~`"#, prefix); let actual = nu!(cwd: dirs.test(), r#"{} `--testbin` nonu `~`"#, nu_path(prefix));
assert!(!actual.out.contains('~')); assert!(!actual.out.contains('~'));
}, },
) )
@ -404,8 +412,8 @@ fn external_command_receives_raw_binary_data(prefix: &str) {
Playground::setup("external command receives raw binary data", |dirs, _| { Playground::setup("external command receives raw binary data", |dirs, _| {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
"0x[deadbeef] | {}nu `--testbin` input_bytes_length", "0x[deadbeef] | {} `--testbin` input_bytes_length",
prefix nu_path(prefix)
); );
assert_eq!(actual.out, r#"4"#); assert_eq!(actual.out, r#"4"#);
}) })
@ -494,9 +502,9 @@ fn quotes_trimmed_when_shelling_out(prefix: &str) {
// regression test for a bug where we weren't trimming quotes around string args before shelling out to cmd.exe // regression test for a bug where we weren't trimming quotes around string args before shelling out to cmd.exe
let actual = nu!( let actual = nu!(
r#" r#"
{}nu `--testbin` cococo "foo" {} `--testbin` cococo "foo"
"#, "#,
prefix nu_path(prefix)
); );
assert_eq!(actual.out, "foo"); assert_eq!(actual.out, "foo");
@ -565,8 +573,9 @@ fn expand_command_if_list(#[case] prefix: &str) {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), cwd: dirs.test(),
r#" r#"
let cmd = [nu `--testbin`]; {}$cmd meow foo.txt let cmd = ['{}' `--testbin`]; {}$cmd meow foo.txt
"#, "#,
nu_path(""),
prefix prefix
); );

View File

@ -19,7 +19,8 @@ pub(crate) fn compile_call(
let decl = working_set.get_decl(call.decl_id); let decl = working_set.get_decl(call.decl_id);
// Check if this call has --help - if so, just redirect to `help` // Check if this call has --help - if so, just redirect to `help`
if call.named_iter().any(|(name, _, _)| name.item == "help") { // Special case the `nu` extern: we want to forward --help to the script
if call.named_iter().any(|(name, _, _)| name.item == "help") && decl.name() != "nu" {
let name = working_set let name = working_set
.find_decl_name(call.decl_id) // check for name in scope .find_decl_name(call.decl_id) // check for name in scope
.and_then(|name| std::str::from_utf8(name).ok()) .and_then(|name| std::str::from_utf8(name).ok())

View File

@ -715,6 +715,14 @@ impl CustomExample {
result: self.result.clone(), result: self.result.clone(),
} }
} }
pub fn from_example(example: Example<'_>) -> Self {
Self {
example: example.example.to_string(),
description: example.description.to_string(),
result: example.result,
}
}
} }
#[derive(Clone)] #[derive(Clone)]

View File

@ -1,9 +1,10 @@
use nu_engine::{command_prelude::*, get_full_help}; use nu_engine::{command_prelude::*, get_full_help};
use nu_parser::{escape_for_script_arg, parse}; use nu_parser::{KnownExternal, escape_for_script_arg, parse};
use nu_protocol::{ use nu_protocol::{
CustomExample,
ast::{Expr, Expression}, ast::{Expr, Expression},
engine::StateWorkingSet, engine::StateWorkingSet,
report_parse_error, report_parse_error, report_shell_error,
}; };
use nu_utils::{escape_quote_string, stdout_write_all_and_flush}; use nu_utils::{escape_quote_string, stdout_write_all_and_flush};
@ -518,3 +519,24 @@ impl Command for Nu {
] ]
} }
} }
/// Add the [`Nu`] command as an extern so we get help and flag completions
pub(crate) fn add_nu_extern(engine_state: &mut EngineState) {
let nu_extern = KnownExternal {
signature: Box::new(Nu.signature()),
attributes: vec![],
examples: Nu
.examples()
.into_iter()
.map(CustomExample::from_example)
.collect(),
};
let mut working_set = nu_protocol::engine::StateWorkingSet::new(engine_state);
working_set.add_decl(Box::new(nu_extern));
let delta = working_set.render();
if let Err(err) = engine_state.merge_delta(delta) {
report_shell_error(engine_state, &err);
}
}

View File

@ -15,7 +15,7 @@ use crate::{
config_files::set_config_path, config_files::set_config_path,
logger::{configure, logger}, logger::{configure, logger},
}; };
use command::gather_commandline_args; use command::{add_nu_extern, gather_commandline_args};
use log::{Level, trace}; use log::{Level, trace};
use miette::Result; use miette::Result;
use nu_cli::gather_parent_env_vars; use nu_cli::gather_parent_env_vars;
@ -79,6 +79,7 @@ fn main() -> Result<()> {
let mut working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state); let mut working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(nu_cli::NuHighlight)); working_set.add_decl(Box::new(nu_cli::NuHighlight));
working_set.add_decl(Box::new(nu_cli::Print)); working_set.add_decl(Box::new(nu_cli::Print));
working_set.render() working_set.render()
}; };
@ -202,6 +203,9 @@ fn main() -> Result<()> {
std::process::exit(1) std::process::exit(1)
}); });
// this has to happen after `parse_command_args` since that hides the `Nu` decl
add_nu_extern(&mut engine_state);
experimental_options::load(&engine_state, &parsed_nu_cli_args, !script_name.is_empty()); experimental_options::load(&engine_state, &parsed_nu_cli_args, !script_name.is_empty());
// keep this condition in sync with the branches at the end // keep this condition in sync with the branches at the end
@ -383,9 +387,6 @@ fn main() -> Result<()> {
"chop" => test_bins::chop(), "chop" => test_bins::chop(),
"repeater" => test_bins::repeater(), "repeater" => test_bins::repeater(),
"repeat_bytes" => test_bins::repeat_bytes(), "repeat_bytes" => test_bins::repeat_bytes(),
// Important: nu_repl must be called with `--testbin=nu_repl`
// `--testbin nu_repl` will not work due to argument count logic
// in test_bins.rs
"nu_repl" => test_bins::nu_repl(), "nu_repl" => test_bins::nu_repl(),
"input_bytes_length" => test_bins::input_bytes_length(), "input_bytes_length" => test_bins::input_bytes_length(),
_ => std::process::exit(1), _ => std::process::exit(1),

View File

@ -45,7 +45,6 @@ fn echo_one_env(arg: &str, to_stdout: bool) {
/// If it's not present, panic instead /// If it's not present, panic instead
pub fn echo_env_mixed() { pub fn echo_env_mixed() {
let args = args(); let args = args();
let args = &args[1..];
if args.len() != 3 { if args.len() != 3 {
panic!( panic!(
@ -75,11 +74,11 @@ pub fn echo_env_mixed() {
pub fn cococo() { pub fn cococo() {
let args: Vec<String> = args(); let args: Vec<String> = args();
if args.len() > 1 { if !args.is_empty() {
// Write back out all the arguments passed // Write back out all the arguments passed
// if given at least 1 instead of chickens // if given at least 1 instead of chickens
// speaking co co co. // speaking co co co.
println!("{}", &args[1..].join(" ")); println!("{}", args.join(" "));
} else { } else {
println!("cococo"); println!("cococo");
} }
@ -89,7 +88,7 @@ pub fn cococo() {
pub fn meow() { pub fn meow() {
let args: Vec<String> = args(); let args: Vec<String> = args();
for arg in args.iter().skip(1) { for arg in args.iter() {
let contents = std::fs::read_to_string(arg).expect("Expected a filepath"); let contents = std::fs::read_to_string(arg).expect("Expected a filepath");
println!("{contents}"); println!("{contents}");
} }
@ -102,7 +101,7 @@ pub fn meowb() {
let stdout = io::stdout(); let stdout = io::stdout();
let mut handle = stdout.lock(); let mut handle = stdout.lock();
for arg in args.iter().skip(1) { for arg in args.iter() {
let buf = std::fs::read(arg).expect("Expected a filepath"); let buf = std::fs::read(arg).expect("Expected a filepath");
handle.write_all(&buf).expect("failed to write to stdout"); handle.write_all(&buf).expect("failed to write to stdout");
} }
@ -118,7 +117,7 @@ pub fn relay() {
/// nu --testbin nonu a b c /// nu --testbin nonu a b c
/// abc /// abc
pub fn nonu() { pub fn nonu() {
args().iter().skip(1).for_each(|arg| print!("{arg}")); args().iter().for_each(|arg| print!("{arg}"));
} }
/// Repeat a string or char N times /// Repeat a string or char N times
@ -128,8 +127,7 @@ pub fn nonu() {
/// testtesttesttesttest /// testtesttesttesttest
pub fn repeater() { pub fn repeater() {
let mut stdout = io::stdout(); let mut stdout = io::stdout();
let args = args(); let mut args = args().into_iter();
let mut args = args.iter().skip(1);
let letter = args.next().expect("needs a character to iterate"); let letter = args.next().expect("needs a character to iterate");
let count = args.next().expect("need the number of times to iterate"); let count = args.next().expect("need the number of times to iterate");
@ -144,8 +142,7 @@ pub fn repeater() {
/// A version of repeater that can output binary data, even null bytes /// A version of repeater that can output binary data, even null bytes
pub fn repeat_bytes() { pub fn repeat_bytes() {
let mut stdout = io::stdout(); let mut stdout = io::stdout();
let args = args(); let mut args = args().into_iter();
let mut args = args.iter().skip(1);
while let (Some(binary), Some(count)) = (args.next(), args.next()) { while let (Some(binary), Some(count)) = (args.next(), args.next()) {
let bytes: Vec<u8> = (0..binary.len()) let bytes: Vec<u8> = (0..binary.len())
@ -173,7 +170,6 @@ pub fn iecho() {
let mut stdout = io::stdout(); let mut stdout = io::stdout();
let _ = args() let _ = args()
.iter() .iter()
.skip(1)
.cycle() .cycle()
.try_for_each(|v| writeln!(stdout, "{v}")); .try_for_each(|v| writeln!(stdout, "{v}"));
} }
@ -364,6 +360,18 @@ pub fn input_bytes_length() {
} }
fn args() -> Vec<String> { fn args() -> Vec<String> {
// skip (--testbin bin_name args) // skip `nu` path (first argument)
std::env::args().skip(2).collect() // then skip --testbin=foo OR --testbin foo
std::env::args().skip(1).fold(vec![], |mut acc, it| {
// if we have --testbin foo, skip the previous argument (--testbin) and this argument (foo)
if acc.last().is_some_and(|last| last == "--testbin") {
acc.pop();
return acc;
}
// if we have --testbin=foo, skip it
if !it.starts_with("--testbin=") {
acc.push(it);
}
acc
})
} }

View File

@ -173,3 +173,9 @@ fn known_external_arg_internally_quoted_options() -> TestResult {
"--option=test", "--option=test",
) )
} }
// Verify that the KnownExternal for the `nu` binary exists
#[test]
fn known_external_nu() -> TestResult {
run_test_contains("help nu", "Usage")
}

View File

@ -422,27 +422,27 @@ fn append_assign_takes_pipeline() -> TestResult {
#[test] #[test]
fn assign_bare_external_fails() { fn assign_bare_external_fails() {
let result = nu!("$env.FOO = nu --testbin cococo"); let result = nu!("$env.FOO = cargo --version");
assert!(!result.status.success()); assert!(!result.status.success());
assert!(result.err.contains("must be explicit")); assert!(result.err.contains("must be explicit"));
} }
#[test] #[test]
fn assign_bare_external_with_caret() { fn assign_bare_external_with_caret() {
let result = nu!("$env.FOO = ^nu --testbin cococo"); let result = nu!("$env.FOO = ^cargo --version");
assert!(result.status.success()); assert!(result.status.success());
} }
#[test] #[test]
fn assign_backtick_quoted_external_fails() { fn assign_backtick_quoted_external_fails() {
let result = nu!("$env.FOO = `nu` --testbin cococo"); let result = nu!("$env.FOO = `cargo` --version");
assert!(!result.status.success()); assert!(!result.status.success());
assert!(result.err.contains("must be explicit")); assert!(result.err.contains("must be explicit"));
} }
#[test] #[test]
fn assign_backtick_quoted_external_with_caret() { fn assign_backtick_quoted_external_with_caret() {
let result = nu!("$env.FOO = ^`nu` --testbin cococo"); let result = nu!("$env.FOO = ^`cargo` --version");
assert!(result.status.success()); assert!(result.status.success());
} }

View File

@ -290,7 +290,7 @@ fn scope_externs_sorted() {
]; ];
let actual = nu!(&inp.join("; ")); let actual = nu!(&inp.join("; "));
assert_eq!(actual.out, "abc"); assert!(actual.out.starts_with("abc"));
} }
#[test] #[test]

View File

@ -609,15 +609,15 @@ mod external_command_arguments {
#[test] #[test]
fn remove_quotes_in_shell_arguments() { fn remove_quotes_in_shell_arguments() {
let actual = nu!("nu --testbin cococo expression='-r -w'"); let actual = nu!("^nu --testbin cococo expression='-r -w'");
assert_eq!(actual.out, "expression=-r -w"); assert_eq!(actual.out, "expression=-r -w");
let actual = nu!(r#"nu --testbin cococo expression="-r -w""#); let actual = nu!(r#"^nu --testbin cococo expression="-r -w""#);
assert_eq!(actual.out, "expression=-r -w"); assert_eq!(actual.out, "expression=-r -w");
let actual = nu!("nu --testbin cococo expression='-r -w'"); let actual = nu!("^nu --testbin cococo expression='-r -w'");
assert_eq!(actual.out, "expression=-r -w"); assert_eq!(actual.out, "expression=-r -w");
let actual = nu!(r#"nu --testbin cococo expression="-r\" -w""#); let actual = nu!(r#"^nu --testbin cococo expression="-r\" -w""#);
assert_eq!(actual.out, r#"expression=-r" -w"#); assert_eq!(actual.out, r#"expression=-r" -w"#);
let actual = nu!(r#"nu --testbin cococo expression='-r\" -w'"#); let actual = nu!(r#"^nu --testbin cococo expression='-r\" -w'"#);
assert_eq!(actual.out, r#"expression=-r\" -w"#); assert_eq!(actual.out, r#"expression=-r\" -w"#);
} }
} }
@ -722,7 +722,7 @@ fn external_error_with_backtrace() {
#[test] #[test]
fn sub_external_expression_with_and_op_should_raise_proper_error() { fn sub_external_expression_with_and_op_should_raise_proper_error() {
let actual = nu!("(nu --testbin cococo false) and true"); let actual = nu!("(^nu --testbin cococo false) and true");
assert!( assert!(
actual actual
.err .err