1use std::io::Write;
4
5use clap::builder::styling;
6pub use futures::future::BoxFuture;
7
8use crate::{BuiltinError, CommandArg, commands, error, extensions, results};
9
10#[allow(type_alias_bounds)]
17pub type CommandExecuteFunc<SE: extensions::ShellExtensions> =
18 fn(
19 commands::ExecutionContext<'_, SE>,
20 Vec<commands::CommandArg>,
21 ) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>>;
22
23pub type CommandContentFunc =
31 fn(&str, ContentType, &ContentOptions) -> Result<String, error::Error>;
32
33pub trait Command: clap::Parser {
35 type Error: BuiltinError + 'static;
37
38 fn new<I>(args: I) -> Result<Self, clap::Error>
50 where
51 I: IntoIterator<Item = String>,
52 {
53 if !Self::takes_plus_options() {
54 Self::try_parse_from(args)
55 } else {
56 let args = args.into_iter();
57
58 let (lower, _) = args.size_hint();
59
60 let mut updated_args = Vec::with_capacity(lower);
65 let mut options_ended = false;
66 for arg in args {
67 if options_ended {
68 updated_args.push(arg);
69 } else if let Some(plus_options) = arg.strip_prefix("+") {
70 updated_args.extend(plus_options.chars().map(|c| format!("--+{c}")));
71 } else {
72 options_ended = arg == "--";
73 updated_args.push(arg);
74 }
75 }
76
77 Self::try_parse_from(updated_args)
78 }
79 }
80
81 fn takes_plus_options() -> bool {
84 false
85 }
86
87 fn execute<SE: extensions::ShellExtensions>(
99 &self,
100 context: commands::ExecutionContext<'_, SE>,
101 ) -> impl std::future::Future<Output = Result<results::ExecutionResult, Self::Error>>
102 + std::marker::Send;
103
104 fn get_content(
118 name: &str,
119 content_type: ContentType,
120 options: &ContentOptions,
121 ) -> Result<String, error::Error> {
122 let mut clap_command = Self::command()
123 .styles(shrub_help_styles())
124 .next_line_help(false);
125 clap_command.set_bin_name(name);
126
127 let s = match content_type {
128 ContentType::DetailedHelp => {
129 let rendered = clap_command.render_help();
130 if options.colorized {
131 rendered.ansi().to_string()
132 } else {
133 rendered.to_string()
134 }
135 }
136 ContentType::ShortUsage => get_builtin_short_usage(name, &clap_command),
137 ContentType::ShortDescription => get_builtin_short_description(name, &clap_command),
138 ContentType::ManPage => get_builtin_man_page(name, &clap_command)?,
139 };
140
141 Ok(s)
142 }
143}
144
145pub trait DeclarationCommand: Command {
148 fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>);
154}
155
156pub enum ContentType {
158 DetailedHelp,
160 ShortUsage,
162 ShortDescription,
164 ManPage,
166}
167
168#[derive(Default)]
170pub struct ContentOptions {
171 pub colorized: bool,
173}
174
175#[derive(Clone)]
177pub struct Registration<SE: extensions::ShellExtensions> {
178 pub execute_func: CommandExecuteFunc<SE>,
180
181 pub content_func: CommandContentFunc,
183
184 pub disabled: bool,
186
187 pub special_builtin: bool,
189
190 pub declaration_builtin: bool,
192}
193
194impl<SE: extensions::ShellExtensions> Registration<SE> {
195 #[must_use]
197 pub const fn special(self) -> Self {
198 Self {
199 special_builtin: true,
200 ..self
201 }
202 }
203}
204
205fn get_builtin_man_page(_name: &str, _command: &clap::Command) -> Result<String, error::Error> {
206 error::unimp("man page rendering is not yet implemented")
207}
208
209fn get_builtin_short_description(name: &str, command: &clap::Command) -> String {
210 let about = command
211 .get_about()
212 .map_or_else(String::new, |s| s.to_string());
213
214 std::format!("{name} - {about}\n")
215}
216
217fn get_builtin_short_usage(name: &str, command: &clap::Command) -> String {
218 let mut usage = String::new();
219
220 let mut needs_space = false;
221
222 let mut optional_short_opts = vec![];
223 let mut required_short_opts = vec![];
224 for opt in command.get_opts() {
225 if opt.is_hide_set() {
226 continue;
227 }
228
229 if let Some(c) = opt.get_short() {
230 if !opt.is_required_set() {
231 optional_short_opts.push(c);
232 } else {
233 required_short_opts.push(c);
234 }
235 }
236 }
237
238 if !optional_short_opts.is_empty() {
239 if needs_space {
240 usage.push(' ');
241 }
242
243 usage.push('[');
244 usage.push('-');
245 for c in optional_short_opts {
246 usage.push(c);
247 }
248
249 usage.push(']');
250 needs_space = true;
251 }
252
253 if !required_short_opts.is_empty() {
254 if needs_space {
255 usage.push(' ');
256 }
257
258 usage.push('-');
259 for c in required_short_opts {
260 usage.push(c);
261 }
262
263 needs_space = true;
264 }
265
266 for pos in command.get_positionals() {
267 if pos.is_hide_set() {
268 continue;
269 }
270
271 if !pos.is_required_set() {
272 if needs_space {
273 usage.push(' ');
274 }
275
276 usage.push('[');
277 needs_space = false;
278 }
279
280 if let Some(names) = pos.get_value_names() {
281 for name in names {
282 if needs_space {
283 usage.push(' ');
284 }
285
286 usage.push_str(name);
287 needs_space = true;
288 }
289 }
290
291 if !pos.is_required_set() {
292 usage.push(']');
293 needs_space = true;
294 }
295 }
296
297 std::format!("{name}: {name} {usage}\n")
298}
299
300fn shrub_help_styles() -> clap::builder::Styles {
301 styling::Styles::styled()
302 .header(
303 styling::AnsiColor::Yellow.on_default()
304 | styling::Effects::BOLD
305 | styling::Effects::UNDERLINE,
306 )
307 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
308 .literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
309 .placeholder(styling::AnsiColor::Cyan.on_default())
310}
311
312pub fn parse_known<T: clap::Parser, S>(
343 args: impl IntoIterator<Item = S>,
344) -> (T, Option<impl Iterator<Item = S>>)
345where
346 S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
347{
348 let mut args = args.into_iter();
349 let mut hyphen = None;
353 let args_before_hyphen = args.by_ref().take_while(|a| {
354 let is_hyphen = *a == "--";
355 if is_hyphen {
356 hyphen = Some(a.clone());
357 }
358 !is_hyphen
359 });
360 let parsed_args = T::parse_from(args_before_hyphen);
361 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
362 (parsed_args, raw_args)
363}
364
365pub fn try_parse_known<T: clap::Parser>(
374 args: impl IntoIterator<Item = String>,
375) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
376 let mut args = args.into_iter();
377 let mut hyphen = None;
378 let args_before_hyphen = args.by_ref().take_while(|a| {
379 let is_hyphen = a == "--";
380 if is_hyphen {
381 hyphen = Some(a.clone());
382 }
383 !is_hyphen
384 });
385 let parsed_args = T::try_parse_from(args_before_hyphen)?;
386
387 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
388 Ok((parsed_args, raw_args))
389}
390
391pub trait SimpleCommand {
393 fn get_content(
400 name: &str,
401 content_type: ContentType,
402 options: &ContentOptions,
403 ) -> Result<String, error::Error>;
404
405 fn execute<SE: extensions::ShellExtensions, I: Iterator<Item = S>, S: AsRef<str>>(
412 context: commands::ExecutionContext<'_, SE>,
413 args: I,
414 ) -> Result<results::ExecutionResult, error::Error>;
415}
416
417pub fn simple_builtin<B: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>()
420-> Registration<SE> {
421 Registration {
422 execute_func: exec_simple_builtin::<B, SE>,
423 content_func: B::get_content,
424 disabled: false,
425 special_builtin: false,
426 declaration_builtin: false,
427 }
428}
429
430pub fn builtin<B: Command + Send + Sync, SE: extensions::ShellExtensions>() -> Registration<SE> {
433 Registration {
434 execute_func: exec_builtin::<B, SE>,
435 content_func: get_builtin_content::<B>,
436 disabled: false,
437 special_builtin: false,
438 declaration_builtin: false,
439 }
440}
441
442pub fn decl_builtin<B: DeclarationCommand + Send + Sync, SE: extensions::ShellExtensions>()
446-> Registration<SE> {
447 Registration {
448 execute_func: exec_declaration_builtin::<B, SE>,
449 content_func: get_builtin_content::<B>,
450 disabled: false,
451 special_builtin: false,
452 declaration_builtin: true,
453 }
454}
455
456#[allow(clippy::too_long_first_doc_paragraph)]
457pub fn raw_arg_builtin<
464 B: DeclarationCommand + Default + Send + Sync,
465 SE: extensions::ShellExtensions,
466>() -> Registration<SE> {
467 Registration {
468 execute_func: exec_raw_arg_builtin::<B, SE>,
469 content_func: get_builtin_content::<B>,
470 disabled: false,
471 special_builtin: false,
472 declaration_builtin: true,
473 }
474}
475
476fn get_builtin_content<T: Command + Send + Sync>(
477 name: &str,
478 content_type: ContentType,
479 options: &ContentOptions,
480) -> Result<String, error::Error> {
481 T::get_content(name, content_type, options)
482}
483
484fn exec_simple_builtin<T: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>(
485 context: commands::ExecutionContext<'_, SE>,
486 args: Vec<CommandArg>,
487) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
488 Box::pin(async move { exec_simple_builtin_impl::<T, SE>(context, args).await })
489}
490
491#[expect(clippy::unused_async)]
492async fn exec_simple_builtin_impl<
493 T: SimpleCommand + Send + Sync,
494 SE: extensions::ShellExtensions,
495>(
496 context: commands::ExecutionContext<'_, SE>,
497 args: Vec<CommandArg>,
498) -> Result<results::ExecutionResult, error::Error> {
499 let plain_args = args.into_iter().map(|arg| match arg {
500 CommandArg::String(s) => s,
501 CommandArg::Assignment(a) => a.to_string(),
502 });
503
504 T::execute(context, plain_args)
505}
506
507fn exec_builtin<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
508 context: commands::ExecutionContext<'_, SE>,
509 args: Vec<CommandArg>,
510) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
511 Box::pin(async move { exec_builtin_impl::<T, SE>(context, args).await })
512}
513
514async fn exec_builtin_impl<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
515 context: commands::ExecutionContext<'_, SE>,
516 args: Vec<CommandArg>,
517) -> Result<results::ExecutionResult, error::Error> {
518 let plain_args = args.into_iter().map(|arg| match arg {
519 CommandArg::String(s) => s,
520 CommandArg::Assignment(a) => a.to_string(),
521 });
522
523 let result = T::new(plain_args);
524 let command = match result {
525 Ok(command) => command,
526 Err(e) => {
527 let _ = writeln!(context.stderr(), "{e}");
528 return Ok(results::ExecutionExitCode::InvalidUsage.into());
529 }
530 };
531
532 call_builtin(command, context).await
533}
534
535fn exec_declaration_builtin<
536 T: DeclarationCommand + Send + Sync,
537 SE: extensions::ShellExtensions,
538>(
539 context: commands::ExecutionContext<'_, SE>,
540 args: Vec<CommandArg>,
541) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
542 Box::pin(async move { exec_declaration_builtin_impl::<T, SE>(context, args).await })
543}
544
545async fn exec_declaration_builtin_impl<
546 T: DeclarationCommand + Send + Sync,
547 SE: extensions::ShellExtensions,
548>(
549 context: commands::ExecutionContext<'_, SE>,
550 args: Vec<CommandArg>,
551) -> Result<results::ExecutionResult, error::Error> {
552 let mut options = vec![];
553 let mut declarations = vec![];
554
555 for (i, arg) in args.into_iter().enumerate() {
556 match arg {
557 CommandArg::String(s)
558 if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
559 {
560 options.push(s);
561 }
562 _ => declarations.push(arg),
563 }
564 }
565
566 let result = T::new(options);
567 let mut command = match result {
568 Ok(command) => command,
569 Err(e) => {
570 let _ = writeln!(context.stderr(), "{e}");
571 return Ok(results::ExecutionExitCode::InvalidUsage.into());
572 }
573 };
574
575 command.set_declarations(declarations);
576
577 call_builtin(command, context).await
578}
579
580fn exec_raw_arg_builtin<
581 T: DeclarationCommand + Default + Send + Sync,
582 SE: extensions::ShellExtensions,
583>(
584 context: commands::ExecutionContext<'_, SE>,
585 args: Vec<CommandArg>,
586) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
587 Box::pin(async move { exec_raw_arg_builtin_impl::<T, SE>(context, args).await })
588}
589
590async fn exec_raw_arg_builtin_impl<
591 T: DeclarationCommand + Default + Send + Sync,
592 SE: extensions::ShellExtensions,
593>(
594 context: commands::ExecutionContext<'_, SE>,
595 args: Vec<CommandArg>,
596) -> Result<results::ExecutionResult, error::Error> {
597 let mut command = T::default();
598 command.set_declarations(args);
599
600 call_builtin(command, context).await
601}
602
603async fn call_builtin(
604 command: impl Command,
605 context: commands::ExecutionContext<'_, impl extensions::ShellExtensions>,
606) -> Result<results::ExecutionResult, error::Error> {
607 let builtin_name = context.command_name.clone();
608 let result = command
609 .execute(context)
610 .await
611 .map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
612
613 Ok(result)
614}