diff --git a/README.md b/README.md index 3739c856..54e5090f 100644 --- a/README.md +++ b/README.md @@ -63,19 +63,20 @@ These are the clients I tried but failed to compile or run: Options: - -h --help Show this screen - -v --version Show version information - -l --list List all commands in the cache - -f --render Render a specific markdown file - -o --os Override the operating system [linux, osx, sunos, windows] - -u --update Update the local cache - -c --clear-cache Clear the local cache - -p --pager Use a pager to page output - -m --markdown Display the raw markdown instead of rendering it - -q --quiet Suppress informational messages - --config-path Show config file path - --seed-config Create a basic config - --color Control when to use color [always, auto, never] [default: auto] + -h --help Show this screen + -v --version Show version information + -l --list List all commands in the cache + -f --render Render a specific markdown file + -o --os Override the operating system [linux, osx, sunos, windows] + -L --language Override the language settings + -u --update Update the local cache + -c --clear-cache Clear the local cache + -p --pager Use a pager to page output + -m --markdown Display the raw markdown instead of rendering it + -q --quiet Suppress informational messages + --config-path Show config file path + --seed-config Create a basic config + --color Control when to use color [always, auto, never] [default: auto] Examples: diff --git a/src/cache.rs b/src/cache.rs index bc097e85..9023aafd 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -2,7 +2,7 @@ use std::env; use std::ffi::OsStr; use std::fs; use std::io::Read; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use app_dirs::{get_app_root, AppDataType}; use flate2::read::GzDecoder; @@ -142,41 +142,53 @@ impl Cache { } } + /// Check for pages for a given platform in one of the given languages. + fn find_page_for_platform( + name: &str, + cache_dir: &Path, + platform: &str, + language_dirs: &[String], + ) -> Option { + language_dirs + .iter() + .map(|lang_dir| cache_dir.join(lang_dir).join(platform).join(name)) + .find(|path| path.exists() && path.is_file()) + } + /// Search for a page and return the path to it. - pub fn find_page(&self, name: &str) -> Option { - // Build page file name + pub fn find_page(&self, name: &str, languages: &[String]) -> Option { let page_filename = format!("{}.md", name); // Get platform dir - let platforms_dir = match Self::get_cache_dir() { - Ok(cache_dir) => cache_dir.join("tldr-master").join("pages"), + let cache_dir = match Self::get_cache_dir() { + Ok(cache_dir) => cache_dir.join("tldr-master"), Err(e) => { log::error!("Could not get cache directory: {}", e); return None; } }; - // Determine platform - let platform = self.get_platform_dir(); + let lang_dirs: Vec = languages + .iter() + .map(|lang| { + if lang == "en" { + String::from("pages") + } else { + format!("pages.{}", lang) + } + }) + .collect(); - // Search for the page in the platform specific directory - if let Some(pf) = platform { - let path = platforms_dir.join(&pf).join(&page_filename); - if path.exists() && path.is_file() { - return Some(path); + // Try to find a platform specific path first. + if let Some(pf) = self.get_platform_dir() { + let pf_path = Self::find_page_for_platform(&page_filename, &cache_dir, pf, &lang_dirs); + if pf_path.is_some() { + return pf_path; } } - // If platform is not supported or if platform specific page does not exist, - // look up the page in the "common" directory. - let path = platforms_dir.join("common").join(&page_filename); - - // Return it if it exists, otherwise give up and return `None` - if path.exists() && path.is_file() { - Some(path) - } else { - None - } + // Did not find platform specific results, fall back to "common" + Self::find_page_for_platform(&page_filename, &cache_dir, "common", &lang_dirs) } /// Return the available pages. diff --git a/src/main.rs b/src/main.rs index 532c6781..eab0841c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ #[cfg(feature = "logging")] extern crate env_logger; +use std::collections::HashSet; use std::env; use std::fs::File; use std::io::BufRead; @@ -60,19 +61,20 @@ Usage: Options: - -h --help Show this screen - -v --version Show version information - -l --list List all commands in the cache - -f --render Render a specific markdown file - -o --os Override the operating system [linux, osx, sunos, windows] - -u --update Update the local cache - -c --clear-cache Clear the local cache - -p --pager Use a pager to page output - -m --markdown Display the raw markdown instead of rendering it - -q --quiet Suppress informational messages - --config-path Show config file path - --seed-config Create a basic config - --color Control when to use color [always, auto, never] [default: auto] + -h --help Show this screen + -v --version Show version information + -l --list List all commands in the cache + -f --render Render a specific markdown file + -o --os Override the operating system [linux, osx, sunos, windows] + -L --language Override the language settings + -u --update Update the local cache + -c --clear-cache Clear the local cache + -p --pager Use a pager to page output + -m --markdown Display the raw markdown instead of rendering it + -q --quiet Suppress informational messages + --config-path Show config file path + --seed-config Create a basic config + --color Control when to use color [always, auto, never] [default: auto] Examples: @@ -109,6 +111,7 @@ struct Args { flag_seed_config: bool, flag_markdown: bool, flag_color: ColorOptions, + flag_language: Option, } /// Print page by path @@ -290,6 +293,46 @@ fn get_os() -> OsType { OsType::Other } +fn get_languages( + env_lang: Result, + env_language: Result, +) -> Vec { + // Language list according to + // https://github.com/tldr-pages/tldr/blob/master/CLIENT-SPECIFICATION.md#language + + if let Ok(lang) = env_lang { + let language = env_language.unwrap_or_default(); + let mut locales: Vec<&str> = language.split(':').collect(); + locales.push(&lang); + locales.push("en"); + + let mut lang_list = Vec::new(); + let mut found_languages = HashSet::new(); + + for locale in &locales { + if locale.len() >= 5 && locale.chars().nth(2) == Some('_') { + // Language with country code + let lang = &locale[..5]; + if found_languages.insert(lang) { + lang_list.push(lang); + } + } + if locale.len() >= 2 && *locale != "POSIX" { + // Language code + let lang = &locale[..2]; + if found_languages.insert(lang) { + lang_list.push(lang); + } + } + } + + return lang_list.iter().map(|&s| String::from(s)).collect(); + } + + // Without the LANG environment variable, only English pages should be looked up. + vec!["en".into()] +} + fn main() { // Initialize logger init_log(); @@ -417,8 +460,15 @@ fn main() { check_cache(&args, enable_styles); } + let languages = if let Some(ref lang) = args.flag_language { + // Language overwritten by console argument + vec![lang.clone()] + } else { + get_languages(std::env::var("LANG"), std::env::var("LANGUAGE")) + }; + // Search for command in cache - if let Some(path) = cache.find_page(&command) { + if let Some(path) = cache.find_page(&command, &languages) { if let Err(msg) = print_page(&path, args.flag_markdown, &config) { eprintln!("{}", msg); process::exit(1); @@ -444,7 +494,7 @@ fn main() { #[cfg(test)] mod test { - use crate::{Args, OsType, USAGE}; + use crate::{get_languages, Args, OsType, USAGE}; use docopt::{Docopt, Error}; fn test_helper(argv: &[&str]) -> Result { @@ -463,4 +513,47 @@ mod test { let argv = vec!["cp", "--os", "lindows"]; assert!(!test_helper(&argv).is_ok()); } + + #[test] + fn test_language_missing_lang_env() { + let lang_list = get_languages(Err(std::env::VarError::NotPresent), Ok("de:fr".into())); + assert_eq!(lang_list, vec!["en"]); + let lang_list = get_languages( + Err(std::env::VarError::NotPresent), + Err(std::env::VarError::NotPresent), + ); + assert_eq!(lang_list, vec!["en"]); + } + + #[test] + fn test_language_missing_language_env() { + let lang_list = get_languages(Ok("de".into()), Err(std::env::VarError::NotPresent)); + assert_eq!(lang_list, vec!["de", "en"]); + } + + #[test] + fn test_language_preference_order() { + let lang_list = get_languages(Ok("de".into()), Ok("fr:cn".into())); + assert_eq!(lang_list, vec!["fr", "cn", "de", "en"]); + } + + #[test] + fn test_language_country_code_expansion() { + let lang_list = get_languages(Ok("pt_BR".into()), Err(std::env::VarError::NotPresent)); + assert_eq!(lang_list, vec!["pt_BR", "pt", "en"]); + } + + #[test] + fn test_language_ignore_posix_and_c() { + let lang_list = get_languages(Ok("POSIX".into()), Err(std::env::VarError::NotPresent)); + assert_eq!(lang_list, vec!["en"]); + let lang_list = get_languages(Ok("C".into()), Err(std::env::VarError::NotPresent)); + assert_eq!(lang_list, vec!["en"]); + } + + #[test] + fn test_language_no_duplicates() { + let lang_list = get_languages(Ok("de".into()), Ok("fr:de:cn:de".into())); + assert_eq!(lang_list, vec!["fr", "de", "cn", "en"]); + } }