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 semver::Version;
16use tracing::instrument;
17
18use crate::model::SearchMode;
19
20#[derive(Parser)]
28#[cfg_attr(test, derive(Debug))]
29#[command(
30 author,
31 version,
32 verbatim_doc_comment,
33 infer_subcommands = true,
34 subcommand_required = true,
35 after_long_help = include_str!("_examples/cli.txt")
36)]
37pub struct Cli {
38 #[arg(long, hide = true)]
42 pub skip_execution: bool,
43
44 #[arg(long, hide = true)]
48 pub extra_line: bool,
49
50 #[arg(long, hide = true)]
55 pub file_output: Option<String>,
56
57 #[command(name = "command", subcommand)]
59 pub process: CliProcess,
60}
61
62#[derive(Subcommand)]
63#[cfg_attr(test, derive(Debug))]
64pub enum CliProcess {
65 #[cfg(debug_assertions)]
66 Query(QueryProcess),
68
69 #[command(after_long_help = include_str!("_examples/init.txt"))]
71 Init(InitProcess),
72
73 Config(ConfigProcess),
75
76 Logs(LogsProcess),
78
79 #[command(after_long_help = include_str!("_examples/new.txt"))]
81 New(Interactive<BookmarkCommandProcess>),
82
83 #[command(after_long_help = include_str!("_examples/search.txt"))]
85 Search(Interactive<SearchCommandsProcess>),
86
87 #[command(after_long_help = include_str!("_examples/replace.txt"))]
93 Replace(Interactive<VariableReplaceProcess>),
94
95 #[command(after_long_help = include_str!("_examples/fix.txt"))]
100 Fix(CommandFixProcess),
101
102 #[command(after_long_help = include_str!("_examples/export.txt"))]
106 Export(Interactive<ExportItemsProcess>),
107
108 #[command(after_long_help = include_str!("_examples/import.txt"))]
110 Import(Interactive<ImportItemsProcess>),
111
112 #[cfg(feature = "tldr")]
113 #[command(name = "tldr", subcommand)]
115 Tldr(TldrProcess),
116
117 #[command(subcommand)]
119 Completion(CompletionProcess),
120
121 #[cfg(feature = "self-update")]
122 Update(UpdateProcess),
124
125 Changelog(ChangelogProcess),
127}
128
129#[cfg(feature = "tldr")]
130#[derive(Subcommand)]
131#[cfg_attr(test, derive(Debug))]
132pub enum TldrProcess {
133 #[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
137 Fetch(TldrFetchProcess),
138
139 #[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
141 Clear(TldrClearProcess),
142}
143
144#[derive(Subcommand)]
145#[cfg_attr(test, derive(Debug))]
146pub enum CompletionProcess {
147 #[command(after_long_help = include_str!("_examples/completion_new.txt"))]
149 New(Interactive<CompletionNewProcess>),
150 #[command(after_long_help = include_str!("_examples/completion_delete.txt"))]
152 Delete(CompletionDeleteProcess),
153 #[command(alias = "ls", after_long_help = include_str!("_examples/completion_list.txt"))]
155 List(Interactive<CompletionListProcess>),
156}
157
158#[derive(Args, Debug)]
162pub struct Interactive<T: FromArgMatches + Args> {
163 #[command(flatten)]
165 pub process: T,
166
167 #[command(flatten)]
169 pub opts: InteractiveOptions,
170}
171
172#[derive(Args, Debug)]
174pub struct InteractiveOptions {
175 #[arg(short = 'i', long)]
177 pub interactive: bool,
178
179 #[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
181 pub inline: bool,
182
183 #[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
185 pub full_screen: bool,
186}
187
188#[cfg(debug_assertions)]
189#[derive(Args, Debug)]
191pub struct QueryProcess {
192 #[arg(default_value = "-")]
194 pub sql: FileOrStdin,
195}
196
197#[derive(Args, Debug)]
199pub struct InitProcess {
200 #[arg(value_enum)]
202 pub shell: Shell,
203
204 #[arg(long, group = "output_type")]
206 pub completions: bool,
207
208 #[arg(long, group = "output_type")]
210 pub integration: bool,
211}
212
213#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
214pub enum Shell {
215 Bash,
216 Zsh,
217 Fish,
218 #[value(alias("pwsh"))]
219 Powershell,
220 #[value(alias("nu"))]
221 Nushell,
222}
223
224#[derive(Args, Debug)]
226pub struct ConfigProcess {
227 #[arg(short = 'p', long)]
229 pub path: bool,
230}
231
232#[derive(Args, Debug)]
234pub struct LogsProcess {
235 #[arg(short = 'p', long)]
237 pub path: bool,
238}
239
240#[derive(Args, Debug)]
242pub struct BookmarkCommandProcess {
243 #[arg(required_unless_present = "interactive")]
247 pub command: Option<String>,
248
249 #[arg(short = 'a', long)]
251 pub alias: Option<String>,
252
253 #[arg(short = 'd', long)]
255 pub description: Option<String>,
256
257 #[arg(long)]
259 pub ai: bool,
260}
261
262#[derive(Args, Debug)]
264pub struct SearchCommandsProcess {
265 pub query: Option<String>,
267
268 #[arg(short = 'm', long)]
270 pub mode: Option<SearchMode>,
271
272 #[arg(short = 'u', long)]
274 pub user_only: bool,
275
276 #[arg(long, requires = "query")]
278 pub ai: bool,
279}
280
281#[derive(Args, Debug)]
283pub struct VariableReplaceProcess {
284 #[arg(default_value = "-")]
288 pub command: MaybeStdin<String>,
289
290 #[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
294 pub values: Vec<(String, Option<String>)>,
295
296 #[arg(short = 'E', long)]
305 pub use_env: bool,
306}
307
308#[derive(Args, Debug)]
310pub struct CommandFixProcess {
311 pub command: String,
313
314 #[arg(long, value_name = "HISTORY")]
318 pub history: Option<String>,
319}
320
321#[derive(Args, Clone, Debug)]
323pub struct ExportItemsProcess {
324 #[arg(default_value = "-")]
328 pub location: String,
329 #[arg(long, group = "location_type")]
331 pub file: bool,
332 #[arg(long, group = "location_type")]
334 pub http: bool,
335 #[arg(long, group = "location_type")]
337 pub gist: bool,
338 #[arg(long, value_name = "REGEX")]
342 pub filter: Option<Regex>,
343 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
348 pub headers: Vec<(HeaderName, HeaderValue)>,
349 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
353 pub method: HttpMethod,
354}
355
356#[derive(Args, Clone, Debug)]
358pub struct ImportItemsProcess {
359 #[arg(default_value = "-", required_unless_present = "history")]
363 pub location: String,
364 #[arg(long)]
366 pub ai: bool,
367 #[arg(long)]
371 pub dry_run: bool,
372 #[arg(long, group = "location_type")]
374 pub file: bool,
375 #[arg(long, group = "location_type")]
377 pub http: bool,
378 #[arg(long, group = "location_type")]
380 pub gist: bool,
381 #[arg(long, value_enum, group = "location_type", requires = "ai")]
383 pub history: Option<HistorySource>,
384 #[arg(long, value_name = "REGEX")]
388 pub filter: Option<Regex>,
389 #[arg(short = 't', long = "add-tag", value_name = "TAG")]
394 pub tags: Vec<String>,
395 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
400 pub headers: Vec<(HeaderName, HeaderValue)>,
401 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
405 pub method: HttpMethod,
406}
407
408#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
409pub enum HistorySource {
410 Bash,
411 Zsh,
412 Fish,
413 #[value(alias("pwsh"))]
414 Powershell,
415 #[value(alias("nu"))]
416 Nushell,
417 Atuin,
418}
419
420#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
421pub enum HttpMethod {
422 GET,
423 POST,
424 PUT,
425 PATCH,
426}
427impl From<HttpMethod> for Method {
428 fn from(value: HttpMethod) -> Self {
429 match value {
430 HttpMethod::GET => Method::GET,
431 HttpMethod::POST => Method::POST,
432 HttpMethod::PUT => Method::PUT,
433 HttpMethod::PATCH => Method::PATCH,
434 }
435 }
436}
437
438#[cfg(feature = "tldr")]
439#[derive(Args, Debug)]
441pub struct TldrFetchProcess {
442 pub category: Option<String>,
446
447 #[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
452 pub commands: Vec<String>,
453
454 #[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
458 pub filter_commands: Option<FileOrStdin>,
459}
460
461#[cfg(feature = "tldr")]
462#[derive(Args, Debug)]
464pub struct TldrClearProcess {
465 pub category: Option<String>,
469}
470
471#[derive(Args, Debug)]
473pub struct CompletionNewProcess {
474 #[arg(short = 'c', long)]
476 pub command: Option<String>,
477 #[arg(required_unless_present = "interactive")]
479 pub variable: Option<String>,
480 #[arg(required_unless_present_any = ["interactive", "ai"])]
482 pub provider: Option<String>,
483 #[arg(long)]
485 pub ai: bool,
486}
487
488#[derive(Args, Debug)]
490pub struct CompletionDeleteProcess {
491 #[arg(short = 'c', long)]
493 pub command: Option<String>,
494 pub variable: String,
496}
497
498#[derive(Args, Debug)]
500pub struct CompletionListProcess {
501 pub command: Option<String>,
503}
504
505#[cfg(feature = "self-update")]
506#[derive(Args, Debug)]
508pub struct UpdateProcess {}
509
510#[derive(Args, Debug)]
512pub struct ChangelogProcess {
513 #[arg(long, alias = "since", value_parser = parse_version, default_value = env!("CARGO_PKG_VERSION"))]
515 pub from: Version,
516
517 #[arg(long, alias = "until", value_parser = parse_version)]
519 pub to: Option<Version>,
520
521 #[arg(long, conflicts_with = "minor")]
523 pub major: bool,
524
525 #[arg(long, conflicts_with = "major")]
527 pub minor: bool,
528}
529
530impl Cli {
531 #[instrument]
533 pub fn parse_extended() -> Self {
534 let mut cmd = Self::command_for_update();
536
537 let style = cmd.get_styles().clone();
539 let dimmed = style.get_placeholder().dimmed();
540 let plain_examples_header = "Examples:";
541 let styled_examples_header = format!(
542 "{}Examples:{}",
543 style.get_usage().render(),
544 style.get_usage().render_reset()
545 );
546 style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
547
548 let matches = cmd.get_matches();
550
551 match Cli::from_arg_matches(&matches) {
553 Ok(args) => args,
554 Err(err) => err.exit(),
555 }
556 }
557}
558
559fn style_after_long_help(
560 command_ref: &mut Command,
561 dimmed: &Style,
562 plain_examples_header: &str,
563 styled_examples_header: &str,
564) {
565 let mut command = std::mem::take(command_ref);
566 if let Some(after_long_help) = command.get_after_long_help() {
567 let current_help_text = after_long_help.to_string();
568 let modified_help_text = current_help_text
569 .replace(plain_examples_header, styled_examples_header)
571 .lines()
573 .map(|line| {
574 if line.trim_start().starts_with('#') {
575 format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
576 } else {
577 line.to_string()
578 }
579 }).join("\n");
580 command = command.after_long_help(modified_help_text);
581 }
582 for subcommand_ref in command.get_subcommands_mut() {
583 style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
584 }
585 *command_ref = command;
586}
587
588fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
590 if let Some((var, value)) = env.split_once('=') {
591 Ok((var.to_owned(), Some(value.to_owned())))
592 } else {
593 Ok((env.to_owned(), None))
594 }
595}
596
597fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
599 if let Some((name, value)) = env.split_once(':') {
600 Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
601 } else {
602 Err(eyre!("Missing a colon between the header name and value"))
603 }
604}
605
606fn parse_version(s: &str) -> Result<Version, <Version as FromStr>::Err> {
608 let version_str = s.strip_prefix('v').unwrap_or(s);
610
611 version_str.parse()
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn test_cli_asserts() {
621 Cli::command().debug_assert()
622 }
623}