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}