diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca3deae..36d1f55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - rust: [1.53, stable] + rust: [1.54, stable] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 @@ -60,7 +60,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.53 + toolchain: 1.54 override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 diff --git a/Cargo.lock b/Cargo.lock index 72a085f..14828af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,36 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "combine" version = "4.6.1" @@ -221,18 +251,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "docopt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" -dependencies = [ - "lazy_static", - "regex", - "serde", - "strsim 0.10.0", -] - [[package]] name = "either" version = "1.6.1" @@ -450,6 +468,15 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -799,6 +826,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +[[package]] +name = "os_str_bytes" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" +dependencies = [ + "memchr", +] + [[package]] name = "pager" version = "0.16.0" @@ -882,6 +918,30 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1223,7 +1283,7 @@ dependencies = [ "app_dirs2", "assert_cmd", "atty", - "docopt", + "clap", "env_logger", "escargot", "filetime", @@ -1262,6 +1322,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + [[package]] name = "thiserror" version = "1.0.29" @@ -1400,6 +1466,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -1424,6 +1496,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 6eb8fee..5d053f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ path = "src/main.rs" ansi_term = "0.12.0" app_dirs = { version = "2", package = "app_dirs2" } atty = "0.2" -docopt = "1" +clap = { version = "3.0.0-beta.5", features = ["std", "derive", "color", "suggestions" ], default-features = false } env_logger = { version = "0.9", optional = true } log = "0.4" reqwest = { version = "0.11.3", features = ["blocking", "rustls-tls", "rustls-tls-native-roots"], default-features = false } diff --git a/clippy.toml b/clippy.toml index 349b690..ece14b8 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.53" +msrv = "1.54" diff --git a/docs/src/usage.md b/docs/src/usage.md index c8fa308..6fba933 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -4,6 +4,7 @@ Tealdeer is straightforward to use, through the binary named `tldr`. You can view the available options using `tldr --help`: + ``` -{{#include ../../src/usage.docopt}} +{{#include usage.txt}} ``` diff --git a/docs/src/usage.txt b/docs/src/usage.txt new file mode 100644 index 0000000..695eda1 --- /dev/null +++ b/docs/src/usage.txt @@ -0,0 +1,28 @@ +tealdeer 1.4.1 + +Danilo Bargen + +A fast TLDR client + +USAGE: + tldr [OPTIONS] [COMMAND]... + +ARGS: + ... The command to show (e.g. `tar` or `git log`) + +OPTIONS: + -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 + -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 + --show-paths Show file and directory paths used by tealdeer + --config-path Show config file path + --seed-config Create a basic config + --color Control whether to use color [always, auto, never] + -v, --version Print the version + -h, --help Print help information diff --git a/fish_tealdeer b/fish_tealdeer index 1aa95c5..7f2c63f 100644 --- a/fish_tealdeer +++ b/fish_tealdeer @@ -7,7 +7,7 @@ complete -c tldr -s h -l help -d 'Print the help message.' -f complete -c tldr -s v -l version -d 'Show version information.' -f complete -c tldr -s l -l list -d 'List all commands in the cache.' -f complete -c tldr -s f -l render -d 'Render a specific markdown file.' -r -complete -c tldr -s o -l os -d 'Override the operating system.' -xa 'linux osx sunos windows other' +complete -c tldr -s o -l os -d 'Override the operating system.' -xa 'linux osx sunos windows' complete -c tldr -s u -l update -d 'Update the local cache.' -f complete -c tldr -s c -l clear-cache -d 'Clear the local cache.' -f complete -c tldr -s p -l pager -d 'Use a pager to page output.' -f diff --git a/src/cache.rs b/src/cache.rs index 0161de4..5bfccb5 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -201,7 +201,6 @@ impl Cache { OsType::OsX => Some("osx"), OsType::SunOs => Some("sunos"), OsType::Windows => Some("windows"), - OsType::Other => None, } } diff --git a/src/main.rs b/src/main.rs index e87bc5c..03277dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,10 +20,9 @@ use std::{env, path::PathBuf, process}; use ansi_term::{Color, Style}; use app_dirs::AppInfo; use atty::Stream; -use docopt::Docopt; +use clap::{AppSettings, Parser}; #[cfg(not(target_os = "windows"))] use pager::Pager; -use serde_derive::Deserialize; mod cache; mod config; @@ -48,31 +47,82 @@ const APP_INFO: AppInfo = AppInfo { name: NAME, author: NAME, }; -const VERSION: &str = env!("CARGO_PKG_VERSION"); -const USAGE: &str = include_str!("usage.docopt"); const ARCHIVE_URL: &str = "https://tldr.sh/assets/tldr.zip"; #[cfg(not(target_os = "windows"))] const PAGER_COMMAND: &str = "less -R"; -#[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Deserialize)] +#[derive(Parser, Debug)] +#[clap(about = "A fast TLDR client", author, version)] +#[clap(setting = AppSettings::ArgRequiredElseHelp)] +#[clap(setting = AppSettings::HelpRequired)] +#[clap(setting = AppSettings::DeriveDisplayOrder)] 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_pager: bool, - flag_quiet: bool, - flag_show_paths: bool, - flag_config_path: bool, - flag_seed_config: bool, - flag_markdown: bool, - flag_color: ColorOptions, - flag_language: Option, + /// The command to show (e.g. `tar` or `git log`) + #[clap(min_values = 1)] + command: Vec, + + /// List all commands in the cache + #[clap(short = 'l', long = "list")] + list: bool, + + /// Render a specific markdown file + #[clap( + short = 'f', + long = "render", + value_name = "FILE", + conflicts_with = "command" + )] + render: Option, + + /// Override the operating system [linux, osx, sunos, windows] + #[clap(short = 'o', long = "os", requires = "command")] + os: Option, + + /// Override the language + #[clap(short = 'L', long = "language")] + language: Option, + + /// Update the local cache + #[clap(short = 'u', long = "update")] + update: bool, + + /// Clear the local cache + #[clap(short = 'c', long = "clear-cache")] + clear_cache: bool, + + /// Use a pager to page output + #[clap(short = 'p', long = "pager", requires = "command")] + pager: bool, + + /// Display the raw markdown instead of rendering it + #[clap(short = 'm', long = "markdown", requires = "command")] + markdown: bool, + + /// Suppress informational messages + #[clap(short = 'q', long = "quiet")] + quiet: bool, + + /// Show file and directory paths used by tealdeer + #[clap(long = "show-paths")] + show_paths: bool, + + /// Show config file path + #[clap(long = "config-path")] + config_path: bool, + + /// Create a basic config + #[clap(long = "seed-config")] + seed_config: bool, + + /// Control whether to use color [always, auto, never] + #[clap(long = "color", value_name = "WHEN")] + color: Option, + + /// Print the version + // Note: We override the version flag because clap uses `-V` by default, + // while TLDR specification requires `-v` to be used. + #[clap(short = 'v', long = "version")] + version: bool, } /// Set up display pager @@ -89,7 +139,7 @@ fn configure_pager() { /// The cache should get updated if this was requested by the user, or if auto /// updates are enabled and the cache age is longer than the auto update interval. fn should_update_cache(args: &Args, config: &Config) -> bool { - args.flag_update + args.update || (config.updates.auto_update && Cache::last_update().map_or(true, |ago| ago >= config.updates.auto_update_interval)) } @@ -104,7 +154,7 @@ enum CheckCacheResult { fn check_cache(args: &Args, enable_styles: bool) -> CheckCacheResult { match Cache::freshness() { CacheFreshness::Fresh => CheckCacheResult::CacheFound, - CacheFreshness::Stale(_) if args.flag_quiet => CheckCacheResult::CacheFound, + CacheFreshness::Stale(_) if args.quiet => CheckCacheResult::CacheFound, CacheFreshness::Stale(age) => { let warning_style = if enable_styles { Style::new().fg(Color::Yellow) @@ -313,28 +363,19 @@ fn main() { 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); - } + let args: Args = Args::parse(); // Show config file and path, pass through - if args.flag_config_path { + if args.config_path { eprintln!("Warning: The --config-path flag is deprecated, use --show-paths instead"); show_config_path(); } - if args.flag_show_paths { + if args.show_paths { show_paths(); } // Create a basic config and exit - if args.flag_seed_config { + if args.seed_config { create_config_and_exit(); } @@ -344,7 +385,7 @@ fn main() { #[cfg(not(target_os = "windows"))] let ansi_support = true; - let enable_styles = match args.flag_color { + let enable_styles = match args.color.unwrap_or(ColorOptions::Auto) { // Attempt to use styling if instructed ColorOptions::Always => true, // Enable styling if: @@ -371,20 +412,20 @@ fn main() { } }; - if args.flag_pager || config.display.use_pager { + if args.pager || config.display.use_pager { configure_pager(); } // Specify target OS - let os: OsType = match args.flag_os { + let os: OsType = match args.os { Some(os) => os, None => get_os(), }; // If a local file was passed in, render it and exit - if let Some(ref file) = args.flag_render { + if let Some(ref file) = args.render { let path = PageLookupResult::with_page(PathBuf::from(file)); - if let Err(msg) = print_page(&path, args.flag_markdown, &config) { + if let Err(msg) = print_page(&path, args.markdown, &config) { eprintln!("{}", msg); process::exit(1); } else { @@ -396,13 +437,13 @@ fn main() { let cache = Cache::new(ARCHIVE_URL, os); // Clear cache, pass through - if args.flag_clear_cache { - clear_cache(args.flag_quiet); + if args.clear_cache { + clear_cache(args.quiet); } // Cache update, pass through let cache_updated = if should_update_cache(&args, &config) { - update_cache(&cache, args.flag_quiet); + update_cache(&cache, args.quiet); true } else { false @@ -410,14 +451,14 @@ fn main() { // Check cache presence and freshness if !cache_updated - && (args.flag_list || args.arg_command.is_some()) + && (args.list || !args.command.is_empty()) && check_cache(&args, enable_styles) == CheckCacheResult::CacheMissing { process::exit(1); } // List cached commands and exit - if args.flag_list { + if args.list { // Get list of pages let pages = cache.list_pages().unwrap_or_else(|e| { eprintln!("Could not get list of pages: {}", e.message()); @@ -430,12 +471,12 @@ fn main() { } // Show command from cache - if let Some(ref command) = args.arg_command { - let command = command.join("-"); + if !args.command.is_empty() { + let command = args.command.join("-"); let languages = args - .flag_language - .map_or_else(get_languages_from_env, |flag_lang| vec![flag_lang]); + .language + .map_or_else(get_languages_from_env, |lang| vec![lang]); // Search for command in cache if let Some(page) = cache.find_page( @@ -443,13 +484,13 @@ fn main() { &languages, config.directories.custom_pages_dir.as_deref(), ) { - if let Err(msg) = print_page(&page, args.flag_markdown, &config) { + if let Err(msg) = print_page(&page, args.markdown, &config) { eprintln!("{}", msg); process::exit(1); } process::exit(0); } else { - if !args.flag_quiet { + if !args.quiet { eprintln!("Page {} not found in cache", &command); eprintln!("Try updating with `tldr --update`, or submit a pull request to:"); eprintln!("https://github.com/tldr-pages/tldr"); @@ -457,36 +498,11 @@ fn main() { process::exit(1); } } - - // Some flags can be run without a command. - if !(args.flag_update || args.flag_clear_cache || args.flag_config_path || args.flag_show_paths) - { - eprintln!("{}", USAGE); - process::exit(1); - } } #[cfg(test)] mod test { - use crate::{get_languages, Args, OsType, USAGE}; - use docopt::{Docopt, Error}; - - fn test_helper(argv: &[&str]) -> Result { - Docopt::new(USAGE).and_then(|d| d.argv(argv).deserialize()) - } - - #[test] - fn test_docopt_os_case_insensitive() { - let argv = ["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 = ["cp", "--os", "lindows"]; - assert!(!test_helper(&argv).is_ok()); - } + use crate::get_languages; mod language { use super::*; diff --git a/src/types.rs b/src/types.rs index 180185a..2d938a5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ //! Shared types used in tealdeer. -use std::fmt; +use std::{fmt, str}; use serde_derive::{Deserialize, Serialize}; @@ -12,7 +12,6 @@ pub enum OsType { OsX, SunOs, Windows, - Other, } impl fmt::Display for OsType { @@ -22,7 +21,23 @@ impl fmt::Display for OsType { Self::OsX => write!(f, "macOS / BSD"), Self::SunOs => write!(f, "SunOS"), Self::Windows => write!(f, "Windows"), - Self::Other => write!(f, "Unknown OS"), + } + } +} + +impl str::FromStr for OsType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "linux" => Ok(Self::Linux), + "osx" => Ok(Self::OsX), + "sunos" => Ok(Self::SunOs), + "windows" => Ok(Self::Windows), + other => Err(format!( + "Unknown OS: {}. Possible values: linux, osx, sunos, windows", + other + )), } } } @@ -35,6 +50,22 @@ pub enum ColorOptions { Never, } +impl str::FromStr for ColorOptions { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "always" => Ok(Self::Always), + "auto" => Ok(Self::Auto), + "never" => Ok(Self::Never), + other => Err(format!( + "Unknown color option: {}. Possible values: always, auto, never", + other + )), + } + } +} + #[derive(Debug, Eq, PartialEq)] pub enum LineType { Empty, diff --git a/src/usage.docopt b/src/usage.docopt deleted file mode 100644 index 9ba0968..0000000 --- a/src/usage.docopt +++ /dev/null @@ -1,36 +0,0 @@ -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, 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 - --show-paths Show file and directory paths used by tealdeer - --config-path Show config file path (deprecated) - --seed-config Create a basic config - --color Control when to use color [always, auto, never] [default: auto] - -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