1use std::{collections::HashMap, io::Write, rc::Rc};
4
5use rustyline::{self, completion::FilenameCompleter, error::ReadlineError};
6use shell_words;
7use textwrap;
8use thiserror;
9use trie_rs::{Trie, TrieBuilder};
10
11use crate::command::{ArgsError, Command, CommandStatus, CriticalError};
12use crate::completion::{completion_candidates, Completion};
13
14pub const RESERVED: &[(&str, &str)] = &[("help", "Show this help message"), ("quit", "Quit repl")];
16
17pub struct Repl {
28 description: String,
29 prompt: String,
30 text_width: usize,
31 commands: HashMap<String, Vec<Command>>,
32 trie: Rc<Trie<u8>>,
33 editor: rustyline::Editor<Completion>,
34 out: Box<dyn Write>,
35 predict_commands: bool,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum LoopStatus {
41 Continue,
43 Break,
45}
46
47pub struct ReplBuilder {
59 commands: Vec<(String, Command)>,
60 description: String,
61 prompt: String,
62 text_width: usize,
63 editor_config: rustyline::config::Config,
64 out: Box<dyn Write>,
65 with_hints: bool,
66 with_completion: bool,
67 with_filename_completion: bool,
68 predict_commands: bool,
69}
70
71#[derive(Debug, thiserror::Error)]
73pub enum BuilderError {
74 #[error("more than one command with name '{0}' added")]
76 DuplicateCommands(String),
77 #[error("name '{0}' cannot be parsed correctly, thus would be impossible to call")]
79 InvalidName(String),
80 #[error("'{0}' is a reserved command name")]
82 ReservedName(String),
83}
84
85pub(crate) fn split_args(line: &str) -> Result<Vec<String>, shell_words::ParseError> {
86 shell_words::split(line)
87}
88
89impl Default for ReplBuilder {
90 fn default() -> Self {
91 ReplBuilder {
92 prompt: "> ".into(),
93 text_width: 80,
94 description: Default::default(),
95 commands: Default::default(),
96 out: Box::new(std::io::stderr()),
97 editor_config: rustyline::config::Config::builder()
98 .output_stream(rustyline::OutputStreamType::Stderr) .completion_type(rustyline::CompletionType::List)
100 .build(),
101 with_hints: true,
102 with_completion: true,
103 with_filename_completion: false,
104 predict_commands: true,
105 }
106 }
107}
108
109macro_rules! setters {
110 ($( $(#[$meta:meta])* $name:ident: $type:ty )+) => {
111 $(
112 $(#[$meta])*
113 pub fn $name<T: Into<$type>>(mut self, v: T) -> Self {
114 self.$name = v.into();
115 self
116 }
117 )+
118 };
119}
120
121impl ReplBuilder {
122 setters! {
123 description: String
125 prompt: String
127 text_width: usize
129 editor_config: rustyline::config::Config
131 out: Box<dyn Write>
137 with_hints: bool
155 with_completion: bool
157 with_filename_completion: bool
159 predict_commands: bool
166 }
167
168 pub fn add(mut self, name: &str, cmd: Command) -> Self {
170 self.commands.push((name.into(), cmd));
171 self
172 }
173
174 pub fn build(self) -> Result<Repl, BuilderError> {
176 let mut commands: HashMap<String, Vec<Command>> = HashMap::new();
177 let mut trie = TrieBuilder::new();
178 for (name, cmd) in self.commands {
179 let cmds = commands.entry(name.clone()).or_default();
180 let args = split_args(&name).map_err(|_e| BuilderError::InvalidName(name.clone()))?;
181 if args.len() != 1 || name.is_empty() {
182 return Err(BuilderError::InvalidName(name));
183 } else if RESERVED.iter().any(|(n, _)| *n == name) {
184 return Err(BuilderError::ReservedName(name));
185 } else if cmds.iter().any(|c| c.arg_types() == cmd.arg_types()) {
186 return Err(BuilderError::DuplicateCommands(name));
187 }
188 cmds.push(cmd);
189 trie.push(name);
190 }
191 for (name, _) in RESERVED.iter() {
192 trie.push(name);
193 }
194
195 let trie = Rc::new(trie.build());
196 let helper = Completion {
197 trie: trie.clone(),
198 with_hints: self.with_hints,
199 with_completion: self.with_completion,
200 filename_completer: if self.with_filename_completion {
201 Some(FilenameCompleter::new())
202 } else {
203 None
204 },
205 };
206 let mut editor = rustyline::Editor::with_config(self.editor_config);
207 editor.set_helper(Some(helper));
208
209 Ok(Repl {
210 description: self.description,
211 prompt: self.prompt,
212 text_width: self.text_width,
213 commands,
214 trie,
215 editor,
216 out: self.out,
217 predict_commands: self.predict_commands,
218 })
219 }
220}
221
222impl Repl {
223 pub fn builder() -> ReplBuilder {
225 ReplBuilder::default()
226 }
227
228 fn format_help_entries(&self, entries: &[(String, String)]) -> String {
229 if entries.is_empty() {
230 return String::new();
231 }
232 let width = entries
233 .iter()
234 .map(|(sig, _)| sig)
235 .max_by_key(|sig| sig.len())
236 .unwrap()
237 .len();
238 entries
239 .iter()
240 .map(|(sig, desc)| {
241 let indent = " ".repeat(width + 2 + 2);
242 let opts = textwrap::Options::new(self.text_width)
243 .initial_indent("")
244 .subsequent_indent(&indent);
245 let line = format!(" {sig:width$} {desc}");
246 textwrap::fill(&line, opts)
247 })
248 .fold(String::new(), |mut out, next| {
249 out.push('\n');
250 out.push_str(&next);
251 out
252 })
253 }
254
255 pub fn help(&self) -> String {
257 let mut names: Vec<_> = self.commands.keys().collect();
258 names.sort();
259
260 let signature =
261 |name: &String, args_info: &Vec<String>| format!("{} {}", name, args_info.join(" "));
262 let user: Vec<_> = self
263 .commands
264 .iter()
265 .flat_map(|(name, cmds)| {
266 cmds.iter()
267 .map(move |cmd| (signature(name, &cmd.arg_types()), cmd.description.clone()))
268 })
269 .collect();
270
271 let other: Vec<_> = RESERVED
272 .iter()
273 .map(|(name, desc)| ((*name).to_string(), desc.to_string()))
274 .collect();
275
276 let msg = format!(
277 r#"
278{}
279
280Available commands:
281{}
282
283Other commands:
284{}
285 "#,
286 self.description,
287 self.format_help_entries(&user),
288 self.format_help_entries(&other)
289 );
290 msg.trim().into()
291 }
292
293 async fn handle_line(&mut self, line: &str) -> anyhow::Result<LoopStatus> {
294 let args = match split_args(line) {
296 Err(err) => {
297 writeln!(&mut self.out, "Error: {err}")?;
298 return Ok(LoopStatus::Continue);
299 }
300 Ok(args) => args,
301 };
302 let prefix = &args[0];
303 let mut candidates = completion_candidates(&self.trie, prefix);
304 let exact = !candidates.is_empty() && &candidates[0] == prefix;
305 let can_take_first = !candidates.is_empty() && (exact || self.predict_commands);
306 if !can_take_first {
307 writeln!(&mut self.out, "Command not found: {prefix}")?;
308 if candidates.len() > 1 || (!self.predict_commands && !exact) {
309 candidates.sort();
310 writeln!(&mut self.out, "Candidates:\n {}", candidates.join("\n "))?;
311 }
312 writeln!(&mut self.out, "Use 'help' to see available commands.")?;
313 Ok(LoopStatus::Continue)
314 } else {
315 let name = &candidates[0];
316 let tail: Vec<_> = args[1..].iter().map(String::as_str).collect();
317 match self.handle_command(name, &tail).await {
318 Ok(CommandStatus::Done) => Ok(LoopStatus::Continue),
319 Ok(CommandStatus::Quit) => Ok(LoopStatus::Break),
320 Err(err) if err.downcast_ref::<CriticalError>().is_some() => Err(err),
321 Err(err) => {
322 writeln!(&mut self.out, "Error: {err}")?;
324 if err.is::<ArgsError>() {
325 let cmds = self.commands.get_mut(name).unwrap();
327 writeln!(&mut self.out, "Usage:")?;
328 for cmd in cmds.iter() {
329 writeln!(
330 &mut self.out,
331 " {} {}",
332 name,
333 cmd.args_info
334 .clone()
335 .into_iter()
336 .map(|info| info.to_string())
337 .collect::<Vec<_>>()
338 .join(" ")
339 )?;
340 }
341 }
342 Ok(LoopStatus::Continue)
343 }
344 }
345 }
346 }
347
348 pub async fn next(&mut self) -> anyhow::Result<LoopStatus> {
350 match self.editor.readline(&self.prompt) {
351 Ok(line) => {
352 if !line.trim().is_empty() {
353 self.editor.add_history_entry(line.trim());
354 self.handle_line(&line).await
355 } else {
356 Ok(LoopStatus::Continue)
357 }
358 }
359 Err(ReadlineError::Interrupted) => {
360 writeln!(&mut self.out, "CTRL-C")?;
361 Ok(LoopStatus::Break)
362 }
363 Err(ReadlineError::Eof) => Ok(LoopStatus::Break),
364 Err(err) => {
366 writeln!(&mut self.out, "Error: {err:?}")?;
367 Ok(LoopStatus::Continue)
368 }
369 }
370 }
371
372 async fn handle_command(&mut self, name: &str, args: &[&str]) -> anyhow::Result<CommandStatus> {
373 match name {
374 "help" => {
375 let help = self.help();
376 writeln!(&mut self.out, "{help}")?;
377 Ok(CommandStatus::Done)
378 }
379 "quit" => Ok(CommandStatus::Quit),
380 _ => {
381 let mut last_arg_err = None;
386 let cmds = self.commands.get_mut(name).unwrap();
387 for cmd in cmds.iter_mut() {
388 match cmd.execute(args).await {
389 Err(e) => {
390 if !e.is::<ArgsError>() {
391 return Err(e);
392 } else {
393 last_arg_err = Some(Err(e));
394 }
395 }
396 other => return other,
397 }
398 }
399 last_arg_err.unwrap()
401 }
402 }
403 }
404
405 pub async fn run(&mut self) -> anyhow::Result<()> {
407 while self.next().await? == LoopStatus::Continue {}
408 Ok(())
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use crate::command::{CommandArgInfo, CommandArgType, ExecuteCommand, TrivialCommandHandler};
416 use std::future::Future;
417 use std::pin::Pin;
418
419 #[test]
420 fn builder_duplicate() {
421 let command_x_1 = Command::new("Command X", vec![], Box::new(TrivialCommandHandler::new()));
422
423 let command_x_2 = Command::new(
424 "Command X 2",
425 vec![],
426 Box::new(TrivialCommandHandler::new()),
427 );
428
429 let result = Repl::builder()
430 .add("name_x", command_x_1)
431 .add("name_x", command_x_2)
432 .build();
433
434 assert!(matches!(result, Err(BuilderError::DuplicateCommands(_))));
435 }
436
437 #[test]
438 fn builder_overload() {
439 let command_x_1 = Command::new(
440 "Command X".into(),
441 vec![],
442 Box::new(TrivialCommandHandler::new()),
443 );
444
445 let command_x_2 = Command::new(
446 "Command X 2",
447 vec![CommandArgInfo::new(CommandArgType::I32)],
448 Box::new(TrivialCommandHandler::new()),
449 );
450
451 #[rustfmt::skip]
452 let result = Repl::builder()
453 .add("name_x", command_x_1)
454 .add("name_x", command_x_2)
455 .build();
456 assert!(matches!(result, Ok(_)));
457 }
458
459 #[test]
460 fn builder_empty() {
461 let command_empty = Command::new("", vec![], Box::new(TrivialCommandHandler::new()));
462
463 let result = Repl::builder().add("", command_empty).build();
464 assert!(matches!(result, Err(BuilderError::InvalidName(_))));
465 }
466
467 #[test]
468 fn builder_spaces() {
469 let command_empty = Command::new("", vec![], Box::new(TrivialCommandHandler::new()));
470
471 let result = Repl::builder()
472 .add("name-with spaces", command_empty)
473 .build();
474 assert!(matches!(result, Err(BuilderError::InvalidName(_))));
475 }
476
477 #[test]
478 fn builder_reserved() {
479 let command_help = Command::new("", vec![], Box::new(TrivialCommandHandler::new()));
480
481 let result = Repl::builder().add("help", command_help).build();
482 assert!(matches!(result, Err(BuilderError::ReservedName(_))));
483
484 let command_quit = Command::new("", vec![], Box::new(TrivialCommandHandler::new()));
485
486 let result = Repl::builder().add("quit", command_quit).build();
487 assert!(matches!(result, Err(BuilderError::ReservedName(_))));
488 }
489
490 #[tokio::test]
491 async fn repl_quits() {
492 let command_foo = Command::new(
493 "description",
494 vec![],
495 Box::new(TrivialCommandHandler::new()),
496 );
497
498 let mut repl = Repl::builder().add("foo", command_foo).build().unwrap();
499 assert_eq!(
500 repl.handle_line("quit".into()).await.unwrap(),
501 LoopStatus::Break
502 );
503
504 struct QuittingCommandHandler {}
505 impl QuittingCommandHandler {
506 pub fn new() -> Self {
507 Self {}
508 }
509 async fn handle_command(
510 &mut self,
511 _args: Vec<String>,
512 ) -> anyhow::Result<CommandStatus> {
513 Ok(CommandStatus::Quit)
514 }
515 }
516 impl ExecuteCommand for QuittingCommandHandler {
517 fn execute(
518 &mut self,
519 args: Vec<String>,
520 _args_info: Vec<CommandArgInfo>,
521 ) -> Pin<Box<dyn Future<Output = anyhow::Result<CommandStatus>> + '_>> {
522 Box::pin(self.handle_command(args))
523 }
524 }
525 let command_quit = Command::new(
526 "description",
527 vec![],
528 Box::new(QuittingCommandHandler::new()),
529 );
530
531 let mut repl = Repl::builder().add("foo", command_quit).build().unwrap();
532 assert_eq!(
533 repl.handle_line("foo".into()).await.unwrap(),
534 LoopStatus::Break
535 );
536 }
537}