use std::str::FromStr;
use clap::{
Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum,
builder::{ValueParser, styling::Style},
};
use clap_stdin::{FileOrStdin, MaybeStdin};
use color_eyre::{Result, eyre::eyre};
use itertools::Itertools;
use regex::Regex;
use reqwest::{
Method,
header::{HeaderName, HeaderValue},
};
use semver::Version;
use tracing::instrument;
use crate::model::SearchMode;
#[derive(Parser)]
#[cfg_attr(test, derive(Debug))]
#[command(
author,
version,
verbatim_doc_comment,
infer_subcommands = true,
subcommand_required = true,
after_long_help = include_str!("_examples/cli.txt")
)]
pub struct Cli {
#[arg(long, hide = true)]
pub skip_execution: bool,
#[arg(long, hide = true)]
pub extra_line: bool,
#[arg(long, hide = true)]
pub file_output: Option<String>,
#[command(name = "command", subcommand)]
pub process: CliProcess,
}
#[derive(Subcommand)]
#[cfg_attr(test, derive(Debug))]
pub enum CliProcess {
#[cfg(debug_assertions)]
Query(QueryProcess),
#[command(after_long_help = include_str!("_examples/init.txt"))]
Init(InitProcess),
Config(ConfigProcess),
Logs(LogsProcess),
#[command(after_long_help = include_str!("_examples/new.txt"))]
New(Interactive<BookmarkCommandProcess>),
#[command(after_long_help = include_str!("_examples/search.txt"))]
Search(Interactive<SearchCommandsProcess>),
#[command(after_long_help = include_str!("_examples/replace.txt"))]
Replace(Interactive<VariableReplaceProcess>),
#[command(after_long_help = include_str!("_examples/fix.txt"))]
Fix(CommandFixProcess),
#[command(after_long_help = include_str!("_examples/export.txt"))]
Export(Interactive<ExportItemsProcess>),
#[command(after_long_help = include_str!("_examples/import.txt"))]
Import(Interactive<ImportItemsProcess>),
#[cfg(feature = "tldr")]
#[command(name = "tldr", subcommand)]
Tldr(TldrProcess),
#[command(subcommand)]
Completion(CompletionProcess),
#[cfg(feature = "self-update")]
Update(UpdateProcess),
Changelog(ChangelogProcess),
}
#[cfg(feature = "tldr")]
#[derive(Subcommand)]
#[cfg_attr(test, derive(Debug))]
pub enum TldrProcess {
#[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
Fetch(TldrFetchProcess),
#[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
Clear(TldrClearProcess),
}
#[derive(Subcommand)]
#[cfg_attr(test, derive(Debug))]
pub enum CompletionProcess {
#[command(after_long_help = include_str!("_examples/completion_new.txt"))]
New(Interactive<CompletionNewProcess>),
#[command(after_long_help = include_str!("_examples/completion_delete.txt"))]
Delete(CompletionDeleteProcess),
#[command(alias = "ls", after_long_help = include_str!("_examples/completion_list.txt"))]
List(Interactive<CompletionListProcess>),
}
#[derive(Args, Debug)]
pub struct Interactive<T: FromArgMatches + Args> {
#[command(flatten)]
pub process: T,
#[command(flatten)]
pub opts: InteractiveOptions,
}
#[derive(Args, Debug)]
pub struct InteractiveOptions {
#[arg(short = 'i', long)]
pub interactive: bool,
#[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
pub inline: bool,
#[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
pub full_screen: bool,
}
#[cfg(debug_assertions)]
#[derive(Args, Debug)]
pub struct QueryProcess {
#[arg(default_value = "-")]
pub sql: FileOrStdin,
}
#[derive(Args, Debug)]
pub struct InitProcess {
#[arg(value_enum)]
pub shell: Shell,
}
#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
pub enum Shell {
Bash,
Zsh,
Fish,
#[value(alias("pwsh"))]
Powershell,
#[value(alias("nu"))]
Nushell,
}
#[derive(Args, Debug)]
pub struct ConfigProcess {
#[arg(short = 'p', long)]
pub path: bool,
}
#[derive(Args, Debug)]
pub struct LogsProcess {
#[arg(short = 'p', long)]
pub path: bool,
}
#[derive(Args, Debug)]
pub struct BookmarkCommandProcess {
#[arg(required_unless_present = "interactive")]
pub command: Option<String>,
#[arg(short = 'a', long)]
pub alias: Option<String>,
#[arg(short = 'd', long)]
pub description: Option<String>,
#[arg(long)]
pub ai: bool,
}
#[derive(Args, Debug)]
pub struct SearchCommandsProcess {
pub query: Option<String>,
#[arg(short = 'm', long)]
pub mode: Option<SearchMode>,
#[arg(short = 'u', long)]
pub user_only: bool,
#[arg(long, requires = "query")]
pub ai: bool,
}
#[derive(Args, Debug)]
pub struct VariableReplaceProcess {
#[arg(default_value = "-")]
pub command: MaybeStdin<String>,
#[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
pub values: Vec<(String, Option<String>)>,
#[arg(short = 'E', long)]
pub use_env: bool,
}
#[derive(Args, Debug)]
pub struct CommandFixProcess {
pub command: String,
#[arg(long, value_name = "HISTORY")]
pub history: Option<String>,
}
#[derive(Args, Clone, Debug)]
pub struct ExportItemsProcess {
#[arg(default_value = "-")]
pub location: String,
#[arg(long, group = "location_type")]
pub file: bool,
#[arg(long, group = "location_type")]
pub http: bool,
#[arg(long, group = "location_type")]
pub gist: bool,
#[arg(long, value_name = "REGEX")]
pub filter: Option<Regex>,
#[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
pub headers: Vec<(HeaderName, HeaderValue)>,
#[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
pub method: HttpMethod,
}
#[derive(Args, Clone, Debug)]
pub struct ImportItemsProcess {
#[arg(default_value = "-", required_unless_present = "history")]
pub location: String,
#[arg(long)]
pub ai: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long, group = "location_type")]
pub file: bool,
#[arg(long, group = "location_type")]
pub http: bool,
#[arg(long, group = "location_type")]
pub gist: bool,
#[arg(long, value_enum, group = "location_type", requires = "ai")]
pub history: Option<HistorySource>,
#[arg(long, value_name = "REGEX")]
pub filter: Option<Regex>,
#[arg(short = 't', long = "add-tag", value_name = "TAG")]
pub tags: Vec<String>,
#[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
pub headers: Vec<(HeaderName, HeaderValue)>,
#[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
pub method: HttpMethod,
}
#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
pub enum HistorySource {
Bash,
Zsh,
Fish,
#[value(alias("pwsh"))]
Powershell,
#[value(alias("nu"))]
Nushell,
Atuin,
}
#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
pub enum HttpMethod {
GET,
POST,
PUT,
PATCH,
}
impl From<HttpMethod> for Method {
fn from(value: HttpMethod) -> Self {
match value {
HttpMethod::GET => Method::GET,
HttpMethod::POST => Method::POST,
HttpMethod::PUT => Method::PUT,
HttpMethod::PATCH => Method::PATCH,
}
}
}
#[cfg(feature = "tldr")]
#[derive(Args, Debug)]
pub struct TldrFetchProcess {
pub category: Option<String>,
#[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
pub commands: Vec<String>,
#[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
pub filter_commands: Option<FileOrStdin>,
}
#[cfg(feature = "tldr")]
#[derive(Args, Debug)]
pub struct TldrClearProcess {
pub category: Option<String>,
}
#[derive(Args, Debug)]
pub struct CompletionNewProcess {
#[arg(short = 'c', long)]
pub command: Option<String>,
#[arg(required_unless_present = "interactive")]
pub variable: Option<String>,
#[arg(required_unless_present_any = ["interactive", "ai"])]
pub provider: Option<String>,
#[arg(long)]
pub ai: bool,
}
#[derive(Args, Debug)]
pub struct CompletionDeleteProcess {
#[arg(short = 'c', long)]
pub command: Option<String>,
pub variable: String,
}
#[derive(Args, Debug)]
pub struct CompletionListProcess {
pub command: Option<String>,
}
#[cfg(feature = "self-update")]
#[derive(Args, Debug)]
pub struct UpdateProcess {}
#[derive(Args, Debug)]
pub struct ChangelogProcess {
#[arg(long, alias = "since", value_parser = parse_version, default_value = env!("CARGO_PKG_VERSION"))]
pub from: Version,
#[arg(long, alias = "until", value_parser = parse_version)]
pub to: Option<Version>,
#[arg(long, conflicts_with = "minor")]
pub major: bool,
#[arg(long, conflicts_with = "major")]
pub minor: bool,
}
impl Cli {
#[instrument]
pub fn parse_extended() -> Self {
let mut cmd = Self::command_for_update();
let style = cmd.get_styles().clone();
let dimmed = style.get_placeholder().dimmed();
let plain_examples_header = "Examples:";
let styled_examples_header = format!(
"{}Examples:{}",
style.get_usage().render(),
style.get_usage().render_reset()
);
style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
let matches = cmd.get_matches();
match Cli::from_arg_matches(&matches) {
Ok(args) => args,
Err(err) => err.exit(),
}
}
}
fn style_after_long_help(
command_ref: &mut Command,
dimmed: &Style,
plain_examples_header: &str,
styled_examples_header: &str,
) {
let mut command = std::mem::take(command_ref);
if let Some(after_long_help) = command.get_after_long_help() {
let current_help_text = after_long_help.to_string();
let modified_help_text = current_help_text
.replace(plain_examples_header, styled_examples_header)
.lines()
.map(|line| {
if line.trim_start().starts_with('#') {
format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
} else {
line.to_string()
}
}).join("\n");
command = command.after_long_help(modified_help_text);
}
for subcommand_ref in command.get_subcommands_mut() {
style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
}
*command_ref = command;
}
fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
if let Some((var, value)) = env.split_once('=') {
Ok((var.to_owned(), Some(value.to_owned())))
} else {
Ok((env.to_owned(), None))
}
}
fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
if let Some((name, value)) = env.split_once(':') {
Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
} else {
Err(eyre!("Missing a colon between the header name and value"))
}
}
fn parse_version(s: &str) -> Result<Version, <Version as FromStr>::Err> {
let version_str = s.strip_prefix('v').unwrap_or(s);
version_str.parse()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_asserts() {
Cli::command().debug_assert()
}
}