1use std::str::FromStr;
2
3use clap::{
4 Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum,
5 builder::{ValueParser, styling::Style},
6};
7use clap_stdin::{FileOrStdin, MaybeStdin};
8use color_eyre::{Result, eyre::eyre};
9use itertools::Itertools;
10use regex::Regex;
11use reqwest::{
12 Method,
13 header::{HeaderName, HeaderValue},
14};
15use tracing::instrument;
16
17use crate::model::SearchMode;
18
19#[derive(Parser)]
27#[cfg_attr(debug_assertions, derive(Debug))]
28#[command(
29 author,
30 version,
31 verbatim_doc_comment,
32 infer_subcommands = true,
33 after_long_help = include_str!("_examples/cli.txt")
34)]
35pub struct Cli {
36 #[arg(long, hide = true)]
40 pub skip_execution: bool,
41
42 #[arg(long, hide = true)]
46 pub extra_line: bool,
47
48 #[arg(long, hide = true)]
53 pub file_output: Option<String>,
54
55 #[command(name = "command", subcommand)]
57 pub process: CliProcess,
58}
59
60#[derive(Subcommand)]
61#[cfg_attr(debug_assertions, derive(Debug))]
62pub enum CliProcess {
63 #[cfg(debug_assertions)]
64 Query(QueryProcess),
66
67 #[command(after_long_help = include_str!("_examples/init.txt"))]
69 Init(InitProcess),
70
71 #[command(after_long_help = include_str!("_examples/new.txt"))]
73 New(Interactive<BookmarkCommandProcess>),
74
75 #[command(after_long_help = include_str!("_examples/search.txt"))]
77 Search(Interactive<SearchCommandsProcess>),
78
79 #[command(after_long_help = include_str!("_examples/replace.txt"))]
85 Replace(Interactive<VariableReplaceProcess>),
86
87 #[command(after_long_help = include_str!("_examples/fix.txt"))]
92 Fix(CommandFixProcess),
93
94 #[command(after_long_help = include_str!("_examples/export.txt"))]
98 Export(Interactive<ExportCommandsProcess>),
99
100 #[command(after_long_help = include_str!("_examples/import.txt"))]
102 Import(Interactive<ImportCommandsProcess>),
103
104 #[command(name = "tldr", subcommand)]
106 Tldr(TldrProcess),
107}
108
109#[derive(Subcommand)]
110#[cfg_attr(debug_assertions, derive(Debug))]
111pub enum TldrProcess {
112 #[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
116 Fetch(TldrFetchProcess),
117
118 #[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
120 Clear(TldrClearProcess),
121}
122
123#[derive(Args, Debug)]
127pub struct Interactive<T: FromArgMatches + Args> {
128 #[command(flatten)]
130 pub process: T,
131
132 #[command(flatten)]
134 pub opts: InteractiveOptions,
135}
136
137#[derive(Args, Debug)]
139pub struct InteractiveOptions {
140 #[arg(short = 'i', long)]
142 pub interactive: bool,
143
144 #[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
146 pub inline: bool,
147
148 #[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
150 pub full_screen: bool,
151}
152
153#[cfg(debug_assertions)]
154#[derive(Args, Debug)]
156pub struct QueryProcess {
157 #[arg(default_value = "-")]
159 pub sql: FileOrStdin,
160}
161
162#[derive(Args, Debug)]
164pub struct InitProcess {
165 #[arg(value_enum)]
167 pub shell: Shell,
168}
169
170#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
171pub enum Shell {
172 Bash,
173 Zsh,
174 Fish,
175 Powershell,
176}
177
178#[derive(Args, Debug)]
180pub struct BookmarkCommandProcess {
181 #[arg(required_unless_present = "interactive")]
185 pub command: Option<String>,
186
187 #[arg(short = 'a', long)]
189 pub alias: Option<String>,
190
191 #[arg(short = 'd', long)]
193 pub description: Option<String>,
194
195 #[arg(long)]
197 pub ai: bool,
198}
199
200#[derive(Args, Debug)]
202pub struct SearchCommandsProcess {
203 pub query: Option<String>,
205
206 #[arg(short = 'm', long)]
208 pub mode: Option<SearchMode>,
209
210 #[arg(short = 'u', long)]
212 pub user_only: bool,
213
214 #[arg(long, requires = "query")]
216 pub ai: bool,
217}
218
219#[derive(Args, Debug)]
221pub struct VariableReplaceProcess {
222 #[arg(default_value = "-")]
226 pub command: MaybeStdin<String>,
227
228 #[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
232 pub values: Vec<(String, Option<String>)>,
233
234 #[arg(short = 'E', long)]
243 pub use_env: bool,
244}
245
246#[derive(Args, Debug)]
248pub struct CommandFixProcess {
249 pub command: String,
251
252 #[arg(long, value_name = "HISTORY")]
256 pub history: Option<String>,
257}
258
259#[derive(Args, Clone, Debug)]
261pub struct ExportCommandsProcess {
262 #[arg(default_value = "-")]
266 pub location: String,
267 #[arg(long, group = "location_type")]
269 pub file: bool,
270 #[arg(long, group = "location_type")]
272 pub http: bool,
273 #[arg(long, group = "location_type")]
275 pub gist: bool,
276 #[arg(long, value_name = "REGEX")]
280 pub filter: Option<Regex>,
281 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
286 pub headers: Vec<(HeaderName, HeaderValue)>,
287 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
291 pub method: HttpMethod,
292}
293
294#[derive(Args, Clone, Debug)]
296pub struct ImportCommandsProcess {
297 #[arg(default_value = "-", required_unless_present = "history")]
301 pub location: String,
302 #[arg(long)]
304 pub ai: bool,
305 #[arg(long)]
309 pub dry_run: bool,
310 #[arg(long, group = "location_type")]
312 pub file: bool,
313 #[arg(long, group = "location_type")]
315 pub http: bool,
316 #[arg(long, group = "location_type")]
318 pub gist: bool,
319 #[arg(long, value_enum, group = "location_type", requires = "ai")]
321 pub history: Option<HistorySource>,
322 #[arg(long, value_name = "REGEX")]
326 pub filter: Option<Regex>,
327 #[arg(short = 't', long = "add-tag", value_name = "TAG")]
332 pub tags: Vec<String>,
333 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
338 pub headers: Vec<(HeaderName, HeaderValue)>,
339 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
343 pub method: HttpMethod,
344}
345
346#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
347pub enum HistorySource {
348 Bash,
349 Zsh,
350 Fish,
351 Powershell,
352 Atuin,
353}
354
355#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
356pub enum HttpMethod {
357 GET,
358 POST,
359 PUT,
360 PATCH,
361}
362impl From<HttpMethod> for Method {
363 fn from(value: HttpMethod) -> Self {
364 match value {
365 HttpMethod::GET => Method::GET,
366 HttpMethod::POST => Method::POST,
367 HttpMethod::PUT => Method::PUT,
368 HttpMethod::PATCH => Method::PATCH,
369 }
370 }
371}
372
373#[derive(Args, Debug)]
375pub struct TldrFetchProcess {
376 pub category: Option<String>,
380
381 #[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
386 pub commands: Vec<String>,
387
388 #[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
392 pub filter_commands: Option<FileOrStdin>,
393}
394
395#[derive(Args, Debug)]
397pub struct TldrClearProcess {
398 pub category: Option<String>,
402}
403
404impl Cli {
405 #[instrument]
407 pub fn parse_extended() -> Self {
408 let mut cmd = Self::command_for_update();
410
411 let style = cmd.get_styles().clone();
413 let dimmed = style.get_placeholder().dimmed();
414 let plain_examples_header = "Examples:";
415 let styled_examples_header = format!(
416 "{}Examples:{}",
417 style.get_usage().render(),
418 style.get_usage().render_reset()
419 );
420 style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
421
422 let matches = cmd.get_matches();
424
425 match Cli::from_arg_matches(&matches) {
427 Ok(args) => args,
428 Err(err) => err.exit(),
429 }
430 }
431}
432
433fn style_after_long_help(
434 command_ref: &mut Command,
435 dimmed: &Style,
436 plain_examples_header: &str,
437 styled_examples_header: &str,
438) {
439 let mut command = std::mem::take(command_ref);
440 if let Some(after_long_help) = command.get_after_long_help() {
441 let current_help_text = after_long_help.to_string();
442 let modified_help_text = current_help_text
443 .replace(plain_examples_header, styled_examples_header)
445 .lines()
447 .map(|line| {
448 if line.trim_start().starts_with('#') {
449 format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
450 } else {
451 line.to_string()
452 }
453 }).join("\n");
454 command = command.after_long_help(modified_help_text);
455 }
456 for subcommand_ref in command.get_subcommands_mut() {
457 style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
458 }
459 *command_ref = command;
460}
461
462fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
463 if let Some((var, value)) = env.split_once('=') {
464 Ok((var.to_owned(), Some(value.to_owned())))
465 } else {
466 Ok((env.to_owned(), None))
467 }
468}
469
470fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
471 if let Some((name, value)) = env.split_once(':') {
472 Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
473 } else {
474 Err(eyre!("Missing a colon between the header name and value"))
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_cli_asserts() {
484 Cli::command().debug_assert()
485 }
486}