2023-02-03 23:34:29 +08:00
|
|
|
//! Flags to enable upcoming features
|
|
|
|
|
2023-08-09 06:16:04 +08:00
|
|
|
use crate::wchar::prelude::*;
|
2023-02-03 23:34:29 +08:00
|
|
|
use std::sync::atomic::AtomicBool;
|
|
|
|
use std::sync::atomic::Ordering;
|
|
|
|
|
2024-01-02 04:29:05 +08:00
|
|
|
/// The list of flags.
|
|
|
|
#[repr(u8)]
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
pub enum FeatureFlag {
|
|
|
|
/// Whether ^ is supported for stderr redirection.
|
|
|
|
stderr_nocaret,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
2024-01-02 04:29:05 +08:00
|
|
|
/// Whether ? is supported as a glob.
|
|
|
|
qmark_noglob,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
2024-01-02 04:29:05 +08:00
|
|
|
/// Whether string replace -r double-unescapes the replacement.
|
|
|
|
string_replace_backslash,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
2024-01-02 04:29:05 +08:00
|
|
|
/// Whether "&" is not-special if followed by a word character.
|
|
|
|
ampersand_nobg_in_token,
|
2023-02-03 23:34:29 +08:00
|
|
|
}
|
|
|
|
|
2023-07-10 04:33:02 +08:00
|
|
|
struct Features {
|
2023-02-03 23:34:29 +08:00
|
|
|
// Values for the flags.
|
|
|
|
// These are atomic to "fix" a race reported by tsan where tests of feature flags and other
|
|
|
|
// tests which use them conceptually race.
|
2023-07-10 04:33:02 +08:00
|
|
|
values: [AtomicBool; METADATA.len()],
|
2023-02-03 23:34:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Metadata about feature flags.
|
2023-07-10 04:33:02 +08:00
|
|
|
pub struct FeatureMetadata {
|
2023-02-03 23:34:29 +08:00
|
|
|
/// The flag itself.
|
2023-07-10 04:33:02 +08:00
|
|
|
pub flag: FeatureFlag,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
|
|
|
/// User-presentable short name of the feature flag.
|
2023-07-10 04:33:02 +08:00
|
|
|
pub name: &'static wstr,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
|
|
|
/// Comma-separated list of feature groups.
|
2023-07-10 04:33:02 +08:00
|
|
|
pub groups: &'static wstr,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
|
|
|
/// User-presentable description of the feature flag.
|
2023-07-10 04:33:02 +08:00
|
|
|
pub description: &'static wstr,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
|
|
|
/// Default flag value.
|
2023-07-10 04:33:02 +08:00
|
|
|
pub default_value: bool,
|
2023-02-03 23:34:29 +08:00
|
|
|
|
|
|
|
/// Whether the value can still be changed or not.
|
2023-07-10 04:33:02 +08:00
|
|
|
pub read_only: bool,
|
2023-02-03 23:34:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// The metadata, indexed by flag.
|
2023-07-10 08:56:57 +08:00
|
|
|
pub const METADATA: &[FeatureMetadata] = &[
|
2023-02-03 23:34:29 +08:00
|
|
|
FeatureMetadata {
|
2023-02-08 06:18:51 +08:00
|
|
|
flag: FeatureFlag::stderr_nocaret,
|
2024-01-13 02:10:56 +08:00
|
|
|
name: L!("stderr-nocaret"),
|
|
|
|
groups: L!("3.0"),
|
|
|
|
description: L!("^ no longer redirects stderr (historical, can no longer be changed)"),
|
2023-02-03 23:34:29 +08:00
|
|
|
default_value: true,
|
|
|
|
read_only: true,
|
|
|
|
},
|
|
|
|
FeatureMetadata {
|
2023-02-08 06:18:51 +08:00
|
|
|
flag: FeatureFlag::qmark_noglob,
|
2024-01-13 02:10:56 +08:00
|
|
|
name: L!("qmark-noglob"),
|
2024-01-26 01:26:48 +08:00
|
|
|
groups: L!("3.0"),
|
2024-01-13 02:10:56 +08:00
|
|
|
description: L!("? no longer globs"),
|
2024-01-25 11:17:36 +08:00
|
|
|
default_value: true,
|
2023-02-03 23:34:29 +08:00
|
|
|
read_only: false,
|
|
|
|
},
|
|
|
|
FeatureMetadata {
|
2023-02-08 06:18:51 +08:00
|
|
|
flag: FeatureFlag::string_replace_backslash,
|
2024-01-13 02:10:56 +08:00
|
|
|
name: L!("regex-easyesc"),
|
|
|
|
groups: L!("3.1"),
|
|
|
|
description: L!("string replace -r needs fewer \\'s"),
|
2023-02-03 23:34:29 +08:00
|
|
|
default_value: true,
|
|
|
|
read_only: false,
|
|
|
|
},
|
|
|
|
FeatureMetadata {
|
2023-02-08 06:18:51 +08:00
|
|
|
flag: FeatureFlag::ampersand_nobg_in_token,
|
2024-01-13 02:10:56 +08:00
|
|
|
name: L!("ampersand-nobg-in-token"),
|
|
|
|
groups: L!("3.4"),
|
|
|
|
description: L!("& only backgrounds if followed by a separator"),
|
2023-02-03 23:34:29 +08:00
|
|
|
default_value: true,
|
|
|
|
read_only: false,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2023-07-10 12:50:46 +08:00
|
|
|
thread_local!(
|
2024-01-04 03:57:28 +08:00
|
|
|
#[cfg(test)]
|
2023-07-10 12:50:46 +08:00
|
|
|
static LOCAL_FEATURES: std::cell::RefCell<Option<Features>> = std::cell::RefCell::new(None);
|
|
|
|
);
|
|
|
|
|
2023-02-03 23:34:29 +08:00
|
|
|
/// The singleton shared feature set.
|
2023-07-10 04:33:02 +08:00
|
|
|
static FEATURES: Features = Features::new();
|
|
|
|
|
|
|
|
/// Perform a feature test on the global set of features.
|
|
|
|
pub fn test(flag: FeatureFlag) -> bool {
|
2024-01-04 03:57:28 +08:00
|
|
|
#[cfg(test)]
|
2023-07-10 12:50:46 +08:00
|
|
|
{
|
|
|
|
LOCAL_FEATURES.with(|fc| fc.borrow().as_ref().unwrap_or(&FEATURES).test(flag))
|
|
|
|
}
|
2024-01-04 03:57:28 +08:00
|
|
|
#[cfg(not(test))]
|
2023-07-10 12:50:46 +08:00
|
|
|
{
|
|
|
|
FEATURES.test(flag)
|
|
|
|
}
|
2023-07-10 04:33:02 +08:00
|
|
|
}
|
|
|
|
|
2023-07-10 08:56:57 +08:00
|
|
|
pub use test as feature_test;
|
2023-07-10 04:33:02 +08:00
|
|
|
|
|
|
|
/// Set a flag.
|
2024-01-04 03:57:28 +08:00
|
|
|
#[cfg(test)]
|
2023-10-09 05:22:27 +08:00
|
|
|
pub fn set(flag: FeatureFlag, value: bool) {
|
2023-07-10 12:50:46 +08:00
|
|
|
LOCAL_FEATURES.with(|fc| fc.borrow().as_ref().unwrap_or(&FEATURES).set(flag, value));
|
2023-07-10 04:33:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Parses a comma-separated feature-flag string, updating ourselves with the values.
|
|
|
|
/// Feature names or group names may be prefixed with "no-" to disable them.
|
|
|
|
/// The special group name "all" may be used for those who like to live on the edge.
|
|
|
|
/// Unknown features are silently ignored.
|
|
|
|
pub fn set_from_string<'a>(str: impl Into<&'a wstr>) {
|
2023-07-10 12:50:46 +08:00
|
|
|
let wstr: &wstr = str.into();
|
2024-01-04 03:57:28 +08:00
|
|
|
#[cfg(test)]
|
2023-07-10 12:50:46 +08:00
|
|
|
{
|
|
|
|
LOCAL_FEATURES.with(|fc| {
|
|
|
|
fc.borrow()
|
|
|
|
.as_ref()
|
|
|
|
.unwrap_or(&FEATURES)
|
|
|
|
.set_from_string(wstr)
|
|
|
|
});
|
|
|
|
}
|
2024-01-04 03:57:28 +08:00
|
|
|
#[cfg(not(test))]
|
2023-07-10 12:50:46 +08:00
|
|
|
{
|
|
|
|
FEATURES.set_from_string(wstr)
|
|
|
|
}
|
2023-07-10 04:33:02 +08:00
|
|
|
}
|
2023-02-03 23:34:29 +08:00
|
|
|
|
2023-02-08 06:18:51 +08:00
|
|
|
impl Features {
|
2023-07-04 04:39:54 +08:00
|
|
|
const fn new() -> Self {
|
2023-02-08 06:18:51 +08:00
|
|
|
Features {
|
2023-07-04 04:39:54 +08:00
|
|
|
values: [
|
2023-07-10 04:33:02 +08:00
|
|
|
AtomicBool::new(METADATA[0].default_value),
|
|
|
|
AtomicBool::new(METADATA[1].default_value),
|
|
|
|
AtomicBool::new(METADATA[2].default_value),
|
|
|
|
AtomicBool::new(METADATA[3].default_value),
|
2023-07-04 04:39:54 +08:00
|
|
|
],
|
2023-02-03 23:34:29 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-10 04:33:02 +08:00
|
|
|
fn test(&self, flag: FeatureFlag) -> bool {
|
2024-01-02 04:29:05 +08:00
|
|
|
self.values[flag as usize].load(Ordering::SeqCst)
|
2023-02-03 23:34:29 +08:00
|
|
|
}
|
|
|
|
|
2023-07-10 04:33:02 +08:00
|
|
|
fn set(&self, flag: FeatureFlag, value: bool) {
|
2024-01-02 04:29:05 +08:00
|
|
|
self.values[flag as usize].store(value, Ordering::SeqCst)
|
2023-02-03 23:34:29 +08:00
|
|
|
}
|
|
|
|
|
2024-01-13 03:31:07 +08:00
|
|
|
fn set_from_string(&self, str: &wstr) {
|
2024-01-13 02:10:56 +08:00
|
|
|
let whitespace = L!("\t\n\0x0B\0x0C\r ").as_char_slice();
|
2023-02-03 23:34:29 +08:00
|
|
|
for entry in str.as_char_slice().split(|c| *c == ',') {
|
|
|
|
if entry.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Trim leading and trailing whitespace
|
|
|
|
let entry = &entry[entry.iter().take_while(|c| whitespace.contains(c)).count()..];
|
|
|
|
let entry =
|
|
|
|
&entry[..entry.len() - entry.iter().take_while(|c| whitespace.contains(c)).count()];
|
|
|
|
|
|
|
|
// A "no-" prefix inverts the sense.
|
2024-01-13 02:10:56 +08:00
|
|
|
let (name, value) = match entry.strip_prefix(L!("no-").as_char_slice()) {
|
2023-02-03 23:34:29 +08:00
|
|
|
Some(suffix) => (suffix, false),
|
|
|
|
None => (entry, true),
|
|
|
|
};
|
|
|
|
// Look for a feature with this name. If we don't find it, assume it's a group name and set
|
|
|
|
// all features whose group contain it. Do nothing even if the string is unrecognized; this
|
|
|
|
// is to allow uniform invocations of fish (e.g. disable a feature that is only present in
|
|
|
|
// future versions).
|
|
|
|
// The special name 'all' may be used for those who like to live on the edge.
|
2023-07-10 04:33:02 +08:00
|
|
|
if let Some(md) = METADATA.iter().find(|md| md.name == name) {
|
2023-02-03 23:34:29 +08:00
|
|
|
// Only change it if it's not read-only.
|
|
|
|
// Don't complain if it is, this is typically set from a variable.
|
|
|
|
if !md.read_only {
|
|
|
|
self.set(md.flag, value);
|
|
|
|
}
|
|
|
|
} else {
|
2023-07-10 08:56:57 +08:00
|
|
|
for md in METADATA {
|
2024-01-13 02:10:56 +08:00
|
|
|
if md.groups == name || name == L!("all") {
|
2023-02-03 23:34:29 +08:00
|
|
|
if !md.read_only {
|
|
|
|
self.set(md.flag, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-04 03:57:28 +08:00
|
|
|
#[cfg(test)]
|
2023-07-10 12:50:46 +08:00
|
|
|
pub fn scoped_test(flag: FeatureFlag, value: bool, test_fn: impl FnOnce()) {
|
|
|
|
LOCAL_FEATURES.with(|fc| {
|
|
|
|
assert!(
|
|
|
|
fc.borrow().is_none(),
|
|
|
|
"scoped_test() does not support nesting"
|
|
|
|
);
|
|
|
|
|
|
|
|
let f = Features::new();
|
|
|
|
f.set(flag, value);
|
|
|
|
*fc.borrow_mut() = Some(f);
|
|
|
|
|
|
|
|
test_fn();
|
|
|
|
|
|
|
|
*fc.borrow_mut() = None;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-02-03 23:34:29 +08:00
|
|
|
#[test]
|
|
|
|
fn test_feature_flags() {
|
2023-07-10 04:33:02 +08:00
|
|
|
let f = Features::new();
|
2024-01-13 02:10:56 +08:00
|
|
|
f.set_from_string(L!("stderr-nocaret,nonsense"));
|
2023-02-08 06:18:51 +08:00
|
|
|
assert!(f.test(FeatureFlag::stderr_nocaret));
|
2024-01-13 02:10:56 +08:00
|
|
|
f.set_from_string(L!("stderr-nocaret,no-stderr-nocaret,nonsense"));
|
2023-02-08 06:18:51 +08:00
|
|
|
assert!(f.test(FeatureFlag::stderr_nocaret));
|
2023-02-03 23:34:29 +08:00
|
|
|
|
|
|
|
// Ensure every metadata is represented once.
|
2023-07-10 04:33:02 +08:00
|
|
|
let mut counts: [usize; METADATA.len()] = [0; METADATA.len()];
|
2023-07-10 08:56:57 +08:00
|
|
|
for md in METADATA {
|
2024-01-02 04:29:05 +08:00
|
|
|
counts[md.flag as usize] += 1;
|
2023-02-03 23:34:29 +08:00
|
|
|
}
|
|
|
|
for count in counts {
|
|
|
|
assert_eq!(count, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
assert_eq!(
|
2024-01-02 04:29:05 +08:00
|
|
|
METADATA[FeatureFlag::stderr_nocaret as usize].name,
|
2024-01-13 02:10:56 +08:00
|
|
|
L!("stderr-nocaret")
|
2023-02-03 23:34:29 +08:00
|
|
|
);
|
|
|
|
}
|
2023-07-10 12:50:46 +08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_scoped() {
|
|
|
|
scoped_test(FeatureFlag::qmark_noglob, true, || {
|
|
|
|
assert!(test(FeatureFlag::qmark_noglob));
|
|
|
|
});
|
|
|
|
|
|
|
|
set(FeatureFlag::qmark_noglob, true);
|
|
|
|
|
|
|
|
scoped_test(FeatureFlag::qmark_noglob, false, || {
|
|
|
|
assert!(!test(FeatureFlag::qmark_noglob));
|
|
|
|
});
|
|
|
|
|
|
|
|
set(FeatureFlag::qmark_noglob, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[should_panic]
|
|
|
|
fn test_nested_scopes_not_supported() {
|
|
|
|
scoped_test(FeatureFlag::qmark_noglob, true, || {
|
|
|
|
scoped_test(FeatureFlag::qmark_noglob, false, || {});
|
|
|
|
});
|
|
|
|
}
|