confget 5.0.1

Parse configuration files.
Documentation
/*
 * Copyright (c) 2021, 2022  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
//! 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!("^{}$", 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 '{}' variable should have been present in {:?}",
                    name, sect_data
                ))
            })?;
            let output_name = format!("{}{}{}", config.name_prefix, name, 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<_, _>>()
}