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