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: &'static [(&'static str, &'static str)] =
16 &[("help", "Show this help message"), ("quit", "Quit repl")];
17
18pub struct Repl<'a> {
29 description: String,
30 prompt: String,
31 text_width: usize,
32 commands: HashMap<String, Vec<Command<'a>>>,
33 trie: Rc<Trie<u8>>,
34 editor: rustyline::Editor<Completion>,
35 out: Box<dyn Write>,
36 predict_commands: bool,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum LoopStatus {
42 Continue,
44 Break,
46}
47
48pub struct ReplBuilder<'a> {
60 commands: Vec<(String, Command<'a>)>,
61 description: String,
62 prompt: String,
63 text_width: usize,
64 editor_config: rustyline::config::Config,
65 out: Box<dyn Write>,
66 with_hints: bool,
67 with_completion: bool,
68 with_filename_completion: bool,
69 predict_commands: bool,
70}
71
72#[derive(Debug, thiserror::Error)]
74pub enum BuilderError {
75 #[error("more than one command with name '{0}' added")]
77 DuplicateCommands(String),
78 #[error("name '{0}' cannot be parsed correctly, thus would be impossible to call")]
80 InvalidName(String),
81 #[error("'{0}' is a reserved command name")]
83 ReservedName(String),
84}
85
86pub(crate) fn split_args(line: &str) -> Result<Vec<String>, shell_words::ParseError> {
87 shell_words::split(line)
88}
89
90impl<'a> Default for ReplBuilder<'a> {
91 fn default() -> Self {
92 ReplBuilder {
93 prompt: "> ".into(),
94 text_width: 80,
95 description: Default::default(),
96 commands: Default::default(),
97 out: Box::new(std::io::stderr()),
98 editor_config: rustyline::config::Config::builder()
99 .output_stream(rustyline::OutputStreamType::Stderr) .completion_type(rustyline::CompletionType::List)
101 .build(),
102 with_hints: true,
103 with_completion: true,
104 with_filename_completion: false,
105 predict_commands: true,
106 }
107 }
108}
109
110macro_rules! setters {
111 ($( $(#[$meta:meta])* $name:ident: $type:ty )+) => {
112 $(
113 $(#[$meta])*
114 pub fn $name<T: Into<$type>>(mut self, v: T) -> Self {
115 self.$name = v.into();
116 self
117 }
118 )+
119 };
120}
121
122impl<'a> ReplBuilder<'a> {
123 setters! {
124 description: String
126 prompt: String
128 text_width: usize
130 editor_config: rustyline::config::Config
132 out: Box<dyn Write>
138 with_hints: bool
156 with_completion: bool
158 with_filename_completion: bool
160 predict_commands: bool
167 }
168
169 pub fn add(mut self, name: &str, cmd: Command<'a>) -> Self {
171 self.commands.push((name.into(), cmd));
172 self
173 }
174
175 pub fn build(self) -> Result<Repl<'a>, BuilderError> {
177 let mut commands: HashMap<String, Vec<Command<'a>>> = HashMap::new();
178 let mut trie = TrieBuilder::new();
179 for (name, cmd) in self.commands.into_iter() {
180 let cmds = commands.entry(name.clone()).or_default();
181 let args = split_args(&name).map_err(|_e| BuilderError::InvalidName(name.clone()))?;
182 if args.len() != 1 || name.is_empty() {
183 return Err(BuilderError::InvalidName(name));
184 } else if RESERVED.iter().any(|(n, _)| *n == name) {
185 return Err(BuilderError::ReservedName(name));
186 } else if cmds.iter().any(|c| c.arg_types() == cmd.arg_types()) {
187 return Err(BuilderError::DuplicateCommands(name));
188 }
189 cmds.push(cmd);
190 trie.push(name);
191 }
192 for (name, _) in RESERVED.iter() {
193 trie.push(name);
194 }
195
196 let trie = Rc::new(trie.build());
197 let helper = Completion {
198 trie: trie.clone(),
199 with_hints: self.with_hints,
200 with_completion: self.with_completion,
201 filename_completer: if self.with_filename_completion {
202 Some(FilenameCompleter::new())
203 } else {
204 None
205 },
206 };
207 let mut editor = rustyline::Editor::with_config(self.editor_config);
208 editor.set_helper(Some(helper));
209
210 Ok(Repl {
211 description: self.description,
212 prompt: self.prompt,
213 text_width: self.text_width,
214 commands,
215 trie,
216 editor,
217 out: self.out,
218 predict_commands: self.predict_commands,
219 })
220 }
221}
222
223impl<'a> Repl<'a> {
224 pub fn builder() -> ReplBuilder<'a> {
226 ReplBuilder::default()
227 }
228
229 fn format_help_entries(&self, entries: &[(String, String)]) -> String {
230 if entries.is_empty() {
231 return "".into();
232 }
233 let width = entries
234 .iter()
235 .map(|(sig, _)| sig)
236 .max_by_key(|sig| sig.len())
237 .unwrap()
238 .len();
239 entries
240 .iter()
241 .map(|(sig, desc)| {
242 let indent = " ".repeat(width + 2 + 2);
243 let opts = textwrap::Options::new(self.text_width)
244 .initial_indent("")
245 .subsequent_indent(&indent);
246 let line = format!(" {:width$} {}", sig, desc, width = width);
247 textwrap::fill(&line, &opts)
248 })
249 .fold(String::new(), |mut out, next| {
250 out.push_str("\n");
251 out.push_str(&next);
252 out
253 })
254 }
255
256 pub fn help(&self) -> String {
258 let mut names: Vec<_> = self.commands.keys().collect();
259 names.sort();
260
261 let signature =
262 |name: &String, args_info: &Vec<String>| format!("{} {}", name, args_info.join(" "));
263 let user: Vec<_> = self.commands
264 .iter()
265 .map(|(name, cmds)| {
266 cmds.iter().map(move |cmd| (signature(&name, &cmd.args_info), cmd.description.clone()))
267 })
268 .flatten()
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 fn handle_line(&mut self, line: String) -> 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.len() == 1 && &candidates[0] == prefix;
305 if candidates.len() != 1 || (!self.predict_commands && !exact) {
306 writeln!(&mut self.out, "Command not found: {}", prefix)?;
307 if candidates.len() > 1 || (!self.predict_commands && !exact) {
308 candidates.sort();
309 writeln!(&mut self.out, "Candidates:\n {}", candidates.join("\n "))?;
310 }
311 writeln!(&mut self.out, "Use 'help' to see available commands.")?;
312 Ok(LoopStatus::Continue)
313 } else {
314 let name = &candidates[0];
315 let tail: Vec<_> = args[1..].iter().map(|s| s.as_str()).collect();
316 match self.handle_command(name, &tail) {
317 Ok(CommandStatus::Done) => Ok(LoopStatus::Continue),
318 Ok(CommandStatus::Quit) => Ok(LoopStatus::Break),
319 Err(err) if err.downcast_ref::<CriticalError>().is_some() => Err(err),
320 Err(err) => {
321 writeln!(&mut self.out, "Error: {}", err)?;
323 if err.is::<ArgsError>() {
324 let cmds = self.commands.get_mut(name).unwrap();
326 writeln!(&mut self.out, "Usage:")?;
327 for cmd in cmds.iter() {
328 writeln!(&mut self.out, " {} {}", name, cmd.args_info.join(" "))?;
329 }
330 }
331 Ok(LoopStatus::Continue)
332 }
333 }
334 }
335 }
336
337 pub fn next(&mut self) -> anyhow::Result<LoopStatus> {
339 match self.editor.readline(&self.prompt) {
340 Ok(line) => {
341 if !line.trim().is_empty() {
342 self.editor.add_history_entry(line.trim());
343 self.handle_line(line)
344 } else {
345 Ok(LoopStatus::Continue)
346 }
347 }
348 Err(ReadlineError::Interrupted) => {
349 writeln!(&mut self.out, "CTRL-C")?;
350 Ok(LoopStatus::Break)
351 }
352 Err(ReadlineError::Eof) => Ok(LoopStatus::Break),
353 Err(err) => {
355 writeln!(&mut self.out, "Error: {:?}", err)?;
356 Ok(LoopStatus::Continue)
357 }
358 }
359 }
360
361 fn handle_command(&mut self, name: &str, args: &[&str]) -> anyhow::Result<CommandStatus> {
362 match name {
363 "help" => {
364 let help = self.help();
365 writeln!(&mut self.out, "{}", help)?;
366 Ok(CommandStatus::Done)
367 }
368 "quit" => Ok(CommandStatus::Quit),
369 _ => {
370 let mut last_arg_err = None;
375 let cmds = self.commands.get_mut(name).unwrap();
376 for cmd in cmds.iter_mut() {
377 match cmd.run(args) {
378 Err(e) => {
379 if !e.is::<ArgsError>() {
380 return Err(e);
381 } else {
382 last_arg_err = Some(Err(e));
383 }
384 },
385 other => return other,
386 }
387 }
388 last_arg_err.unwrap()
390 }
391 }
392 }
393
394 pub fn run(&mut self) -> anyhow::Result<()> {
396 while let LoopStatus::Continue = self.next()? {}
397 Ok(())
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use crate::command;
405
406 #[test]
407 fn builder_duplicate() {
408 let result = Repl::builder()
409 .add("name_x", command!("", () => || Ok(CommandStatus::Done)))
410 .add("name_x", command!("", () => || Ok(CommandStatus::Done)))
411 .build();
412 assert!(matches!(result, Err(BuilderError::DuplicateCommands(_))));
413 }
414
415 #[test]
416 fn builder_non_duplicate() {
417 let result = Repl::builder()
418 .add("name_x", command!("", (a: String) => |_| Ok(CommandStatus::Done)))
419 .add("name_x", command!("", (b: i32) => |_| Ok(CommandStatus::Done)))
420 .build();
421 assert!(matches!(result, Ok(_)));
422 }
423
424 #[test]
425 fn builder_empty() {
426 let result = Repl::builder()
427 .add("", command!("", () => || Ok(CommandStatus::Done)))
428 .build();
429 assert!(matches!(result, Err(BuilderError::InvalidName(_))));
430 }
431
432 #[test]
433 fn builder_spaces() {
434 let result = Repl::builder()
435 .add(
436 "name-with spaces",
437 command!("", () => || Ok(CommandStatus::Done)),
438 )
439 .build();
440 assert!(matches!(result, Err(BuilderError::InvalidName(_))));
441 }
442
443 #[test]
444 fn builder_reserved() {
445 let result = Repl::builder()
446 .add("help", command!("", () => || Ok(CommandStatus::Done)))
447 .build();
448 assert!(matches!(result, Err(BuilderError::ReservedName(_))));
449 let result = Repl::builder()
450 .add("quit", command!("", () => || Ok(CommandStatus::Done)))
451 .build();
452 assert!(matches!(result, Err(BuilderError::ReservedName(_))));
453 }
454
455 #[test]
456 fn repl_quits() {
457 let mut repl = Repl::builder()
458 .add(
459 "foo",
460 command!("description", () => || Ok(CommandStatus::Done)),
461 )
462 .build()
463 .unwrap();
464 assert_eq!(repl.handle_line("quit".into()).unwrap(), LoopStatus::Break);
465 let mut repl = Repl::builder()
466 .add(
467 "foo",
468 command!("description", () => || Ok(CommandStatus::Quit)),
469 )
470 .build()
471 .unwrap();
472 assert_eq!(repl.handle_line("foo".into()).unwrap(), LoopStatus::Break);
473 }
474}