Add infrastructure for experimental options (#16028)

Co-authored-by: Bahex <Bahex@users.noreply.github.com>
This commit is contained in:
Piepmatz
2025-07-01 18:36:51 +02:00
committed by GitHub
parent f4136aa3f4
commit a86a0dd16e
19 changed files with 731 additions and 24 deletions

11
Cargo.lock generated
View File

@ -3541,6 +3541,7 @@ dependencies = [
"nu-cmd-plugin", "nu-cmd-plugin",
"nu-command", "nu-command",
"nu-engine", "nu-engine",
"nu-experimental",
"nu-explore", "nu-explore",
"nu-lsp", "nu-lsp",
"nu-parser", "nu-parser",
@ -3658,6 +3659,7 @@ dependencies = [
"miette", "miette",
"nu-cmd-base", "nu-cmd-base",
"nu-engine", "nu-engine",
"nu-experimental",
"nu-parser", "nu-parser",
"nu-protocol", "nu-protocol",
"nu-utils", "nu-utils",
@ -3736,6 +3738,7 @@ dependencies = [
"nu-cmd-lang", "nu-cmd-lang",
"nu-color-config", "nu-color-config",
"nu-engine", "nu-engine",
"nu-experimental",
"nu-glob", "nu-glob",
"nu-json", "nu-json",
"nu-parser", "nu-parser",
@ -3829,6 +3832,14 @@ dependencies = [
"nu-utils", "nu-utils",
] ]
[[package]]
name = "nu-experimental"
version = "0.105.2"
dependencies = [
"itertools 0.14.0",
"thiserror 2.0.12",
]
[[package]] [[package]]
name = "nu-explore" name = "nu-explore"
version = "0.105.2" version = "0.105.2"

View File

@ -24,36 +24,37 @@ pkg-fmt = "zip"
[workspace] [workspace]
members = [ 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-cli",
"crates/nu-engine",
"crates/nu-parser",
"crates/nu-system",
"crates/nu-cmd-base", "crates/nu-cmd-base",
"crates/nu-cmd-extra", "crates/nu-cmd-extra",
"crates/nu-cmd-lang", "crates/nu-cmd-lang",
"crates/nu-cmd-plugin", "crates/nu-cmd-plugin",
"crates/nu-command",
"crates/nu-color-config", "crates/nu-color-config",
"crates/nu-command",
"crates/nu-derive-value",
"crates/nu-engine",
"crates/nu-experimental",
"crates/nu-explore", "crates/nu-explore",
"crates/nu-json", "crates/nu-json",
"crates/nu-lsp", "crates/nu-lsp",
"crates/nu-pretty-hex", "crates/nu-parser",
"crates/nu-protocol",
"crates/nu-derive-value",
"crates/nu-plugin",
"crates/nu-plugin-core", "crates/nu-plugin-core",
"crates/nu-plugin-engine", "crates/nu-plugin-engine",
"crates/nu-plugin-protocol", "crates/nu-plugin-protocol",
"crates/nu-plugin-test-support", "crates/nu-plugin-test-support",
"crates/nu_plugin_inc", "crates/nu-plugin",
"crates/nu_plugin_gstat", "crates/nu-pretty-hex",
"crates/nu_plugin_example", "crates/nu-protocol",
"crates/nu_plugin_query",
"crates/nu_plugin_custom_values",
"crates/nu_plugin_formats",
"crates/nu_plugin_polars",
"crates/nu_plugin_stress_internals",
"crates/nu-std", "crates/nu-std",
"crates/nu-system",
"crates/nu-table", "crates/nu-table",
"crates/nu-term-grid", "crates/nu-term-grid",
"crates/nu-test-support", "crates/nu-test-support",
@ -163,6 +164,7 @@ syn = "2.0"
sysinfo = "0.33" sysinfo = "0.33"
tabled = { version = "0.20", default-features = false } tabled = { version = "0.20", default-features = false }
tempfile = "3.20" tempfile = "3.20"
thiserror = "2.0.12"
titlecase = "3.6" titlecase = "3.6"
toml = "0.8" toml = "0.8"
trash = "5.2" trash = "5.2"
@ -203,11 +205,12 @@ workspace = true
[dependencies] [dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.105.2" } nu-cli = { path = "./crates/nu-cli", version = "0.105.2" }
nu-cmd-base = { path = "./crates/nu-cmd-base", 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-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-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-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-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-explore = { path = "./crates/nu-explore", version = "0.105.2" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.105.2" } nu-lsp = { path = "./crates/nu-lsp/", version = "0.105.2" }
nu-parser = { path = "./crates/nu-parser", version = "0.105.2" } nu-parser = { path = "./crates/nu-parser", version = "0.105.2" }

View File

@ -16,6 +16,7 @@ workspace = true
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.105.2", default-features = false } 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-parser = { path = "../nu-parser", version = "0.105.2" }
nu-protocol = { path = "../nu-protocol", version = "0.105.2", default-features = false } nu-protocol = { path = "../nu-protocol", version = "0.105.2", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false }

View File

@ -188,6 +188,17 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
); );
} }
record.push(
"experimental_options",
Value::string(
nu_experimental::ALL
.iter()
.map(|option| format!("{}={}", option.identifier(), option.get()))
.join(", "),
span,
),
);
Ok(Value::record(record, span).into_pipeline_data()) Ok(Value::record(record, span).into_pipeline_data())
} }

View File

@ -16,9 +16,11 @@ bench = false
workspace = true workspace = true
[dependencies] [dependencies]
nu-ansi-term = { workspace = true }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.105.2" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.105.2" }
nu-color-config = { path = "../nu-color-config", version = "0.105.2" } nu-color-config = { path = "../nu-color-config", version = "0.105.2" }
nu-engine = { path = "../nu-engine", version = "0.105.2", default-features = false } nu-engine = { path = "../nu-engine", version = "0.105.2", default-features = false }
nu-experimental = { path = "../nu-experimental", version = "0.105.2" }
nu-glob = { path = "../nu-glob", version = "0.105.2" } nu-glob = { path = "../nu-glob", version = "0.105.2" }
nu-json = { path = "../nu-json", version = "0.105.2" } nu-json = { path = "../nu-json", version = "0.105.2" }
nu-parser = { path = "../nu-parser", version = "0.105.2" } nu-parser = { path = "../nu-parser", version = "0.105.2" }
@ -29,7 +31,6 @@ nu-system = { path = "../nu-system", version = "0.105.2" }
nu-table = { path = "../nu-table", version = "0.105.2" } nu-table = { path = "../nu-table", version = "0.105.2" }
nu-term-grid = { path = "../nu-term-grid", version = "0.105.2" } nu-term-grid = { path = "../nu-term-grid", version = "0.105.2" }
nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false } nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false }
nu-ansi-term = { workspace = true }
nuon = { path = "../nuon", version = "0.105.2" } nuon = { path = "../nuon", version = "0.105.2" }
alphanumeric-sort = { workspace = true } alphanumeric-sort = { workspace = true }

View File

@ -0,0 +1,64 @@
use nu_engine::command_prelude::*;
use nu_experimental::Status;
#[derive(Clone)]
pub struct DebugExperimentalOptions;
impl Command for DebugExperimentalOptions {
fn name(&self) -> &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<PipelineData, ShellError> {
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,
))
}
}

View File

@ -1,6 +1,7 @@
mod ast; mod ast;
mod debug_; mod debug_;
mod env; mod env;
mod experimental_options;
mod explain; mod explain;
mod info; mod info;
mod inspect; mod inspect;
@ -21,6 +22,7 @@ mod view_span;
pub use ast::Ast; pub use ast::Ast;
pub use debug_::Debug; pub use debug_::Debug;
pub use env::DebugEnv; pub use env::DebugEnv;
pub use experimental_options::DebugExperimentalOptions;
pub use explain::Explain; pub use explain::Explain;
pub use info::DebugInfo; pub use info::DebugInfo;
pub use inspect::Inspect; pub use inspect::Inspect;

View File

@ -153,6 +153,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
Ast, Ast,
Debug, Debug,
DebugEnv, DebugEnv,
DebugExperimentalOptions,
DebugInfo, DebugInfo,
DebugProfile, DebugProfile,
Explain, Explain,

View File

@ -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

View File

@ -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<M: options::ExperimentalOptionMarker> DynExperimentalOptionMarker for M {
fn identifier(&self) -> &'static str {
M::IDENTIFIER
}
fn description(&self) -> &'static str {
M::DESCRIPTION
}
fn status(&self) -> Status {
M::STATUS
}
}

View File

@ -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;
}

View File

@ -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());
}
}
}

View File

@ -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<Item = (Cow<'i, str>, Option<Cow<'i, str>>, 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<usize>` pointing to the
/// part of the environment variable that triggered it.
pub fn parse_env() -> Vec<(ParseWarning, Range<usize>)> {
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<String> {
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."))
}
}
}
}

View File

@ -0,0 +1,44 @@
use std::sync::atomic::{AtomicU8, Ordering};
/// Store an `Option<bool>` as an atomic.
///
/// # Implementation Detail
/// This stores the `Option<bool>` 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<bool>) -> Self {
Self(AtomicU8::new(match initial {
None => 0,
Some(true) => 1,
Some(false) => 2,
}))
}
pub fn store(&self, value: impl Into<Option<bool>>, order: Ordering) {
self.0.store(
match value.into() {
None => 0,
Some(true) => 1,
Some(false) => 2,
},
order,
);
}
pub fn load(&self, order: Ordering) -> Option<bool> {
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()
}
}

View File

@ -16,11 +16,11 @@ bench = false
workspace = true workspace = true
[dependencies] [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-glob = { path = "../nu-glob", version = "0.105.2" }
nu-path = { path = "../nu-path", version = "0.105.2" } nu-path = { path = "../nu-path", version = "0.105.2" }
nu-system = { path = "../nu-system", 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 } brotli = { workspace = true, optional = true }
bytes = { workspace = true } bytes = { workspace = true }
@ -38,7 +38,7 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
strum_macros = { workspace = true } strum_macros = { workspace = true }
thiserror = "2.0.12" thiserror = { workspace = true }
typetag = "0.2" typetag = "0.2"
os_pipe = { workspace = true, optional = true, features = ["io_safety"] } os_pipe = { workspace = true, optional = true, features = ["io_safety"] }
log = { workspace = true } log = { workspace = true }

View File

@ -93,6 +93,13 @@ pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError)
report_error(working_set, error); 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) { fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
eprintln!("Error: {:?}", CliError(error, working_set)); eprintln!("Error: {:?}", CliError(error, working_set));
// reset vt processing, aka ansi because illbehaved externals can break it // reset vt processing, aka ansi because illbehaved externals can break it

View File

@ -35,9 +35,20 @@ pub(crate) fn gather_commandline_args() -> (Vec<String>, String, Vec<String>) {
} }
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
"--plugin-config" => args.next().map(|a| escape_quote_string(&a)), "--plugin-config" => args.next().map(|a| escape_quote_string(&a)),
"--log-level" | "--log-target" | "--log-include" | "--log-exclude" | "--testbin" "--log-level"
| "--threads" | "-t" | "--include-path" | "--lsp" | "--ide-goto-def" | "--log-target"
| "--ide-hover" | "--ide-complete" | "--ide-check" => args.next(), | "--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")] #[cfg(feature = "plugin")]
"--plugins" => args.next(), "--plugins" => args.next(),
_ => None, _ => None,
@ -107,6 +118,7 @@ pub(crate) fn parse_commandline_args(
let error_style: Option<Value> = let error_style: Option<Value> =
call.get_flag(engine_state, &mut stack, "error-style")?; call.get_flag(engine_state, &mut stack, "error-style")?;
let no_newline = call.get_named_arg("no-newline"); let no_newline = call.get_named_arg("no-newline");
let experimental_options = call.get_flag_expr("experimental-options");
// ide flags // ide flags
let lsp = call.has_flag(engine_state, &mut stack, "lsp")?; 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 log_exclude = extract_list(log_exclude, "string", |expr| expr.as_string())?;
let execute = extract_contents(execute)?; let execute = extract_contents(execute)?;
let include_path = extract_contents(include_path)?; 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")?; let help = call.has_flag(engine_state, &mut stack, "help")?;
@ -251,6 +265,7 @@ pub(crate) fn parse_commandline_args(
table_mode, table_mode,
error_style, error_style,
no_newline, no_newline,
experimental_options,
}); });
} }
} }
@ -292,6 +307,7 @@ pub(crate) struct NushellCliArgs {
pub(crate) ide_complete: Option<Value>, pub(crate) ide_complete: Option<Value>,
pub(crate) ide_check: Option<Value>, pub(crate) ide_check: Option<Value>,
pub(crate) ide_ast: Option<Spanned<String>>, pub(crate) ide_ast: Option<Spanned<String>>,
pub(crate) experimental_options: Option<Vec<Spanned<String>>>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -452,6 +468,12 @@ impl Command for Nu {
"run internal test binary", "run internal test binary",
None, None,
) )
.named(
"experimental-options",
SyntaxShape::List(Box::new(SyntaxShape::String)),
"enable or disable experimental options",
None,
)
.optional( .optional(
"script file", "script file",
SyntaxShape::Filepath, SyntaxShape::Filepath,

View File

@ -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()
}

View File

@ -1,6 +1,7 @@
mod command; mod command;
mod command_context; mod command_context;
mod config_files; mod config_files;
mod experimental_options;
mod ide; mod ide;
mod logger; mod logger;
mod run; mod run;
@ -201,6 +202,8 @@ fn main() -> Result<()> {
std::process::exit(1) 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 // keep this condition in sync with the branches at the end
engine_state.is_interactive = parsed_nu_cli_args.interactive_shell.is_some() engine_state.is_interactive = parsed_nu_cli_args.interactive_shell.is_some()
|| (parsed_nu_cli_args.testbin.is_none() || (parsed_nu_cli_args.testbin.is_none()