tzselect-rs 0.1.0

Rust port of upstream tzselect.ksh — the interactive tzdb timezone selector
Documentation
//! `tzselect-rs` — CLI front end for the [`tzselect_rs`] interactive selector.
//!
//! Mirrors upstream `tzselect`: interaction on stderr/stdin, the chosen `TZ` on
//! stdout. `$TZDIR` (default `.`) locates `iso3166.tab` / `zone1970.tab` and the
//! compiled zones; `date` is shelled out exactly as the script does.
#![forbid(unsafe_code)]

use std::io::{BufRead, IsTerminal, Write};
use std::path::Path;
use std::process::{Command, ExitCode};

use tzselect_rs::{run, Host, Options};

struct RealHost {
    stdin: std::io::Lines<std::io::StdinLock<'static>>,
}

impl Host for RealHost {
    fn read_line(&mut self) -> Option<String> {
        match self.stdin.next() {
            Some(Ok(s)) => Some(s),
            _ => None,
        }
    }
    fn err(&mut self, s: &str) {
        let mut e = std::io::stderr();
        let _ = e.write_all(s.as_bytes());
        let _ = e.flush();
    }
    fn out(&mut self, s: &str) {
        let mut o = std::io::stdout();
        let _ = o.write_all(s.as_bytes());
        let _ = o.flush();
    }
    fn read_table(&self, path: &str) -> Option<String> {
        std::fs::read_to_string(path).ok()
    }
    fn run_date(&self, tz: &str) -> Option<String> {
        let out = Command::new("date")
            .env("LANG", "C")
            .env("TZ", tz)
            .output()
            .ok()?;
        Some(
            String::from_utf8_lossy(&out.stdout)
                .trim_end_matches('\n')
                .to_string(),
        )
    }
    fn run_date_fmt(&self, tz: &str, fmt: &str) -> Option<String> {
        let out = Command::new("date")
            .env("TZ", tz)
            .arg(format!("+{fmt}"))
            .output()
            .ok()?;
        Some(
            String::from_utf8_lossy(&out.stdout)
                .trim_end_matches('\n')
                .to_string(),
        )
    }
    fn zone_readable(&self, path: &str) -> bool {
        Path::new(path).is_file() && std::fs::File::open(path).is_ok()
    }
    fn stdout_is_tty(&self) -> bool {
        std::io::stdout().is_terminal()
    }
}

fn main() -> ExitCode {
    let mut o = Options {
        tzdir: std::env::var("TZDIR").unwrap_or_else(|_| {
            std::env::current_dir()
                .map(|p| p.to_string_lossy().into_owned())
                .unwrap_or_else(|_| ".".to_string())
        }),
        tzversion: option_env!("TZVERSION")
            .unwrap_or(env!("CARGO_PKG_VERSION"))
            .to_string(),
        ..Options::default()
    };

    // Arg parsing (tzselect.ksh:138-164).
    let args: Vec<String> = std::env::args().skip(1).collect();
    let mut i = 0;
    let mut positional: Vec<String> = Vec::new();
    let mut opts_done = false;
    while i < args.len() {
        let a = &args[i];
        if opts_done || !a.starts_with('-') || a == "-" {
            positional.push(a.clone());
            i += 1;
            continue;
        }
        if a == "--" {
            opts_done = true;
            i += 1;
            continue;
        }
        if let Some(long) = a.strip_prefix("--") {
            match long {
                "help" => {
                    println!("{}", tzselect_rs::usage(&o));
                    return ExitCode::SUCCESS;
                }
                "version" => {
                    println!("tzselect {}{}", o.pkgversion, o.tzversion);
                    return ExitCode::SUCCESS;
                }
                _ => {
                    eprintln!(
                        "{}: --{long}: unknown option; try '{} --help'",
                        o.argv0, o.argv0
                    );
                    return ExitCode::FAILURE;
                }
            }
        }
        // Short option cluster: -c/-n/-t take a value (attached or next arg).
        let body = &a[1..];
        let (flag, rest) = body.split_at(1);
        let take_val = |rest: &str, i: &mut usize| -> Option<String> {
            if !rest.is_empty() {
                Some(rest.to_string())
            } else {
                *i += 1;
                args.get(*i).cloned()
            }
        };
        match flag {
            "c" => match take_val(rest, &mut i) {
                Some(v) => o.coord = Some(v),
                None => {
                    eprintln!("{}: option requires an argument -- 'c'", o.argv0);
                    return ExitCode::FAILURE;
                }
            },
            "n" => match take_val(rest, &mut i) {
                Some(v) => o.location_limit = v.parse().unwrap_or(o.location_limit),
                None => {
                    eprintln!("{}: option requires an argument -- 'n'", o.argv0);
                    return ExitCode::FAILURE;
                }
            },
            "t" => match take_val(rest, &mut i) {
                Some(v) => o.zonetabtype = v,
                None => {
                    eprintln!("{}: option requires an argument -- 't'", o.argv0);
                    return ExitCode::FAILURE;
                }
            },
            other => {
                eprintln!("{}: illegal option -- {other}", o.argv0);
                eprintln!("{}: try '{} --help'", o.argv0, o.argv0);
                return ExitCode::FAILURE;
            }
        }
        i += 1;
    }
    if let Some(first) = positional.first() {
        eprintln!("{}: {first}: unknown argument", o.argv0);
        return ExitCode::FAILURE;
    }

    let mut host = RealHost {
        stdin: std::io::stdin().lock().lines(),
    };
    match run(&o, &mut host) {
        0 => ExitCode::SUCCESS,
        _ => ExitCode::FAILURE,
    }
}