cnf 0.6.1

Distribution-agnostic 'command not found'-handler
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
// This file is part of cnf, available at <https://gitlab.com/hartang/rust/cnf>

//! # Convenient and typo-safe environment variable handling
//!
//! This module serves multiple purposes. First, it attempts to make environment-variable handling
//! typo-safe, by leveraging the type system and an enum-type of valid variables to catch typos at
//! compile time. Second, due to the use of one central enum defining all env variables, editors
//! can suggest valid environment variables in e.g. tab completions. Third, users wondering which
//! environment variables are supported can simply glance into the source code in one central
//! location to see the raw values.

/// Application-wide environment variables.
///
/// Unless noted otherwise, the variable names (as seen here in Rust) translate to their environment
/// name counterparts by:
///
/// - Prefixing `CNF_`
/// - Separating CamelCase words with `_`
/// - Writing all characters in caps
///
/// I.e. `AliasPrivileged` becomes `CNF_ALIAS_PRIVILEGED`.
#[derive(Debug, strum::Display, Clone, Copy)]
pub enum Env {
    /// If this variable is defined, an alias is executed with elevated privileges.
    #[strum(serialize = "CNF_ALIAS_PRIVILEGED")]
    AliasPrivileged,
    /// Name or absolute path of the `cnf` executable to resolve aliases with.
    ///
    /// This is only used when resolving aliases and can be used to override the `cnf` version e.g.
    /// for development purposes to test newer features, like so:
    ///
    /// ```bash
    /// $ CNF_ALIAS_EXECUTABLE="$PWD/target/debug/cnf" podman image ls
    /// ```
    #[strum(serialize = "CNF_ALIAS_EXECUTABLE")]
    AliasExecutable,
    /// Set by the application to keep track of how many recursion levels are currently used by
    /// e.g. alias execution.
    #[strum(serialize = "CNF_RECURSION_DEPTH")]
    RecursionDepth,
    /// Overwrite the log level set in the application configuration.
    ///
    /// Adheres roughly to `env_logger` syntax, see here:
    /// <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>
    #[strum(serialize = "CNF_LOG_LEVEL")]
    LogLevel,
    /// Alternative to [`Env::LogLevel`] for compatibility with the Rust ecosystem.
    ///
    /// Refer to [`Env::LogLevel`] for documentation. **Translates to environment variable
    /// `RUST_LOG`!**
    #[strum(serialize = "RUST_LOG")]
    RustLog,
}

impl Env {
    /// Returns true if an environment variable is defined and set to an arbitrary value.
    pub fn is_set(&self) -> bool {
        self.get_raw().is_some()
    }

    /// Get the raw value of an environment variable. Refer to [std::env::var_os] for what a value
    /// of `None` may mean.
    pub fn get_raw(&self) -> Option<std::ffi::OsString> {
        std::env::var_os(self.to_string())
    }

    /// Set the raw value of an environment variable.
    ///
    /// # Safety
    ///
    /// On most operating systems, this is only ever safe to call in single-threaded parts of the
    /// program. For additional details, refer to the documentation of [`std::env::set_var`].
    #[allow(unsafe_code)]
    pub unsafe fn set_raw(&self, value: impl AsRef<std::ffi::OsStr>) {
        unsafe { std::env::set_var(self.to_string(), value) }
    }

    /// Read an environment variable to a concrete type, if possible. Returns `None` if either:
    ///
    /// - [Env::get] returns `None`
    /// - The variable value cannot be parsed into a valid UTF-8 string
    /// - The variable value cannot be parsed into the requested type
    pub fn get<T: std::str::FromStr>(&self) -> Option<T> {
        self.get_raw()
            .and_then(|s| s.into_string().ok())
            .and_then(|s| T::from_str(s.as_ref()).ok())
    }

    /// Set an environment variable to the string representation of `value`.
    ///
    /// # Safety
    ///
    /// See [`Env::set_raw`] for details.
    #[allow(unsafe_code)]
    pub unsafe fn set<T: std::fmt::Display>(&self, value: T) {
        unsafe { self.set_raw(value.to_string()) }
    }
}