Argument parsing: Switch from docopt to clap

This commit is contained in:
Danilo Bargen 2020-03-07 17:50:12 +01:00
commit 5ad7dcfa34
11 changed files with 255 additions and 138 deletions

View file

@ -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

104
Cargo.lock generated
View file

@ -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"

View file

@ -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 }

View file

@ -1 +1 @@
msrv = "1.53"
msrv = "1.54"

View file

@ -4,6 +4,7 @@ Tealdeer is straightforward to use, through the binary named `tldr`.
You can view the available options using `tldr --help`:
<!-- Note: To update the file below, run `cargo run -- --help > docs/src/usage.txt`. -->
```
{{#include ../../src/usage.docopt}}
{{#include usage.txt}}
```

28
docs/src/usage.txt Normal file
View file

@ -0,0 +1,28 @@
tealdeer 1.4.1
Danilo Bargen <mail@dbrgn.ch>
A fast TLDR client
USAGE:
tldr [OPTIONS] [COMMAND]...
ARGS:
<COMMAND>... The command to show (e.g. `tar` or `git log`)
OPTIONS:
-l, --list List all commands in the cache
-f, --render <FILE> Render a specific markdown file
-o, --os <OS> Override the operating system [linux, osx, sunos, windows]
-L, --language <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 <WHEN> Control whether to use color [always, auto, never]
-v, --version Print the version
-h, --help Print help information

View file

@ -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

View file

@ -201,7 +201,6 @@ impl Cache {
OsType::OsX => Some("osx"),
OsType::SunOs => Some("sunos"),
OsType::Windows => Some("windows"),
OsType::Other => None,
}
}

View file

@ -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<Vec<String>>,
flag_help: bool,
flag_version: bool,
flag_list: bool,
flag_render: Option<String>,
flag_os: Option<OsType>,
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<String>,
/// The command to show (e.g. `tar` or `git log`)
#[clap(min_values = 1)]
command: Vec<String>,
/// 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<String>,
/// Override the operating system [linux, osx, sunos, windows]
#[clap(short = 'o', long = "os", requires = "command")]
os: Option<OsType>,
/// Override the language
#[clap(short = 'L', long = "language")]
language: Option<String>,
/// 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<ColorOptions>,
/// 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<Args, Error> {
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::*;

View file

@ -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<Self, Self::Err> {
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<Self, Self::Err> {
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,

View file

@ -1,36 +0,0 @@
Usage:
tldr [options] <command>...
tldr [options]
Options:
-h --help Show this screen
-v --version Show version information
-l --list List all commands in the cache
-f --render <file> Render a specific markdown file
-o --os <type> Override the operating system [linux, osx, sunos, windows]
-L --language <lang> 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 <when> 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