confget 5.1.2

Parse configuration files.
Documentation
/*
 * SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
 * SPDX-License-Identifier: BSD-2-Clause
 */
//! Format the variable names and values as specified by the configuration.
//!
//! The [`filter_vars`] function is mainly used by the `confget`
//! command-line tool, but e.g. the "format safely for Bourne shell use"
//! handling may also be useful to other consumers.

use std::collections::HashMap;

use anyhow::Error as AnyError;
use regex::Regex;

use crate::defs::{ConfgetError, Config, FileData, SectionData};

/// The name and value of a variable, formatted according to the configuration.
///
/// This is the structure returned by the [`filter_vars`] function.
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)]
#[allow(clippy::module_name_repetitions)]
pub struct FormatOutput {
    /// The name of the variable as found in the input.
    pub name: String,
    /// The value of the variable as found in the input.
    pub value: String,
    /// The name of the variable, formatted according to the configuration.
    pub output_name: String,
    /// The value of the variable, formatted according to the configuration.
    pub output_value: String,
    /// The combination of `output_name` and `output_value` as defined by
    /// the configuration (e.g.
    /// [show_var_name][`crate::defs::Config::show_var_name`]).
    pub output_full: String,
}

/// Convert the a user-supplied fnmatch glob pattern to a regular expression string.
///
/// # Errors
/// The conversion resulted in an invalid regular expression pattern.
fn compile_fnmatch(glob_pattern: &str) -> Result<Regex, ConfgetError> {
    let pattern_vec: Vec<String> = glob_pattern
        .chars()
        .map(|chr| match chr {
            '?' => ".".to_owned(),
            '*' => ".*".to_owned(),
            '.' | '+' | '(' | ')' | '[' | ']' => format!("\\{chr}"),
            _ => chr.to_string(),
        })
        .collect();
    let pattern = format!(
        "^{pat}$",
        pat = pattern_vec.iter().cloned().collect::<String>()
    );
    Regex::new(&pattern).map_err(|err| {
        ConfgetError::Glob(
            glob_pattern.to_owned(),
            AnyError::new(ConfgetError::Regex(pattern, err)),
        )
    })
}

/// Process a user-supplied regular expression pattern.
///
/// # Errors
/// Invalid regular expression pattern.
fn compile_regex(pattern: &str) -> Result<Regex, ConfgetError> {
    Regex::new(pattern).map_err(|err| ConfgetError::Regex(pattern.to_owned(), err))
}

/// Process the variable names read from the INI-style file.
///
/// # Errors
/// Invalid fnmatch/glob or regex pattern.
fn get_varnames_only<'data>(
    config: &'data Config,
    sect_data: &HashMap<&'data String, &String>,
) -> Result<Vec<&'data String>, ConfgetError> {
    let mut res: Vec<&String> = if config.list_all {
        sect_data.keys().copied().collect()
    } else if config.match_var_names {
        let regexes: Vec<Regex> = config
            .varnames
            .iter()
            .map(|pat| {
                if config.match_regex {
                    compile_regex(pat)
                } else {
                    compile_fnmatch(pat)
                }
            })
            .collect::<Result<_, _>>()?;
        sect_data
            .keys()
            .filter(|value| regexes.iter().any(|re| re.is_match(value)))
            .copied()
            .collect()
    } else {
        config
            .varnames
            .iter()
            .filter(|name| sect_data.contains_key(name))
            .collect()
    };
    res.sort();
    Ok(res)
}

/// Process the variable names and/or values as requested.
///
/// # Errors
/// Invalid fnmatch/glob or regex pattern.
fn get_varnames<'data>(
    config: &'data Config,
    sect_data: &HashMap<&'data String, &String>,
) -> Result<Vec<&'data String>, ConfgetError> {
    let varnames = get_varnames_only(config, sect_data)?;
    if let Some(ref pattern) = config.match_var_values {
        let re = if config.match_regex {
            compile_regex(pattern)?
        } else {
            compile_fnmatch(pattern)?
        };
        Ok(varnames
            .iter()
            .copied()
            .filter(|name| {
                sect_data
                    .get(name)
                    .map_or(false, |value| re.is_match(value))
            })
            .collect())
    } else {
        Ok(varnames)
    }
}

/// Select only the variables requested by the configuration.
///
/// This function is mainly used by the `confget` command-line tool.
/// It examines the [Config][`crate::defs::Config`] object and
/// returns the variables that have been selected by its settings
/// (`varname`, `list_all`, `match_var_names`, etc.) from
/// the appropriate sections (`section`, `section_override`, etc.).
///
/// # Errors
/// Invalid fnmatch/glob or regex pattern specified.
#[inline]
pub fn filter_vars(
    config: &Config,
    data: &FileData,
    section: &str,
) -> Result<Vec<FormatOutput>, ConfgetError> {
    let empty: SectionData = SectionData::new();
    let sect_iter_first = if config.section_override {
        data.get("").map_or_else(|| empty.iter(), HashMap::iter)
    } else {
        empty.iter()
    };
    let sect_iter_second = data
        .get(section)
        .map_or_else(|| empty.iter(), HashMap::iter);
    let sect_data: HashMap<&String, &String> = sect_iter_first.chain(sect_iter_second).collect();
    get_varnames(config, &sect_data)?
        .iter()
        .map(|name| {
            let value = sect_data.get(name).ok_or_else(|| {
                ConfgetError::Internal(format!(
                    "Internal error: the '{name}' variable should have been present in {sect_data:?}"
                ))
            })?;
            let output_name = format!("{prefix}{name}{suffix}", prefix=config.name_prefix, suffix=config.name_suffix);
            let output_value = if config.shell_escape {
                shell_words::quote(value).to_string()
            } else {
                (*value).to_string()
            };
            let output_full = if config.show_var_name {
                format!("{output_name}={output_value}")
            } else {
                output_value.clone()
            };
            Ok(FormatOutput {
                name: (*name).to_string(),
                value: (*value).to_string(),
                output_name,
                output_value,
                output_full,
            })
        })
        .collect::<Result<_, _>>()
}