utf8-locale 1.0.0

Detect a UTF-8-capable locale for running child processes in
Documentation
#![deny(missing_docs)]
/*
 * Copyright (c) 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.
 */
//! u8loc - run a command in a UTF-8-capable locale

use std::collections::HashMap;
use std::env;
use std::os::unix::process::CommandExt;
use std::process::Command;

use anyhow::{bail, Context, Error as AnyError, Result};
use getopts::Options;
use utf8_locale::{LanguagesDetect, Utf8Detect};

#[derive(Debug)]
enum Mode {
    Features,
    QueryEnv(String, bool),
    QueryList,
    QueryPreferred,
    Run(Vec<String>, bool),
}

#[derive(Debug)]
struct Config {
    mode: Mode,
}

fn parse_args() -> Result<Config> {
    let mut parser = Options::new();

    parser.optflag(
        "",
        "features",
        "display the features supported by the program and exit",
    );
    parser.optflag(
        "p",
        "",
        "use a locale specified in the LANG and LC_* variables if appropriate",
    );
    parser.optopt(
        "q",
        "",
        "output the value of an environment variable",
        "NAME",
    );
    parser.optflag(
        "r",
        "",
        "run the specified program in a UTF-8-friendly environment",
    );

    let args: Vec<String> = env::args().collect();
    let opts = parser
        .parse(args.split_first().context("Not even a program name?")?.1)
        .context("Could not parse the command-line options")?;

    let preferred = opts.opt_present("p");
    match opts
        .opt_get::<String>("q")
        .context("Could not obtain the argument of the -q command-line option")?
    {
        Some(query) => {
            if opts.opt_present("r") {
                bail!("Exactly one of the -q and -r options must be specified");
            }
            match &*query {
                "list" => Ok(Config {
                    mode: Mode::QueryList,
                }),
                "preferred" => Ok(Config {
                    mode: Mode::QueryPreferred,
                }),
                var @ ("LC_ALL" | "LANGUAGE") => Ok(Config {
                    mode: Mode::QueryEnv(var.to_owned(), preferred),
                }),
                other => bail!(format!("Invalid query name '{}' specified", other)),
            }
        }
        None => {
            if opts.opt_present("r") {
                if opts.free.is_empty() {
                    bail!("No program specified to run");
                }
                Ok(Config {
                    mode: Mode::Run(opts.free, preferred),
                })
            } else if opts.opt_present("features") {
                Ok(Config {
                    mode: Mode::Features,
                })
            } else {
                bail!("Exactly one of the -q and -r options must be specified");
            }
        }
    }
}

fn show_features() -> Vec<String> {
    vec![format!(
        "Features: u8loc={} query-env=0.1 query-preferred=0.1 run=0.1",
        env!("CARGO_PKG_VERSION")
    )]
}

fn get_env(preferred: bool) -> Result<HashMap<String, String>> {
    let det = if preferred {
        let langs = LanguagesDetect::new()
            .detect()
            .context("Could not determine the list of preferred languages")?;
        Utf8Detect::new().with_languages(langs)
    } else {
        Utf8Detect::new()
    };
    Ok(det.detect().context("Could not detect a UTF-8 locale")?.env)
}

fn query_env(name: &str, preferred: bool) -> Result<Vec<String>> {
    let env = get_env(preferred)?;
    let value = env
        .get(name)
        .with_context(|| format!("Internal error: {:?} should be present in {:?}", name, env))?;
    Ok(vec![format!("{}", value)])
}

fn query_list() -> Vec<String> {
    vec![
        "LANGUAGE  - The LANGUAGE environment variable".to_owned(),
        "LC_ALL    - The LC_ALL environment variable".to_owned(),
        "list      - List the available query parameters".to_owned(),
        "preferred - List the preferred languages as per the locale variables".to_owned(),
    ]
}

fn query_preferred() -> Result<Vec<String>> {
    LanguagesDetect::new()
        .detect()
        .context("Could not determine the list of preferred languages")
}

fn run_program(prog: &[String], preferred: bool) -> Result<Vec<String>> {
    let env = get_env(preferred)?;
    let (prog_name, args) = prog.split_first().context("Not even a program name?")?;
    Err(AnyError::new(
        Command::new(&prog_name)
            .args(args)
            .env_clear()
            .envs(env)
            .exec(),
    )
    .context(format!("Could not execute the {} program", prog_name)))
}

fn run() -> Result<Vec<String>> {
    let cfg = parse_args()?;
    match cfg.mode {
        Mode::Features => Ok(show_features()),
        Mode::QueryEnv(ref name, ref preferred) => query_env(name, *preferred),
        Mode::QueryList => Ok(query_list()),
        Mode::QueryPreferred => query_preferred(),
        Mode::Run(ref prog, ref preferred) => run_program(prog, *preferred),
    }
}

#[allow(clippy::print_stdout)]
fn main() -> Result<()> {
    println!("{}", run()?.join("\n"));
    Ok(())
}