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)]
26#[cfg_attr(debug_assertions, derive(Debug))]
27#[command(
28 author,
29 version,
30 verbatim_doc_comment,
31 infer_subcommands = true,
32 after_long_help = include_str!("_examples/cli.txt")
33)]
34pub struct Cli {
35 #[arg(long, hide = true)]
39 pub skip_execution: bool,
40
41 #[arg(long, hide = true)]
45 pub extra_line: bool,
46
47 #[arg(long, hide = true)]
52 pub file_output: Option<String>,
53
54 #[command(name = "command", subcommand)]
56 pub process: CliProcess,
57}
58
59#[derive(Subcommand)]
60#[cfg_attr(debug_assertions, derive(Debug))]
61pub enum CliProcess {
62 #[cfg(debug_assertions)]
63 Query(QueryProcess),
65
66 #[command(after_long_help = include_str!("_examples/init.txt"))]
68 Init(InitProcess),
69
70 #[command(after_long_help = include_str!("_examples/new.txt"))]
72 New(Interactive<BookmarkCommandProcess>),
73
74 #[command(after_long_help = include_str!("_examples/search.txt"))]
76 Search(Interactive<SearchCommandsProcess>),
77
78 #[command(after_long_help = include_str!("_examples/replace.txt"))]
82 Replace(Interactive<VariableReplaceProcess>),
83
84 #[command(after_long_help = include_str!("_examples/export.txt"))]
88 Export(ExportCommandsProcess),
89
90 #[command(after_long_help = include_str!("_examples/import.txt"))]
92 Import(ImportCommandsProcess),
93
94 #[command(name = "tldr", subcommand)]
96 Tldr(TldrProcess),
97}
98
99#[derive(Subcommand)]
100#[cfg_attr(debug_assertions, derive(Debug))]
101pub enum TldrProcess {
102 #[command(after_long_help = include_str!("_examples/tldr_fetch.txt"))]
106 Fetch(TldrFetchProcess),
107
108 #[command(after_long_help = include_str!("_examples/tldr_clear.txt"))]
110 Clear(TldrClearProcess),
111}
112
113#[derive(Args, Debug)]
117pub struct Interactive<T: FromArgMatches + Args> {
118 #[command(flatten)]
120 pub process: T,
121
122 #[command(flatten)]
124 pub opts: InteractiveOptions,
125}
126
127#[derive(Args, Debug)]
129pub struct InteractiveOptions {
130 #[arg(short = 'i', long)]
132 pub interactive: bool,
133
134 #[arg(short = 'l', long, requires = "interactive", conflicts_with = "full_screen")]
136 pub inline: bool,
137
138 #[arg(short = 'f', long, requires = "interactive", conflicts_with = "inline")]
140 pub full_screen: bool,
141}
142
143#[cfg(debug_assertions)]
144#[derive(Args, Debug)]
146pub struct QueryProcess {
147 #[arg(default_value = "-")]
149 pub sql: FileOrStdin,
150}
151
152#[derive(Args, Debug)]
154pub struct InitProcess {
155 #[arg(value_enum)]
157 pub shell: Shell,
158}
159
160#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
161pub enum Shell {
162 Bash,
163 Zsh,
164 Fish,
165 Powershell,
166}
167
168#[derive(Args, Debug)]
170pub struct BookmarkCommandProcess {
171 #[arg(required_unless_present = "interactive")]
175 pub command: Option<String>,
176
177 #[arg(short = 'a', long)]
179 pub alias: Option<String>,
180
181 #[arg(short = 'd', long)]
183 pub description: Option<String>,
184}
185
186#[derive(Args, Debug)]
188pub struct SearchCommandsProcess {
189 pub query: Option<String>,
191
192 #[arg(short = 'm', long)]
194 pub mode: Option<SearchMode>,
195
196 #[arg(short = 'u', long)]
198 pub user_only: bool,
199}
200
201#[derive(Args, Debug)]
203pub struct VariableReplaceProcess {
204 #[arg(default_value = "-")]
208 pub command: MaybeStdin<String>,
209
210 #[arg(short = 'e', long = "env", value_name = "KEY[=VALUE]", value_parser = ValueParser::new(parse_env_var))]
214 pub values: Vec<(String, Option<String>)>,
215
216 #[arg(short = 'E', long)]
225 pub use_env: bool,
226}
227
228#[derive(Args, Debug)]
230pub struct ExportCommandsProcess {
231 #[arg(default_value = "-")]
235 pub location: String,
236 #[arg(long, group = "location_type")]
238 pub file: bool,
239 #[arg(long, group = "location_type")]
241 pub http: bool,
242 #[arg(long, group = "location_type")]
244 pub gist: bool,
245 #[arg(long, value_name = "REGEX")]
249 pub filter: Option<Regex>,
250 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
255 pub headers: Vec<(HeaderName, HeaderValue)>,
256 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::PUT)]
260 pub method: HttpMethod,
261}
262
263#[derive(Args, Debug)]
265pub struct ImportCommandsProcess {
266 #[arg(default_value = "-")]
270 pub location: String,
271 #[arg(long, group = "location_type")]
273 pub file: bool,
274 #[arg(long, group = "location_type")]
276 pub http: bool,
277 #[arg(long, group = "location_type")]
279 pub gist: bool,
280 #[arg(long, value_name = "REGEX")]
284 pub filter: Option<Regex>,
285 #[arg(long)]
289 pub dry_run: bool,
290 #[arg(short = 't', long = "add-tag", value_name = "TAG")]
295 pub tags: Vec<String>,
296 #[arg(short = 'H', long = "header", value_name = "KEY: VALUE", value_parser = ValueParser::new(parse_header))]
301 pub headers: Vec<(HeaderName, HeaderValue)>,
302 #[arg(short = 'X', long = "request", value_enum, default_value_t = HttpMethod::GET)]
306 pub method: HttpMethod,
307}
308
309#[derive(ValueEnum, Copy, Clone, PartialEq, Eq, Debug)]
310pub enum HttpMethod {
311 GET,
312 POST,
313 PUT,
314 PATCH,
315}
316impl From<HttpMethod> for Method {
317 fn from(value: HttpMethod) -> Self {
318 match value {
319 HttpMethod::GET => Method::GET,
320 HttpMethod::POST => Method::POST,
321 HttpMethod::PUT => Method::PUT,
322 HttpMethod::PATCH => Method::PATCH,
323 }
324 }
325}
326
327#[derive(Args, Debug)]
329pub struct TldrFetchProcess {
330 pub category: Option<String>,
334
335 #[arg(short = 'c', long = "command", value_name = "COMMAND_NAME")]
340 pub commands: Vec<String>,
341
342 #[arg(short = 'C', long, value_name = "FILE_OR_STDIN", num_args = 0..=1, default_missing_value = "-")]
346 pub filter_commands: Option<FileOrStdin>,
347}
348
349#[derive(Args, Debug)]
351pub struct TldrClearProcess {
352 pub category: Option<String>,
356}
357
358impl Cli {
359 #[instrument]
361 pub fn parse_extended() -> Self {
362 let mut cmd = Self::command_for_update();
364
365 let style = cmd.get_styles().clone();
367 let dimmed = style.get_placeholder().dimmed();
368 let plain_examples_header = "Examples:";
369 let styled_examples_header = format!(
370 "{}Examples:{}",
371 style.get_usage().render(),
372 style.get_usage().render_reset()
373 );
374 style_after_long_help(&mut cmd, &dimmed, plain_examples_header, &styled_examples_header);
375
376 let matches = cmd.get_matches();
378
379 match Cli::from_arg_matches(&matches) {
381 Ok(args) => args,
382 Err(err) => err.exit(),
383 }
384 }
385}
386
387fn style_after_long_help(
388 command_ref: &mut Command,
389 dimmed: &Style,
390 plain_examples_header: &str,
391 styled_examples_header: &str,
392) {
393 let mut command = std::mem::take(command_ref);
394 if let Some(after_long_help) = command.get_after_long_help() {
395 let current_help_text = after_long_help.to_string();
396 let modified_help_text = current_help_text
397 .replace(plain_examples_header, styled_examples_header)
399 .lines()
401 .map(|line| {
402 if line.trim_start().starts_with('#') {
403 format!("{}{}{}", dimmed.render(), line, dimmed.render_reset())
404 } else {
405 line.to_string()
406 }
407 }).join("\n");
408 command = command.after_long_help(modified_help_text);
409 }
410 for subcommand_ref in command.get_subcommands_mut() {
411 style_after_long_help(subcommand_ref, dimmed, plain_examples_header, styled_examples_header);
412 }
413 *command_ref = command;
414}
415
416fn parse_env_var(env: &str) -> Result<(String, Option<String>)> {
417 if let Some((var, value)) = env.split_once('=') {
418 Ok((var.to_owned(), Some(value.to_owned())))
419 } else {
420 Ok((env.to_owned(), None))
421 }
422}
423
424fn parse_header(env: &str) -> Result<(HeaderName, HeaderValue)> {
425 if let Some((name, value)) = env.split_once(':') {
426 Ok((HeaderName::from_str(name)?, HeaderValue::from_str(value.trim_start())?))
427 } else {
428 Err(eyre!("Missing a colon between the header name and value"))
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn test_cli_asserts() {
438 Cli::command().debug_assert()
439 }
440}