1use clap::builder::styling;
4use futures::future::BoxFuture;
5use std::io::Write;
6
7use super::{BuiltinError, CommandArg, commands, error, results};
8
9pub type CommandExecuteFunc = fn(
16 commands::ExecutionContext<'_>,
17 Vec<commands::CommandArg>,
18) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>>;
19
20pub type CommandContentFunc = fn(&str, ContentType) -> Result<String, error::Error>;
27
28pub trait Command: clap::Parser {
30 type Error: BuiltinError + 'static;
32
33 fn new<I>(args: I) -> Result<Self, clap::Error>
39 where
40 I: IntoIterator<Item = String>,
41 {
42 if !Self::takes_plus_options() {
43 Self::try_parse_from(args)
44 } else {
45 let mut updated_args = vec![];
48 for arg in args {
49 if let Some(plus_options) = arg.strip_prefix("+") {
50 for c in plus_options.chars() {
51 updated_args.push(format!("--+{c}"));
52 }
53 } else {
54 updated_args.push(arg);
55 }
56 }
57
58 Self::try_parse_from(updated_args)
59 }
60 }
61
62 fn takes_plus_options() -> bool {
64 false
65 }
66
67 fn execute(
74 &self,
75 context: commands::ExecutionContext<'_>,
76 ) -> impl std::future::Future<Output = Result<results::ExecutionResult, Self::Error>>
77 + std::marker::Send;
78
79 fn get_content(name: &str, content_type: ContentType) -> Result<String, error::Error> {
86 let mut clap_command = Self::command()
87 .styles(brush_help_styles())
88 .next_line_help(false);
89 clap_command.set_bin_name(name);
90
91 let s = match content_type {
92 ContentType::DetailedHelp => clap_command.render_help().ansi().to_string(),
93 ContentType::ShortUsage => get_builtin_short_usage(name, &clap_command),
94 ContentType::ShortDescription => get_builtin_short_description(name, &clap_command),
95 ContentType::ManPage => get_builtin_man_page(name, &clap_command)?,
96 };
97
98 Ok(s)
99 }
100}
101
102pub trait DeclarationCommand: Command {
105 fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>);
111}
112
113pub enum ContentType {
115 DetailedHelp,
117 ShortUsage,
119 ShortDescription,
121 ManPage,
123}
124
125#[derive(Clone)]
127pub struct Registration {
128 pub execute_func: CommandExecuteFunc,
130
131 pub content_func: CommandContentFunc,
133
134 pub disabled: bool,
136
137 pub special_builtin: bool,
139
140 pub declaration_builtin: bool,
142}
143
144impl Registration {
145 #[must_use]
147 pub const fn special(self) -> Self {
148 Self {
149 special_builtin: true,
150 ..self
151 }
152 }
153}
154
155fn get_builtin_man_page(_name: &str, _command: &clap::Command) -> Result<String, error::Error> {
156 error::unimp("man page rendering is not yet implemented")
157}
158
159fn get_builtin_short_description(name: &str, command: &clap::Command) -> String {
160 let about = command
161 .get_about()
162 .map_or_else(String::new, |s| s.to_string());
163
164 std::format!("{name} - {about}\n")
165}
166
167fn get_builtin_short_usage(name: &str, command: &clap::Command) -> String {
168 let mut usage = String::new();
169
170 let mut needs_space = false;
171
172 let mut optional_short_opts = vec![];
173 let mut required_short_opts = vec![];
174 for opt in command.get_opts() {
175 if opt.is_hide_set() {
176 continue;
177 }
178
179 if let Some(c) = opt.get_short() {
180 if !opt.is_required_set() {
181 optional_short_opts.push(c);
182 } else {
183 required_short_opts.push(c);
184 }
185 }
186 }
187
188 if !optional_short_opts.is_empty() {
189 if needs_space {
190 usage.push(' ');
191 }
192
193 usage.push('[');
194 usage.push('-');
195 for c in optional_short_opts {
196 usage.push(c);
197 }
198
199 usage.push(']');
200 needs_space = true;
201 }
202
203 if !required_short_opts.is_empty() {
204 if needs_space {
205 usage.push(' ');
206 }
207
208 usage.push('-');
209 for c in required_short_opts {
210 usage.push(c);
211 }
212
213 needs_space = true;
214 }
215
216 for pos in command.get_positionals() {
217 if pos.is_hide_set() {
218 continue;
219 }
220
221 if !pos.is_required_set() {
222 if needs_space {
223 usage.push(' ');
224 }
225
226 usage.push('[');
227 needs_space = false;
228 }
229
230 if let Some(names) = pos.get_value_names() {
231 for name in names {
232 if needs_space {
233 usage.push(' ');
234 }
235
236 usage.push_str(name);
237 needs_space = true;
238 }
239 }
240
241 if !pos.is_required_set() {
242 usage.push(']');
243 needs_space = true;
244 }
245 }
246
247 std::format!("{name}: {name} {usage}\n")
248}
249
250fn brush_help_styles() -> clap::builder::Styles {
251 styling::Styles::styled()
252 .header(
253 styling::AnsiColor::Yellow.on_default()
254 | styling::Effects::BOLD
255 | styling::Effects::UNDERLINE,
256 )
257 .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
258 .literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
259 .placeholder(styling::AnsiColor::Cyan.on_default())
260}
261
262pub fn parse_known<T: clap::Parser, S>(
293 args: impl IntoIterator<Item = S>,
294) -> (T, Option<impl Iterator<Item = S>>)
295where
296 S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
297{
298 let mut args = args.into_iter();
299 let mut hyphen = None;
303 let args_before_hyphen = args.by_ref().take_while(|a| {
304 let is_hyphen = *a == "--";
305 if is_hyphen {
306 hyphen = Some(a.clone());
307 }
308 !is_hyphen
309 });
310 let parsed_args = T::parse_from(args_before_hyphen);
311 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
312 (parsed_args, raw_args)
313}
314
315pub fn try_parse_known<T: clap::Parser>(
319 args: impl IntoIterator<Item = String>,
320) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
321 let mut args = args.into_iter();
322 let mut hyphen = None;
323 let args_before_hyphen = args.by_ref().take_while(|a| {
324 let is_hyphen = a == "--";
325 if is_hyphen {
326 hyphen = Some(a.clone());
327 }
328 !is_hyphen
329 });
330 let parsed_args = T::try_parse_from(args_before_hyphen)?;
331
332 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
333 Ok((parsed_args, raw_args))
334}
335
336pub trait SimpleCommand {
338 fn get_content(name: &str, content_type: ContentType) -> Result<String, error::Error>;
340
341 fn execute<I: Iterator<Item = S>, S: AsRef<str>>(
343 context: commands::ExecutionContext<'_>,
344 args: I,
345 ) -> Result<results::ExecutionResult, error::Error>;
346}
347
348pub fn simple_builtin<B: SimpleCommand + Send + Sync>() -> Registration {
351 Registration {
352 execute_func: exec_simple_builtin::<B>,
353 content_func: B::get_content,
354 disabled: false,
355 special_builtin: false,
356 declaration_builtin: false,
357 }
358}
359
360pub fn builtin<B: Command + Send + Sync>() -> Registration {
363 Registration {
364 execute_func: exec_builtin::<B>,
365 content_func: get_builtin_content::<B>,
366 disabled: false,
367 special_builtin: false,
368 declaration_builtin: false,
369 }
370}
371
372pub fn decl_builtin<B: DeclarationCommand + Send + Sync>() -> Registration {
376 Registration {
377 execute_func: exec_declaration_builtin::<B>,
378 content_func: get_builtin_content::<B>,
379 disabled: false,
380 special_builtin: false,
381 declaration_builtin: true,
382 }
383}
384
385#[allow(clippy::too_long_first_doc_paragraph)]
386pub fn raw_arg_builtin<B: DeclarationCommand + Default + Send + Sync>() -> Registration {
393 Registration {
394 execute_func: exec_raw_arg_builtin::<B>,
395 content_func: get_builtin_content::<B>,
396 disabled: false,
397 special_builtin: false,
398 declaration_builtin: true,
399 }
400}
401
402fn get_builtin_content<T: Command + Send + Sync>(
403 name: &str,
404 content_type: ContentType,
405) -> Result<String, error::Error> {
406 T::get_content(name, content_type)
407}
408
409fn exec_simple_builtin<T: SimpleCommand + Send + Sync>(
410 context: commands::ExecutionContext<'_>,
411 args: Vec<CommandArg>,
412) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
413 Box::pin(async move { exec_simple_builtin_impl::<T>(context, args).await })
414}
415
416#[expect(clippy::unused_async)]
417async fn exec_simple_builtin_impl<T: SimpleCommand + Send + Sync>(
418 context: commands::ExecutionContext<'_>,
419 args: Vec<CommandArg>,
420) -> Result<results::ExecutionResult, error::Error> {
421 let plain_args = args.into_iter().map(|arg| match arg {
422 CommandArg::String(s) => s,
423 CommandArg::Assignment(a) => a.to_string(),
424 });
425
426 T::execute(context, plain_args)
427}
428
429fn exec_builtin<T: Command + Send + Sync>(
430 context: commands::ExecutionContext<'_>,
431 args: Vec<CommandArg>,
432) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
433 Box::pin(async move { exec_builtin_impl::<T>(context, args).await })
434}
435
436async fn exec_builtin_impl<T: Command + Send + Sync>(
437 context: commands::ExecutionContext<'_>,
438 args: Vec<CommandArg>,
439) -> Result<results::ExecutionResult, error::Error> {
440 let plain_args = args.into_iter().map(|arg| match arg {
441 CommandArg::String(s) => s,
442 CommandArg::Assignment(a) => a.to_string(),
443 });
444
445 let result = T::new(plain_args);
446 let command = match result {
447 Ok(command) => command,
448 Err(e) => {
449 writeln!(context.stderr(), "{e}")?;
450 return Ok(results::ExecutionExitCode::InvalidUsage.into());
451 }
452 };
453
454 call_builtin(command, context).await
455}
456
457fn exec_declaration_builtin<T: DeclarationCommand + Send + Sync>(
458 context: commands::ExecutionContext<'_>,
459 args: Vec<CommandArg>,
460) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
461 Box::pin(async move { exec_declaration_builtin_impl::<T>(context, args).await })
462}
463
464async fn exec_declaration_builtin_impl<T: DeclarationCommand + Send + Sync>(
465 context: commands::ExecutionContext<'_>,
466 args: Vec<CommandArg>,
467) -> Result<results::ExecutionResult, error::Error> {
468 let mut options = vec![];
469 let mut declarations = vec![];
470
471 for (i, arg) in args.into_iter().enumerate() {
472 match arg {
473 CommandArg::String(s)
474 if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
475 {
476 options.push(s);
477 }
478 _ => declarations.push(arg),
479 }
480 }
481
482 let result = T::new(options);
483 let mut command = match result {
484 Ok(command) => command,
485 Err(e) => {
486 writeln!(context.stderr(), "{e}")?;
487 return Ok(results::ExecutionExitCode::InvalidUsage.into());
488 }
489 };
490
491 command.set_declarations(declarations);
492
493 call_builtin(command, context).await
494}
495
496fn exec_raw_arg_builtin<T: DeclarationCommand + Default + Send + Sync>(
497 context: commands::ExecutionContext<'_>,
498 args: Vec<CommandArg>,
499) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
500 Box::pin(async move { exec_raw_arg_builtin_impl::<T>(context, args).await })
501}
502
503async fn exec_raw_arg_builtin_impl<T: DeclarationCommand + Default + Send + Sync>(
504 context: commands::ExecutionContext<'_>,
505 args: Vec<CommandArg>,
506) -> Result<results::ExecutionResult, error::Error> {
507 let mut command = T::default();
508 command.set_declarations(args);
509
510 call_builtin(command, context).await
511}
512
513async fn call_builtin(
514 command: impl Command,
515 context: commands::ExecutionContext<'_>,
516) -> Result<results::ExecutionResult, error::Error> {
517 let builtin_name = context.command_name.clone();
518 let result = command
519 .execute(context)
520 .await
521 .map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
522
523 Ok(result)
524}