mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 11:05:40 +02:00
Add infrastructure for experimental options (#16028)
Co-authored-by: Bahex <Bahex@users.noreply.github.com>
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -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"
|
||||
|
37
Cargo.toml
37
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" }
|
||||
|
@ -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 }
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,11 @@ bench = false
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nu-ansi-term = { workspace = true }
|
||||
nu-cmd-base = { path = "../nu-cmd-base", 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-experimental = { path = "../nu-experimental", version = "0.105.2" }
|
||||
nu-glob = { path = "../nu-glob", version = "0.105.2" }
|
||||
nu-json = { path = "../nu-json", 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-term-grid = { path = "../nu-term-grid", version = "0.105.2" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.105.2", default-features = false }
|
||||
nu-ansi-term = { workspace = true }
|
||||
nuon = { path = "../nuon", version = "0.105.2" }
|
||||
|
||||
alphanumeric-sort = { workspace = true }
|
||||
|
64
crates/nu-command/src/debug/experimental_options.rs
Normal file
64
crates/nu-command/src/debug/experimental_options.rs
Normal 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,
|
||||
))
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -153,6 +153,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
||||
Ast,
|
||||
Debug,
|
||||
DebugEnv,
|
||||
DebugExperimentalOptions,
|
||||
DebugInfo,
|
||||
DebugProfile,
|
||||
Explain,
|
||||
|
12
crates/nu-experimental/Cargo.toml
Normal file
12
crates/nu-experimental/Cargo.toml
Normal 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
|
203
crates/nu-experimental/src/lib.rs
Normal file
203
crates/nu-experimental/src/lib.rs
Normal 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
|
||||
}
|
||||
}
|
20
crates/nu-experimental/src/options/example.rs
Normal file
20
crates/nu-experimental/src/options/example.rs
Normal 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;
|
||||
}
|
92
crates/nu-experimental/src/options/mod.rs
Normal file
92
crates/nu-experimental/src/options/mod.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
137
crates/nu-experimental/src/parse.rs
Normal file
137
crates/nu-experimental/src/parse.rs
Normal 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."))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
crates/nu-experimental/src/util.rs
Normal file
44
crates/nu-experimental/src/util.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -35,9 +35,20 @@ pub(crate) fn gather_commandline_args() -> (Vec<String>, String, Vec<String>) {
|
||||
}
|
||||
#[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<Value> =
|
||||
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<Value>,
|
||||
pub(crate) ide_check: Option<Value>,
|
||||
pub(crate) ide_ast: Option<Spanned<String>>,
|
||||
pub(crate) experimental_options: Option<Vec<Spanned<String>>>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
73
src/experimental_options.rs
Normal file
73
src/experimental_options.rs
Normal 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()
|
||||
}
|
@ -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()
|
||||
|
Reference in New Issue
Block a user