1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
//! A module for the shell portion of shi.
//!
//! This module includes all shell-related functionality and interfaces for using shi.
//! Namely, it exposes the `Shell` struct, which is the heart of shi. It makes use of `Command`'s
//! to create a shell interface.

use std::cell::RefCell;
use std::rc::Rc;

use rustyline::error::ReadlineError;

use crate::command::{
    builtin::{ExitCommand, HelpCommand, HelpTreeCommand, HistoryCommand},
    BaseCommand, Command,
};
use crate::command_set::CommandSet;
use crate::error::ShiError;
use crate::parser::{CommandType, Outcome, Parser};
use crate::readline::Readline;
use crate::Result;

/// The shell.
///
/// This gives the shell interface for shi. It is constructed and registered with commands.
/// Execution is done through a run-loop of input/output.
pub struct Shell<'a, S> {
    prompt: &'a str,
    // TODO: We likely should NOT be exporting these, even within the crate. Instead, we should add
    // public getters, perhaps?
    // We need Rc<RefCell> because:
    // * We need Rc because Shell is a self-referencing struct, in that the cmds field is referenced
    // by rl, so we need to allocate this at construction time (at runtime, on the heap) and share
    // references. This calls for Rc.
    // * Rc by itself is not mutable however, but we support adding commands to cmds. So we need
    // RefCell.
    pub(crate) cmds: Rc<RefCell<CommandSet<'a, S>>>,
    pub(crate) builtins: Rc<CommandSet<'a, Self>>,
    pub(crate) rl: Readline<'a, S>,
    parser: Parser,
    history_file: Option<&'a str>,
    state: S,
    pub(crate) terminate: bool,
}

impl<'a> Shell<'a, ()> {
    /// Constructs a new shell with the given prompt, and no state.
    ///
    /// # Arguments
    /// `prompt` - The prompt to display to the user.
    pub fn new(prompt: &'a str) -> Shell<()> {
        let cmds = Rc::new(RefCell::new(CommandSet::new()));
        let builtins = Rc::new(Shell::build_builtins());
        Shell {
            prompt,
            rl: Readline::new(Parser::new(), cmds.clone(), builtins.clone()),
            parser: Parser::new(),
            cmds,
            builtins,
            history_file: None,
            state: (),
            terminate: false,
        }
    }
}

impl<'a, S> Shell<'a, S> {
    /// Constructs the various builtin commands and returns a `CommandSet` of them.
    fn build_builtins() -> CommandSet<'a, Shell<'a, S>>
    where
        S: 'a,
    {
        let mut builtins: CommandSet<'a, Shell<'a, S>> = CommandSet::new();
        builtins.add(Command::new_leaf(HelpCommand::new()));
        builtins.add(Command::new_leaf(HelpTreeCommand::new()));
        builtins.add(Command::new_leaf(ExitCommand::new()));
        builtins.add(Command::new_leaf(HistoryCommand::new()));

        builtins
    }

    /// Constructs a new shell, with the given prompt & state.
    ///
    /// # Arguments
    /// `prompt` - The prompt to display to the user.
    /// `state` - The state that the `Shell` should persist across command invocations.
    pub fn new_with_state(prompt: &'a str, state: S) -> Shell<S>
    where
        S: 'a,
    {
        let cmds = Rc::new(RefCell::new(CommandSet::new()));
        let builtins = Rc::new(Shell::build_builtins());
        Shell {
            prompt,
            rl: Readline::new(Parser::new(), cmds.clone(), builtins.clone()),
            parser: Parser::new(),
            cmds,
            builtins,
            history_file: None,
            state,
            terminate: false,
        }
    }

    /// Registers the given command under this `Shell`.
    ///
    /// # Arguments
    /// `cmd` - The command to register.
    pub fn register(&mut self, cmd: Command<'a, S>) -> Result<()> {
        if self.cmds.borrow().contains(cmd.name()) {
            return Err(ShiError::AlreadyRegistered {
                cmd: cmd.name().to_string(),
            });
        }

        self.cmds.borrow_mut().add(cmd);

        Ok(())
    }

    // TODO: Should we be doing something similar to `rustyline` where we take `P: Path` or
    // whatever it is?
    /// Sets the history file & loads the history from it, if it exists already.
    ///
    /// This is necessary to call if one wishes for their command history to persist across
    /// sessions.
    ///
    /// # Arguments
    /// `history-file` - The path to the history file.
    pub fn set_and_load_history_file(&mut self, history_file: &'a str) -> Result<()> {
        self.rl.load_history(history_file)?;
        self.history_file = Some(history_file);
        Ok(())
    }

    /// Saves the history.
    ///
    /// This is effectively a no-op if no history file has been set.
    ///
    /// This must also be called to actually persist the current session's history. It is necessary
    /// to persist the history if one wishes to see it in future sessions.
    pub fn save_history(&mut self) -> Result<()> {
        if let Some(history_file) = self.history_file {
            self.rl.save_history(history_file)?;
        }
        Ok(())
    }

    pub(crate) fn parse<'b>(&mut self, line: &'b str) -> Outcome<'b> {
        self.parser.parse(line, &self.cmds.borrow(), &self.builtins)
    }

    /// Eval executes a single loop of the shell's run-loop.
    ///
    /// In other words, it takes a single input line and executes on it; `run()` is a loop over
    /// `eval()`.
    ///
    /// # Arguments
    /// `line` - The line to evaluate.
    pub fn eval(&mut self, line: &str) -> Result<String> {
        self.rl.add_history_entry(line);
        let outcome = self.parse(line);

        if !outcome.complete {
            return Err(outcome
                .error()
                .expect("incomplete parse, but failed to produce an error")); // This should never happen.
        }

        match outcome.cmd_type {
            CommandType::Custom => {
                // TODO: This recursive walking through the arguments when we pass this into the
                // ParentCommand is redundant, since we already did that work when we parsed
                // things. We should avoid doing this.
                if let Some(base_cmd_name) = outcome.cmd_path.first() {
                    if let Some(base_cmd) = self.cmds.borrow().get(base_cmd_name) {
                        let args: Vec<String> =
                            line.split(' ').skip(1).map(|s| s.to_string()).collect();
                        base_cmd.validate_args(&args)?;
                        return base_cmd.execute(&mut self.state, &args);
                    }
                }

                Err(ShiError::UnrecognizedCommand {
                    got: line.to_string(),
                })
            }
            CommandType::Builtin => {
                if let Some(base_cmd_name) = outcome.cmd_path.first() {
                    if let Some(base_cmd) = self.builtins.clone().get(base_cmd_name) {
                        let args: Vec<String> =
                            line.split(' ').skip(1).map(|s| s.to_string()).collect();
                        base_cmd.validate_args(&args)?;
                        return base_cmd.execute(self, &args);
                    }
                }

                Err(ShiError::UnrecognizedCommand {
                    got: line.to_string(),
                })
            }
            CommandType::Unknown => Err(outcome
                .error()
                .expect("parsed an Unknown, but failed to produce an error")), // This should never happen.
        }
    }

    /// Executes the shell's run-loop.
    ///
    /// This will run indefinitely until the user exits, otherwise terminates the shell or
    /// process or the shell encounters an error and stops.
    ///
    /// Note that invalid command invocations, e.g., nonexistent commands, are not considered fatal
    /// errors and do _not_ cause a return from this method.
    pub fn run(&mut self) -> Result<()> {
        while !self.terminate {
            let input = self.rl.readline(self.prompt);

            match input {
                Ok(line) => match self.eval(&line) {
                    Ok(output) => println!("{}", output),
                    Err(err) => println!("Error: {}", err),
                },
                Err(ReadlineError::Interrupted) => {
                    println!("-> CTRL+C; bye.");
                    break;
                }
                Err(ReadlineError::Eof) => {
                    println!("-> CTRL+D; bye.");
                    break;
                }
                Err(err) => {
                    println!("Error: {:?}", err);
                    break;
                }
            }
        }

        self.save_history()?;

        Ok(())
    }
}

#[cfg(test)]
pub mod test {
    use super::*;

    use crate::Result;
    use crate::{cmd, parent};

    use pretty_assertions::assert_eq;

    // TODO: Replace or add more tests that trigger the full codepath of the shell.
    #[test]
    fn issue6() -> Result<()> {
        let mut shell = Shell::new("| ");
        shell.register(parent!(
            "server",
            cmd!("listen", "Start listening on the given port", |_, args| {
                Ok(format!("start: {:?}", args))
            }),
            cmd!("unlisten", "stop listening", |_, args| {
                Ok(format!("stop: {:?}", args))
            })
        ))?;

        let output = shell.eval("server listen")?;
        assert_eq!(output, "start: []");

        let output = shell.eval("server listen foo")?;
        assert_eq!(output, "start: [\"foo\"]");

        let output = shell.eval("server unlisten")?;
        assert_eq!(output, "stop: []");

        let output = shell.eval("server unlisten foo")?;
        assert_eq!(output, "stop: [\"foo\"]");

        Ok(())
    }
}