nu_experimental/
parse.rs

1use crate::{ALL, ExperimentalOption, Status};
2use itertools::Itertools;
3use std::{borrow::Cow, env, ops::Range, sync::atomic::Ordering};
4use thiserror::Error;
5
6/// Environment variable used to load experimental options from.
7///
8/// May be used like this: `NU_EXPERIMENTAL_OPTIONS=example nu`.
9pub const ENV: &str = "NU_EXPERIMENTAL_OPTIONS";
10
11/// Warnings that can happen while parsing experimental options.
12#[derive(Debug, Clone, Error, Eq, PartialEq)]
13pub enum ParseWarning {
14    /// The given identifier doesn't match any known experimental option.
15    #[error("Unknown experimental option `{0}`")]
16    Unknown(String),
17
18    /// The assignment wasn't valid. Only `true` or `false` is accepted.
19    #[error("Invalid assignment for `{identifier}`, expected `true` or `false`, got `{1}`", identifier = .0.identifier())]
20    InvalidAssignment(&'static ExperimentalOption, String),
21
22    /// The assignment for "all" wasn't valid. Only `true` or `false` is accepted.
23    #[error("Invalid assignment for `all`, expected `true` or `false`, got `{0}`")]
24    InvalidAssignmentAll(String),
25
26    /// This experimental option is deprecated as this is now the default behavior.
27    #[error("The experimental option `{identifier}` is deprecated as this is now the default behavior.", identifier = .0.identifier())]
28    DeprecatedDefault(&'static ExperimentalOption),
29
30    /// This experimental option is deprecated and will be removed in the future.
31    #[error("The experimental option `{identifier}` is deprecated and will be removed in a future release", identifier = .0.identifier())]
32    DeprecatedDiscard(&'static ExperimentalOption),
33}
34
35/// Parse and activate experimental options.
36///
37/// This is the recommended way to activate options, as it handles [`ParseWarning`]s properly
38/// and is easy to hook into.
39///
40/// When the key `"all"` is encountered, [`set_all`](super::set_all) is used to set all
41/// experimental options that aren't deprecated.
42/// This allows opting (or opting out of) all experimental options that are currently available for
43/// testing.
44///
45/// The `iter` argument should yield:
46/// - the identifier of the option
47/// - an optional assignment value (`true`/`false`)
48/// - a context value, which is returned with any warning
49///
50/// This way you don't need to manually track which input caused which warning.
51pub fn parse_iter<'i, Ctx: Clone>(
52    iter: impl Iterator<Item = (Cow<'i, str>, Option<Cow<'i, str>>, Ctx)>,
53) -> Vec<(ParseWarning, Ctx)> {
54    let mut warnings = Vec::new();
55    for (key, val, ctx) in iter {
56        if key == "all" {
57            let val = match parse_val(val.as_deref()) {
58                Ok(val) => val,
59                Err(s) => {
60                    warnings.push((ParseWarning::InvalidAssignmentAll(s.to_owned()), ctx));
61                    continue;
62                }
63            };
64            // SAFETY: This is part of the expected parse function to be called at initialization.
65            unsafe { super::set_all(val) };
66            continue;
67        }
68
69        let Some(option) = ALL.iter().find(|option| option.identifier() == key.trim()) else {
70            warnings.push((ParseWarning::Unknown(key.to_string()), ctx));
71            continue;
72        };
73
74        match option.status() {
75            Status::DeprecatedDiscard => {
76                warnings.push((ParseWarning::DeprecatedDiscard(option), ctx.clone()));
77            }
78            Status::DeprecatedDefault => {
79                warnings.push((ParseWarning::DeprecatedDefault(option), ctx.clone()));
80            }
81            _ => {}
82        }
83
84        let val = match parse_val(val.as_deref()) {
85            Ok(val) => val,
86            Err(s) => {
87                warnings.push((ParseWarning::InvalidAssignment(option, s.to_owned()), ctx));
88                continue;
89            }
90        };
91
92        option.value.store(val, Ordering::Relaxed);
93    }
94
95    warnings
96}
97
98fn parse_val(val: Option<&str>) -> Result<bool, &str> {
99    match val.map(str::trim) {
100        None => Ok(true),
101        Some("true") => Ok(true),
102        Some("false") => Ok(false),
103        Some(s) => Err(s),
104    }
105}
106
107/// Parse experimental options from the [`ENV`] environment variable.
108///
109/// Uses [`parse_iter`] internally. Each warning includes a `Range<usize>` pointing to the
110/// part of the environment variable that triggered it.
111pub fn parse_env() -> Vec<(ParseWarning, Range<usize>)> {
112    let Ok(env) = env::var(ENV) else {
113        return vec![];
114    };
115
116    let mut entries = Vec::new();
117    let mut start = 0;
118    for (idx, c) in env.char_indices() {
119        if c == ',' {
120            entries.push((&env[start..idx], start..idx));
121            start = idx + 1;
122        }
123    }
124    entries.push((&env[start..], start..env.len()));
125
126    parse_iter(entries.into_iter().map(|(entry, span)| {
127        entry
128            .split_once("=")
129            .map(|(key, val)| (key.into(), Some(val.into()), span.clone()))
130            .unwrap_or((entry.into(), None, span))
131    }))
132}
133
134impl ParseWarning {
135    /// A code to represent the variant.
136    ///
137    /// This may be used with crates like [`miette`](https://docs.rs/miette) to provide error codes.
138    pub fn code(&self) -> &'static str {
139        match self {
140            Self::Unknown(_) => "nu::experimental_option::unknown",
141            Self::InvalidAssignment(_, _) => "nu::experimental_option::invalid_assignment",
142            Self::InvalidAssignmentAll(_) => "nu::experimental_option::invalid_assignment_all",
143            Self::DeprecatedDefault(_) => "nu::experimental_option::deprecated_default",
144            Self::DeprecatedDiscard(_) => "nu::experimental_option::deprecated_discard",
145        }
146    }
147
148    /// Provide some help depending on the variant.
149    ///
150    /// This may be used with crates like [`miette`](https://docs.rs/miette) to provide a help
151    /// message.
152    pub fn help(&self) -> Option<String> {
153        match self {
154            Self::Unknown(_) => Some(format!(
155                "Known experimental options are: {}",
156                ALL.iter().map(|option| option.identifier()).join(", ")
157            )),
158            Self::InvalidAssignment(_, _) => None,
159            Self::InvalidAssignmentAll(_) => None,
160            Self::DeprecatedDiscard(_) => None,
161            Self::DeprecatedDefault(_) => {
162                Some(String::from("You can safely remove this option now."))
163            }
164        }
165    }
166}