//! One reading of every boolean atgc takes from the environment. //! //! `ATGC_NO_INPUT`, `ATGC_DEBUG`, `ATGC_USE_BOBBIN`, `ATGC_USE_WEB` and the //! off-switch on each `ATGC_*_LOG` path are all the same shape — a variable //! whose value is a word meaning yes or no — and they were read three //! different ways. The `pr` sources module rejected `false` and `no`; the //! non-interactive gate and the debug switch accepted anything that was not //! `0`, so `ATGC_NO_INPUT=false` turned non-interactive mode *on*; the log //! paths accepted `off` and nothing else. Same prefix, same shape, opposite //! answers for the same word, and which one you got depended on knowing //! which subsystem read it. //! //! The permissive reading wins, and the reason is asymmetric harm. Reading //! `false` as on is a setting that does the opposite of what it says, and //! the user has no way to discover it short of the source: they typed a word //! that means no and got yes. Reading an unrecognised word as on costs //! somebody who typed `ATGC_DEBUG=nope` a screen of debug output, which is //! visible in the first second and fixed by unsetting it. Refusing an //! unrecognised word outright was considered and dropped: these are switches //! people export in a shell profile, and a hard failure on every subsequent //! command over a spelling is a worse trade than a switch that is on. //! //! [`NO_COLOR`](https://no-color.org/) deliberately does **not** come //! through here. Its spec says any non-empty value disables color whatever //! the value is, `NO_COLOR=false` included, and a tool that made an //! exception of that spelling would be the odd one out on a machine rather //! than consistent with itself. It is read where it is used, in //! [`crate::term::say`] and [`crate::term::hyperlink`]. /// The words that read as "off", beside unset: the empty string, `0`, /// `false`, `no` and `off`, in any case and with surrounding whitespace /// ignored. /// /// The empty string is off because that is what an unset variable looks like /// after a shell has interpolated it — `ATGC_USE_BOBBIN="$MAYBE"` — and the /// same reasoning [`crate::clients::endpoints`] applies to its host /// overrides. Trailing whitespace is ignored because it survives a /// copy-pasted `export` more often than anyone would guess. const OFF: [&str; 5] = ["", "0", "false", "no", "off"]; /// Whether a value means "off". The half of the reading that the `ATGC_*_LOG` /// paths need, since for them any *other* value is a filename rather than a /// yes. pub(crate) fn is_off(value: &str) -> bool { OFF.contains(&value.trim().to_ascii_lowercase().as_str()) } /// Whether the environment variable `name` is switched on. /// /// A value that is not valid UTF-8 counts as on: it is set, it is not empty, /// and it cannot be any of the words in [`OFF`], so the only reading left is /// the one the user's typing supports. pub(crate) fn switch(name: &str) -> bool { on(std::env::var_os(name) .map(|v| v.to_string_lossy().into_owned()) .as_deref()) } /// The decision itself, kept pure and taking the value as an argument so the /// whole table can be asserted without setting a variable that every other /// test in the process would then see. See [`crate::docs::testing`]. fn on(value: Option<&str>) -> bool { value.is_some_and(|v| !is_off(v)) } #[cfg(test)] mod tests { use super::{is_off, on}; /// The whole table, in one place, because the bug this module exists for /// was two readings that agreed on `1` and `0` and disagreed on /// everything a person would actually type. An unset variable and every /// spelling of no are off; every other value, including a misspelling /// and a value that is not a word at all, is on. #[test] fn every_spelling_of_no_is_off_and_everything_else_is_on() { for off in [ "", " ", "0", "false", "FALSE", "False", "no", "NO", "off", "OFF", " false ", "\tno\n", ] { assert!(!on(Some(off)), "{off:?} should be off"); assert!(is_off(off), "{off:?} should be off"); } for switched_on in [ "1", "true", "TRUE", "yes", "on", "2", "-1", "nope", "falsey", "/tmp/atgc.jsonl", ] { assert!(on(Some(switched_on)), "{switched_on:?} should be on"); assert!(!is_off(switched_on), "{switched_on:?} should be on"); } } /// Unset is off, and is the only input that is not a string. The log /// paths distinguish it from every value — unset means "the default /// path", `off` means "no file" — which is why [`is_off`] takes a `&str` /// and cannot answer this question. #[test] fn an_unset_variable_is_off() { assert!(!on(None)); } }