Skip to main content

nu_experimental/
lib.rs

1//! Experimental Options for the Nu codebase.
2//!
3//! This crate defines all experimental options used in Nushell.
4//!
5//! An [`ExperimentalOption`] is basically a fancy global boolean.
6//! It should be set very early during initialization and lets us switch between old and new
7//! behavior for parts of the system.
8//!
9//! The goal is to have a consistent way to handle experimental flags across the codebase, and to
10//! make it easy to find all available options.
11//!
12//! # Usage
13//!
14//! Using an option is simple:
15//!
16//! ```rust
17//! if nu_experimental::EXAMPLE.get() {
18//!     // new behavior
19//! } else {
20//!     // old behavior
21//! }
22//! ```
23//!
24//! # Adding New Options
25//!
26//! 1. Create a new module in `options.rs`.
27//! 2. Define a marker struct and implement `ExperimentalOptionMarker` for it.
28//! 3. Add a new static using `ExperimentalOption::new`.
29//! 4. Add the static to [`ALL`].
30//!
31//! That's it. See [`EXAMPLE`] in `options/example.rs` for a complete example.
32//!
33//! # For Users
34//!
35//! Users can view enabled options using either `version` or `debug experimental-options`.
36//!
37//! To enable or disable options, use either the [`NU_EXPERIMENTAL_OPTIONS`](ENV) environment
38//! variable, or pass them via CLI using `--experimental-options`.
39//!
40//! ## Environment variable
41//!
42//! Set [`ENV`] before launching `nu` (comma-separated list of options):
43//!
44//! ```sh
45//! NU_EXPERIMENTAL_OPTIONS=example=true,pipefail=false nu
46//! ```
47//!
48//! ## Command line (`--experimental-options`)
49//!
50//! Each `--experimental-options` flag takes **one** shell argument. That argument can be a
51//! single option, a comma-separated list, or a bracketed list (with optional spaces). You can
52//! repeat the flag to add more options.
53//!
54//! ```sh
55//! nu --experimental-options example=true
56//! nu --experimental-options '[example=true, pipefail=false]'
57//! nu --experimental-options example=true --experimental-options pipefail=false
58//! ```
59//!
60//! To run a script with experimental options, pass the script path **after** the option value
61//! (the script is not part of the option list):
62//!
63//! ```sh
64//! nu --experimental-options '[example=true]' script.nu
65//! ```
66//!
67//! # For Embedders
68//!
69//! If you're embedding Nushell, prefer using [`parse_env`] or [`parse_iter`] to load options.
70//!
71//! `parse_iter` is useful if you want to feed in values from other sources.
72//! Since options are expected to stay stable during runtime, make sure to do this early.
73//!
74//! You can also call [`ExperimentalOption::set`] manually, but be careful with that.
75
76use crate::util::AtomicMaybe;
77use std::{any::TypeId, fmt::Debug, hash::Hash, sync::atomic::Ordering};
78
79mod options;
80mod parse;
81mod util;
82
83pub use options::*;
84pub use parse::*;
85
86/// The status of an experimental option.
87///
88/// An option can either be disabled by default ([`OptIn`](Self::OptIn)) or enabled by default
89/// ([`OptOut`](Self::OptOut)), depending on its expected stability.
90///
91/// Experimental options can be deprecated in two ways:
92/// - If the feature becomes default behavior, it's marked as
93///   [`DeprecatedDefault`](Self::DeprecatedDefault).
94/// - If the feature is being fully removed, it's marked as
95///   [`DeprecatedDiscard`](Self::DeprecatedDiscard) and triggers a warning.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Status {
98    /// Disabled by default.
99    OptIn,
100    /// Enabled by default.
101    OptOut,
102    /// Deprecated as an experimental option; now default behavior.
103    DeprecatedDefault,
104    /// Deprecated; the feature will be removed and triggers a warning.
105    DeprecatedDiscard,
106}
107
108/// Experimental option (aka feature flag).
109///
110/// This struct holds one experimental option that can change some part of Nushell's behavior.
111/// These options let users opt in or out of experimental changes while keeping the rest stable.
112/// They're useful for testing new ideas and giving users a way to go back to older behavior if needed.
113///
114/// You can find all options in the statics of [`nu_experimental`](crate).
115/// Everything there, except [`ALL`], is a toggleable option.
116/// `ALL` gives a full list and can be used to check which options are set.
117///
118/// The [`Debug`] implementation shows the option's identifier, stability, and current value.
119/// To also include the description in the output, use the
120/// [plus sign](std::fmt::Formatter::sign_plus), e.g. `format!("{OPTION:+#?}")`.
121pub struct ExperimentalOption {
122    value: AtomicMaybe,
123    marker: &'static (dyn DynExperimentalOptionMarker + Send + Sync),
124}
125
126impl ExperimentalOption {
127    /// Construct a new `ExperimentalOption`.
128    ///
129    /// This should only be used to define a single static for a marker.
130    pub(crate) const fn new(
131        marker: &'static (dyn DynExperimentalOptionMarker + Send + Sync),
132    ) -> Self {
133        Self {
134            value: AtomicMaybe::new(None),
135            marker,
136        }
137    }
138
139    pub fn identifier(&self) -> &'static str {
140        self.marker.identifier()
141    }
142
143    pub fn description(&self) -> &'static str {
144        self.marker.description()
145    }
146
147    pub fn status(&self) -> Status {
148        self.marker.status()
149    }
150
151    pub fn since(&self) -> Version {
152        self.marker.since()
153    }
154
155    pub fn issue_id(&self) -> u32 {
156        self.marker.issue()
157    }
158
159    pub fn issue_url(&self) -> String {
160        format!(
161            "https://github.com/nushell/nushell/issues/{}",
162            self.marker.issue()
163        )
164    }
165
166    pub fn get(&self) -> bool {
167        self.value
168            .load(Ordering::Relaxed)
169            .unwrap_or_else(|| match self.marker.status() {
170                Status::OptIn => false,
171                Status::OptOut => true,
172                Status::DeprecatedDiscard => false,
173                Status::DeprecatedDefault => false,
174            })
175    }
176
177    /// Sets the state of an experimental option.
178    ///
179    /// # Safety
180    /// This method is unsafe to emphasize that experimental options are not designed to change
181    /// dynamically at runtime.
182    /// Changing their state at arbitrary points can lead to inconsistent behavior.
183    /// You should set experimental options only during initialization, before the application fully
184    /// starts.
185    pub unsafe fn set(&self, value: bool) {
186        self.value.store(value, Ordering::Relaxed);
187    }
188
189    /// Unsets an experimental option, resetting it to an uninitialized state.
190    ///
191    /// # Safety
192    /// Like [`set`](Self::set), this method is unsafe to highlight that experimental options should
193    /// remain stable during runtime.
194    /// Only unset options in controlled, initialization contexts to avoid unpredictable behavior.
195    pub unsafe fn unset(&self) {
196        self.value.store(None, Ordering::Relaxed);
197    }
198}
199
200impl Debug for ExperimentalOption {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        let add_description = f.sign_plus();
203        let mut debug_struct = f.debug_struct("ExperimentalOption");
204        debug_struct.field("identifier", &self.identifier());
205        debug_struct.field("value", &self.get());
206        debug_struct.field("stability", &self.status());
207        if add_description {
208            debug_struct.field("description", &self.description());
209        }
210        debug_struct.finish()
211    }
212}
213
214impl PartialEq for ExperimentalOption {
215    fn eq(&self, other: &Self) -> bool {
216        // if both underlying atomics point to the same value, we talk about the same option
217        self.value.as_ptr() == other.value.as_ptr()
218    }
219}
220
221impl Eq for ExperimentalOption {}
222
223impl PartialOrd for ExperimentalOption {
224    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
225        Some(self.cmp(other))
226    }
227}
228
229impl Ord for ExperimentalOption {
230    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
231        self.identifier().cmp(other.identifier())
232    }
233}
234
235impl Hash for ExperimentalOption {
236    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
237        // this should ensure that we don't have prefixes
238        TypeId::of::<Self>().hash(state);
239        self.identifier().hash(state);
240    }
241}
242
243/// Sets the state of all experimental option that aren't deprecated.
244///
245/// # Safety
246/// This method is unsafe to emphasize that experimental options are not designed to change
247/// dynamically at runtime.
248/// Changing their state at arbitrary points can lead to inconsistent behavior.
249/// You should set experimental options only during initialization, before the application fully
250/// starts.
251pub unsafe fn set_all(value: bool) {
252    for option in ALL {
253        match option.status() {
254            // SAFETY: The safety bounds for `ExperimentalOption.set` are the same as this function.
255            Status::OptIn | Status::OptOut => unsafe { option.set(value) },
256            Status::DeprecatedDefault | Status::DeprecatedDiscard => {}
257        }
258    }
259}
260
261pub(crate) trait DynExperimentalOptionMarker {
262    fn identifier(&self) -> &'static str;
263    fn description(&self) -> &'static str;
264    fn status(&self) -> Status;
265    fn since(&self) -> Version;
266    fn issue(&self) -> u32;
267}
268
269impl<M: options::ExperimentalOptionMarker> DynExperimentalOptionMarker for M {
270    fn identifier(&self) -> &'static str {
271        M::IDENTIFIER
272    }
273
274    fn description(&self) -> &'static str {
275        M::DESCRIPTION
276    }
277
278    fn status(&self) -> Status {
279        M::STATUS
280    }
281
282    fn since(&self) -> Version {
283        M::SINCE
284    }
285
286    fn issue(&self) -> u32 {
287        M::ISSUE
288    }
289}