//! An implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust. // // Copyright (c) 2015-2018 tealdeer developers // // Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. All files in the project carrying such notice may not be // copied, modified, or distributed except according to those terms. #![deny(clippy::all)] #![warn(clippy::pedantic)] #![allow(clippy::similar_names)] #![allow(clippy::stutter)] #[cfg(feature = "logging")] extern crate env_logger; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; use std::process; use std::time::Duration; use ansi_term::Color; use app_dirs::AppInfo; use docopt::Docopt; use serde_derive::Deserialize; mod cache; mod config; mod error; mod formatter; mod tokenizer; mod types; use crate::cache::Cache; use crate::config::{get_config_path, make_default_config, Config}; use crate::error::TealdeerError::{CacheError, ConfigError, UpdateError}; use crate::formatter::print_lines; use crate::tokenizer::Tokenizer; use crate::types::OsType; const NAME: &str = "tealdeer"; const APP_INFO: AppInfo = AppInfo { name: NAME, author: NAME, }; const VERSION: &str = env!("CARGO_PKG_VERSION"); const USAGE: &str = " Usage: tldr [options] ... tldr [options] 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] -u --update Update the local cache -c --clear-cache Clear the local cache -q --quiet Suppress informational messages --config-path Show config file path --seed-config Create a basic config Examples: $ tldr tar $ tldr --list To control the cache: $ tldr --update $ tldr --clear-cache To render a local file (for testing): $ tldr --render /path/to/file.md "; const ARCHIVE_URL: &str = "https://github.com/tldr-pages/tldr/archive/master.tar.gz"; const MAX_CACHE_AGE: Duration = Duration::from_secs(2_592_000); // 30 days #[derive(Debug, Deserialize)] struct Args { arg_command: Option>, flag_help: bool, flag_version: bool, flag_list: bool, flag_render: Option, flag_os: Option, flag_update: bool, flag_clear_cache: bool, flag_quiet: bool, flag_config_path: bool, flag_seed_config: bool, } /// Print page by path fn print_page(path: &Path) -> Result<(), String> { // Open file let file = File::open(path).map_err(|msg| format!("Could not open file: {}", msg))?; let reader = BufReader::new(file); // Look up config file, if none is found fall back to default config. let config = match Config::load() { Ok(config) => config, Err(ConfigError(msg)) => { eprintln!("Could not load config: {}", msg); process::exit(1); } Err(e) => { eprintln!("Could not load config: {}", e); process::exit(1); } }; // Create tokenizer and print output let mut tokenizer = Tokenizer::new(reader); print_lines(&mut tokenizer, &config); Ok(()) } /// Check the cache for freshness fn check_cache(args: &Args, cache: &Cache) { if !args.flag_update { match cache.last_update() { Some(ago) if ago > MAX_CACHE_AGE => { if args.flag_quiet { return; } println!( "{}", Color::Red.paint(format!( "Cache wasn't updated for more than {} days.\n\ You should probably run `tldr --update` soon.", MAX_CACHE_AGE.as_secs() / 24 / 3600 )) ); } None => { eprintln!("Cache not found. Please run `tldr --update`."); process::exit(1); } _ => {} } }; } #[cfg(feature = "logging")] fn init_log() { env_logger::init(); } #[cfg(not(feature = "logging"))] fn init_log() {} #[cfg(target_os = "linux")] fn get_os() -> OsType { OsType::Linux } #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly"))] fn get_os() -> OsType { OsType::OsX } #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly")))] fn get_os() -> OsType { OsType::Other } fn main() { // Initialize logger init_log(); // Parse arguments let args: Args = Docopt::new(USAGE) .and_then(|d| d.deserialize()) .unwrap_or_else(|e| e.exit()); // Show version and exit if args.flag_version { let os = get_os(); println!("{} v{} ({})", NAME, VERSION, os); process::exit(0); } // Specify target OS let os: OsType = match args.flag_os { Some(os) => os, None => get_os(), }; // Initialize cache let cache = Cache::new(ARCHIVE_URL, os); // Clear cache, pass through if args.flag_clear_cache { cache.clear().unwrap_or_else(|e| { match e { CacheError(msg) | ConfigError(msg) | UpdateError(msg) => { eprintln!("Could not delete cache: {}", msg) } }; process::exit(1); }); if !args.flag_quiet { println!("Successfully deleted cache."); } } // Update cache, pass through if args.flag_update { cache.update().unwrap_or_else(|e| { match e { CacheError(msg) | ConfigError(msg) | UpdateError(msg) => { eprintln!("Could not update cache: {}", msg) } }; process::exit(1); }); if !args.flag_quiet { println!("Successfully updated cache."); } } // Show config file and path, pass through if args.flag_config_path { match get_config_path() { Ok(config_file_path) => { println!("Config path is: {}", config_file_path.to_str().unwrap()); } Err(ConfigError(msg)) => { eprintln!("Could not look up config_path: {}", msg); process::exit(1); } Err(_) => { eprintln!("Unknown error"); process::exit(1); } } } // Create a basic config and exit if args.flag_seed_config { match make_default_config() { Ok(config_file_path) => { println!( "Successfully created seed config file here: {}", config_file_path.to_str().unwrap() ); process::exit(0); } Err(ConfigError(msg)) => { eprintln!("Could not create seed config: {}", msg); process::exit(1); } Err(_) => { eprintln!("Unkown error"); process::exit(1); } } } // Render local file and exit if let Some(ref file) = args.flag_render { let path = PathBuf::from(file); if let Err(msg) = print_page(&path) { eprintln!("{}", msg); process::exit(1); } else { process::exit(0); }; } // List cached commands and exit if args.flag_list { // Check cache for freshness check_cache(&args, &cache); // Get list of pages let pages = cache.list_pages().unwrap_or_else(|e| { match e { CacheError(msg) | ConfigError(msg) | UpdateError(msg) => { eprintln!("Could not get list of pages: {}", msg) } } process::exit(1); }); // Print pages println!("{}", pages.join(", ")); process::exit(0); } // Show command from cache if let Some(ref command) = args.arg_command { let command = command.join("-"); // Check cache for freshness check_cache(&args, &cache); // Search for command in cache if let Some(path) = cache.find_page(&command) { if let Err(msg) = print_page(&path) { eprintln!("{}", msg); process::exit(1); } else { process::exit(0); } } else { if !args.flag_quiet { println!("Page {} not found in cache", &command); println!("Try updating with `tldr --update`, or submit a pull request to:"); println!("https://github.com/tldr-pages/tldr"); } process::exit(1); } } // Some flags can be run without a command. if !(args.flag_update || args.flag_clear_cache || args.flag_config_path) { eprintln!("{}", USAGE); process::exit(1); } } #[cfg(test)] mod test { use docopt::{Docopt, Error}; use crate::{Args, OsType, USAGE}; fn test_helper(argv: &[&str]) -> Result { Docopt::new(USAGE).and_then(|d| d.argv(argv.iter()).deserialize()) } #[test] fn test_docopt_os_case_insensitive() { let argv = vec!["cp", "--os", "LiNuX"]; let os = test_helper(&argv).unwrap().flag_os.unwrap(); assert_eq!(OsType::Linux, os); } #[test] fn test_docopt_expect_error() { let argv = vec!["cp", "--os", "lindows"]; assert!(!test_helper(&argv).is_ok()); } }