1use clap::builder::styling;
4use futures::future::BoxFuture;
5use std::io::Write;
6
7use crate::{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>(
291 args: impl IntoIterator<Item = S>,
292) -> (T, Option<impl Iterator<Item = S>>)
293where
294 S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
295{
296 let mut args = args.into_iter();
297 let mut hyphen = None;
301 let args_before_hyphen = args.by_ref().take_while(|a| {
302 let is_hyphen = *a == "--";
303 if is_hyphen {
304 hyphen = Some(a.clone());
305 }
306 !is_hyphen
307 });
308 let parsed_args = T::parse_from(args_before_hyphen);
309 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
310 (parsed_args, raw_args)
311}
312
313pub fn try_parse_known<T: clap::Parser>(
317 args: impl IntoIterator<Item = String>,
318) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
319 let mut args = args.into_iter();
320 let mut hyphen = None;
321 let args_before_hyphen = args.by_ref().take_while(|a| {
322 let is_hyphen = a == "--";
323 if is_hyphen {
324 hyphen = Some(a.clone());
325 }
326 !is_hyphen
327 });
328 let parsed_args = T::try_parse_from(args_before_hyphen)?;
329
330 let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
331 Ok((parsed_args, raw_args))
332}
333
334pub trait SimpleCommand {
336 fn get_content(name: &str, content_type: ContentType) -> Result<String, error::Error>;
338
339 fn execute<I: Iterator<Item = S>, S: AsRef<str>>(
341 context: commands::ExecutionContext<'_>,
342 args: I,
343 ) -> Result<results::ExecutionResult, error::Error>;
344}
345
346pub fn simple_builtin<B: SimpleCommand + Send + Sync>() -> Registration {
349 Registration {
350 execute_func: exec_simple_builtin::<B>,
351 content_func: B::get_content,
352 disabled: false,
353 special_builtin: false,
354 declaration_builtin: false,
355 }
356}
357
358pub fn builtin<B: Command + Send + Sync>() -> Registration {
361 Registration {
362 execute_func: exec_builtin::<B>,
363 content_func: get_builtin_content::<B>,
364 disabled: false,
365 special_builtin: false,
366 declaration_builtin: false,
367 }
368}
369
370pub fn decl_builtin<B: DeclarationCommand + Send + Sync>() -> Registration {
374 Registration {
375 execute_func: exec_declaration_builtin::<B>,
376 content_func: get_builtin_content::<B>,
377 disabled: false,
378 special_builtin: false,
379 declaration_builtin: true,
380 }
381}
382
383#[allow(clippy::too_long_first_doc_paragraph)]
384pub fn raw_arg_builtin<B: DeclarationCommand + Default + Send + Sync>() -> Registration {
391 Registration {
392 execute_func: exec_raw_arg_builtin::<B>,
393 content_func: get_builtin_content::<B>,
394 disabled: false,
395 special_builtin: false,
396 declaration_builtin: true,
397 }
398}
399
400fn get_builtin_content<T: Command + Send + Sync>(
401 name: &str,
402 content_type: ContentType,
403) -> Result<String, error::Error> {
404 T::get_content(name, content_type)
405}
406
407fn exec_simple_builtin<T: SimpleCommand + Send + Sync>(
408 context: commands::ExecutionContext<'_>,
409 args: Vec<CommandArg>,
410) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
411 Box::pin(async move { exec_simple_builtin_impl::<T>(context, args).await })
412}
413
414#[expect(clippy::unused_async)]
415async fn exec_simple_builtin_impl<T: SimpleCommand + Send + Sync>(
416 context: commands::ExecutionContext<'_>,
417 args: Vec<CommandArg>,
418) -> Result<results::ExecutionResult, error::Error> {
419 let plain_args = args.into_iter().map(|arg| match arg {
420 CommandArg::String(s) => s,
421 CommandArg::Assignment(a) => a.to_string(),
422 });
423
424 T::execute(context, plain_args)
425}
426
427fn exec_builtin<T: Command + Send + Sync>(
428 context: commands::ExecutionContext<'_>,
429 args: Vec<CommandArg>,
430) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
431 Box::pin(async move { exec_builtin_impl::<T>(context, args).await })
432}
433
434async fn exec_builtin_impl<T: Command + Send + Sync>(
435 context: commands::ExecutionContext<'_>,
436 args: Vec<CommandArg>,
437) -> Result<results::ExecutionResult, error::Error> {
438 let plain_args = args.into_iter().map(|arg| match arg {
439 CommandArg::String(s) => s,
440 CommandArg::Assignment(a) => a.to_string(),
441 });
442
443 let result = T::new(plain_args);
444 let command = match result {
445 Ok(command) => command,
446 Err(e) => {
447 writeln!(context.stderr(), "{e}")?;
448 return Ok(results::ExecutionExitCode::InvalidUsage.into());
449 }
450 };
451
452 call_builtin(command, context).await
453}
454
455fn exec_declaration_builtin<T: DeclarationCommand + Send + Sync>(
456 context: commands::ExecutionContext<'_>,
457 args: Vec<CommandArg>,
458) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
459 Box::pin(async move { exec_declaration_builtin_impl::<T>(context, args).await })
460}
461
462async fn exec_declaration_builtin_impl<T: DeclarationCommand + Send + Sync>(
463 context: commands::ExecutionContext<'_>,
464 args: Vec<CommandArg>,
465) -> Result<results::ExecutionResult, error::Error> {
466 let mut options = vec![];
467 let mut declarations = vec![];
468
469 for (i, arg) in args.into_iter().enumerate() {
470 match arg {
471 CommandArg::String(s)
472 if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
473 {
474 options.push(s);
475 }
476 _ => declarations.push(arg),
477 }
478 }
479
480 let result = T::new(options);
481 let mut command = match result {
482 Ok(command) => command,
483 Err(e) => {
484 writeln!(context.stderr(), "{e}")?;
485 return Ok(results::ExecutionExitCode::InvalidUsage.into());
486 }
487 };
488
489 command.set_declarations(declarations);
490
491 call_builtin(command, context).await
492}
493
494fn exec_raw_arg_builtin<T: DeclarationCommand + Default + Send + Sync>(
495 context: commands::ExecutionContext<'_>,
496 args: Vec<CommandArg>,
497) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
498 Box::pin(async move { exec_raw_arg_builtin_impl::<T>(context, args).await })
499}
500
501async fn exec_raw_arg_builtin_impl<T: DeclarationCommand + Default + Send + Sync>(
502 context: commands::ExecutionContext<'_>,
503 args: Vec<CommandArg>,
504) -> Result<results::ExecutionResult, error::Error> {
505 let mut command = T::default();
506 command.set_declarations(args);
507
508 call_builtin(command, context).await
509}
510
511async fn call_builtin(
512 command: impl Command,
513 context: commands::ExecutionContext<'_>,
514) -> Result<results::ExecutionResult, error::Error> {
515 let builtin_name = context.command_name.clone();
516 let result = command
517 .execute(context)
518 .await
519 .map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
520
521 Ok(result)
522}