gosh_repl/
repl.rs

1// [[file:../gosh-shell.note::7643ea86][7643ea86]]
2use gut::prelude::*;
3use std::path::{Path, PathBuf};
4// 7643ea86 ends here
5
6// [[file:../gosh-shell.note::f90f0bfb][f90f0bfb]]
7mod helper;
8// f90f0bfb ends here
9
10// [[file:../gosh-shell.note::845cbd1e][845cbd1e]]
11use rustyline::{history::FileHistory, Editor};
12type MyEditor<R> = Editor<helper::MyHelper<R>, FileHistory>;
13
14/// An shell-like REPL interpreter.
15pub struct Interpreter<A> {
16    prompt: String,
17    history_file: Option<PathBuf>,
18    action: A,
19}
20// 845cbd1e ends here
21
22// [[file:../gosh-shell.note::aa47dc5f][aa47dc5f]]
23impl<A: Actionable> Interpreter<A> {
24    /// Interpret one line.
25    fn continue_interpret_line(&mut self, line: &str) -> bool {
26        if let Some(mut args) = shlex::split(line) {
27            assert!(args.len() >= 1);
28            args.insert(0, self.prompt.to_owned());
29
30            match A::try_parse_from(&args) {
31                // apply subcommand
32                Ok(x) => match self.action.act_on(&x) {
33                    Ok(exit) => {
34                        if exit {
35                            return false;
36                        }
37                    }
38                    Err(e) => {
39                        eprintln!("{:?}", e);
40                    }
41                },
42                // show subcommand usage
43                Err(e) => println!("{:}", e),
44            }
45            true
46        } else {
47            eprintln!("Invalid quoting: {line:?}");
48            false
49        }
50    }
51
52    fn continue_read_eval_print<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> bool {
53        match editor.readline(&self.prompt) {
54            Err(rustyline::error::ReadlineError::Eof) => false,
55            Ok(line) => {
56                let line = line.trim();
57                if !line.is_empty() {
58                    let _ = editor.add_history_entry(line);
59                    self.continue_interpret_line(&line)
60                } else {
61                    true
62                }
63            }
64            Err(e) => {
65                eprintln!("{}", e);
66                false
67            }
68        }
69    }
70}
71
72fn create_readline_editor<R: HelpfulCommand>() -> Result<Editor<helper::MyHelper<R>, FileHistory>> {
73    use rustyline::{ColorMode, CompletionType, Config};
74
75    #[cfg(not(windows))]
76    let builder = Config::builder().completion_type(CompletionType::Fuzzy);
77    #[cfg(windows)]
78    let builder = Config::builder().completion_type(CompletionType::Circular);
79    let config = builder
80        .color_mode(ColorMode::Enabled)
81        .history_ignore_dups(true)?
82        .history_ignore_space(true)
83        .max_history_size(1000)?
84        .build();
85
86    let mut rl = Editor::with_config(config)?;
87    let h = self::helper::MyHelper::new();
88    rl.set_helper(Some(h));
89    Ok(rl)
90}
91// aa47dc5f ends here
92
93// [[file:../gosh-shell.note::360871b3][360871b3]]
94impl<A: Actionable> Interpreter<A> {
95    fn load_history<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> Result<()> {
96        if let Some(h) = self.history_file.as_ref() {
97            editor.load_history(h).context("no history")?;
98        }
99        Ok(())
100    }
101
102    fn save_history<R: HelpfulCommand>(&mut self, editor: &mut MyEditor<R>) -> Result<()> {
103        if let Some(h) = self.history_file.as_ref() {
104            editor.save_history(h).context("write history file")?;
105        }
106        Ok(())
107    }
108}
109// 360871b3 ends here
110
111// [[file:../gosh-shell.note::05b99d70][05b99d70]]
112impl<A: Actionable> Interpreter<A> {
113    pub fn interpret_script(&mut self, script: &str) -> Result<()> {
114        let lines = script.lines().filter(|s| !s.trim().is_empty());
115        for line in lines {
116            debug!("Execute: {:?}", line);
117            if !self.continue_interpret_line(&line) {
118                break;
119            }
120        }
121
122        Ok(())
123    }
124
125    pub fn interpret_script_file(&mut self, script_file: &Path) -> Result<()> {
126        let s = gut::fs::read_file(script_file)?;
127        self.interpret_script(&s)?;
128        Ok(())
129    }
130}
131// 05b99d70 ends here
132
133// [[file:../gosh-shell.note::9fdf556e][9fdf556e]]
134/// Defines actions for REPL commands
135pub trait Actionable {
136    type Command: clap::Parser;
137
138    /// Take action on REPL commands. Return Ok(true) will exit shell
139    /// loop.
140    fn act_on(&mut self, cmd: &Self::Command) -> Result<bool>;
141
142    /// parse Command from shell line input.
143    fn try_parse_from<I, T>(iter: I) -> Result<Self::Command>
144    where
145        I: IntoIterator<Item = T>,
146        T: Into<std::ffi::OsString> + Clone,
147    {
148        use clap::Parser;
149
150        let r = Self::Command::try_parse_from(iter)?;
151        Ok(r)
152    }
153}
154
155/// Define command completion
156pub trait HelpfulCommand {
157    fn get_subcommands() -> Vec<String>;
158    fn suitable_for_path_complete(line: &str, pos: usize) -> bool;
159}
160
161impl<T: clap::CommandFactory> HelpfulCommand for T {
162    fn get_subcommands() -> Vec<String> {
163        let app = Self::command();
164        app.get_subcommands().map(|s| s.get_name().into()).collect()
165    }
166
167    /// try to complete when current char is a path separator: foo ./
168    fn suitable_for_path_complete(line: &str, pos: usize) -> bool {
169        line[..pos]
170            .chars()
171            .last()
172            .map(|x| std::path::is_separator(x))
173            .unwrap_or(false)
174    }
175}
176// 9fdf556e ends here
177
178// [[file:../gosh-shell.note::f3bcb018][f3bcb018]]
179impl<A: Actionable> Interpreter<A> {
180    #[track_caller]
181    pub fn new(action: A) -> Self {
182        Self {
183            prompt: "> ".to_string(),
184            // editor: create_readline_editor::<R>().unwrap(),
185            history_file: None,
186            action,
187        }
188    }
189}
190
191impl<A: Actionable> Interpreter<A> {
192    /// Set absolute path to history file for permanently storing command history.
193    pub fn with_history_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
194        let p = path.into();
195        self.history_file = Some(p);
196        self
197    }
198
199    /// Set prompting string for REPL.
200    pub fn with_prompt(mut self, s: &str) -> Self {
201        self.prompt = s.into();
202        self
203    }
204
205    /// Entry point for REPL.
206    pub fn run(&mut self) -> Result<()> {
207        let version = env!("CARGO_PKG_VERSION");
208        println!("This is the interactive parser, version {}.", version);
209        println!("Enter \"help\" or \"?\" for a list of commands.");
210        println!("Press Ctrl-D or enter \"quit\" or \"q\" to exit.");
211        println!("");
212
213        let mut editor = create_readline_editor::<A::Command>()?;
214        let _ = self.load_history(&mut editor);
215        while self.continue_read_eval_print(&mut editor) {
216            debug!("excuted one loop");
217        }
218        self.save_history(&mut editor)?;
219
220        Ok(())
221    }
222}
223// f3bcb018 ends here