1use clap::builder::styling;
4pub use futures::future::BoxFuture;
5use std::io::Write;
6
7use crate::{BuiltinError, CommandArg, commands, error, extensions, results};
8
9#[allow(type_alias_bounds)]
16pub type CommandExecuteFunc<SE: extensions::ShellExtensions> =
17 fn(
18 commands::ExecutionContext<'_, SE>,
19 Vec<commands::CommandArg>,
20 ) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>>;
21
22pub type CommandContentFunc =
30 fn(&str, ContentType, &ContentOptions) -> Result<String, error::Error>;
31
32pub trait Command: clap::Parser {
34 type Error: BuiltinError + 'static;
36
37 fn new<I>(args: I) -> Result<Self, clap::Error>
43 where
44 I: IntoIterator<Item = String>,
45 {
46 if !Self::takes_plus_options() {
47 Self::try_parse_from(args)
48 } else {
49 let mut updated_args = vec![];
52 for arg in args {
53 if let Some(plus_options) = arg.strip_prefix("+") {
54 for c in plus_options.chars() {
55 updated_args.push(format!("--+{c}"));
56 }
57 } else {
58 updated_args.push(arg);
59 }
60 }
61
62 Self::try_parse_from(updated_args)
63 }
64 }
65
66 fn takes_plus_options() -> bool {
68 false
69 }
70
71 fn execute<SE: extensions::ShellExtensions>(
78 &self,
79 context: commands::ExecutionContext<'_, SE>,
80 ) -> impl std::future::Future<Output = Result<results::ExecutionResult, Self::Error>>
81 + std::marker::Send;
82
83 fn get_content(
91 name: &str,
92 content_type: ContentType,
93 options: &ContentOptions,
94 ) -> Result<String, error::Error> {
95 let mut clap_command = Self::command()
96 .styles(brush_help_styles())
97 .next_line_help(false);
98 clap_command.set_bin_name(name);
99
100 let s = match content_type {
101 ContentType::DetailedHelp => {
102 let rendered = clap_command.render_help();
103 if options.colorized {
104 rendered.ansi().to_string()
105 } else {
106 rendered.to_string()
107 }
108 }
109 ContentType::ShortUsage => get_builtin_short_usage(name, &clap_command),
110 ContentType::ShortDescription => get_builtin_short_description(name, &clap_command),
111 ContentType::ManPage => get_builtin_man_page(name, &clap_command)?,
112 };
113
114 Ok(s)
115 }
116}
117
118pub trait DeclarationCommand: Command {
121 fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>);
127}
128
129pub enum ContentType {
131 DetailedHelp,
133 ShortUsage,
135 ShortDescription,
137 ManPage,
139}
140
141#[derive(Default)]
143pub struct ContentOptions {
144 pub colorized: bool,
146}
147
148#[derive(Clone)]
150pub struct Registration<SE: extensions::ShellExtensions> {
151 pub execute_func: CommandExecuteFunc<SE>,
153
154 pub content_func: CommandContentFunc,
156
157 pub disabled: bool,
159
160 pub special_builtin: bool,
162
163 pub declaration_builtin: bool,
165}
166
167impl<SE: extensions::ShellExtensions> Registration<SE> {
168 #[must_use]
170 pub const fn special(self) -> Self {
171 Self {
172 special_builtin: true,
173 ..self
174 }
175 }
176}
177
178fn get_builtin_man_page(_name: &str, _command: &clap::Command) -> Result<String, error::Error> {
179 error::unimp("man page rendering is not yet implemented")
180}
181
182fn get_builtin_short_description(name: &str, command: &clap::Command) -> String {
183 let about = command
184 .get_about()
185 .map_or_else(String::new, |s| s.to_string());
186
187 std::format!("{name} - {about}\n")
188}
189
190fn get_builtin_short_usage(name: &str, command: &clap::Command) -> String {
191 let mut usage = String::new();
192
193 let mut needs_space = false;
194
195 let mut optional_short_opts = vec![];
196 let mut required_short_opts = vec![];
197 for opt in command.get_opts() {
198 if opt.is_hide_set() {
199 continue;
200 }
201
202 if let Some(c) = opt.get_short() {
203 if !opt.is_required_set() {
204 optional_short_opts.push(c);
205 } else {
206 required_short_opts.push(c);
207 }
208 }
209 }
210
211 if !optional_short_opts.is_empty() {
212 if needs_space {
213 usage.push(' ');
214 }
215
216 usage.push('[');
217 usage.push('-');
218 for c in optional_short_opts {
219 usage.push(c);
220 }
221
222 usage.push(']');
223 needs_space = true;
224 }
225
226 if !required_short_opts.is_empty() {
227 if needs_space {
228 usage.push(' ');
229 }
230
231 usage.push('-');
232 for c in required_short_opts {
233 usage.push(c);
234 }
235
236 needs_space = true;
237 }
238
239 for pos in command.get_positionals() {
240 if pos.is_hide_set() {
241 continue;
242 }
243
244 if !pos.is_required_set() {
245 if needs_space {
246 usage.push(' ');
247 }
248
249 usage.push('[');
250 needs_space = false;
251 }
252
253 if let Some(names) = pos.get_value_names() {
254 for name in names {
255 if needs_space {
256 usage.push(' ');
257 }
258
259 usage.push_str(name);
260 needs_space = true;
261 }
262 }
263
264 if !pos.is_required_set() {
265 usage.push(']');
266 needs_space = true;
267 }
268 }
269
270 std::format!("{name}: {name} {usage}\n")
271}
272
273fn brush_help_styles() -> clap::builder::Styles {
274 styling::Styles::styled()
275 .header(
276 styling::AnsiColor::Yellow.on_default()
277 | styling::Effects::BOLD
278 | styling::Effects::UNDERLINE,
279 )
280 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
281 .literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
282 .placeholder(styling::AnsiColor::Cyan.on_default())
283}
284
285pub fn parse_known<T: clap::Parser, S>(
314 args: impl IntoIterator<Item = S>,
315) -> (T, Option<impl Iterator<Item = S>>)
316where
317 S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
318{
319 let mut args = args.into_iter();
320 let mut hyphen = None;
324 let args_before_hyphen = args.by_ref().take_while(|a| {
325 let is_hyphen = *a == "--";
326 if is_hyphen {
327 hyphen = Some(a.clone());
328 }
329 !is_hyphen
330 });
331 let parsed_args = T::parse_from(args_before_hyphen);
332 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
333 (parsed_args, raw_args)
334}
335
336pub fn try_parse_known<T: clap::Parser>(
340 args: impl IntoIterator<Item = String>,
341) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
342 let mut args = args.into_iter();
343 let mut hyphen = None;
344 let args_before_hyphen = args.by_ref().take_while(|a| {
345 let is_hyphen = a == "--";
346 if is_hyphen {
347 hyphen = Some(a.clone());
348 }
349 !is_hyphen
350 });
351 let parsed_args = T::try_parse_from(args_before_hyphen)?;
352
353 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
354 Ok((parsed_args, raw_args))
355}
356
357pub trait SimpleCommand {
359 fn get_content(
361 name: &str,
362 content_type: ContentType,
363 options: &ContentOptions,
364 ) -> Result<String, error::Error>;
365
366 fn execute<SE: extensions::ShellExtensions, I: Iterator<Item = S>, S: AsRef<str>>(
368 context: commands::ExecutionContext<'_, SE>,
369 args: I,
370 ) -> Result<results::ExecutionResult, error::Error>;
371}
372
373pub fn simple_builtin<B: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>()
376-> Registration<SE> {
377 Registration {
378 execute_func: exec_simple_builtin::<B, SE>,
379 content_func: B::get_content,
380 disabled: false,
381 special_builtin: false,
382 declaration_builtin: false,
383 }
384}
385
386pub fn builtin<B: Command + Send + Sync, SE: extensions::ShellExtensions>() -> Registration<SE> {
389 Registration {
390 execute_func: exec_builtin::<B, SE>,
391 content_func: get_builtin_content::<B>,
392 disabled: false,
393 special_builtin: false,
394 declaration_builtin: false,
395 }
396}
397
398pub fn decl_builtin<B: DeclarationCommand + Send + Sync, SE: extensions::ShellExtensions>()
402-> Registration<SE> {
403 Registration {
404 execute_func: exec_declaration_builtin::<B, SE>,
405 content_func: get_builtin_content::<B>,
406 disabled: false,
407 special_builtin: false,
408 declaration_builtin: true,
409 }
410}
411
412#[allow(clippy::too_long_first_doc_paragraph)]
413pub fn raw_arg_builtin<
420 B: DeclarationCommand + Default + Send + Sync,
421 SE: extensions::ShellExtensions,
422>() -> Registration<SE> {
423 Registration {
424 execute_func: exec_raw_arg_builtin::<B, SE>,
425 content_func: get_builtin_content::<B>,
426 disabled: false,
427 special_builtin: false,
428 declaration_builtin: true,
429 }
430}
431
432fn get_builtin_content<T: Command + Send + Sync>(
433 name: &str,
434 content_type: ContentType,
435 options: &ContentOptions,
436) -> Result<String, error::Error> {
437 T::get_content(name, content_type, options)
438}
439
440fn exec_simple_builtin<T: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>(
441 context: commands::ExecutionContext<'_, SE>,
442 args: Vec<CommandArg>,
443) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
444 Box::pin(async move { exec_simple_builtin_impl::<T, SE>(context, args).await })
445}
446
447#[expect(clippy::unused_async)]
448async fn exec_simple_builtin_impl<
449 T: SimpleCommand + Send + Sync,
450 SE: extensions::ShellExtensions,
451>(
452 context: commands::ExecutionContext<'_, SE>,
453 args: Vec<CommandArg>,
454) -> Result<results::ExecutionResult, error::Error> {
455 let plain_args = args.into_iter().map(|arg| match arg {
456 CommandArg::String(s) => s,
457 CommandArg::Assignment(a) => a.to_string(),
458 });
459
460 T::execute(context, plain_args)
461}
462
463fn exec_builtin<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
464 context: commands::ExecutionContext<'_, SE>,
465 args: Vec<CommandArg>,
466) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
467 Box::pin(async move { exec_builtin_impl::<T, SE>(context, args).await })
468}
469
470async fn exec_builtin_impl<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
471 context: commands::ExecutionContext<'_, SE>,
472 args: Vec<CommandArg>,
473) -> Result<results::ExecutionResult, error::Error> {
474 let plain_args = args.into_iter().map(|arg| match arg {
475 CommandArg::String(s) => s,
476 CommandArg::Assignment(a) => a.to_string(),
477 });
478
479 let result = T::new(plain_args);
480 let command = match result {
481 Ok(command) => command,
482 Err(e) => {
483 let _ = writeln!(context.stderr(), "{e}");
484 return Ok(results::ExecutionExitCode::InvalidUsage.into());
485 }
486 };
487
488 call_builtin(command, context).await
489}
490
491fn exec_declaration_builtin<
492 T: DeclarationCommand + Send + Sync,
493 SE: extensions::ShellExtensions,
494>(
495 context: commands::ExecutionContext<'_, SE>,
496 args: Vec<CommandArg>,
497) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
498 Box::pin(async move { exec_declaration_builtin_impl::<T, SE>(context, args).await })
499}
500
501async fn exec_declaration_builtin_impl<
502 T: DeclarationCommand + Send + Sync,
503 SE: extensions::ShellExtensions,
504>(
505 context: commands::ExecutionContext<'_, SE>,
506 args: Vec<CommandArg>,
507) -> Result<results::ExecutionResult, error::Error> {
508 let mut options = vec![];
509 let mut declarations = vec![];
510
511 for (i, arg) in args.into_iter().enumerate() {
512 match arg {
513 CommandArg::String(s)
514 if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
515 {
516 options.push(s);
517 }
518 _ => declarations.push(arg),
519 }
520 }
521
522 let result = T::new(options);
523 let mut command = match result {
524 Ok(command) => command,
525 Err(e) => {
526 let _ = writeln!(context.stderr(), "{e}");
527 return Ok(results::ExecutionExitCode::InvalidUsage.into());
528 }
529 };
530
531 command.set_declarations(declarations);
532
533 call_builtin(command, context).await
534}
535
536fn exec_raw_arg_builtin<
537 T: DeclarationCommand + Default + Send + Sync,
538 SE: extensions::ShellExtensions,
539>(
540 context: commands::ExecutionContext<'_, SE>,
541 args: Vec<CommandArg>,
542) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
543 Box::pin(async move { exec_raw_arg_builtin_impl::<T, SE>(context, args).await })
544}
545
546async fn exec_raw_arg_builtin_impl<
547 T: DeclarationCommand + Default + Send + Sync,
548 SE: extensions::ShellExtensions,
549>(
550 context: commands::ExecutionContext<'_, SE>,
551 args: Vec<CommandArg>,
552) -> Result<results::ExecutionResult, error::Error> {
553 let mut command = T::default();
554 command.set_declarations(args);
555
556 call_builtin(command, context).await
557}
558
559async fn call_builtin(
560 command: impl Command,
561 context: commands::ExecutionContext<'_, impl extensions::ShellExtensions>,
562) -> Result<results::ExecutionResult, error::Error> {
563 let builtin_name = context.command_name.clone();
564 let result = command
565 .execute(context)
566 .await
567 .map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
568
569 Ok(result)
570}