diff --git a/Cargo.lock b/Cargo.lock index 86241fecb3..38e7f56be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3541,6 +3541,7 @@ dependencies = [ "nu-cmd-plugin", "nu-command", "nu-engine", + "nu-experimental", "nu-explore", "nu-lsp", "nu-parser", @@ -3658,6 +3659,7 @@ dependencies = [ "miette", "nu-cmd-base", "nu-engine", + "nu-experimental", "nu-parser", "nu-protocol", "nu-utils", @@ -3736,6 +3738,7 @@ dependencies = [ "nu-cmd-lang", "nu-color-config", "nu-engine", + "nu-experimental", "nu-glob", "nu-json", "nu-parser", @@ -3829,6 +3832,14 @@ dependencies = [ "nu-utils", ] +[[package]] +name = "nu-experimental" +version = "0.105.2" +dependencies = [ + "itertools 0.14.0", + "thiserror 2.0.12", +] + [[package]] name = "nu-explore" version = "0.105.2" diff --git a/Cargo.toml b/Cargo.toml index 3c3f373912..95d2487679 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,36 +24,37 @@ pkg-fmt = "zip" [workspace] members = [ + "crates/nu_plugin_custom_values", + "crates/nu_plugin_example", + "crates/nu_plugin_formats", + "crates/nu_plugin_gstat", + "crates/nu_plugin_inc", + "crates/nu_plugin_polars", + "crates/nu_plugin_query", + "crates/nu_plugin_stress_internals", "crates/nu-cli", - "crates/nu-engine", - "crates/nu-parser", - "crates/nu-system", "crates/nu-cmd-base", "crates/nu-cmd-extra", "crates/nu-cmd-lang", "crates/nu-cmd-plugin", - "crates/nu-command", "crates/nu-color-config", + "crates/nu-command", + "crates/nu-derive-value", + "crates/nu-engine", + "crates/nu-experimental", "crates/nu-explore", "crates/nu-json", "crates/nu-lsp", - "crates/nu-pretty-hex", - "crates/nu-protocol", - "crates/nu-derive-value", - "crates/nu-plugin", + "crates/nu-parser", "crates/nu-plugin-core", "crates/nu-plugin-engine", "crates/nu-plugin-protocol", "crates/nu-plugin-test-support", - "crates/nu_plugin_inc", - "crates/nu_plugin_gstat", - "crates/nu_plugin_example", - "crates/nu_plugin_query", - "crates/nu_plugin_custom_values", - "crates/nu_plugin_formats", - "crates/nu_plugin_polars", - "crates/nu_plugin_stress_internals", + "crates/nu-plugin", + "crates/nu-pretty-hex", + "crates/nu-protocol", "crates/nu-std", + "crates/nu-system", "crates/nu-table", "crates/nu-term-grid", "crates/nu-test-support", @@ -163,6 +164,7 @@ syn = "2.0" sysinfo = "0.33" tabled = { version = "0.20", default-features = false } tempfile = "3.20" +thiserror = "2.0.12" titlecase = "3.6" toml = "0.8" trash = "5.2" @@ -203,11 +205,12 @@ workspace = true [dependencies] nu-cli = { path = "./crates/nu-cli", version = "0.105.2" } nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.105.2" } +nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.105.2" } nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.105.2" } nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.105.2", optional = true } -nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.105.2" } nu-command = { path = "./crates/nu-command", version = "0.105.2", default-features = false, features = ["os"] } nu-engine = { path = "./crates/nu-engine", version = "0.105.2" } +nu-experimental = { path = "./crates/nu-experimental", version = "0.105.2" } nu-explore = { path = "./crates/nu-explore", version = "0.105.2" } nu-lsp = { path = "./crates/nu-lsp/", version = "0.105.2" } nu-parser = { path = "./crates/nu-parser", version = "0.105.2" } diff --git a/crates/nu-cmd-lang/Cargo.toml b/crates/nu-cmd-lang/Cargo.toml index 0f371cce50..c67805efd4 100644 --- a/crates/nu-cmd-lang/Cargo.toml +++ b/crates/nu-cmd-lang/Cargo.toml @@ -16,6 +16,7 @@ workspace = true [dependencies] nu-engine = { path = "../nu-engine", version = "0.105.2", default-features = false } +nu-experimental = { path = "../nu-experimental", version = "0.105.2" } nu-parser = { path = "../nu-parser", version = "0.105.2" } nu-protocol = { path = "../nu-protocol", version = "0.105.2", default-features = false } nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } diff --git a/crates/nu-cmd-lang/src/core_commands/version.rs b/crates/nu-cmd-lang/src/core_commands/version.rs index eee2d344eb..3033a9da00 100644 --- a/crates/nu-cmd-lang/src/core_commands/version.rs +++ b/crates/nu-cmd-lang/src/core_commands/version.rs @@ -188,6 +188,17 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result &str { + "debug experimental-options" + } + + fn signature(&self) -> Signature { + Signature::new(self.name()) + .input_output_type( + Type::Nothing, + Type::Table(Box::from([ + (String::from("identifier"), Type::String), + (String::from("enabled"), Type::Bool), + (String::from("status"), Type::String), + (String::from("description"), Type::String), + ])), + ) + .add_help() + .category(Category::Debug) + } + + fn description(&self) -> &str { + "Show all experimental options." + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::Value( + Value::list( + nu_experimental::ALL + .iter() + .map(|option| { + Value::record( + nu_protocol::record! { + "identifier" => Value::string(option.identifier(), call.head), + "enabled" => Value::bool(option.get(), call.head), + "status" => Value::string(match option.status() { + Status::OptIn => "opt-in", + Status::OptOut => "opt-out", + Status::DeprecatedDiscard => "deprecated-discard", + Status::DeprecatedDefault => "deprecated-default" + }, call.head), + "description" => Value::string(option.description(), call.head), + }, + call.head, + ) + }) + .collect(), + call.head, + ), + None, + )) + } +} diff --git a/crates/nu-command/src/debug/mod.rs b/crates/nu-command/src/debug/mod.rs index 19f1c6da6d..be68c8caec 100644 --- a/crates/nu-command/src/debug/mod.rs +++ b/crates/nu-command/src/debug/mod.rs @@ -1,6 +1,7 @@ mod ast; mod debug_; mod env; +mod experimental_options; mod explain; mod info; mod inspect; @@ -21,6 +22,7 @@ mod view_span; pub use ast::Ast; pub use debug_::Debug; pub use env::DebugEnv; +pub use experimental_options::DebugExperimentalOptions; pub use explain::Explain; pub use info::DebugInfo; pub use inspect::Inspect; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 57e1ed815d..4d7ed8cc69 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -153,6 +153,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { Ast, Debug, DebugEnv, + DebugExperimentalOptions, DebugInfo, DebugProfile, Explain, diff --git a/crates/nu-experimental/Cargo.toml b/crates/nu-experimental/Cargo.toml new file mode 100644 index 0000000000..564e5f6c65 --- /dev/null +++ b/crates/nu-experimental/Cargo.toml @@ -0,0 +1,12 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Nushell experimental options" +edition = "2024" +license = "MIT" +name = "nu-experimental" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-experimental" +version = "0.105.2" + +[dependencies] +itertools.workspace = true +thiserror.workspace = true diff --git a/crates/nu-experimental/src/lib.rs b/crates/nu-experimental/src/lib.rs new file mode 100644 index 0000000000..58d63a4a4c --- /dev/null +++ b/crates/nu-experimental/src/lib.rs @@ -0,0 +1,203 @@ +//! Experimental Options for the Nu codebase. +//! +//! This crate defines all experimental options used in Nushell. +//! +//! An [`ExperimentalOption`] is basically a fancy global boolean. +//! It should be set very early during initialization and lets us switch between old and new +//! behavior for parts of the system. +//! +//! The goal is to have a consistent way to handle experimental flags across the codebase, and to +//! make it easy to find all available options. +//! +//! # Usage +//! +//! Using an option is simple: +//! +//! ```rust +//! if nu_experimental::EXAMPLE.get() { +//! // new behavior +//! } else { +//! // old behavior +//! } +//! ``` +//! +//! # Adding New Options +//! +//! 1. Create a new module in `options.rs`. +//! 2. Define a marker struct and implement `ExperimentalOptionMarker` for it. +//! 3. Add a new static using `ExperimentalOption::new`. +//! 4. Add the static to [`ALL`]. +//! +//! That's it. See [`EXAMPLE`] in `options/example.rs` for a complete example. +//! +//! # For Users +//! +//! Users can view enabled options using either `version` or `debug experimental-options`. +//! +//! To enable or disable options, use either the `NU_EXPERIMENTAL_OPTIONS` environment variable +//! (see [`ENV`]), or pass them via CLI using `--experimental-options`, e.g.: +//! +//! ```sh +//! nu --experimental-options=[example] +//! ``` +//! +//! # For Embedders +//! +//! If you're embedding Nushell, prefer using [`parse_env`] or [`parse_iter`] to load options. +//! +//! `parse_iter` is useful if you want to feed in values from other sources. +//! Since options are expected to stay stable during runtime, make sure to do this early. +//! +//! You can also call [`ExperimentalOption::set`] manually, but be careful with that. + +use crate::util::AtomicMaybe; +use std::{fmt::Debug, sync::atomic::Ordering}; + +mod options; +mod parse; +mod util; + +pub use options::*; +pub use parse::*; + +/// The status of an experimental option. +/// +/// An option can either be disabled by default ([`OptIn`](Self::OptIn)) or enabled by default +/// ([`OptOut`](Self::OptOut)), depending on its expected stability. +/// +/// Experimental options can be deprecated in two ways: +/// - If the feature becomes default behavior, it's marked as +/// [`DeprecatedDefault`](Self::DeprecatedDefault). +/// - If the feature is being fully removed, it's marked as +/// [`DeprecatedDiscard`](Self::DeprecatedDiscard) and triggers a warning. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// Disabled by default. + OptIn, + /// Enabled by default. + OptOut, + /// Deprecated as an experimental option; now default behavior. + DeprecatedDefault, + /// Deprecated; the feature will be removed and triggers a warning. + DeprecatedDiscard, +} + +/// Experimental option (aka feature flag). +/// +/// This struct holds one experimental option that can change some part of Nushell's behavior. +/// These options let users opt in or out of experimental changes while keeping the rest stable. +/// They're useful for testing new ideas and giving users a way to go back to older behavior if needed. +/// +/// You can find all options in the statics of [`nu_experimental`](crate). +/// Everything there, except [`ALL`], is a toggleable option. +/// `ALL` gives a full list and can be used to check which options are set. +/// +/// The [`Debug`] implementation shows the option's identifier, stability, and current value. +/// To also include the description in the output, use the +/// [plus sign](std::fmt::Formatter::sign_plus), e.g. `format!("{OPTION:+#?}")`. +pub struct ExperimentalOption { + value: AtomicMaybe, + marker: &'static (dyn DynExperimentalOptionMarker + Send + Sync), +} + +impl ExperimentalOption { + /// Construct a new `ExperimentalOption`. + /// + /// This should only be used to define a single static for a marker. + pub(crate) const fn new( + marker: &'static (dyn DynExperimentalOptionMarker + Send + Sync), + ) -> Self { + Self { + value: AtomicMaybe::new(None), + marker, + } + } + + pub fn identifier(&self) -> &'static str { + self.marker.identifier() + } + + pub fn description(&self) -> &'static str { + self.marker.description() + } + + pub fn status(&self) -> Status { + self.marker.status() + } + + pub fn get(&self) -> bool { + self.value + .load(Ordering::Relaxed) + .unwrap_or_else(|| match self.marker.status() { + Status::OptIn => false, + Status::OptOut => true, + Status::DeprecatedDiscard => false, + Status::DeprecatedDefault => false, + }) + } + + /// Sets the state of an experimental option. + /// + /// # Safety + /// This method is unsafe to emphasize that experimental options are not designed to change + /// dynamically at runtime. + /// Changing their state at arbitrary points can lead to inconsistent behavior. + /// You should set experimental options only during initialization, before the application fully + /// starts. + pub unsafe fn set(&self, value: bool) { + self.value.store(value, Ordering::Relaxed); + } + + /// Unsets an experimental option, resetting it to an uninitialized state. + /// + /// # Safety + /// Like [`set`](Self::set), this method is unsafe to highlight that experimental options should + /// remain stable during runtime. + /// Only unset options in controlled, initialization contexts to avoid unpredictable behavior. + pub unsafe fn unset(&self) { + self.value.store(None, Ordering::Relaxed); + } +} + +impl Debug for ExperimentalOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let add_description = f.sign_plus(); + let mut debug_struct = f.debug_struct("ExperimentalOption"); + debug_struct.field("identifier", &self.identifier()); + debug_struct.field("value", &self.get()); + debug_struct.field("stability", &self.status()); + if add_description { + debug_struct.field("description", &self.description()); + } + debug_struct.finish() + } +} + +impl PartialEq for ExperimentalOption { + fn eq(&self, other: &Self) -> bool { + // if both underlying atomics point to the same value, we talk about the same option + self.value.as_ptr() == other.value.as_ptr() + } +} + +impl Eq for ExperimentalOption {} + +pub(crate) trait DynExperimentalOptionMarker { + fn identifier(&self) -> &'static str; + fn description(&self) -> &'static str; + fn status(&self) -> Status; +} + +impl DynExperimentalOptionMarker for M { + fn identifier(&self) -> &'static str { + M::IDENTIFIER + } + + fn description(&self) -> &'static str { + M::DESCRIPTION + } + + fn status(&self) -> Status { + M::STATUS + } +} diff --git a/crates/nu-experimental/src/options/example.rs b/crates/nu-experimental/src/options/example.rs new file mode 100644 index 0000000000..51ddadfc35 --- /dev/null +++ b/crates/nu-experimental/src/options/example.rs @@ -0,0 +1,20 @@ +use crate::*; + +/// Example experimental option. +/// +/// This shows how experimental options should be implemented and documented. +/// Reading this static's documentation alone should clearly explain what the +/// option changes and how it interacts with the rest of the codebase. +/// +/// Use this pattern when adding real experimental options. +pub static EXAMPLE: ExperimentalOption = ExperimentalOption::new(&Example); + +// No documentation needed here since this type isn't public. +// The static above provides all necessary details. +struct Example; + +impl ExperimentalOptionMarker for Example { + const IDENTIFIER: &'static str = "example"; + const DESCRIPTION: &'static str = "This is an example of an experimental option."; + const STATUS: Status = Status::DeprecatedDiscard; +} diff --git a/crates/nu-experimental/src/options/mod.rs b/crates/nu-experimental/src/options/mod.rs new file mode 100644 index 0000000000..343629eb51 --- /dev/null +++ b/crates/nu-experimental/src/options/mod.rs @@ -0,0 +1,92 @@ +#![allow( + private_interfaces, + reason = "The marker structs don't need to be exposed, only the static values." +)] + +use crate::*; + +mod example; + +/// Marker trait for defining experimental options. +/// +/// Implement this trait to mark a struct as metadata for an [`ExperimentalOption`]. +/// It provides all necessary information about an experimental feature directly in code, +/// without needing external documentation. +/// +/// The `STATUS` field is especially important as it controls whether the feature is enabled +/// by default and how users should interpret its reliability. +pub(crate) trait ExperimentalOptionMarker { + /// Unique identifier for this experimental option. + /// + /// Must be a valid Rust identifier. + /// Used when parsing to toggle specific experimental options, + /// and may also serve as a user-facing label. + const IDENTIFIER: &'static str; + + /// Brief description explaining what this option changes. + /// + /// Displayed to users in help messages or summaries without needing to visit external docs. + const DESCRIPTION: &'static str; + + /// Indicates the status of an experimental status. + /// + /// Options marked [`Status::OptIn`] are disabled by default while options marked with + /// [`Status::OptOut`] are enabled by default. + /// Experimental options that stabilize should be marked as [`Status::DeprecatedDefault`] while + /// options that will be removed should be [`Status::DeprecatedDiscard`]. + const STATUS: Status; +} + +// Export only the static values. +// The marker structs are not relevant and needlessly clutter the generated docs. +pub use example::EXAMPLE; + +// Include all experimental option statics in here. +// This will test them and add them to the parsing list. + +/// A list of all available experimental options. +/// +/// Use this to show users every experimental option, including their descriptions, +/// identifiers, and current state. +pub static ALL: &[&ExperimentalOption] = &[&EXAMPLE]; + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + + #[test] + fn assert_identifiers_are_unique() { + let list: Vec<_> = ALL.iter().map(|opt| opt.identifier()).collect(); + let set: HashSet<_> = HashSet::from_iter(&list); + assert_eq!(list.len(), set.len()); + } + + #[test] + fn assert_identifiers_are_valid() { + for option in ALL { + let identifier = option.identifier(); + assert!(!identifier.is_empty()); + + let mut chars = identifier.chars(); + let first = chars.next().expect("not empty"); + assert!(first.is_alphabetic()); + assert!(first.is_lowercase()); + + for char in chars { + assert!(char.is_alphanumeric()); + if char.is_alphabetic() { + assert!(char.is_lowercase()); + } + } + } + } + + #[test] + fn assert_description_not_empty() { + for option in ALL { + assert!(!option.description().is_empty()); + } + } +} diff --git a/crates/nu-experimental/src/parse.rs b/crates/nu-experimental/src/parse.rs new file mode 100644 index 0000000000..679135345c --- /dev/null +++ b/crates/nu-experimental/src/parse.rs @@ -0,0 +1,137 @@ +use crate::{ALL, ExperimentalOption, Status}; +use itertools::Itertools; +use std::{borrow::Cow, env, ops::Range, sync::atomic::Ordering}; +use thiserror::Error; + +/// Environment variable used to load experimental options from. +/// +/// May be used like this: `NU_EXPERIMENTAL_OPTIONS=example nu`. +pub const ENV: &str = "NU_EXPERIMENTAL_OPTIONS"; + +/// Warnings that can happen while parsing experimental options. +#[derive(Debug, Clone, Error, Eq, PartialEq)] +pub enum ParseWarning { + /// The given identifier doesn't match any known experimental option. + #[error("Unknown experimental option `{0}`")] + Unknown(String), + + /// The assignment wasn't valid. Only `true` or `false` is accepted. + #[error("Invalid assignment for `{identifier}`, expected `true` or `false`, got `{1}`", identifier = .0.identifier())] + InvalidAssignment(&'static ExperimentalOption, String), + + /// This experimental option is deprecated as this is now the default behavior. + #[error("The experimental option `{identifier}` is deprecated as this is now the default behavior.", identifier = .0.identifier())] + DeprecatedDefault(&'static ExperimentalOption), + + /// This experimental option is deprecated and will be removed in the future. + #[error("The experimental option `{identifier}` is deprecated and will be removed in a future release", identifier = .0.identifier())] + DeprecatedDiscard(&'static ExperimentalOption), +} + +/// Parse and activate experimental options. +/// +/// This is the recommended way to activate options, as it handles [`ParseWarning`]s properly +/// and is easy to hook into. +/// +/// The `iter` argument should yield: +/// - the identifier of the option +/// - an optional assignment value (`true`/`false`) +/// - a context value, which is returned with any warning +/// +/// This way you don't need to manually track which input caused which warning. +pub fn parse_iter<'i, Ctx>( + iter: impl Iterator, Option>, Ctx)>, +) -> Vec<(ParseWarning, Ctx)> { + let mut warnings = Vec::new(); + for (key, val, ctx) in iter { + let Some(option) = ALL.iter().find(|option| option.identifier() == key.trim()) else { + warnings.push((ParseWarning::Unknown(key.to_string()), ctx)); + continue; + }; + + match option.status() { + Status::DeprecatedDiscard => { + warnings.push((ParseWarning::DeprecatedDiscard(option), ctx)); + continue; + } + Status::DeprecatedDefault => { + warnings.push((ParseWarning::DeprecatedDefault(option), ctx)); + continue; + } + _ => {} + } + + let val = match val.as_deref().map(str::trim) { + None => true, + Some("true") => true, + Some("false") => false, + Some(s) => { + warnings.push((ParseWarning::InvalidAssignment(option, s.to_owned()), ctx)); + continue; + } + }; + + option.value.store(val, Ordering::Relaxed); + } + + warnings +} + +/// Parse experimental options from the [`ENV`] environment variable. +/// +/// Uses [`parse_iter`] internally. Each warning includes a `Range` pointing to the +/// part of the environment variable that triggered it. +pub fn parse_env() -> Vec<(ParseWarning, Range)> { + let Ok(env) = env::var(ENV) else { + return vec![]; + }; + + let mut entries = Vec::new(); + let mut start = 0; + for (idx, c) in env.char_indices() { + if c == ',' { + entries.push((&env[start..idx], start..idx)); + start = idx + 1; + } + } + entries.push((&env[start..], start..env.len())); + + parse_iter(entries.into_iter().map(|(entry, span)| { + entry + .split_once("=") + .map(|(key, val)| (key.into(), Some(val.into()), span.clone())) + .unwrap_or((entry.into(), None, span)) + })) +} + +impl ParseWarning { + /// A code to represent the variant. + /// + /// This may be used with crates like [`miette`](https://docs.rs/miette) to provide error codes. + pub fn code(&self) -> &'static str { + match self { + Self::Unknown(_) => "nu::experimental_option::unknown", + Self::InvalidAssignment(_, _) => "nu::experimental_option::invalid_assignment", + Self::DeprecatedDefault(_) => "nu::experimental_option::deprecated_default", + Self::DeprecatedDiscard(_) => "nu::experimental_option::deprecated_discard", + } + } + + /// Provide some help depending on the variant. + /// + /// This may be used with crates like [`miette`](https://docs.rs/miette) to provide a help + /// message. + pub fn help(&self) -> Option { + match self { + Self::Unknown(_) => Some(format!( + "Known experimental options are: {}", + ALL.iter().map(|option| option.identifier()).join(", ") + )), + Self::InvalidAssignment(_, _) => None, + Self::DeprecatedDiscard(_) => None, + Self::DeprecatedDefault(_) => { + Some(String::from("You can safely remove this option now.")) + } + } + } +} diff --git a/crates/nu-experimental/src/util.rs b/crates/nu-experimental/src/util.rs new file mode 100644 index 0000000000..0648347005 --- /dev/null +++ b/crates/nu-experimental/src/util.rs @@ -0,0 +1,44 @@ +use std::sync::atomic::{AtomicU8, Ordering}; + +/// Store an `Option` as an atomic. +/// +/// # Implementation Detail +/// This stores the `Option` via its three states as `u8` representing: +/// - `None` as `0` +/// - `Some(true)` as `1` +/// - `Some(false)` as `2` +pub struct AtomicMaybe(AtomicU8); + +impl AtomicMaybe { + pub const fn new(initial: Option) -> Self { + Self(AtomicU8::new(match initial { + None => 0, + Some(true) => 1, + Some(false) => 2, + })) + } + + pub fn store(&self, value: impl Into>, order: Ordering) { + self.0.store( + match value.into() { + None => 0, + Some(true) => 1, + Some(false) => 2, + }, + order, + ); + } + + pub fn load(&self, order: Ordering) -> Option { + match self.0.load(order) { + 0 => None, + 1 => Some(true), + 2 => Some(false), + _ => unreachable!("inner atomic is not exposed and can only set 0 to 2"), + } + } + + pub fn as_ptr(&self) -> *mut u8 { + self.0.as_ptr() + } +} diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index 63ff05fbbb..a4f0fcf64f 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -16,11 +16,11 @@ bench = false workspace = true [dependencies] -nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } +nu-derive-value = { path = "../nu-derive-value", version = "0.105.2" } nu-glob = { path = "../nu-glob", version = "0.105.2" } nu-path = { path = "../nu-path", version = "0.105.2" } nu-system = { path = "../nu-system", version = "0.105.2" } -nu-derive-value = { path = "../nu-derive-value", version = "0.105.2" } +nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } brotli = { workspace = true, optional = true } bytes = { workspace = true } @@ -38,7 +38,7 @@ serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } -thiserror = "2.0.12" +thiserror = { workspace = true } typetag = "0.2" os_pipe = { workspace = true, optional = true, features = ["io_safety"] } log = { workspace = true } diff --git a/crates/nu-protocol/src/errors/cli_error.rs b/crates/nu-protocol/src/errors/cli_error.rs index 96bb779ebb..7b899d77f0 100644 --- a/crates/nu-protocol/src/errors/cli_error.rs +++ b/crates/nu-protocol/src/errors/cli_error.rs @@ -93,6 +93,13 @@ pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError) report_error(working_set, error); } +pub fn report_experimental_option_warning( + working_set: &StateWorkingSet, + warning: &dyn miette::Diagnostic, +) { + report_warning(working_set, warning); +} + fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) { eprintln!("Error: {:?}", CliError(error, working_set)); // reset vt processing, aka ansi because illbehaved externals can break it diff --git a/src/command.rs b/src/command.rs index 17a78e0342..c394687fee 100644 --- a/src/command.rs +++ b/src/command.rs @@ -35,9 +35,20 @@ pub(crate) fn gather_commandline_args() -> (Vec, String, Vec) { } #[cfg(feature = "plugin")] "--plugin-config" => args.next().map(|a| escape_quote_string(&a)), - "--log-level" | "--log-target" | "--log-include" | "--log-exclude" | "--testbin" - | "--threads" | "-t" | "--include-path" | "--lsp" | "--ide-goto-def" - | "--ide-hover" | "--ide-complete" | "--ide-check" => args.next(), + "--log-level" + | "--log-target" + | "--log-include" + | "--log-exclude" + | "--testbin" + | "--threads" + | "-t" + | "--include-path" + | "--lsp" + | "--ide-goto-def" + | "--ide-hover" + | "--ide-complete" + | "--ide-check" + | "--experimental-options" => args.next(), #[cfg(feature = "plugin")] "--plugins" => args.next(), _ => None, @@ -107,6 +118,7 @@ pub(crate) fn parse_commandline_args( let error_style: Option = call.get_flag(engine_state, &mut stack, "error-style")?; let no_newline = call.get_named_arg("no-newline"); + let experimental_options = call.get_flag_expr("experimental-options"); // ide flags let lsp = call.has_flag(engine_state, &mut stack, "lsp")?; @@ -201,6 +213,8 @@ pub(crate) fn parse_commandline_args( let log_exclude = extract_list(log_exclude, "string", |expr| expr.as_string())?; let execute = extract_contents(execute)?; let include_path = extract_contents(include_path)?; + let experimental_options = + extract_list(experimental_options, "string", |expr| expr.as_string())?; let help = call.has_flag(engine_state, &mut stack, "help")?; @@ -251,6 +265,7 @@ pub(crate) fn parse_commandline_args( table_mode, error_style, no_newline, + experimental_options, }); } } @@ -292,6 +307,7 @@ pub(crate) struct NushellCliArgs { pub(crate) ide_complete: Option, pub(crate) ide_check: Option, pub(crate) ide_ast: Option>, + pub(crate) experimental_options: Option>>, } #[derive(Clone)] @@ -452,6 +468,12 @@ impl Command for Nu { "run internal test binary", None, ) + .named( + "experimental-options", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "enable or disable experimental options", + None, + ) .optional( "script file", SyntaxShape::Filepath, diff --git a/src/experimental_options.rs b/src/experimental_options.rs new file mode 100644 index 0000000000..19954f76e9 --- /dev/null +++ b/src/experimental_options.rs @@ -0,0 +1,73 @@ +use std::borrow::Borrow; + +use nu_protocol::{ + cli_error::report_experimental_option_warning, + engine::{EngineState, StateWorkingSet}, +}; + +use crate::command::NushellCliArgs; + +// 1. Parse experimental options from env +// 2. See if we should have any and disable all of them if not +// 3. Parse CLI arguments, if explicitly mentioned, let's enable them +pub fn load(engine_state: &EngineState, cli_args: &NushellCliArgs, has_script: bool) { + let working_set = StateWorkingSet::new(engine_state); + + if !should_disable_experimental_options(has_script, cli_args) { + let env_content = std::env::var(nu_experimental::ENV).unwrap_or_default(); + let env_offset = format!("{}=", nu_experimental::ENV).len(); + + for (env_warning, span) in nu_experimental::parse_env() { + let span_offset = (span.start + env_offset)..(span.end + env_offset); + let mut diagnostic = miette::diagnostic!( + severity = miette::Severity::Warning, + code = env_warning.code(), + labels = vec![miette::LabeledSpan::new_with_span(None, span_offset)], + "{}", + env_warning, + ); + if let Some(help) = env_warning.help() { + diagnostic = diagnostic.with_help(help); + } + + let error = miette::Error::from(diagnostic).with_source_code(format!( + "{}={}", + nu_experimental::ENV, + env_content + )); + report_experimental_option_warning(&working_set, error.borrow()); + } + } + + for (cli_arg_warning, ctx) in + nu_experimental::parse_iter(cli_args.experimental_options.iter().flatten().map(|entry| { + entry + .item + .split_once("=") + .map(|(key, val)| (key.into(), Some(val.into()), entry)) + .unwrap_or((entry.item.clone().into(), None, entry)) + })) + { + let diagnostic = miette::diagnostic!( + severity = miette::Severity::Warning, + code = cli_arg_warning.code(), + labels = vec![miette::LabeledSpan::new_with_span(None, ctx.span)], + "{}", + cli_arg_warning, + ); + match cli_arg_warning.help() { + Some(help) => { + report_experimental_option_warning(&working_set, &diagnostic.with_help(help)) + } + None => report_experimental_option_warning(&working_set, &diagnostic), + } + } +} + +fn should_disable_experimental_options(has_script: bool, cli_args: &NushellCliArgs) -> bool { + has_script + || cli_args.commands.is_some() + || cli_args.execute.is_some() + || cli_args.no_config_file.is_some() + || cli_args.login_shell.is_some() +} diff --git a/src/main.rs b/src/main.rs index ec8bce7cfb..520c0ba6af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod command; mod command_context; mod config_files; +mod experimental_options; mod ide; mod logger; mod run; @@ -201,6 +202,8 @@ fn main() -> Result<()> { std::process::exit(1) }); + experimental_options::load(&engine_state, &parsed_nu_cli_args, !script_name.is_empty()); + // keep this condition in sync with the branches at the end engine_state.is_interactive = parsed_nu_cli_args.interactive_shell.is_some() || (parsed_nu_cli_args.testbin.is_none()